Exemple #1
0
class FanTuning:
    FANNABLE_CELEBRITY_SITUATION = TunablePackSafeReference(description='\n        Situation that will store all celebrity Sims that can be used to\n        spawn fan or stan situations. \n        ', manager=services.get_instance_manager(Types.SITUATION), class_restrictions='FannableCelebritySimsSituation')
    FAN_TARGETTING_TAG = TunableTag(description='\n        Tag applied to celebrities that can be targeted by fans.\n        \n        Used in conjunction with autonomy modifiers so we can consider\n        celebrities without caring about whether or not they are on or off lot.\n        ', filter_prefixes=('Func',))
    FAN_SITUATION_TAG = TunableTag(description='\n        Tag which delineates which situations are fan situations.\n        ', filter_prefixes=('Situation',))
    STAN_DISABLING_BITS = TunableSet(description='\n        Rel bits, which if any are found on the stan, prevents the stan\n        from kicking off their situation. \n        \n        Used to prevent a stan from being too stanny. \n        ', tunable=TunableReference(services.get_instance_manager(Types.RELATIONSHIP_BIT), pack_safe=True))
    STAN_FILTER = TunablePackSafeReference(description='\n        Filter used to find the stan for a given Sim.  This should \n        only contain the minimum required filter terms for a Stan.\n        ', manager=services.get_instance_manager(Types.SIM_FILTER))
    STAN_PERK = TunablePackSafeReference(description='\n        Perk used to determine if a Sim is stannable.\n        ', manager=services.get_instance_manager(Types.BUCKS_PERK))

    @classproperty
    def are_fans_supported(cls):
        return cls.FANNABLE_CELEBRITY_SITUATION is not None
Exemple #2
0
 def recipe_factory_tuning(pack_safe=False):
     return {
         'recipe_tag':
         TunableTag(
             description=
             '\n                The recipe tag to use to create the object.\n                ',
             filter_prefixes=('Recipe', ),
             pack_safe=pack_safe)
     }
Exemple #3
0
class TrendTuning:
    TREND_DATA = TunableList(
        description='\n        A list of data about trends.\n        ',
        tunable=TunableTuple(
            description=
            '\n            The data about this trend.\n            ',
            trend_tag=TunableTag(
                description=
                '\n                The tag for this trend.\n                ',
                filter_prefixes=('func_trend', )),
            trend_type=TunableEnumEntry(
                description=
                '\n                The type of this trend.\n                ',
                tunable_type=TrendType,
                default=TrendType.INVALID,
                invalid_enums=(TrendType.INVALID, )),
            trend_name=TunableLocalizedString(
                description=
                '\n                The name for this trend. This will show up in a bulleted\n                list when a player researches current trends.\n                '
            )))
    TREND_REFRESH_COOLDOWN = TunableTimeSpan(
        description=
        '\n        The amount of time it takes before trends refresh.\n        ',
        default_days=2)
    TREND_TIME_REMAINING_DESCRIPTION = TunableMapping(
        description=
        '\n        A mapping of thresholds, in Sim Hours, to descriptions used when\n        describing the amount of time remaining in the study trends\n        notification.\n        ',
        key_name='sim_hours',
        key_type=int,
        value_name='description_string',
        value_type=TunableLocalizedString())
    TODDLER_CHILD_TREND = TunableTag(
        description=
        '\n        The tag we use to indicate Toddler or Child trends.\n        ',
        filter_prefixes=('func_trend', ))
    CELEBRITY_TREND = TunableTag(
        description=
        '\n        The tag we use to indicate Celebrity Trends.\n        ',
        filter_prefixes=('func_trend', ))
    TRENDLESS_VIDEO_DEFINITION = TunableReference(
        description=
        '\n        The object definition to use if a Sim records a trendless video.\n        ',
        manager=services.definition_manager(),
        pack_safe=True)
Exemple #4
0
class TunableWeightedTagList(metaclass=TunedInstanceMetaclass,
                             manager=services.get_instance_manager(
                                 sims4.resources.Types.TAG_SET)):
    INSTANCE_TUNABLES = {
        'weighted_tags':
        TunableList(
            description='\n            A list of weighted tags.\n            ',
            tunable=TunableTuple(
                description=
                '\n                A tag and the weight associated with it.\n                ',
                tag=TunableTag(),
                weight=TunableRange(tunable_type=float, default=1, minimum=0)))
    }
class _TestRecipeByTag(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'required_tag':
        TunableTag(
            description=
            '\n            The tag that must exist on the recipe\n            ',
            filter_prefixes=('Recipe', ))
    }

    def test_recipe(self, recipe):
        if self.required_tag not in recipe.recipe_tags:
            return TestResult(
                False,
                f'{recipe} does not have required tag {self.required_tag}')
        return TestResult.TRUE
Exemple #6
0
class VehicleTuning:
    SURFACE_FAVORITES = TunableMapping(
        description=
        '\n        Tuning that specifies which favorite tag to search for when a Sim\n        attempts to deploy a vehicle on a given surface.\n        \n        Example: Sim is in the water and wants to deploy a water vehicle. They\n        have both an Aqua Zip and an Island Canoe, but only the Aqua Zip is the\n        favorite. We want to ask the favorites tracker if a given favorite\n        water vehicle has been set, which is based on the tag tuned here.\n        ',
        key_name='Surface',
        value_name='Favorite Tag',
        key_type=TunableEnumEntry(
            description=
            '\n            The Surface we want to apply a favorite tag to. If the Sim is on\n            this surface and has an opportunity to deploy a vehicle, then we\n            use the corresponding tag to choose it.\n            ',
            tunable_type=SurfaceType,
            default=SurfaceType.SURFACETYPE_WORLD,
            invalid_enums=(SurfaceType.SURFACETYPE_UNKNOWN, )),
        value_type=TunableTag(
            description=
            '\n            The favorite tag we search the inventory for when deploying vehicles.\n            ',
            filter_prefixes=('Func', )))
Exemple #7
0
class BaseCivicPolicyProvider(ComponentContainer, HasStatisticComponent, HasTunableFactory, AutoFactoryInit):
    CIVIC_POLICY_SCHEDULE = TunableTuple(description='\n        Global schedule to control when voting on civic policies is active.\n        ', voting_open=TunableTimeOfWeek(description='\n            The time of the week that voting for civic policies starts.\n            ', default_day=Days.MONDAY, default_hour=8, default_minute=0), voting_close=TunableTimeOfWeek(description='\n            The time of the week that voting for civic policies ends.  Votes are\n            tallied and policies are modified at this time.\n            ', default_day=Days.SUNDAY, default_hour=16, default_minute=0), voting_close_warning_duration=TunableTimeSpan(description='\n            Duration before the Voting Close to warn players that voting is about to close.\n            ', default_hours=8), schedule_text=TunableLocalizedStringFactory(description='\n            Text for the schedule string.\n            '))
    INFLUENCE_BUCK_TYPE = TunableEnumEntry(description='\n        The type of Bucks used to hold Influence.\n        ', tunable_type=BucksType, default=BucksType.INVALID, pack_safe=True)
    INFLUENCE_TO_VOTE_COST = Tunable(description='\n        The amount of influence used with 1 vote.\n        ', tunable_type=int, default=10, export_modes=ExportModes.All)
    REPEAL_PETITION_THRESHOLD = Tunable(description='\n        The number of petition signatures required to have a policy repealed.\n        ', tunable_type=int, default=10, export_modes=ExportModes.All)
    COMMUNITY_BOARD_TAG = TunableTag(description='\n        The tag of the community boards so we can find them in the world.\n        ')
    VOTING_OPEN_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period opens.\n        ')
    VOTING_OPEN_MAX_ENABLED_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period opens with maximum enabled policies.\n        ')
    VOTING_CLOSE_WARNING_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting close approaches.\n        ')
    VOTING_CLOSE_WARNING_MAX_ENABLED_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting close approaches with maximum enabled policies and\n        a policy being repealed.\n        ')
    VOTING_CLOSE_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period closes.\n        ')
    VOTING_CLOSE_MAX_ENABLED_NOTIFICATION_SUCCESS = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period closes with maximum enabled\n        policies and a policy being successfully repealed.\n        ')
    VOTING_CLOSE_MAX_ENABLED_NOTIFICATION_FAIL = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period closes with maximum enabled\n        policies and a policy being unsuccessfully repealed.\n        ')
    VOTING_CONTINUATION_AUTONOMY_COMMODITIES = TunableList(description='\n        A list of static commodities that will be solved for by autonomy to\n        find and push the vote interaction after viewing the community board.\n        ', tunable=StaticCommodity.TunableReference(description='\n            A static commodity that is solved for by autonomy to find the vote\n            interaction to push. \n            ', pack_safe=True))
    COMMUNITY_BOARD_TEXT = TunableTuple(voting_closed_policy_tooltip_text=TunableLocalizedStringFactory(description="\n            String to insert into the policy tooltips when voting isn't possible\n            because voting is closed.\n            "), voting_open_add_policy_tooltip_text=TunableLocalizedStringFactory(description="\n            Text for the tooltip on the add policy button when it's disabled because\n            voting is open. \n            "), ineligible_voter_policy_tooltip_text=TunableLocalizedStringFactory(description="\n            String to insert into the policy tooltips when voting isn't possible because\n            the sim (first token) lives on a different street.\n            "), ineligible_voter_confirm_tooltip_text=TunableLocalizedStringFactory(description='\n            Text for the tooltip on the confirm button when the button is disabled because\n            the sim (first token) lives on a different street.\n            '), no_room_confirm_tooltip_text=TunableLocalizedStringFactory(description='\n            Text for the tooltip on the confirm button when the button is disabled because\n            already full up on enacted policies.\n            '), no_room_policy_tooltip_text=TunableLocalizedStringFactory(description="\n            String to insert into the policy tooltips when voting isn't possible  because\n            already full up on enacted policies.\n            "), add_policy_picker=TunablePickerDialogVariant(description='\n            The item picker dialog.\n            ', available_picker_flags=ObjectPickerTuningFlags.ITEM))
    CALL_TO_ACTIONS = TunableList(description='\n        List of Call to Action that should be started to introduce the Civic Policy features.\n        ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.CALL_TO_ACTION), pack_safe=True))
    FACTORY_TUNABLES = {'civic_policies': TunableSet(description='\n            The civic policies that may be enacted.\n            ', tunable=TunableReference(description='\n                A civic policy.\n                ', manager=services.get_instance_manager(sims4.resources.Types.SNIPPET), class_restrictions=('BaseCivicPolicy',), pack_safe=True, export_modes=ExportModes.All)), 'voting_open_loot': TunableList(description='\n            Loot applied to Resident Sims when voting opens.\n            ', tunable=LootActions.TunableReference(description='\n                Loot to apply on voting open.\n                ', pack_safe=True), export_modes=ExportModes.ServerXML), 'voting_close_loot': TunableList(description='\n            Loot applied to Resident Sims when voting opens.\n            ', tunable=LootActions.TunableReference(description='\n                Loot to apply on voting open.\n                ', pack_safe=True), export_modes=ExportModes.ServerXML), 'community_board_dialog_title': TunableLocalizedStringFactoryVariant(description="\n            The Community Board Dialog's title text.\n            ", export_modes=ExportModes.ServerXML), 'initial_vote_test': TunableTestSet(description='\n            If at least one test passes, and the user option is enabled, initial voting will\n            be performed when voting opens.\n            ', export_modes=ExportModes.ServerXML), 'daily_random_vote_test': TunableTestSet(description='\n            If at least one test passes, and the user option is enabled, daily random voting\n            will be performed at midnight.\n            ', export_modes=ExportModes.ServerXML)}
    CIVIC_POLICY_TEST_EVENTS = (TestEvent.CivicPolicyOpenVoting, TestEvent.CivicPolicyDailyRandomVoting, TestEvent.CivicPolicyCloseVoting)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.add_statistic_component()
        self._enacted_policies = set()
        self._balloted_policies = set()
        self._up_for_repeal_policies = set()
        self._civic_policies = set()
        for policy in self.civic_policies:
            self._civic_policies.add(policy(self))

    @constproperty
    def is_sim():
        return False

    @property
    def is_downloaded(self):
        return False

    def get_enacted_policies(self, tuning=False):
        if tuning:
            return set([type(p) for p in self._enacted_policies])
        return self._enacted_policies

    def get_balloted_policies(self, tuning=False):
        if tuning:
            return set([type(p) for p in self._balloted_policies])
        return self._balloted_policies

    def get_up_for_repeal_policies(self, tuning=False):
        if tuning:
            return set([type(p) for p in self._up_for_repeal_policies])
        return self._up_for_repeal_policies

    def get_dormant_policies(self, tuning=False):
        policies = self._civic_policies - self._enacted_policies - self._balloted_policies - self._up_for_repeal_policies
        if tuning:
            return set([type(p) for p in policies])
        return policies

    def get_civic_policies(self, tuning=False):
        if tuning:
            return self.civic_policies
        return self._civic_policies

    def new_enact_max_count(self):
        return min(len(self.civic_policies), self.max_enacted_policy_count) - len(self._enacted_policies)

    def _reset_voting_statistics(self):
        for policy in self._civic_policies:
            if policy.vote_count_statistic is None:
                logger.error('{} tuned without voting statistic', policy)
            else:
                self.set_stat_value(policy.vote_count_statistic, 0)
        self.commodity_tracker.check_for_unneeded_initial_statistics()
        self.statistic_tracker.check_for_unneeded_initial_statistics()

    def run_sim_voting_loot(self, loot_actions):
        for resolver in self.open_close_voting_loot_resolver_list:
            for loot in loot_actions:
                loot.apply_to_resolver(resolver)

    def run_auto_voting_tests(self, test_set):
        if not services.street_service().enable_automatic_voting:
            return False
        if not test_set:
            return True
        return test_set.run_tests(GlobalResolver())

    def open_voting(self):
        self._reset_voting_statistics()
        self.finalize_startup()
        self.run_sim_voting_loot(self.voting_open_loot)
        if self.run_auto_voting_tests(self.initial_vote_test):
            for policy in self._balloted_policies:
                self.vote(policy, policy.get_initial_vote_count())

    def close_voting(self):

        def get_most_voted_for_policy(policies):
            if not policies:
                return set()
            policy = max(policies, key=lambda policy: self.get_stat_value(policy.vote_count_statistic))
            if self.get_stat_value(policy.vote_count_statistic) <= 0:
                return set()
            return set((policy,))

        balloted_policies = self.get_balloted_policies()
        to_enact = get_most_voted_for_policy(balloted_policies)
        repealable_policies = self.get_up_for_repeal_policies()
        to_repeal = set()
        for policy in repealable_policies:
            if BaseCivicPolicyProvider.REPEAL_PETITION_THRESHOLD <= self.get_stat_value(policy.vote_count_statistic):
                to_repeal.add(policy)
        to_repeal -= to_enact
        to_enact -= self._enacted_policies
        self._enacted_policies -= to_repeal
        for policy in to_repeal:
            policy.repeal()
        to_enact_max_count = self.new_enact_max_count()
        while len(to_enact) > to_enact_max_count:
            to_enact.pop()
        self._enacted_policies.update(to_enact)
        for policy in to_enact:
            policy.enact()
        self._balloted_policies = set()
        self._up_for_repeal_policies = set()
        self._reset_voting_statistics()
        self.finalize_startup()
        self.run_sim_voting_loot(self.voting_close_loot)

    def get_schedule_text(self):
        return self.CIVIC_POLICY_SCHEDULE.schedule_text(self.CIVIC_POLICY_SCHEDULE.voting_open(), self.CIVIC_POLICY_SCHEDULE.voting_close())

    def do_daily_vote(self):
        if self.run_auto_voting_tests(self.daily_random_vote_test):
            for policy in self._balloted_policies:
                self.vote(policy, policy.get_daily_vote_count())

    @property
    def max_enacted_policy_count(self):
        raise NotImplementedError

    @property
    def max_balloted_policy_count(self):
        raise NotImplementedError

    @property
    def initial_balloted_policy_count(self):
        raise NotImplementedError

    @property
    def max_repealable_policy_count(self):
        raise NotImplementedError

    @property
    def open_close_voting_loot_resolver_list(self):
        raise NotImplementedError

    @classproperty
    def provider_type_id(cls):
        raise NotImplementedError

    def get_world_description_id(self):
        return 0

    def is_eligible_voter(self, sim_info):
        raise NotImplementedError

    def is_new_policy_allowed(self, sim_info):
        return False

    def _select_balloted_policies(self):
        self._balloted_policies.clear()
        count_needed = self.initial_balloted_policy_count
        r = random.Random()
        dormant_policies = list(self.get_dormant_policies())
        while dormant_policies:
            while len(self._balloted_policies) < count_needed:
                policy = r.choice(dormant_policies)
                dormant_policies.remove(policy)
                self._balloted_policies.add(policy)

    def finalize_startup(self):
        statistic_component = self.get_component(objects.components.types.STATISTIC_COMPONENT)
        statistic_component.on_finalize_load()
        if not self._civic_policies:
            return
        services.get_event_manager().unregister(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS)
        services.get_event_manager().register(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS)
        if not self._balloted_policies:
            self._select_balloted_policies()
        for policy in self._civic_policies:
            policy.finalize_startup()

    def stop_civic_policy_provider(self):
        services.get_event_manager().unregister(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS)

    def get_policy_instance_for_tuning(self, policy_guid64):
        for inst in self._civic_policies:
            if policy_guid64 == inst.guid64:
                return inst

    def enact(self, policy):
        policy = self.get_policy_instance_for_tuning(policy.guid64)
        if policy is None or policy in self._enacted_policies:
            return False
        if self.new_enact_max_count() == 0:
            return False
        self._enacted_policies.add(policy)
        self._balloted_policies.discard(policy)
        self._up_for_repeal_policies.discard(policy)
        policy.enact()
        return True

    def repeal(self, policy):
        policy = self.get_policy_instance_for_tuning(policy.guid64)
        if policy is None or policy not in self._enacted_policies:
            return False
        self._enacted_policies.discard(policy)
        self._up_for_repeal_policies.discard(policy)
        policy.repeal()
        return True

    def vote(self, policy, count=1, user_directed=False, lobby_interaction=False):
        policy_instance = self.get_policy_instance_for_tuning(policy.guid64)
        if policy_instance is None:
            return False
        return self.vote_by_instance(policy_instance, count, user_directed, lobby_interaction)

    def vote_by_instance(self, policy_instance, count=1, user_directed=False, lobby_interaction=False):
        if policy_instance.vote_count_statistic is not None:
            policy_list = None

            def get_current_rank():
                policy_list.sort(key=lambda policy: (self.get_stat_value(policy.vote_count_statistic), policy.guid64), reverse=True)
                return policy_list.index(policy_instance)

            if user_directed:
                factor = 0
                if policy_instance in self._balloted_policies:
                    policy_list = list(self._balloted_policies)
                    factor = 1
                elif policy_instance in self._up_for_repeal_policies:
                    policy_list = list(self._up_for_repeal_policies)
                    factor = -1
                orig_rank = get_current_rank()
            elif lobby_interaction:
                factor = 0
                if policy_instance in self._balloted_policies:
                    factor = 1
                elif policy_instance in self._up_for_repeal_policies:
                    factor = -1
            value = self.get_stat_value(policy_instance.vote_count_statistic) + count
            self.set_stat_value(policy_instance.vote_count_statistic, value)
            services.street_service().update_community_board_tooltip(self)
            if user_directed:
                if policy_list is not None:
                    with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_VOTE) as hook:
                        hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id())
                        hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64)
                        hook.write_guid(TELEMETRY_FIELD_VOTES, factor*value)
                        hook.write_guid(TELEMETRY_FIELD_PLAYER_VOTES, factor*count)
                        hook.write_guid(TELEMETRY_FIELD_OLD_RANK, orig_rank)
                        hook.write_guid(TELEMETRY_FIELD_NEW_RANK, get_current_rank())
            if lobby_interaction:
                with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_LOBBY) as hook:
                    hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id())
                    hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64)
                    hook.write_guid(TELEMETRY_FIELD_VOTES, factor*value)
            return True
        return False

    def _log_propose_telemetry(self, policy_instance, action):
        with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_PROPOSE) as hook:
            hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id())
            hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64)
            hook.write_guid(TELEMETRY_FIELD_PROPOSE_ACTION, action)

    def add_to_ballot(self, policy_instance):
        if policy_instance.vote_count_statistic is not None and policy_instance not in self._balloted_policies:
            self._balloted_policies.add(policy_instance)
            self._log_propose_telemetry(policy_instance, TELEMETRY_FIELD_ACTION_VALUE_BALLOT)
            return True
        return False

    def add_for_repeal(self, policy):
        policy = self.get_policy_instance_for_tuning(policy.guid64)
        if policy is None:
            return False
        if policy not in self._enacted_policies:
            return False
        if policy in self._up_for_repeal_policies:
            return False
        self._up_for_repeal_policies.add(policy)
        self._log_propose_telemetry(policy, TELEMETRY_FIELD_ACTION_VALUE_REPEAL)
        return True

    def remove_from_repeal(self, policy):
        policy = self.get_policy_instance_for_tuning(policy.guid64)
        if policy is None:
            return False
        if policy not in self._up_for_repeal_policies:
            return False
        self._up_for_repeal_policies.discard(policy)
        self._log_propose_telemetry(policy, TELEMETRY_FIELD_ACTION_VALUE_CANCEL_REPEAL)
        return True

    def save(self, parent_data_msg):
        parent_data_msg.ClearField('policy_data')
        for policy in self._civic_policies:
            policy.save(parent_data_msg)
        parent_data_msg.ClearField('balloted_policy_ids')
        for policy in self._balloted_policies:
            parent_data_msg.balloted_policy_ids.append(policy.guid64)
        parent_data_msg.ClearField('up_for_repeal_policy_ids')
        for policy in self._up_for_repeal_policies:
            parent_data_msg.up_for_repeal_policy_ids.append(policy.guid64)
        parent_data_msg.ClearField('commodity_tracker')
        parent_data_msg.ClearField('statistics_tracker')
        parent_data_msg.ClearField('ranked_statistic_tracker')
        self.update_all_commodities()
        (commodites, _, ranked_statistics) = self.commodity_tracker.save()
        parent_data_msg.commodity_tracker.commodities.extend(commodites)
        regular_statistics = self.statistic_tracker.save()
        parent_data_msg.statistics_tracker.statistics.extend(regular_statistics)
        parent_data_msg.ranked_statistic_tracker.ranked_statistics.extend(ranked_statistics)

    def load(self, parent_data_msg):
        self.commodity_tracker.load(parent_data_msg.commodity_tracker.commodities)
        self.statistic_tracker.load(parent_data_msg.statistics_tracker.statistics)
        self.commodity_tracker.load(parent_data_msg.ranked_statistic_tracker.ranked_statistics)
        self._enacted_policies.clear()
        for policy_data in parent_data_msg.policy_data:
            policy = self.get_policy_instance_for_tuning(policy_data.policy_id)
            if policy:
                policy.load(policy_data)
                if policy.enacted:
                    self._enacted_policies.add(policy)
        for policy_id in parent_data_msg.balloted_policy_ids:
            policy = self.get_policy_instance_for_tuning(policy_id)
            if policy:
                self._balloted_policies.add(policy)
        for policy_id in parent_data_msg.up_for_repeal_policy_ids:
            policy = self.get_policy_instance_for_tuning(policy_id)
            if policy:
                self._up_for_repeal_policies.add(policy)

    def handle_event(self, sim_info, event, resolver):
        if event == TestEvent.CivicPolicyDailyRandomVoting:
            self.do_daily_vote()
        elif event == TestEvent.CivicPolicyOpenVoting:
            self.open_voting()
        elif event == TestEvent.CivicPolicyCloseVoting:
            self.close_voting()

    def get_influence(self, sim_info):
        tracker = BucksUtils.get_tracker_for_bucks_type(self.INFLUENCE_BUCK_TYPE, owner_id=sim_info.id, add_if_none=False)
        if tracker is None:
            return 0
        return tracker.get_bucks_amount_for_type(self.INFLUENCE_BUCK_TYPE)

    def modify_influence(self, sim_info, delta):
        if delta == 0:
            return
        tracker = BucksUtils.get_tracker_for_bucks_type(self.INFLUENCE_BUCK_TYPE, owner_id=sim_info.id, add_if_none=True)
        if tracker is None:
            return
        tracker.try_modify_bucks(self.INFLUENCE_BUCK_TYPE, delta)

    def populate_community_board_op(self, sim_info, op, target_id):
        op.sim_id = sim_info.id
        op.target_id = target_id
        op.influence_points = self.get_influence(sim_info)
        op.title = self.community_board_dialog_title()
        if hasattr(op, 'schedule_text'):
            op.schedule_text = self.get_schedule_text()
        for policy in self._enacted_policies:
            with ProtocolBufferRollback(op.enacted_policies) as enacted_policy:
                enacted_policy.policy_id = policy.guid64
                if policy in self._up_for_repeal_policies:
                    if policy.vote_count_statistic is None:
                        enacted_policy.count = 0
                    else:
                        enacted_policy.count = int(self.get_stat_value(policy.vote_count_statistic))
        for policy in self._balloted_policies:
            with ProtocolBufferRollback(op.balloted_policies) as balloted_policy:
                balloted_policy.policy_id = policy.guid64
                stat = policy.vote_count_statistic
                if stat is None:
                    balloted_policy.count = 0
                else:
                    balloted_policy.count = int(self.get_stat_value(stat))
                    balloted_policy.max_count = stat.max_value
        op.provider_type = self.provider_type_id
        op.new_policy_allowed = self.is_new_policy_allowed(sim_info)
        if not services.street_service().voting_open:
            op.policy_disabled_tooltip = self.COMMUNITY_BOARD_TEXT.voting_closed_policy_tooltip_text()
        if not self.is_eligible_voter(sim_info):
            op.disabled_tooltip = self.COMMUNITY_BOARD_TEXT.ineligible_voter_confirm_tooltip_text(sim_info)
            op.policy_disabled_tooltip = self.COMMUNITY_BOARD_TEXT.ineligible_voter_policy_tooltip_text(sim_info)

    def _on_add_picker_selected(self, dialog):
        tag_objs = dialog.get_result_tags()
        if not tag_objs:
            return
        num_tags = len(tag_objs)
        can_add_more = dialog.max_selectable.number_selectable - num_tags > 0
        if can_add_more:
            can_add_more = len(dialog.picker_rows) > num_tags
        op = CommunityBoardAddPolicy(tag_objs, dialog.target_sim.sim_id, can_add_more)
        Distributor.instance().add_op_with_no_owner(op)

    def create_add_policy_picker(self, sim_info, used_policy_ids):
        resolver = SingleSimResolver(sim_info)
        dialog = self.COMMUNITY_BOARD_TEXT.add_policy_picker(sim_info, resolver=resolver)
        for policy in self.get_dormant_policies():
            if policy.guid64 not in used_policy_ids:
                tooltip = lambda *_, tooltip=policy.display_description: tooltip(sim_info)
                dialog.add_row(BasePickerRow(name=policy.display_name(sim_info), icon=policy.display_icon, tag=policy.guid64, row_tooltip=tooltip))
        dialog.max_selectable.number_selectable = min(len(dialog.picker_rows), self.max_balloted_policy_count - len(self._balloted_policies) - len(used_policy_ids))
        dialog.set_target_sim(sim_info)
        dialog.add_listener(self._on_add_picker_selected)
        dialog.show_dialog()

    def handle_vote_interaction(self, sim_info, target_id, push_continuation):
        sim = sim_info.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS)
        if sim is None:
            return
        target_object = services.object_manager().get(target_id)
        if not target_object:
            return
        for current_interaction in sim.si_state:
            interaction_target = current_interaction.target
            if interaction_target is None:
                continue
            if interaction_target.is_part:
                if interaction_target.part_owner is target_object:
                    target_object = interaction_target
                    break
            if interaction_target is target_object:
                break
        else:
            return
        if push_continuation:
            context = current_interaction.context.clone_for_continuation(current_interaction)
            autonomy_request = autonomy.autonomy_request.AutonomyRequest(sim, FullAutonomy, static_commodity_list=self.VOTING_CONTINUATION_AUTONOMY_COMMODITIES, object_list=(target_object,), insert_strategy=QueueInsertStrategy.NEXT, apply_opportunity_cost=False, is_script_request=True, ignore_user_directed_and_autonomous=True, context=context, si_state_view=sim.si_state, limited_autonomy_allowed=True, autonomy_mode_label_override='ParameterizedAutonomy', off_lot_autonomy_rule_override=UNLIMITED_AUTONOMY_RULE)
            autonomy_service = services.autonomy_service()
            results = autonomy_service.score_all_interactions(autonomy_request)
            chosen_interaction = autonomy_service.choose_best_interaction(results, autonomy_request)
            autonomy_request.invalidate_created_interactions(excluded_si=chosen_interaction)
            if chosen_interaction:
                target_affordance = current_interaction.generate_continuation_affordance(chosen_interaction.affordance)
                sim.push_super_affordance(target_affordance, target_object, context)
        current_interaction.cancel(FinishingType.NATURAL, 'Finished viewing board')
Exemple #8
0
class Skill(HasTunableReference, ProgressiveStatisticCallbackMixin, statistics.continuous_statistic_tuning.TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)):
    SKILL_LEVEL_LIST = TunableMapping(description='\n        A mapping defining the level boundaries for each skill type.\n        ', key_type=SkillLevelType, value_type=TunableList(description='\n            The level boundaries for skill type, specified as a delta from the\n            previous value.\n            ', tunable=Tunable(tunable_type=int, default=0)), tuple_name='SkillLevelListMappingTuple', export_modes=ExportModes.All)
    SKILL_EFFECTIVENESS_GAIN = TunableMapping(description='\n        Skill gain points based on skill effectiveness.\n        ', key_type=SkillEffectiveness, value_type=TunableCurve())
    DYNAMIC_SKILL_INTERVAL = TunableRange(description='\n        Interval used when dynamic loot is used in a\n        PeriodicStatisticChangeElement.\n        ', tunable_type=float, default=1, minimum=1)
    INSTANCE_TUNABLES = {'stat_name': TunableLocalizedString(description='\n            The name of this skill.\n            ', export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'skill_description': TunableLocalizedString(description="\n            The skill's normal description.\n            ", export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'locked_description': TunableLocalizedString(description="\n            The skill description when it's locked.\n            ", allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'icon': TunableIcon(description='\n            Icon to be displayed for the Skill.\n            ', export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'tooltip_icon_list': TunableList(description='\n            A list of icons to show in the tooltip of this\n            skill.\n            ', tunable=TunableIcon(description='\n                Icon that is displayed what types of objects help\n                improve this skill.\n                '), export_modes=(ExportModes.ClientBinary,), tuning_group=GroupNames.UI), 'tutorial': TunableReference(description='\n            Tutorial instance for this skill. This will be used to bring up the\n            skill lesson from the first notification for Sim to know this skill.\n            ', manager=services.get_instance_manager(sims4.resources.Types.TUTORIAL), allow_none=True, class_restrictions=('Tutorial',), tuning_group=GroupNames.UI), 'priority': Tunable(description="\n            Skill priority.  Higher priority skills will trump other skills when\n            being displayed on the UI. When a Sim gains multiple skills at the\n            same time, only the highest priority one will display a progress bar\n            over the Sim's head.\n            ", tunable_type=int, default=1, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'next_level_teaser': TunableList(description='\n            Tooltip which describes what the next level entails.\n            ', tunable=TunableLocalizedString(), export_modes=(ExportModes.ClientBinary,), tuning_group=GroupNames.UI), 'mood_id': TunableReference(description='\n            When this mood is set and active sim matches mood, the UI will\n            display a special effect on the skill bar to represent that this\n            skill is getting a bonus because of the mood.\n            ', manager=services.mood_manager(), allow_none=True, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'stat_asm_param': TunableStatAsmParam.TunableFactory(tuning_group=GroupNames.ANIMATION), 'hidden': Tunable(description='\n            If checked, this skill will be hidden.\n            ', tunable_type=bool, default=False, export_modes=ExportModes.All, tuning_group=GroupNames.AVAILABILITY), 'update_client_for_npcs': Tunable(description="\n            Whether this skill will send update messages to the client\n            for non-active household sims (NPCs).\n            \n            e.g. A toddler's communication skill determines the VOX they use, so\n            the client needs to know the skill level for all toddlers in order\n            for this work properly.\n            ", tunable_type=bool, default=False, tuning_group=GroupNames.UI), 'is_default': Tunable(description='\n            Whether Sim will default has this skill.\n            ', tunable_type=bool, default=False, tuning_group=GroupNames.AVAILABILITY), 'ages': TunableSet(description='\n            Allowed ages for this skill.\n            ', tunable=TunableEnumEntry(tunable_type=Age, default=Age.ADULT, export_modes=ExportModes.All), tuning_group=GroupNames.AVAILABILITY), 'ad_data': TunableList(description='\n            A list of Vector2 points that define the desire curve for this\n            commodity.\n            ', tunable=TunableVector2(description='\n                Point on a Curve\n                ', default=sims4.math.Vector2(0, 0)), tuning_group=GroupNames.AUTONOMY), 'weight': Tunable(description="\n            The weight of the Skill with regards to autonomy.  It's ignored for\n            the purposes of sorting stats, but it's applied when scoring the\n            actual statistic operation for the SI.\n            ", tunable_type=float, default=0.5, tuning_group=GroupNames.AUTONOMY), 'statistic_multipliers': TunableMapping(description='\n            Multipliers this skill applies to other statistics based on its\n            value.\n            ', key_type=TunableReference(description='\n                The statistic this multiplier will be applied to.\n                ', manager=services.statistic_manager(), reload_dependent=True), value_type=TunableTuple(curve=TunableCurve(description='\n                    Tunable curve where the X-axis defines the skill level, and\n                    the Y-axis defines the associated multiplier.\n                    ', x_axis_name='Skill Level', y_axis_name='Multiplier'), direction=TunableEnumEntry(description="\n                    Direction where the multiplier should work on the\n                    statistic.  For example, a tuned decrease for an object's\n                    brokenness rate will not also increase the time it takes to\n                    repair it.\n                    ", tunable_type=StatisticChangeDirection, default=StatisticChangeDirection.INCREASE), use_effective_skill=Tunable(description='\n                    If checked, this modifier will look at the current\n                    effective skill value.  If unchecked, this modifier will\n                    look at the actual skill value.\n                    ', tunable_type=bool, needs_tuning=True, default=True)), tuning_group=GroupNames.MULTIPLIERS), 'success_chance_multipliers': TunableList(description='\n            Multipliers this skill applies to the success chance of\n            affordances.\n            ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'monetary_payout_multipliers': TunableList(description='\n            Multipliers this skill applies to the monetary payout amount of\n            affordances.\n            ', tunable=TunableSkillMultiplier(), tuning_group=GroupNames.MULTIPLIERS), 'tags': TunableList(description='\n            The associated categories of the skill\n            ', tunable=TunableEnumEntry(tunable_type=tag.Tag, default=tag.Tag.INVALID, pack_safe=True), tuning_group=GroupNames.CORE), 'skill_level_type': TunableEnumEntry(description='\n            Skill level list to use.\n            ', tunable_type=SkillLevelType, default=SkillLevelType.MAJOR, export_modes=ExportModes.All, tuning_group=GroupNames.CORE), 'level_data': TunableMapping(description='\n            Level-specific information, such as notifications to be displayed to\n            level up.\n            ', key_type=int, value_type=TunableTuple(level_up_notification=UiDialogNotification.TunableFactory(description='\n                    The notification to display when the Sim obtains this level.\n                    The text will be provided two tokens: the Sim owning the\n                    skill and a number representing the 1-based skill level\n                    ', locked_args={'text_tokens': DEFAULT, 'icon': None, 'primary_icon_response': UiDialogResponse(text=None, ui_request=UiDialogResponse.UiDialogUiRequest.SHOW_SKILL_PANEL), 'secondary_icon': None}), level_up_screen_slam=OptionalTunable(description='\n                    Screen slam to show when reaches this skill level.\n                    Localization Tokens: Sim - {0.SimFirstName}, Skill Name - \n                    {1.String}, Skill Number - {2.Number}\n                    ', tunable=ui.screen_slam.TunableScreenSlamSnippet()), skill_level_buff=OptionalTunable(tunable=TunableReference(description='\n                        The buff to place on a Sim when they reach this specific\n                        level of skill.\n                        ', manager=services.buff_manager())), rewards=TunableList(description='\n                    A reward to give for achieving this level.\n                    ', tunable=rewards.reward_tuning.TunableSpecificReward(pack_safe=True)), loot=TunableList(description='\n                    A loot to apply for achieving this level.\n                    ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.ACTION), class_restrictions=('LootActions',))), super_affordances=TunableSet(description='\n                    Super affordances this adds to the Sim.\n                    ', tunable=TunableReference(description='\n                        A super affordance added to this Sim.\n                        ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), class_restrictions=('SuperInteraction',), pack_safe=True)), target_super_affordances=TunableProvidedAffordances(description='\n                    Super affordances this adds to the target.\n                    ', locked_args={'target': ParticipantType.Object, 'carry_target': ParticipantType.Invalid, 'is_linked': False, 'unlink_if_running': False}), actor_mixers=TunableMapping(description='\n                    Mixers this adds to an associated actor object. (When targeting\n                    something else.)\n                    ', key_type=TunableReference(description='\n                        The super affordance these mixers are associated with.\n                        ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), class_restrictions=('SuperInteraction',), pack_safe=True), value_type=TunableSet(description='\n                        Set of mixer affordances associated with the super affordance.\n                        ', tunable=TunableReference(description='\n                            Linked mixer affordance.\n                            ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), category='asm', class_restrictions=('MixerInteraction',), pack_safe=True)))), tuning_group=GroupNames.CORE), 'age_up_skill_transition_data': OptionalTunable(description='\n            Data used to modify the value of a new skill based on the level\n            of this skill.\n            \n            e.g. Toddler Communication skill transfers into Child Social skill.\n            ', tunable=TunableTuple(new_skill=TunablePackSafeReference(description='\n                    The new skill.\n                    ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)), skill_data=TunableMapping(description="\n                    A mapping between this skill's levels and the\n                    new skill's internal value.\n                    \n                    The keys are user facing skill levels.\n                    \n                    The values are the internal statistic value, not the user\n                    facing skill level.\n                    ", key_type=Tunable(description="\n                        This skill's level.\n                        \n                        This is the actual user facing skill level.\n                        ", tunable_type=int, default=0), value_type=Tunable(description='\n                        The new skill\'s value.\n                        \n                        This is the internal statistic\n                        value, not the user facing skill level."\n                        ', tunable_type=int, default=0))), tuning_group=GroupNames.SPECIAL_CASES), 'skill_unlocks_on_max': TunableList(description='\n            A list of skills that become unlocked when this skill is maxed.\n            ', tunable=TunableReference(description='\n                A skill to unlock.\n                ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=('Skill',), pack_safe=True), tuning_group=GroupNames.SPECIAL_CASES), 'trend_tag': OptionalTunable(description='\n            If enabled, we associate this skill with a particular trend via tag\n            which you can find in trend_tuning.\n            ', tunable=TunableTag(description='\n                The trend tag we associate with this skill\n                ', filter_prefixes=('func_trend',)))}
    REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning', 'decay_rate', '_default_convergence_value')

    def __init__(self, tracker):
        self._skill_level_buff = None
        super().__init__(tracker, self.initial_value)
        self._delta_enabled = True
        self._max_level_update_sent = False

    @classmethod
    def _tuning_loaded_callback(cls):
        super()._tuning_loaded_callback()
        level_list = cls.get_level_list()
        cls.max_level = len(level_list)
        cls.min_value_tuning = 0
        cls.max_value_tuning = sum(level_list)
        cls._default_convergence_value = cls.min_value_tuning
        cls._build_utility_curve_from_tuning_data(cls.ad_data)
        for stat in cls.statistic_multipliers:
            multiplier = cls.statistic_multipliers[stat]
            curve = multiplier.curve
            direction = multiplier.direction
            use_effective_skill = multiplier.use_effective_skill
            stat.add_skill_based_statistic_multiplier(cls, curve, direction, use_effective_skill)
        for multiplier in cls.success_chance_multipliers:
            curve = multiplier.curve
            use_effective_skill = multiplier.use_effective_skill
            for affordance in multiplier.affordance_list:
                affordance.add_skill_multiplier(affordance.success_chance_multipliers, cls, curve, use_effective_skill)
        for multiplier in cls.monetary_payout_multipliers:
            curve = multiplier.curve
            use_effective_skill = multiplier.use_effective_skill
            for affordance in multiplier.affordance_list:
                affordance.add_skill_multiplier(affordance.monetary_payout_multipliers, cls, curve, use_effective_skill)

    @classmethod
    def _verify_tuning_callback(cls):
        success_multiplier_affordances = []
        for multiplier in cls.success_chance_multipliers:
            success_multiplier_affordances.extend(multiplier.affordance_list)
        if len(success_multiplier_affordances) != len(set(success_multiplier_affordances)):
            logger.error("The same affordance has been tuned more than once under {}'s success multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle')
        monetary_payout_multiplier_affordances = []
        for multiplier in cls.monetary_payout_multipliers:
            monetary_payout_multiplier_affordances.extend(multiplier.affordance_list)
        if len(monetary_payout_multiplier_affordances) != len(set(monetary_payout_multiplier_affordances)):
            logger.error("The same affordance has been tuned more than once under {}'s monetary payout multipliers, and they will overwrite each other. Please fix in tuning.", cls, owner='tastle')

    @classproperty
    def skill_type(cls):
        return cls

    @constproperty
    def is_skill():
        return True

    @classproperty
    def autonomy_weight(cls):
        return cls.weight

    @constproperty
    def remove_on_convergence():
        return False

    @classproperty
    def valid_for_stat_testing(cls):
        return True

    @classmethod
    def can_add(cls, owner, force_add=False, **kwargs):
        if force_add:
            return True
        if owner.age not in cls.ages:
            return False
        return super().can_add(owner, **kwargs)

    @classmethod
    def convert_to_user_value(cls, value):
        level_list = cls.get_level_list()
        if not level_list:
            return 0
        current_value = value
        for (level, level_threshold) in enumerate(level_list):
            current_value -= level_threshold
            if current_value < 0:
                return level
        return level + 1

    @classmethod
    def convert_from_user_value(cls, user_value):
        (level_min, _) = cls._get_level_bounds(user_value)
        return level_min

    @classmethod
    def create_skill_update_msg(cls, sim_id, stat_value):
        skill_msg = Commodities_pb2.Skill_Update()
        skill_msg.skill_id = cls.guid64
        skill_msg.curr_points = int(stat_value)
        skill_msg.sim_id = sim_id
        return skill_msg

    @classmethod
    def get_level_list(cls):
        return cls.SKILL_LEVEL_LIST.get(cls.skill_level_type)

    @classmethod
    def get_skill_effectiveness_points_gain(cls, effectiveness_level, level):
        skill_gain_curve = cls.SKILL_EFFECTIVENESS_GAIN.get(effectiveness_level)
        if skill_gain_curve is not None:
            return skill_gain_curve.get(level)
        logger.error('{} does not exist in SKILL_EFFECTIVENESS_GAIN mapping', effectiveness_level)
        return 0

    def _get_level_data_for_skill_level(self, skill_level):
        level_data = self.level_data.get(skill_level)
        if level_data is None:
            logger.debug('No level data found for skill [{}] at level [{}].', self, skill_level)
        return level_data

    @property
    def is_initial_value(self):
        return self.initial_value == self.get_value()

    def should_send_update(self, sim_info, stat_value):
        if sim_info.is_npc and not self.update_client_for_npcs:
            return False
        if self.hidden:
            return False
        if Skill.convert_to_user_value(stat_value) == 0:
            return False
        if self.reached_max_level:
            if self._max_level_update_sent:
                return False
            self._max_level_update_sent = True
        return True

    def on_initial_startup(self):
        super().on_initial_startup()
        skill_level = self.get_user_value()
        self._update_skill_level_buff(skill_level)

    def on_add(self):
        super().on_add()
        self._tracker.owner.add_modifiers_for_skill(self)
        level_data = self._get_level_data_for_skill_level(self.get_user_value())
        if level_data is not None:
            provided_affordances = []
            for provided_affordance in level_data.target_super_affordances:
                provided_affordance_data = ProvidedAffordanceData(provided_affordance.affordance, provided_affordance.object_filter, provided_affordance.allow_self)
                provided_affordances.append(provided_affordance_data)
            self._tracker.add_to_affordance_caches(level_data.super_affordances, provided_affordances)
            self._tracker.add_to_actor_mixer_cache(level_data.actor_mixers)
            sim = self._tracker._owner.get_sim_instance()
            apply_super_affordance_commodity_flags(sim, self, level_data.super_affordances)

    def on_remove(self, on_destroy=False):
        super().on_remove(on_destroy=on_destroy)
        self._destory_callback_handle()
        if not on_destroy:
            self._send_skill_delete_message()
        if self._skill_level_buff is not None:
            self._tracker.owner.remove_buff(self._skill_level_buff)
            self._skill_level_buff = None
        if not on_destroy:
            self._tracker.update_affordance_caches()
        sim = self._tracker._owner.get_sim_instance()
        remove_super_affordance_commodity_flags(sim, self)

    def on_zone_load(self):
        self._max_level_update_sent = False

    def _apply_multipliers_to_continuous_statistics(self):
        for stat in self.statistic_multipliers:
            if stat.continuous:
                owner_stat = self.tracker.get_statistic(stat)
                if owner_stat is not None:
                    owner_stat._recalculate_modified_decay_rate()

    @classproperty
    def default_value(cls):
        return cls.initial_value

    @flexmethod
    @caches.cached
    def get_user_value(cls, inst):
        inst_or_cls = inst if inst is not None else cls
        return super(__class__, inst_or_cls).get_user_value()

    def _clear_user_value_cache(self):
        self.get_user_value.func.cache.clear()

    def set_value(self, value, *args, from_load=False, interaction=None, **kwargs):
        old_value = self.get_value()
        super().set_value(value, *args, **kwargs)
        if not caches.skip_cache:
            self._clear_user_value_cache()
        if from_load:
            return
        event_manager = services.get_event_manager()
        sim_info = self._tracker._owner
        new_value = self.get_value()
        new_level = self.convert_to_user_value(value)
        if old_value == self.initial_value or old_value != new_value:
            event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,))
        old_level = self.convert_to_user_value(old_value)
        if old_level < new_level or old_value == self.initial_value:
            self._apply_multipliers_to_continuous_statistics()
            event_manager.process_event(test_events.TestEvent.SkillLevelChange, sim_info=sim_info, skill=self, new_level=new_level, custom_keys=(self.stat_type,))

    def add_value(self, add_amount, interaction=None, **kwargs):
        old_value = self.get_value()
        if old_value == self.initial_value:
            telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME
        else:
            telemhook = TELEMETRY_HOOK_SKILL_INTERACTION
        super().add_value(add_amount, interaction=interaction)
        if not caches.skip_cache:
            self._clear_user_value_cache()
        if interaction is not None:
            interaction_name = interaction.affordance.__name__
        else:
            interaction_name = TELEMETRY_INTERACTION_NOT_AVAILABLE
        self.on_skill_updated(telemhook, old_value, self.get_value(), interaction_name)

    def _update_value(self):
        old_value = self._value
        if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled:
            last_update = self._last_update
        time_delta = super()._update_value()
        if not caches.skip_cache:
            self._clear_user_value_cache()
        new_value = self._value
        if old_value < new_value:
            event_manager = services.get_event_manager()
            sim_info = self._tracker._owner if self._tracker is not None else None
            if old_value == self.initial_value:
                telemhook = TELEMETRY_HOOK_SKILL_INTERACTION_FIRST_TIME
                self.on_skill_updated(telemhook, old_value, new_value, TELEMETRY_INTERACTION_NOT_AVAILABLE)
            event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,))
            old_level = self.convert_to_user_value(old_value)
            new_level = self.convert_to_user_value(new_value)
            if gsi_handlers.sim_handlers_log.skill_change_archiver.enabled and self.tracker.owner.is_sim:
                gsi_handlers.sim_handlers_log.archive_skill_change(self.tracker.owner, self, time_delta, old_value, new_value, new_level, last_update)
            if old_level < new_level or old_value == self.initial_value:
                if self._tracker is not None:
                    self._tracker.notify_watchers(self.stat_type, self._value, self._value)
                event_manager.process_event(test_events.TestEvent.SkillLevelChange, sim_info=sim_info, skill=self, new_level=new_level, custom_keys=(self.stat_type,))

    def _on_statistic_modifier_changed(self, notify_watcher=True):
        super()._on_statistic_modifier_changed(notify_watcher=notify_watcher)
        if not self.reached_max_level:
            return
        event_manager = services.get_event_manager()
        sim_info = self._tracker._owner if self._tracker is not None else None
        event_manager.process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, skill=self, statistic=self.stat_type, custom_keys=(self.stat_type,))

    def on_skill_updated(self, telemhook, old_value, new_value, affordance_name):
        owner_sim_info = self._tracker._owner
        if owner_sim_info.is_selectable:
            with telemetry_helper.begin_hook(skill_telemetry_writer, telemhook, sim_info=owner_sim_info) as hook:
                hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64)
                hook.write_string(TELEMETRY_FIELD_SKILL_AFFORDANCE, affordance_name)
                hook.write_bool(TELEMETRY_FIELD_SKILL_AFFORDANCE_SUCCESS, True)
                hook.write_int(TELEMETRY_FIELD_SKILL_AFFORDANCE_VALUE_ADD, new_value - old_value)
        if old_value == self.initial_value:
            skill_level = self.convert_to_user_value(old_value)
            self._handle_skill_up(skill_level)

    def _send_skill_delete_message(self):
        if self.tracker.owner.is_npc:
            return
        skill_msg = Commodities_pb2.SkillDelete()
        skill_msg.skill_id = self.guid64
        op = GenericProtocolBufferOp(Operation.SIM_SKILL_DELETE, skill_msg)
        Distributor.instance().add_op(self.tracker.owner, op)

    @staticmethod
    def _callback_handler(stat_inst):
        new_level = stat_inst.get_user_value()
        old_level = new_level - 1
        stat_inst.on_skill_level_up(old_level, new_level)
        stat_inst.refresh_threshold_callback()

    def _handle_skill_up(self, skill_level):
        self._show_level_notification(skill_level)
        self._update_skill_level_buff(skill_level)
        self._try_give_skill_up_payout(skill_level)
        self._tracker.update_affordance_caches()
        sim = self._tracker._owner.get_sim_instance()
        remove_super_affordance_commodity_flags(sim, self)
        super_affordances = tuple(self._tracker.get_cached_super_affordances_gen())
        apply_super_affordance_commodity_flags(sim, self, super_affordances)

    def _recalculate_modified_decay_rate(self):
        pass

    def refresh_level_up_callback(self):
        self._destory_callback_handle()

        def _on_level_up_callback(stat_inst):
            new_level = stat_inst.get_user_value()
            old_level = new_level - 1
            stat_inst.on_skill_level_up(old_level, new_level)
            stat_inst.refresh_level_up_callback()

        self._callback_handle = self.create_and_add_callback_listener(Threshold(self._get_next_level_bound(), operator.ge), _on_level_up_callback)

    def on_skill_level_up(self, old_level, new_level):
        tracker = self.tracker
        sim_info = tracker._owner
        if self.reached_max_level:
            for skill in self.skill_unlocks_on_max:
                skill_instance = tracker.add_statistic(skill, force_add=True)
                skill_instance.set_value(skill.initial_value)
        with telemetry_helper.begin_hook(skill_telemetry_writer, TELEMETRY_HOOK_SKILL_LEVEL_UP, sim_info=sim_info) as hook:
            hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64)
            hook.write_int(TELEMETRY_FIELD_SKILL_LEVEL, new_level)
        self._handle_skill_up(new_level)
        services.get_event_manager().process_event(test_events.TestEvent.SkillValueChange, sim_info=sim_info, statistic=self.stat_type, custom_keys=(self.stat_type,))

    def _show_level_notification(self, skill_level, ignore_npc_check=False):
        sim_info = self._tracker._owner
        if not (ignore_npc_check or not sim_info.is_npc):
            if skill_level == 1:
                tutorial_service = services.get_tutorial_service()
                if tutorial_service is not None and tutorial_service.is_tutorial_running():
                    return
            level_data = self._get_level_data_for_skill_level(skill_level)
            if level_data is not None:
                tutorial_id = None
                if self.tutorial is not None:
                    if skill_level == 1:
                        tutorial_id = self.tutorial.guid64
                notification = level_data.level_up_notification(sim_info, resolver=SingleSimResolver(sim_info))
                notification.show_dialog(icon_override=IconInfoData(icon_resource=self.icon), secondary_icon_override=IconInfoData(obj_instance=sim_info), additional_tokens=(skill_level,), tutorial_id=tutorial_id)
                if level_data.level_up_screen_slam is not None:
                    level_data.level_up_screen_slam.send_screen_slam_message(sim_info, sim_info, self.stat_name, skill_level)

    def _update_skill_level_buff(self, skill_level):
        level_data = self._get_level_data_for_skill_level(skill_level)
        new_buff = level_data.skill_level_buff if level_data is not None else None
        if self._skill_level_buff is not None:
            self._tracker.owner.remove_buff(self._skill_level_buff)
            self._skill_level_buff = None
        if new_buff is not None:
            self._skill_level_buff = self._tracker.owner.add_buff(new_buff)

    def _try_give_skill_up_payout(self, skill_level):
        level_data = self._get_level_data_for_skill_level(skill_level)
        if level_data is None:
            return
        if level_data.rewards:
            for reward in level_data.rewards:
                reward().open_reward(self._tracker.owner, reward_destination=RewardDestination.SIM, reward_source=self)
        if level_data.loot:
            resolver = SingleSimResolver(self._tracker.owner)
            for loot in level_data.loot:
                loot.apply_to_resolver(resolver)

    def force_show_level_notification(self, skill_level):
        self._show_level_notification(skill_level, ignore_npc_check=True)

    @classmethod
    def send_commodity_update_message(cls, sim_info, old_value, new_value):
        stat_instance = sim_info.get_statistic(cls.stat_type, add=False)
        if stat_instance is None or not stat_instance.should_send_update(sim_info, new_value):
            return
        msg = cls.create_skill_update_msg(sim_info.id, new_value)
        add_object_message(sim_info, MSG_SIM_SKILL_UPDATE, msg, False)
        change_rate = stat_instance.get_change_rate()
        hide_progress_bar = False
        if sim_info.is_npc or sim_info.is_skill_bar_suppressed():
            hide_progress_bar = True
        op = distributor.ops.SkillProgressUpdate(cls.guid64, change_rate, new_value, hide_progress_bar)
        distributor.ops.record(sim_info, op)

    def save_statistic(self, commodities, skills, ranked_stats, tracker):
        current_value = self.get_saved_value()
        if current_value == self.initial_value:
            return
        message = protocols.Skill()
        message.name_hash = self.guid64
        message.value = current_value
        if self._time_of_last_value_change:
            message.time_of_last_value_change = self._time_of_last_value_change.absolute_ticks()
        skills.append(message)

    def unlocks_skills_on_max(self):
        return True

    def can_decay(self):
        return False

    def get_skill_provided_affordances(self):
        level_data = self._get_level_data_for_skill_level(self.get_user_value())
        if level_data is None:
            return ((), ())
        return (level_data.super_affordances, level_data.target_super_affordances)

    def get_skill_provided_actor_mixers(self):
        level_data = self._get_level_data_for_skill_level(self.get_user_value())
        if level_data is None:
            return
        return level_data.actor_mixers

    def get_actor_mixers(self, super_interaction):
        level_data = self._get_level_data_for_skill_level(self.get_user_value())
        if level_data is None:
            return []
        mixers = level_data.actor_mixers.get(super_interaction, tuple()) if level_data is not None else []
        return mixers

    @flexmethod
    def populate_localization_token(cls, inst, token):
        inst_or_cls = inst if inst is not None else cls
        token.type = LocalizedStringToken.STRING
        token.text_string = inst_or_cls.stat_name
Exemple #9
0
class FishingTuning:
    BAIT_TAG_DATA_MAP = TunableMapping(
        description=
        '\n        Mapping between fishing bait tag and fishing bait data.\n        ',
        key_type=TunableTag(
            description=
            '\n            The bait tag to which we want to map a bait data.\n            ',
            filter_prefixes=('func_bait', )),
        key_name='Bait Tag',
        value_type=TunableFishingBaitReference(
            description='\n            The bait data.\n            ',
            pack_safe=True),
        value_name='Bait Data')

    @staticmethod
    def get_fishing_bait_data(obj_def):
        bait_data = None
        for (tag, data) in FishingTuning.BAIT_TAG_DATA_MAP.items():
            if obj_def.has_build_buy_tag(tag):
                if not bait_data is None:
                    if bait_data.bait_priority < data.bait_priority:
                        bait_data = data
                bait_data = data
        return bait_data

    @staticmethod
    def get_fishing_bait_data_set(obj_def_ids):
        if obj_def_ids is None:
            return frozenset()
        definition_manager = services.definition_manager()
        bait_data_guids = set()
        for def_id in obj_def_ids:
            bait_def = definition_manager.get(def_id)
            if bait_def is None:
                continue
            bait_data = FishingTuning.get_fishing_bait_data(bait_def)
            if bait_data is None:
                logger.error(
                    'Object {} failed trying to get fishing bait data category. Make sure the object has bait category tag.',
                    bait_def)
            else:
                bait_data_guids.add(bait_data.guid64)
        return bait_data_guids

    @staticmethod
    def get_fishing_bait_description(obj):
        bait_data = FishingTuning.get_fishing_bait_data(obj.definition)
        if bait_data is not None:
            return bait_data.bait_description()

    @staticmethod
    def add_bait_notebook_entry(sim, created_fish, bait):
        if sim.sim_info.notebook_tracker is None:
            return
        sub_entries = None
        if bait:
            bait_data = FishingTuning.get_fishing_bait_data(bait.definition)
            if bait_data is not None:
                sub_entries = (SubEntryData(bait_data.guid64, True), )
        sim.sim_info.notebook_tracker.unlock_entry(
            NotebookCustomTypeTuning.BAIT_NOTEBOOK_ENTRY(
                created_fish.definition.id, sub_entries=sub_entries))
Exemple #10
0
class SetFavoriteLootOp(BaseTargetedLootOperation):
    FACTORY_TUNABLES = {'favorite_type': TunableVariant(description="\n            The type of favorite action to apply.\n            \n            Preferred Object: Sets the object as a sim's preferred object\n            to use for a specific func tag.\n            Favorite Stack: Sets the object's stack of the sim's favorites\n            in their inventory.\n            ", preferred_object=TunableTuple(description='\n                Data for setting this item as preferred.\n                ', tag=TunableTag(description='\n                    The tag that represents this type of favorite.\n                    ', filter_prefixes=('Func',))), locked_args={'favorite_stack': None}, default='preferred_object'), 'unset': Tunable(description='\n            If checked, this will unset the target as the favorite instead of setting\n            it.\n            ', tunable_type=bool, default=False)}

    def __init__(self, favorite_type, unset, **kwargs):
        super().__init__(**kwargs)
        self._favorite_type = favorite_type
        self._unset = unset

    def _apply_to_subject_and_target(self, subject, target, resolver):
        if subject is None or target is None:
            logger.error('Trying to run a SetFavorite loot without a valid Subject or Target')
            return
        if target.is_sim:
            logger.error("Trying to set a Sim {} as a Favorite of another Sim {}. This isn't possible.", target, subject)
            return
        favorites_tracker = subject.sim_info.favorites_tracker
        if favorites_tracker is None:
            logger.error('Trying to set a favorite for Sim {} but they have no favorites tracker.', subject)
            return
        if self._favorite_type is not None:
            if self._unset:
                favorites_tracker.unset_favorite(self._favorite_type.tag, target.id, target.definition.id)
            else:
                favorites_tracker.set_favorite(self._favorite_type.tag, target.id, target.definition.id)
            return
        if self._unset:
            favorites_tracker.unset_favorite_stack(target)
        else:
            favorites_tracker.set_favorite_stack(target)
        target.inventoryitem_component.get_inventory().push_inventory_item_stack_update_msg(target)
class InFootprintTest(HasTunableSingletonFactory, AutoFactoryInit,
                      event_testing.test_base.BaseTest):
    BY_PARTICIPANT = 0
    BY_TAG = 1
    test_events = ()
    FACTORY_TUNABLES = {
        'actor':
        TunableEnumEntry(
            description=
            '\n            The actor whose location will be used.\n            ',
            tunable_type=ParticipantTypeSingleSim,
            default=ParticipantTypeSingleSim.Actor),
        'footprint_target':
        TunableVariant(
            description=
            '\n            The object whose footprint to check against.\n            ',
            by_participant=TunableTuple(
                description=
                '\n                Get footprint from a participant.\n                ',
                locked_args={'target_type': BY_PARTICIPANT},
                participant=TunableEnumEntry(
                    description=
                    '\n                    The participant whose required slot count we consider.\n                    ',
                    tunable_type=ParticipantTypeSingle,
                    default=ParticipantTypeSingle.Object)),
            by_tag=TunableTuple(
                description=
                '\n                Get footprint from an object with this tag. If there are\n                multiple, the test passes as long as one passes.\n                ',
                tag=TunableTag(
                    description=
                    '\n                    Tag to find objects by.\n                    '
                ),
                locked_args={'target_type': BY_TAG}),
            default='by_participant'),
        'footprint_names':
        OptionalTunable(
            description=
            "\n            Specific footprints to check against. If left unspecified, we\n            check against the object's default footprints (i.e. the ones\n            enabled in Medator).\n            ",
            tunable=TunableSet(tunable=TunableStringHash32(
                description=
                '\n                    Name of footprint. Can be looked up in Medator. If in\n                    doubt, consult the modeler.\n                    '
            ),
                               minlength=1)),
        'invert':
        Tunable(
            description=
            '\n            If checked, test will pass if the actor is not in the footprint.\n            ',
            tunable_type=bool,
            default=False)
    }

    def get_expected_args(self):
        kwargs = {}
        kwargs['actors'] = self.actor
        if self.footprint_target.target_type == self.BY_PARTICIPANT:
            kwargs['footprint_target'] = self.footprint_target.participant
        return kwargs

    def _test_if_sim_in_target_footprint(self, sim, target):
        if self.footprint_names is None:
            polygon = target.footprint_polygon
        else:
            polygon = target.get_polygon_from_footprint_name_hashes(
                self.footprint_names)
        if polygon is not None and polygon.contains(sim.position):
            return True
        return False

    @cached_test
    def __call__(self, actors=(), footprint_target=None):
        actor = next(iter(actors), None)
        if actor is None:
            return TestResult(False, 'No actors', tooltip=self.tooltip)
        actor_sim = actor.get_sim_instance()
        if actor_sim is None:
            return TestResult(
                False,
                "Actor is not an instantiated Sim. Can't check position: {}",
                actor[0],
                tooltip=self.tooltip)
        if self.footprint_target.target_type == self.BY_PARTICIPANT:
            if footprint_target is None:
                return TestResult(False,
                                  'Missing participant.',
                                  tooltip=self.tooltip)
            targets = (footprint_target, )
        elif self.footprint_target.target_type == self.BY_TAG:
            targets = services.object_manager().get_objects_with_tag_gen(
                self.footprint_target.tag)
        else:
            return TestResult(False,
                              'Unknown target type: {}',
                              self.footprint_target.target_type,
                              tooltip=self.tooltip)
        if self.invert:
            if any(
                    self._test_if_sim_in_target_footprint(actor_sim, target)
                    for target in targets):
                return TestResult(False,
                                  'In footprint, inverted',
                                  tooltip=self.tooltip)
        elif not any(
                self._test_if_sim_in_target_footprint(actor_sim, target)
                for target in targets):
            return TestResult(False, 'Not in footprint', tooltip=self.tooltip)
        return TestResult.TRUE
class ActingStudioZoneDirector(CareerEventZoneDirector):
    INSTANCE_TUNABLES = {'stage_marks': TunableMapping(description='\n            A mapping of stage marker tags to the interactions that should be\n            added to them for this gig. These interactions will be applied to\n            the stage mark/object on zone load.\n            ', key_name='stage_mark_tag', key_type=TunableTag(description='\n                The tag for the stage mark object the tuned scene interactions\n                should be on.\n                ', filter_prefixes=('func',)), value_name='scene_interactions', value_type=TunableSet(description='\n                The set of interactions that will be added to the stage mark\n                object.\n                ', tunable=TunableReference(description='\n                    A Super Interaction that should be added to the stage mark\n                    object.\n                    ', manager=services.affordance_manager(), class_restrictions='SuperInteraction')), tuning_group=GroupNames.CAREER), 'performance_objects': TunableMapping(description='\n            A mapping of performance objects (i.e. lights, green screen, vfx\n            machine) and the state they should be put into when the performance\n            starts/stops.\n            ', key_name='performance_object_tag', key_type=TunableTag(description='\n                The tag for the performance object.\n                ', filter_prefixes=('func',)), value_name='performance_object_states', value_type=TunableTuple(description="\n                States that should be applied to the objects before, during, and\n                after the performance. If the object doesn't have the necessary\n                state then nothing will happen.\n                ", pre_performance_states=TunableSet(description='\n                    States to set on the object when the zone loads.\n                    ', tunable=TunableTuple(description='\n                        A state to set on an object as well as a perk that will\n                        skip setting the state.\n                        ', state_value=TunableReference(description='\n                            A state value to set on the object.\n                            ', manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue',)), skip_with_perk=OptionalTunable(description='\n                            If enabled, allows skipping this state change if the\n                            active Sim has a tuned perk.\n                            ', tunable=TunableReference(description="\n                                If the active Sim has this perk, this state won't be\n                                set on the tuned objects. For instance, if the Sim\n                                has the Established Name perk, they don't need to\n                                use the hair and makeup chair. This can prevent\n                                those objects from glowing in that case.\n                                ", manager=services.get_instance_manager(sims4.resources.Types.BUCKS_PERK))))), post_performance_states=TunableSet(description='\n                    States set on the object when the performance is over.\n                    ', tunable=TunableReference(description='\n                        A state value to set on the object.\n                        ', manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue',))), performance_states=TunableSet(description='\n                    States to set on the object when the performance starts.\n                    ', tunable=TunableReference(description='\n                        A state value to set on the object.\n                        ', manager=services.get_instance_manager(sims4.resources.Types.OBJECT_STATE), class_restrictions=('ObjectStateValue',)))), tuning_group=GroupNames.CAREER), 'start_performance_interaction': TunableReference(description='\n            A reference to the interaction that indicates the performance is\n            starting. This is what triggers all of the state changes in the\n            Performance Object tuning.\n            ', manager=services.affordance_manager(), class_restrictions='SuperInteraction', tuning_group=GroupNames.CAREER), 'lot_load_loot': TunableMapping(description='\n            A mapping of Object IDs and loots to apply to those objects when the\n            lot loads. This can be used for things like applying specific locks\n            to door.\n            ', key_name='object_tag', key_type=TunableTag(description='\n                All objects with this tag will have the tuned loot applied on\n                lot load..\n                ', filter_prefixes=('func',)), value_name='loot', value_type=TunableSet(description='\n                A set of loots to apply to all objects with the specified tag.\n                ', tunable=TunableVariant(description='\n                    A specific loot to apply.\n                    ', lock_door=LockDoor.TunableFactory())), tuning_group=GroupNames.CAREER), 'thats_a_wrap_audio': TunablePlayAudio(description='\n            The sound to play when the player has completed the performance and\n            the Post Performance Time To Wrap Callout time has passed.\n            '), 'post_performance_time_remaining': TunableTimeSpan(description="\n            This is how long the gig should last once the player completes the\n            final interaction. Regardless of how long the timer shows, once the\n            player finishes the final interaction, we'll set the gig to end in\n            this tuned amount of time.\n            \n            Note: This should be enough time to encompass both the Post\n            Performance Time To Wrap Callout and Post Performance time Between\n            Wrap And Lights time spans.\n            ", default_minutes=20, locked_args={'days': 0}), 'post_performance_time_to_wrap_callout': TunableTimeSpan(description='\n            How long, after the Player completes the entire gig, until the\n            "That\'s a wrap" sound should play.\n            ', default_minutes=5, locked_args={'days': 0, 'hours': 0}), 'post_performance_time_between_wrap_and_lights': TunableTimeSpan(description='\n            How long after the "that\'s a wrap" sound until the post-performance\n            state should be swapped on all the objects (lights, greenscreen,\n            etc.)\n            ', default_minutes=5, locked_args={'days': 0, 'hours': 0})}
    ACTING_STUDIO_EVENTS = (TestEvent.InteractionComplete, TestEvent.MainSituationGoalComplete)
    STATE_PRE_PERFORMANCE = 0
    STATE_PERFORMANCE = 1
    STATE_POST_PERFORMANCE = 2
    SAVE_DATA_STATE = 'acting_studio_state'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._reset_data()

    def _reset_data(self):
        self._stage_marks = set()
        self._performance_object_data = []
        self._post_performance_state_alarm = None
        self._post_performance_call_out_alarm = None
        self._current_state = self.STATE_PRE_PERFORMANCE

    def on_startup(self):
        super().on_startup()
        services.get_event_manager().register(self, self.ACTING_STUDIO_EVENTS)

    def on_cleanup_zone_objects(self):
        object_manager = services.object_manager()
        self._init_stage_marks(object_manager)
        self._init_performance_object_data(object_manager)
        self._apply_lot_load_loot(object_manager)

    def _apply_lot_load_loot(self, object_manager):
        active_sim_info = services.active_sim_info()
        for (tag, loots) in self.lot_load_loot.items():
            objects = object_manager.get_objects_matching_tags((tag,))
            for obj in objects:
                resolver = SingleActorAndObjectResolver(active_sim_info, obj, source=self)
                for loot in loots:
                    loot.apply_to_resolver(resolver)

    def on_shutdown(self):
        super().on_shutdown()
        services.get_event_manager().unregister(self, self.ACTING_STUDIO_EVENTS)
        self._reset_data()

    def on_career_event_stop(self):
        services.get_event_manager().unregister(self, self.ACTING_STUDIO_EVENTS)

    def handle_event(self, sim_info, event, resolver):
        career = services.get_career_service().get_career_in_career_event()
        if career.sim_info is not sim_info:
            return
        if event == TestEvent.InteractionComplete and isinstance(resolver.interaction, self.start_performance_interaction) and not resolver.interaction.has_been_reset:
            self._start_performance()
        elif event == TestEvent.MainSituationGoalComplete:
            self._end_performance(career)

    def _save_custom_zone_director(self, zone_director_proto, writer):
        writer.write_uint32(self.SAVE_DATA_STATE, self._current_state)
        super()._save_custom_zone_director(zone_director_proto, writer)

    def _load_custom_zone_director(self, zone_director_proto, reader):
        if reader is not None:
            self._current_state = reader.read_uint32(self.SAVE_DATA_STATE, self.STATE_PRE_PERFORMANCE)
        super()._load_custom_zone_director(zone_director_proto, reader)

    def _start_performance(self):
        for performance_object_data in self._performance_object_data:
            performance_object_data.set_performance_states()
        self._current_state = self.STATE_PERFORMANCE

    def _end_performance(self, career):
        new_end_time = services.time_service().sim_now + self.post_performance_time_remaining()
        career.set_career_end_time(new_end_time, reset_warning_alarm=False)
        self._post_performance_state_alarm = alarms.add_alarm(self, self.post_performance_time_to_wrap_callout(), self._post_performance_wrap_callout)
        self._current_state = self.STATE_POST_PERFORMANCE

    def _post_performance_wrap_callout(self, _):
        play_tunable_audio(self.thats_a_wrap_audio)
        self._post_performance_state_alarm = alarms.add_alarm(self, self.post_performance_time_between_wrap_and_lights(), self._post_performance_state_change)

    def _post_performance_state_change(self, _):
        for performance_object_data in self._performance_object_data:
            performance_object_data.set_post_performance_states()
        self._post_performance_state_alarm = None

    def _init_stage_marks(self, object_manager):
        for (tag, interactions) in self.stage_marks.items():
            marks = object_manager.get_objects_matching_tags((tag,))
            if not marks:
                continue
            self._stage_marks.update(marks)
            for obj in marks:
                obj.add_dynamic_component(types.STAGE_MARK_COMPONENT, performance_interactions=interactions)

    def _init_performance_object_data(self, object_manager):
        for (tag, states) in self.performance_objects.items():
            performance_objects = object_manager.get_objects_matching_tags((tag,))
            if not performance_objects:
                continue
            performance_object_data = PerformanceObjectData(performance_objects, states.pre_performance_states, states.performance_states, states.post_performance_states)
            self._performance_object_data.append(performance_object_data)
            if self._current_state == self.STATE_PRE_PERFORMANCE:
                performance_object_data.set_pre_performance_states()
Exemple #13
0
class DoCommand(XevtTriggeredElement, HasTunableFactory):
    ARG_TYPE_PARTICIPANT = 0
    ARG_TYPE_LITERAL = 1
    ARG_TYPE_TAG = 3

    @staticmethod
    def _verify_tunable_callback(source, *_, command, **__):
        command_name = command.split(' ', 1)[0]
        command_restrictions = get_command_restrictions(command_name)
        command_type = get_command_type(command_name)
        if command_restrictions is None or command_type is None:
            logger.error('Command {} specified in {} does not exist.',
                         command_name, source)
        else:
            if command_restrictions & CommandRestrictionFlags.RESTRICT_SAVE_UNLOCKED and source.allow_while_save_locked:
                logger.error(
                    'Command {} specified in {} is unavailable during save lock. The interaction should not be available during save lock either.',
                    command_name, source)
            if command_type != CommandType.Live and not source.debug and not source.cheat:
                logger.error(
                    'Command {} is {} command tuned on non-debug interaction {}. The command type should be CommandType.Live.',
                    command_name, command_type, source)
            if command_type < CommandType.Cheat and source.cheat:
                logger.error(
                    'Command {} is {} command tuned on cheat interaction {}. The command type should be CommandType.Cheat or above.',
                    command_name, command_type, source)

    FACTORY_TUNABLES = {
        'command':
        Tunable(description='\n            The command to run.\n            ',
                tunable_type=str,
                default=None),
        'arguments':
        TunableList(
            description=
            "\n            The arguments for this command. Arguments will be added after the\n            command in the order they're listed here.\n            ",
            tunable=TunableVariant(
                description=
                '\n                The argument to use. In most cases, the ID of the participant\n                will be used.\n                ',
                participant=TunableTuple(
                    description=
                    '\n                    An argument that is a participant in the interaction. The\n                    ID will be used as the argument for the command.\n                    ',
                    argument=TunableEnumEntry(
                        description=
                        '\n                        The participant argument. The ID will be used in the\n                        command.\n                        ',
                        tunable_type=ParticipantType,
                        default=ParticipantTypeSingle.Object),
                    locked_args={'arg_type': ARG_TYPE_PARTICIPANT}),
                string=TunableTuple(
                    description=
                    "\n                    An argument that's a string.\n                    ",
                    argument=Tunable(
                        description=
                        '\n                        The string argument.\n                        ',
                        tunable_type=str,
                        default=None),
                    locked_args={'arg_type': ARG_TYPE_LITERAL}),
                number=TunableTuple(
                    description=
                    '\n                    An argument that is a number. This can be a float or an int.\n                    ',
                    argument=Tunable(
                        description=
                        '\n                        The number argument.\n                        ',
                        tunable_type=float,
                        default=0),
                    locked_args={'arg_type': ARG_TYPE_LITERAL}),
                tag=TunableTuple(
                    description=
                    '\n                    An argument that is a tag.\n                    ',
                    argument=TunableTag(
                        description=
                        '\n                        The tag argument.\n                        '
                    ),
                    locked_args={'arg_type': ARG_TYPE_TAG}),
                boolean=TunableTuple(
                    description=
                    '\n                    An argument that is a boolean.\n                    ',
                    argument=Tunable(
                        description=
                        '\n                        The number argument.\n                        ',
                        tunable_type=bool,
                        default=True),
                    locked_args={'arg_type': ARG_TYPE_LITERAL}))),
        'verify_tunable_callback':
        _verify_tunable_callback
    }

    def _do_behavior(self):
        full_command = self.command
        for arg in self.arguments:
            if arg.arg_type == self.ARG_TYPE_PARTICIPANT:
                for participant in self.interaction.get_participants(
                        arg.argument):
                    if hasattr(participant, 'id'):
                        full_command += ' {}'.format(participant.id)
                    else:
                        full_command += ' {}'.format(participant)
                else:
                    if arg.arg_type == self.ARG_TYPE_LITERAL:
                        full_command += ' {}'.format(arg.argument)
                    elif arg.arg_type == self.ARG_TYPE_TAG:
                        full_command += ' {}'.format(int(arg.argument))
                    else:
                        logger.error(
                            'Trying to run the Do Command element with an invalid arg type, {}.',
                            arg.arg_type,
                            owner='trevor')
                        return False
            elif arg.arg_type == self.ARG_TYPE_LITERAL:
                full_command += ' {}'.format(arg.argument)
            elif arg.arg_type == self.ARG_TYPE_TAG:
                full_command += ' {}'.format(int(arg.argument))
            else:
                logger.error(
                    'Trying to run the Do Command element with an invalid arg type, {}.',
                    arg.arg_type,
                    owner='trevor')
                return False
        client_id = services.client_manager().get_first_client_id()
        commands.execute(full_command, client_id)
        return True
Exemple #14
0
    class BaseAppearanceModification(HasTunableSingletonFactory,
                                     AutoFactoryInit):
        FACTORY_TUNABLES = {
            '_is_combinable_with_same_type':
            Tunable(
                description=
                '\n                True if this modifier type is able to be combined with another\n                of its type. If True, and two modifiers conflict, then the tuned\n                priority will be used to resolve the conflict. If False, only\n                a single modifier of this type with the highest priority will be shown.\n                ',
                tunable_type=bool,
                default=True),
            'outfit_type_compatibility':
            OptionalTunable(
                description=
                '\n                If enabled, will verify when switching outfits if the new\n                outfit is compatible with this appearance modifier.\n                ',
                disabled_name="Don't_Test",
                tunable=TunableWhiteBlackList(
                    description=
                    '\n                    The outfit category must match the whitelist and blacklist\n                    to be applied.\n                    ',
                    tunable=TunableEnumEntry(
                        description=
                        '\n                        The outfit category want to test against the \n                        apperance modifier.\n                        ',
                        tunable_type=OutfitCategory,
                        default=OutfitCategory.EVERYDAY))),
            'appearance_modifier_tag':
            OptionalTunable(
                description=
                '\n                If enabled, a tag used to reference this appearance modifier.\n                ',
                tunable=TunableTag(
                    description=
                    '\n                    Tag associated with this appearance modifier.\n                    '
                ))
        }

        def modify_sim_info(self, source_sim_info, modified_sim_info,
                            random_seed):
            raise NotImplementedError(
                'Attempting to use the BaseAppearanceModification base class, use sub-classes instead.'
            )

        @property
        def is_permanent_modification(self):
            return False

        @property
        def modifier_type(self):
            raise NotImplementedError(
                'Attempting to use the BaseAppearanceModification base class, use sub-classes instead.'
            )

        @property
        def is_combinable_with_same_type(self):
            return self._is_combinable_with_same_type

        @property
        def combinable_sorting_key(self):
            raise NotImplementedError(
                'Attempting to use the BaseAppearanceModification base class, use sub-classes instead.'
            )

        def is_compatible_with_outfit(self, outfit_category):
            if self.outfit_type_compatibility is None:
                return True
            return self.outfit_type_compatibility.test_item(outfit_category)
class SituationSchedulerComponent(Component,
                                  HasTunableFactory,
                                  AutoFactoryInit,
                                  allow_dynamic=True,
                                  component_name=SITUATION_SCHEDULER_COMPONENT,
                                  persistence_key=protocols.PersistenceMaster.
                                  PersistableData.SituationSchedulerComponent):
    FACTORY_TUNABLES = {
        'object_based_situations_schedule':
        OptionalTunable(
            description=
            '\n            If enabled, the object provides its own situation schedule.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Data associated with situations schedule.\n                ',
                tag=TunableTag(
                    description=
                    '\n                    An object tag. If the object exist on the zone lot, situations\n                    will be scheduled. The basic assumption is that this tag matches\n                    one of the tags associated with this object, but this is not \n                    enforced.\n                    ',
                    filter_prefixes=('func', )),
                situation_schedule=SituationWeeklyScheduleVariant(
                    description=
                    '\n                    The schedule to trigger the different situations.\n                    ',
                    pack_safe=True,
                    affected_object_cap=True),
                schedule_immediate=Tunable(
                    description=
                    '\n                    This controls the behavior of scheduler if the current time\n                    happens to fall within a schedule entry. If this is True, \n                    a start_callback will trigger immediately for that entry.\n                    If False, the next start_callback will occur on the next entry.\n                    ',
                    tunable_type=bool,
                    default=False),
                consider_off_lot_objects=Tunable(
                    description=
                    '\n                    If True, consider all objects in lot and the open street for\n                    this object situation. If False, only consider objects on\n                    the active lot.\n                    ',
                    tunable_type=bool,
                    default=True),
                tests=TunableTestSet(
                    description=
                    "\n                    Tests to determine if this Tag should be added to the active\n                    Zone Director's Situations Schedule.  Test is performed\n                    when the schedule is rebuilt.  This is currently on Zone\n                    Spin Up and Build Buy Exit.\n                    "
                )))
    }

    def __init__(self, *args, scheduler=None, **kwargs):
        if 'object_based_situations_schedule' not in kwargs:
            kwargs['object_based_situations_schedule'] = None
        super().__init__(*args, **kwargs)
        self._situation_scheduler = scheduler
        self._generated_situation_ids = set()

    @componentmethod
    def set_situation_scheduler(self, scheduler):
        self._destroy_situation_scheduler()
        self._situation_scheduler = scheduler

    @componentmethod
    def create_situation(self, situation_type, **params):
        if not situation_type.situation_meets_starting_requirements():
            return
        situation_manager = services.get_zone_situation_manager()
        self._cleanup_generated_situations(situation_manager)
        running_situation = self._get_same_situation_running(
            situation_manager, situation_type)
        if running_situation is not None:
            situation_manager.destroy_situation_by_id(running_situation.id)
        guest_list = situation_type.get_predefined_guest_list(
        ) or SituationGuestList(invite_only=True)
        merged_params = dict(params,
                             guest_list=guest_list,
                             user_facing=False,
                             scoring_enabled=False,
                             spawn_sims_during_zone_spin_up=True,
                             creation_source=str(self),
                             default_target_id=self.owner.id)
        situation_id = situation_manager.create_situation(
            situation_type, **merged_params)
        if situation_id is None:
            return
        self._generated_situation_ids.add(situation_id)

    def on_remove(self, *_, **__):
        self.destroy_scheduler_and_situations()

    def destroy_scheduler_and_situations(self):
        self._destroy_situation_scheduler()
        self._destroy_generated_situations()

    def _cleanup_generated_situations(self, situation_manager):
        for situation_id in list(self._generated_situation_ids):
            running_situation = situation_manager.get(situation_id)
            if running_situation is None:
                self._generated_situation_ids.remove(situation_id)

    def _get_same_situation_running(self, situation_manager, situation_type):
        for situation_id in self._generated_situation_ids:
            running_situation = situation_manager.get(situation_id)
            if situation_type is type(running_situation):
                return running_situation

    def _destroy_situation_scheduler(self):
        if self._situation_scheduler is not None:
            self._situation_scheduler.destroy()
            self._situation_scheduler = None

    def _destroy_generated_situations(self):
        situation_manager = services.get_zone_situation_manager()
        for situation_id in self._generated_situation_ids:
            situation_manager.destroy_situation_by_id(situation_id)
        self._generated_situation_ids.clear()

    @property
    def can_remove_component(self):
        return not hasattr(self, 'object_based_situations_schedule') or (
            self.object_based_situations_schedule is None or
            (self.object_based_situations_schedule.tag == Tag.INVALID
             or not self.object_based_situations_schedule.situation_schedule))

    def save(self, persistence_master_message):
        persistable_data = protocols.PersistenceMaster.PersistableData()
        persistable_data.type = protocols.PersistenceMaster.PersistableData.SituationSchedulerComponent
        component_data = persistable_data.Extensions[
            protocols.PersistableSituationSchedulerComponent.persistable_data]
        if self._generated_situation_ids:
            component_data.situation_ids.extend(self._generated_situation_ids)
        persistence_master_message.data.extend([persistable_data])

    def load(self, persistable_data):
        component_data = persistable_data.Extensions[
            protocols.PersistableSituationSchedulerComponent.persistable_data]
        for situation_id in component_data.situation_ids:
            self._generated_situation_ids.add(situation_id)
Exemple #16
0
class OceanTuning:
    BEACH_LOCATOR_TAG = TunableTag(
        description=
        '\n        The tag we can use to get the beach locator definition.\n        '
    )
    OCEAN_DATA = TunableMapping(
        description=
        '\n        The species-age mapping to ocean data. This defines what\n        ages and species can wade in the water and what the water level\n        restrictions are as well as beach portal access objects.\n        ',
        key_name='species',
        key_type=TunableEnumEntry(
            description=
            '\n            The extended species that this data is for.\n            ',
            tunable_type=SpeciesExtended,
            default=SpeciesExtended.HUMAN),
        value_name='age_data',
        value_type=TunableList(
            description='\n            The ages and their data.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The ages and their ocean data.\n                ',
                ages=TunableEnumSet(
                    description=
                    '\n                    The age of the actor.\n                    ',
                    enum_type=Age),
                ocean_data=TunableTuple(
                    description=
                    '\n                    The ocean data for this Age.\n                    ',
                    wading_interval=TunableInterval(
                        description=
                        '\n                        The wading interval for Sims at this age and species. The lower\n                        bound indicates the minimum water height required to apply the\n                        wading walkstyle, and the upper bound indicates the maximum\n                        height we can walk into the water until we can potentially\n                        swim.\n                        ',
                        tunable_type=float,
                        default_lower=0.1,
                        default_upper=1.0,
                        minimum=0.01),
                    beach_portal_data=OptionalTunable(
                        description=
                        '\n                        An optional portal definition to allow sims to swim in\n                        the ocean. Without this, Sims at this age and species\n                        cannot swim in the ocean.\n                        ',
                        tunable=TunableReference(
                            description=
                            '\n                            The portals this age/species will use to swim in the ocean.\n                            ',
                            manager=services.snippet_manager(),
                            class_restrictions=('PortalData', ),
                            pack_safe=True)),
                    water_depth_error=TunableRange(
                        description=
                        '\n                        The error, in meters, that we allow for the swimming beach\n                        portals.\n                        ',
                        tunable_type=float,
                        default=0.05,
                        minimum=0.01),
                    swimwear_change_water_depth=TunableRange(
                        description=
                        "\n                        If a Sim's path includes water where the depth is at\n                        least the tuned value, in meters, they will switch into\n                        the outfit based on the outfit change reasonat the \n                        start of the path.\n                        ",
                        tunable_type=float,
                        default=0.1,
                        minimum=0),
                    swimwear_change_outfit_reason=OptionalTunable(
                        description=
                        '\n                        If enabled, the outfit change reason that determines which outfit\n                        category a Sim automatically changes into when \n                        entering water.\n                        ',
                        tunable=TunableEnumEntry(
                            tunable_type=OutfitChangeReason,
                            default=OutfitChangeReason.Invalid,
                            invalid_enums=(OutfitChangeReason.Invalid, )))))))
    beach_locator_definition = None

    @staticmethod
    def get_beach_locator_definition():
        if OceanTuning.beach_locator_definition is None:
            for definition in services.definition_manager(
            ).get_definitions_for_tags_gen((OceanTuning.BEACH_LOCATOR_TAG, )):
                OceanTuning.beach_locator_definition = definition
                break
        return OceanTuning.beach_locator_definition

    @staticmethod
    def get_actor_ocean_data(actor):
        if not actor.is_sim and not isinstance(actor, StubActor):
            return
        species_data = OceanTuning.OCEAN_DATA.get(actor.extended_species, None)
        if species_data is None:
            return
        actor_age = actor.age
        for age_data in species_data:
            if actor_age in age_data.ages:
                return age_data.ocean_data

    @staticmethod
    def get_actor_wading_interval(actor):
        ocean_data = OceanTuning.get_actor_ocean_data(actor)
        if ocean_data is not None:
            return ocean_data.wading_interval
        else:
            interval_actor = actor
            if not isinstance(actor, StubActor):
                if actor.vehicle_component is not None:
                    drivers = actor.get_users(sims_only=True)
                    for driver in drivers:
                        if driver.posture.is_vehicle:
                            if driver.posture.target is actor:
                                interval_actor = driver
                                break
            ocean_data = OceanTuning.get_actor_ocean_data(interval_actor)
            if ocean_data is not None:
                return ocean_data.wading_interval

    @staticmethod
    def get_actor_swimwear_change_info(actor):
        ocean_data = OceanTuning.get_actor_ocean_data(actor)
        if ocean_data is not None:
            return (ocean_data.swimwear_change_water_depth,
                    ocean_data.swimwear_change_outfit_reason)
        return (None, None)

    @staticmethod
    def make_depth_bounds_safe_for_surface_and_sim(routing_surface,
                                                   sim,
                                                   min_water_depth=None,
                                                   max_water_depth=None):
        interval = OceanTuning.get_actor_wading_interval(sim)
        return OceanTuning.make_depth_bounds_safe_for_surface(
            routing_surface,
            wading_interval=interval,
            min_water_depth=min_water_depth,
            max_water_depth=max_water_depth)

    @staticmethod
    def make_depth_bounds_safe_for_surface(routing_surface,
                                           wading_interval=None,
                                           min_water_depth=None,
                                           max_water_depth=None):
        if routing_surface.type == SurfaceType.SURFACETYPE_WORLD:
            surface_min_water_depth = min_water_depth
            if wading_interval is not None:
                if max_water_depth is None:
                    surface_max_water_depth = wading_interval.upper_bound
                else:
                    surface_max_water_depth = min(wading_interval.upper_bound,
                                                  max_water_depth)
                    surface_max_water_depth = 0
            else:
                surface_max_water_depth = 0
        elif routing_surface.type == SurfaceType.SURFACETYPE_POOL:
            if wading_interval is not None:
                if min_water_depth is None:
                    surface_min_water_depth = wading_interval.upper_bound
                else:
                    surface_min_water_depth = max(wading_interval.upper_bound,
                                                  min_water_depth)
            else:
                surface_min_water_depth = min_water_depth
            surface_max_water_depth = max_water_depth
        else:
            surface_min_water_depth = min_water_depth
            surface_max_water_depth = max_water_depth
        return (surface_min_water_depth, surface_max_water_depth)
Exemple #17
0
class TempleTuning:
    TEMPLES = TunableMapping(
        description=
        '\n        A Mapping of Temple Templates (house descriptions) and the data\n        associated with each temple.\n        ',
        key_name='Template House Description',
        key_type=TunableHouseDescription(pack_safe=True),
        value_name='Temple Data',
        value_type=TunableTuple(
            description=
            '\n            The data associated with the mapped temple template.\n            ',
            rooms=TunableMapping(
                description=
                '\n                A mapping of room number to the room data. Room number 0 will be\n                the entrance room to the temple, room 1 will be the first room\n                that needs to be unlocked, and so on.\n                ',
                key_name='Room Number',
                key_type=int,
                value_name='Room Data',
                value_type=TunableTempleRoomData(pack_safe=True)),
            enter_lot_loot=TunableSet(
                description=
                '\n                Loot applied to Sims when they enter or spawn in to this Temple.\n                \n                NOTE: Exit Lot Loot is not guaranteed to be given. For example,\n                if the Sim walks onto the lot, player switches to a different\n                zone, then summons that Sim, that Sim will bypass getting the\n                exit loot.\n                ',
                tunable=LootActions.TunableReference(pack_safe=True)),
            exit_lot_loot=TunableSet(
                description=
                '\n                Loot applied to Sims when they exit or spawn out of this Temple.\n                \n                NOTE: This loot is not guaranteed to be given after Enter Lot\n                Loot. For example, if the Sim walks onto the lot, player\n                switches to a different zone, then summons that Sim, that Sim\n                will bypass getting the exit loot.\n                ',
                tunable=LootActions.TunableReference(pack_safe=True))))
    GATE_TAG = TunableTag(
        description=
        '\n        The tag used to find the gate objects inside Temples.\n        ',
        filter_prefixes=('func_temple', ))
    TRAP_TAG = TunableTag(
        description=
        '\n        The tag used to identify traps inside temples. This will be used to find\n        placeholder traps as well.\n        ',
        filter_prefixes=('func_temple', ))
    CHEST_TAG = TunableTag(
        description=
        "\n        The tag used to identify the final chest of a temple. If it's in the\n        open state, the temple will be considered solved.\n        ",
        filter_prefixes=('func_temple', ))
    CHEST_OPEN_STATE = TunablePackSafeReference(
        description=
        '\n        The state that indicates the chest is open.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.OBJECT_STATE),
        class_restrictions='ObjectStateValue')
    GATE_STATE = TunablePackSafeReference(
        description=
        '\n        The state for temple gates. Used for easy look up.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.OBJECT_STATE),
        class_restrictions='ObjectState')
    GATE_UNLOCK_STATE = TunablePackSafeReference(
        description='\n        The unlock state for temple gates.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.OBJECT_STATE),
        class_restrictions='ObjectStateValue')
    TEMPLE_LOT_DESCRIPTION = TunablePackSafeLotDescription(
        description=
        '\n        A reference to the lot description file for the temple lot. This is used\n        for easier zone ID lookups.\n        '
    )
    GATE_LOCK_LOOT = LockDoor.TunableFactory(
        description=
        '\n        The LockDoor loot to run on the gates in the temple on load when they\n        should be locked.\n        '
    )
    GATE_UNLOCK_LOOT = UnlockDoor.TunableFactory(
        description=
        '\n        The UnlockDoor loot to run on the gates when they should be unlocked.\n        '
    )
    CHEST_OPEN_INTEARCTION = TunableInteractionOfInterest(
        description=
        '\n        A reference to the open interaction for chests. This interaction will be\n        listened for to determine temple completion.\n        '
    )
class ObjectBasedSituationZoneDirectorMixin:
    INSTANCE_TUNABLES = {
        'object_based_situations_schedule':
        TunableMapping(
            description=
            '\n            Mapping of object tag to situations schedule. \n            When the object in the tag is exist on the zone lot, the situations\n            will be spawned based on the schedule.\n            ',
            key_type=TunableTag(
                description=
                '\n                An object tag. If the object exist on the zone lot, situations\n                will be scheduled.\n                ',
                filter_prefixes=('func', )),
            value_type=TunableTuple(
                description=
                '\n                Data associated with situations schedule.\n                ',
                situation_schedule=SituationWeeklyScheduleVariant(
                    description=
                    '\n                    The schedule to trigger the different situations.\n                    ',
                    pack_safe=True,
                    affected_object_cap=True),
                schedule_immediate=Tunable(
                    description=
                    '\n                    This controls the behavior of scheduler if the current time\n                    happens to fall within a schedule entry. If this is True, \n                    a start_callback will trigger immediately for that entry.\n                    If False, the next start_callback will occur on the next entry.\n                    ',
                    tunable_type=bool,
                    default=False),
                consider_off_lot_objects=Tunable(
                    description=
                    '\n                    If True, consider all objects in lot and the open street for\n                    this object situation. If False, only consider objects on\n                    the active lot.\n                    ',
                    tunable_type=bool,
                    default=True))),
        'use_object_provided_situations_schedule':
        Tunable(
            description=
            '\n            If checked, objects on the lot and supplement or replace elements of\n            Object Based Situations Schedule.\n            ',
            tunable_type=bool,
            default=True)
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.affected_objects_map = {}
        self._object_based_situations_schedule = {}

    def create_situations_during_zone_spin_up(self):
        super().create_situations_during_zone_spin_up()
        self._update_object_based_situations_schedule()
        self._cleanup_affected_objects()
        self._setup_affected_objects()

    def on_exit_buildbuy(self):
        super().on_exit_buildbuy()
        self._update_object_based_situations_schedule()
        self._setup_affected_objects()

    def _update_object_based_situations_schedule(self):
        self._object_based_situations_schedule.clear()
        for (object_tag,
             data) in self.object_based_situations_schedule.items():
            self._object_based_situations_schedule[object_tag] = data
        if not self.use_object_provided_situations_schedule:
            return
        object_manager = services.object_manager()
        for obj in object_manager.get_valid_objects_gen():
            situation_scheduler_component = obj.get_component(
                SITUATION_SCHEDULER_COMPONENT)
            if not situation_scheduler_component is None:
                if situation_scheduler_component.can_remove_component:
                    continue
                data = situation_scheduler_component.object_based_situations_schedule
                resolver = SingleObjectResolver(obj)
                if not data.tests is None:
                    if not data.tests.run_tests(resolver):
                        continue
                    self._object_based_situations_schedule[
                        data.tag] = ObjectBasedSituationData(
                            data.situation_schedule, data.schedule_immediate,
                            data.consider_off_lot_objects)

    @staticmethod
    def _cleanup_object(obj):
        situation_scheduler_component = obj.get_component(
            SITUATION_SCHEDULER_COMPONENT)
        if situation_scheduler_component is None:
            return
        if situation_scheduler_component.can_remove_component:
            obj.remove_component(SITUATION_SCHEDULER_COMPONENT)
        else:
            situation_scheduler_component.destroy_scheduler_and_situations()

    def _cleanup_affected_objects(self):
        object_tags = self._object_based_situations_schedule.keys()
        object_manager = services.object_manager()
        for obj in object_manager.get_valid_objects_gen():
            if not obj.has_any_tag(object_tags):
                self._cleanup_object(obj)

    def _setup_affected_objects(self):
        object_manager = services.object_manager()
        for (object_tag,
             data) in self._object_based_situations_schedule.items():
            object_cap = self._get_current_affected_object_cap(
                data.situation_schedule)
            tagged_objects = []
            if data.consider_off_lot_objects:
                tagged_objects = list(
                    object_manager.get_objects_with_tag_gen(object_tag))
            else:
                tagged_objects = [
                    obj for obj in object_manager.get_objects_with_tag_gen(
                        object_tag) if obj.is_on_active_lot()
                ]
            if not tagged_objects:
                continue
            if object_tag not in self.affected_objects_map:
                self.affected_objects_map[object_tag] = WeakSet()
            affected_objects = self.affected_objects_map[object_tag]
            while len(affected_objects) < object_cap:
                while tagged_objects:
                    obj_to_add = tagged_objects.pop()
                    if obj_to_add in affected_objects:
                        continue
                    scheduler = data.situation_schedule(
                        start_callback=self._start_situations,
                        schedule_immediate=data.schedule_immediate,
                        extra_data=obj_to_add)
                    if obj_to_add.has_component(SITUATION_SCHEDULER_COMPONENT):
                        obj_to_add.set_situation_scheduler(scheduler)
                    else:
                        obj_to_add.add_dynamic_component(
                            SITUATION_SCHEDULER_COMPONENT, scheduler=scheduler)
                    affected_objects.add(obj_to_add)
            while len(affected_objects) > object_cap:
                while affected_objects:
                    obj_to_remove = affected_objects.pop()
                    self._cleanup_object(obj_to_remove)

    def _start_situations(self, scheduler, alarm_data, obj):
        self._setup_affected_objects()
        if not scheduler.extra_data.has_component(
                SITUATION_SCHEDULER_COMPONENT):
            return
        if hasattr(alarm_data.entry, 'weighted_situations'):
            (situation, params
             ) = WeightedSituationsWeeklySchedule.get_situation_and_params(
                 alarm_data.entry)
        else:
            situation = alarm_data.entry.situation
            params = {}
        if situation is None:
            return
        obj.create_situation(situation, **params)

    def _get_current_affected_object_cap(self, schedule):
        current_time = services.time_service().sim_now
        (best_time, alarm_data) = schedule().time_until_next_scheduled_event(
            current_time, schedule_immediate=True)
        if best_time is None or best_time > TimeSpan.ZERO:
            current_affected_object_cap = 0
        else:
            current_affected_object_cap = alarm_data[
                0].entry.affected_object_cap
        return current_affected_object_cap
class JungleOpenStreetDirector(OpenStreetDirectorBase):
    DEFAULT_LOCK = LockAllWithSimIdExceptionData(lock_priority=LockPriority.PLAYER_LOCK, lock_sides=LockSide.LOCK_FRONT, should_persist=True, except_actor=False, except_household=False)
    PATH_LOCKED = 0
    PATH_UNAVAILABLE = 1
    PATH_UNLOCKED = 2
    MIN_CLEAR_COMMODITY = 0
    TEMPLE_STATE_NEEDS_RESET = 0
    TEMPLE_STATE_RESET = 1
    TEMPLE_STATE_IN_PROGRESS = 2
    TEMPLE_STATE_COMPLETE = 3
    TREASURE_CHEST_CLOSED = 0
    TREASURE_CHEST_OPEN = 1
    TEMPLE_PATH_OBSTACLE = TunableTag(description='\n        The tag for the path obstacle that leads to the Temple. This will be\n        used to gain a reference to it when the temple resets.\n        ', filter_prefixes=('Func',))
    TEMPLE_PATH_OBSTACLE_LOCK_STATE = TunableStateValueReference(description='\n        Indicates the temple is locked. This will be used to lock the\n        Path Obstacle.\n        ', pack_safe=True)
    TEMPLE_PATH_OBSTACLE_UNLOCK_STATE = TunableStateValueReference(description='\n        The unlock state for the path obstacles. Set when we load a brand new\n        vacation in the jungle.\n        ', pack_safe=True)
    TEMPLE_VENUE_TUNING = TunablePackSafeReference(description='\n        The venue for the temple zone.\n        ', manager=services.get_instance_manager(sims4.resources.Types.VENUE))
    TEMPLE_LOCK_COMMODITY = TunablePackSafeReference(description='\n        The commodity that controls the temple lock.\n        ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions='Commodity')
    INSTANCE_TUNABLES = {'path_obstacle_data': TunableMapping(description='\n            Tuned data for the path obstacles in the open street. \n            \n            This includes which conditional layer the path obstacle is attached\n            to and what state that layer is in when the obstacle is locked.\n            ', key_name='obstacle_tag_id', key_type=TunableTag(description='\n                A tag for a specific path obstacle object that we might want\n                to mark blocked or access_PermanentlyBlocked. \n                ', filter_prefixes=('Func',)), value_name='obstacle_data', value_type=TunableTuple(description='\n                All of the data associated with the path obstacle.\n                ', always_available=Tunable(description='\n                    If True then this particular path obstacle is always \n                    available to be cleared and traveled through.\n                    \n                    If False then this path obstacle is subject to randomly\n                    being available or unavailable depending on the travel\n                    group.\n                    ', tunable_type=bool, default=False), layers=TunableList(description='\n                    A list of conditional layers and the status the layer starts\n                    in (visible/hidden) that are associated with this path\n                    obstacle.\n                    ', tunable=TunableTuple(description='\n                        Data about which conditional layer the obstacle is associated\n                        with and what state it is in.\n                        ', conditional_layer=TunableReference(description='\n                            A reference to the Conditional Layer found in the open streets.\n                            ', manager=services.get_instance_manager(sims4.resources.Types.CONDITIONAL_LAYER)), visible=Tunable(description='\n                            Whether or not the conditional layer is show/hidden when\n                            the corresponding path obstacle is locked.\n                            \n                            Checked signifies that the layer is visible when the\n                            obstacle is locked.\n                            \n                            Unchecked signifies that the layer is hidden when the \n                            obstacle is locked.\n                            ', tunable_type=bool, default=True), immediate=Tunable(description='\n                            If checked then the layer will load immediately. If\n                            not checked then the layer will load over time.\n                            ', tunable_type=bool, default=False))))), 'num_of_paths_available': TunableRange(description='\n            The number of paths that are available when a vacation group \n            arrives in the jungle for the first time.\n            ', tunable_type=int, minimum=0, default=1), 'clear_path_interaction': TunableInteractionOfInterest(description='\n            A reference to the interaction that a Sim runs in order to clear\n            the path obstacle so they can use the portal.\n            '), 'permanently_blocked_state': TunableStateValueReference(description='\n            The state the blocked path obstacles should be set to if they \n            cannot be cleared.\n            '), 'path_unlocked_state': TunableStateValueReference(description='\n            The state the blocked path obstacles should be set to if they are \n            unlocked.\n            '), 'path_clearing_commodity': TunableReference(description='\n            The commodity that has to reach 100 in order for a path to be\n            considered clear.\n            ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)), 'treasure_chest_tag': TunableTag(description='\n            The tag used to identify a treasure chest.\n            '), 'treasure_chest_open_state': TunableStateValueReference(description='\n            The state that a treasure chest is in when it has already been \n            opened.\n            '), 'treasure_chest_closed_state': TunableStateValueReference(description='\n            The state that a treasure chest is in when it is still closed.\n            ')}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._obstacle_status = {}
        self._path_obstacles = {}
        self._treasure_chest_status = {}
        travel_group_manager = services.travel_group_manager()
        household = services.active_household()
        travel_group = travel_group_manager.get_travel_group_by_household(household)
        if travel_group is None:
            logger.error("Trying to initialize the Jungle Open Street Director but there doesn't appear to be a travel group for the current household.")
            self._current_travel_group_id = None
        else:
            self._current_travel_group_id = travel_group.id
        services.get_event_manager().register_single_event(self, TestEvent.InteractionComplete)
        self._current_temple_id = None
        self._temple_state = self.TEMPLE_STATE_NEEDS_RESET
        self._last_time_saved = None

    def on_startup(self):
        super().on_startup()
        object_manager = services.object_manager()
        self._path_obstacles = self._get_path_obstacles()
        if self._current_travel_group_id in self._obstacle_status:
            path_obstacle_data = self._obstacle_status[self._current_travel_group_id]
            for (tag, status, progress) in path_obstacle_data:
                obstacles = object_manager.get_objects_matching_tags((tag,))
                for obstacle in obstacles:
                    if tag == self.TEMPLE_PATH_OBSTACLE:
                        if not obstacle.state_value_active(self.TEMPLE_PATH_OBSTACLE_UNLOCK_STATE):
                            self._update_temple_lock_commodity()
                            if obstacle.state_value_active(self.TEMPLE_PATH_OBSTACLE_UNLOCK_STATE):
                                status = JungleOpenStreetDirector.PATH_LOCKED
                            else:
                                status = JungleOpenStreetDirector.PATH_UNAVAILABLE
                            progress = 0
                    if status == JungleOpenStreetDirector.PATH_LOCKED:
                        self._lock_path_obstacle(obstacle, tag)
                    elif status == JungleOpenStreetDirector.PATH_UNAVAILABLE:
                        self._permanently_lock_path_obstacle(obstacle, tag)
                    elif status == JungleOpenStreetDirector.PATH_UNLOCKED:
                        self._unlock_path_obstacle(obstacle, tag)
                    else:
                        logger.error('Trying to setup an object that has a tag status that is not known. {}', status)
                    obstacle.set_stat_value(self.path_clearing_commodity, progress)
            if self._current_travel_group_id in self._treasure_chest_status:
                treasure_chest_data = self._treasure_chest_status[self._current_travel_group_id]
                for (obj_id, status) in treasure_chest_data:
                    chest = object_manager.get(obj_id)
                    if chest is None:
                        continue
                    if status == JungleOpenStreetDirector.TREASURE_CHEST_OPEN:
                        chest.set_state(self.treasure_chest_open_state.state, self.treasure_chest_open_state)
                    else:
                        chest.set_state(self.treasure_chest_closed_state.state, self.treasure_chest_closed_state)
        else:
            always_available_paths = []
            possible_paths = []
            for (obj, tag) in self._path_obstacles.items():
                if self.path_obstacle_data[tag].always_available:
                    always_available_paths.append((obj, tag))
                else:
                    possible_paths.append((obj, tag))
            available_paths = random.sample(possible_paths, min(len(possible_paths), self.num_of_paths_available))
            unavailable_paths = [path for path in possible_paths if path not in available_paths]
            for (path_obstacle, tag) in itertools.chain(always_available_paths, available_paths):
                self._lock_path_obstacle(path_obstacle, tag, reset_commodity=True)
            for (path_obstacle, tag) in unavailable_paths:
                self._permanently_lock_path_obstacle(path_obstacle, tag)
            for chest in object_manager.get_objects_matching_tags((self.treasure_chest_tag,)):
                chest.set_state(self.treasure_chest_closed_state.state, self.treasure_chest_closed_state)
            travel_group_manager = services.travel_group_manager()
            travel_groups = travel_group_manager.get_travel_group_ids_in_region()
            for group_id in travel_groups:
                if group_id == self._current_travel_group_id:
                    continue
                group = travel_group_manager.get(group_id)
                if group.played:
                    break
            else:
                self._setup_for_first_travel_group()
        if self._temple_needs_reset():
            self.reset_temple()

    def _update_temple_lock_commodity(self):
        obstacle = self._get_temple_entrance_obstacle()
        lock_tracker = obstacle.get_tracker(JungleOpenStreetDirector.TEMPLE_LOCK_COMMODITY)
        lock_stat = lock_tracker.get_statistic(JungleOpenStreetDirector.TEMPLE_LOCK_COMMODITY)
        lock_stat.update_commodity_to_time(self._last_time_saved)
        lock_state = JungleOpenStreetDirector.TEMPLE_PATH_OBSTACLE_UNLOCK_STATE.state
        obstacle.state_component.set_state_from_stat(lock_state, lock_stat)

    def _temple_needs_reset(self):
        if self._temple_state == self.TEMPLE_STATE_NEEDS_RESET:
            return True
        elif self._temple_state == self.TEMPLE_STATE_COMPLETE:
            sim_info_manager = services.sim_info_manager()
            temple_zones = tuple(services.venue_service().get_zones_for_venue_type_gen(self.TEMPLE_VENUE_TUNING))
            if len(temple_zones) != 1:
                logger.error('Found either 0 or more than 1 zone that is set as a temple venue. There can be only one!')
            temple_zone_id = next(iter(temple_zones))
            if not any(sim.zone_id == temple_zone_id and sim.is_played_sim for sim in sim_info_manager.get_all()):
                return True
        return False

    def _setup_for_first_travel_group(self):
        self._current_temple_id = None
        self._temple_state = self.TEMPLE_STATE_NEEDS_RESET
        self._unlock_temple_obstacle()

    def on_shutdown(self):
        super().on_shutdown()
        services.get_event_manager().unregister_single_event(self, TestEvent.InteractionComplete)
        self._path_obstacles.clear()

    def _get_path_obstacles(self):
        object_manager = services.object_manager()
        path_obstacles = {}
        for obstacle_tag in self.path_obstacle_data:
            obstacles = object_manager.get_objects_matching_tags((obstacle_tag,))
            for obstacle in obstacles:
                path_obstacles[obstacle] = obstacle_tag
        return path_obstacles

    def _lock_path_obstacle(self, path_obstacle, obstacle_tag, reset_commodity=False):
        path_obstacle.add_lock_data(JungleOpenStreetDirector.DEFAULT_LOCK)
        self._setup_corresponding_layers(obstacle_tag)
        if reset_commodity:
            path_obstacle.set_stat_value(self.path_clearing_commodity, JungleOpenStreetDirector.MIN_CLEAR_COMMODITY)

    def _permanently_lock_path_obstacle(self, path_obstacle, obstacle_tag):
        path_obstacle.add_lock_data(JungleOpenStreetDirector.DEFAULT_LOCK)
        path_obstacle.set_state(self.permanently_blocked_state.state, self.permanently_blocked_state)
        self._setup_corresponding_layers(obstacle_tag)

    def _unlock_path_obstacle(self, path_obstacle, obstacle_tag):
        path_obstacle.set_state(self.path_unlocked_state.state, self.path_unlocked_state)
        self._setup_corresponding_layers(obstacle_tag, unlock=True)

    def _setup_corresponding_layers(self, path_obstacle_tag, unlock=False):
        obstacle_datas = self.path_obstacle_data[path_obstacle_tag]
        for obstacle_data in obstacle_datas.layers:
            if unlock == obstacle_data.visible:
                self.remove_layer_objects(obstacle_data.conditional_layer)
            elif obstacle_data.immediate:
                self.load_layer_immediately(obstacle_data.conditional_layer)
            else:
                self.load_layer_gradually(obstacle_data.conditional_layer)

    @classproperty
    def priority(cls):
        return OpenStreetDirectorPriority.DEFAULT

    def handle_event(self, sim_info, event, resolver, **kwargs):
        if resolver(self.clear_path_interaction):
            obstacle = resolver.interaction.target
            statistic = obstacle.get_stat_instance(self.path_clearing_commodity)
            if statistic is not None:
                statistic_value = statistic.get_value()
                if statistic_value >= statistic.max_value:
                    obstacle.set_state(self.path_unlocked_state.state, self.path_unlocked_state)
                    self._setup_corresponding_layers(self._path_obstacles[obstacle], unlock=True)

    def _save_custom_open_street_director(self, street_director_proto, writer):
        group_ids = []
        tags = []
        tag_status = []
        clear_progress = []
        for (group_id, path_obstacle_data) in self._obstacle_status.items():
            if group_id == self._current_travel_group_id:
                continue
            for (tag, status, progress) in path_obstacle_data:
                group_ids.append(group_id)
                tags.append(tag)
                tag_status.append(status)
                clear_progress.append(progress)
        if self._current_travel_group_id is None:
            return
        for (path_obstacle, tag) in self._path_obstacles.items():
            group_ids.append(self._current_travel_group_id)
            tags.append(tag)
            tag_status.append(self._get_tag_status(path_obstacle))
            clear_progress.append(path_obstacle.get_stat_value(self.path_clearing_commodity))
        writer.write_uint64s(GROUP_TOKEN, group_ids)
        writer.write_uint64s(TAG_TOKEN, tags)
        writer.write_uint64s(TAG_STATUS_TOKEN, tag_status)
        writer.write_floats(CLEAR_PROGRESS_TOKEN, clear_progress)
        writer.write_uint64(CURRENT_TEMPLE_ID, self._current_temple_id)
        writer.write_uint32(TEMPLE_STATE, self._temple_state)
        writer.write_uint64(LAST_TIME_SAVED, services.time_service().sim_now.absolute_ticks())
        self._save_treasure_chest_data(writer)

    def _get_tag_status(self, path_obstacle):
        if path_obstacle.state_value_active(self.permanently_blocked_state):
            return JungleOpenStreetDirector.PATH_UNAVAILABLE
        if path_obstacle.state_value_active(self.path_unlocked_state):
            return JungleOpenStreetDirector.PATH_UNLOCKED
        return JungleOpenStreetDirector.PATH_LOCKED

    def _load_custom_open_street_director(self, street_director_proto, reader):
        if reader is None:
            return
        travel_group_manager = services.travel_group_manager()
        group_ids = reader.read_uint64s(GROUP_TOKEN, [])
        tags = reader.read_uint64s(TAG_TOKEN, [])
        tag_status = reader.read_uint64s(TAG_STATUS_TOKEN, [])
        clear_progress = reader.read_floats(CLEAR_PROGRESS_TOKEN, 0)
        self._current_temple_id = reader.read_uint64(CURRENT_TEMPLE_ID, 0)
        self._temple_state = reader.read_uint32(TEMPLE_STATE, self.TEMPLE_STATE_NEEDS_RESET)
        last_time_saved = reader.read_uint64(LAST_TIME_SAVED, 0)
        for (index, group_id) in enumerate(group_ids):
            if not travel_group_manager.get(group_id):
                continue
            if group_id not in self._obstacle_status:
                self._obstacle_status[group_id] = []
            path_obstacles = self._obstacle_status[group_id]
            path_obstacles.append((tags[index], tag_status[index], clear_progress[index]))
        self._last_time_saved = DateAndTime(last_time_saved)
        self._load_treasure_chest_data(reader)

    @property
    def current_temple_id(self):
        return self._current_temple_id

    def get_next_temple_id(self):
        if self._temple_state == self.TEMPLE_STATE_RESET:
            return self._current_temple_id

    def reset_temple(self, new_id=None, force=False):
        if self._temple_state == self.TEMPLE_STATE_COMPLETE and not force:
            self._lock_temple_obstacle()
            self._update_temple_lock_commodity()
        self._current_temple_id = self._get_new_temple_id(new_id=new_id)
        self._temple_state = self.TEMPLE_STATE_RESET
        self._update_temple_id_for_client()

    def set_temple_in_progress(self):
        self._temple_state = self.TEMPLE_STATE_IN_PROGRESS

    def set_temple_complete(self):
        self._temple_state = self.TEMPLE_STATE_COMPLETE

    def _set_temple_obstacle_state(self, state_value):
        obstacle = self._get_temple_entrance_obstacle()
        if obstacle is not None:
            obstacle.set_state(state_value.state, state_value)

    def _get_temple_entrance_obstacle(self):
        obstacle = services.object_manager().get_objects_matching_tags((self.TEMPLE_PATH_OBSTACLE,))
        if len(obstacle) != 1:
            logger.error('There should only be one Temple Entrance Path Obstacle. Found {} instead.', len(obstacle), owner='trevor')
            return
        return next(iter(obstacle))

    def _lock_temple_obstacle(self):
        self._set_temple_obstacle_state(self.TEMPLE_PATH_OBSTACLE_LOCK_STATE)

    def _unlock_temple_obstacle(self):
        self._set_temple_obstacle_state(self.TEMPLE_PATH_OBSTACLE_UNLOCK_STATE)

    def _get_new_temple_id(self, new_id=None):
        temples = list(TempleTuning.TEMPLES.keys())
        if new_id is not None and new_id in temples and new_id != self._current_temple_id:
            return new_id
        if self._current_temple_id is not None:
            temples.remove(self._current_temple_id)
        return random.choice(temples)

    def _update_temple_id_for_client(self):
        for proto in services.get_persistence_service().zone_proto_buffs_gen():
            if proto.lot_description_id == TempleTuning.TEMPLE_LOT_DESCRIPTION:
                proto.pending_house_desc_id = self._current_temple_id

    def _save_treasure_chest_data(self, writer):
        group_ids = []
        obj_ids = []
        status_ids = []
        for (group_id, treasure_chest_data) in self._treasure_chest_status.items():
            if group_id == self._current_travel_group_id:
                continue
            for (obj_id, curr_status) in treasure_chest_data:
                group_ids.append(group_id)
                obj_ids.append(obj_id)
                status_ids.append(curr_status)
        if self._current_travel_group_id is None:
            return
        for chest in services.object_manager().get_objects_matching_tags((self.treasure_chest_tag,)):
            if chest.is_on_active_lot():
                continue
            group_ids.append(self._current_travel_group_id)
            obj_ids.append(chest.id)
            status_ids.append(self._get_treasure_chest_status(chest))
        writer.write_uint64s(TREASURE_CHEST_GROUP, group_ids)
        writer.write_uint64s(TREASURE_CHEST_ID, obj_ids)
        writer.write_uint64s(TREASURE_CHEST_STATUS, status_ids)

    def _get_treasure_chest_status(self, chest):
        if chest.state_value_active(self.treasure_chest_open_state):
            return JungleOpenStreetDirector.TREASURE_CHEST_OPEN
        return JungleOpenStreetDirector.TREASURE_CHEST_CLOSED

    def _load_treasure_chest_data(self, reader):
        if reader is None:
            return
        travel_group_manager = services.travel_group_manager()
        group_ids = reader.read_uint64s(TREASURE_CHEST_GROUP, [])
        obj_ids = reader.read_uint64s(TREASURE_CHEST_ID, [])
        status = reader.read_uint64s(TREASURE_CHEST_STATUS, [])
        for (index, group_id) in enumerate(group_ids):
            if not travel_group_manager.get(group_id):
                continue
            if group_id not in self._treasure_chest_status:
                self._treasure_chest_status[group_id] = []
            treasure_chest = self._treasure_chest_status[group_id]
            treasure_chest.append((obj_ids[index], status[index]))
class FavoritesTest(HasTunableSingletonFactory, AutoFactoryInit,
                    event_testing.test_base.BaseTest):
    FACTORY_TUNABLES = {
        'subject':
        TunableEnumEntry(
            description=
            "\n            The subject who's favorite we're testing against.\n            ",
            tunable_type=ParticipantTypeSim,
            default=ParticipantTypeSim.Actor),
        'target':
        TunableEnumEntry(
            description=
            '\n            The potential favorite object to test against.\n            ',
            tunable_type=ParticipantTypeObject,
            default=ParticipantTypeObject.Object),
        'favorite_type':
        TunableVariant(
            description=
            "\n            The type of favorite that we are testing.\n            \n            Preferred Object: Test whether the object is a sim's preferred object\n            to use for a specific func tag.\n            Favorite Stack: Test whether the object is in one of the sim's favorite stacks\n            in their inventory.\n            ",
            preferred_object=TunableTuple(
                description=
                "\n                Test whether the object is a sim's preferred object for\n                a specified tag.\n                ",
                tag=TunableTag(
                    description=
                    '\n                    The tag that represents this type of favorite.\n                    ',
                    filter_prefixes=('Func', )),
                instance_must_match=Tunable(
                    description=
                    '\n                    If checked, the object instance must match the instance\n                    of the favorite object for this test to pass (barring the\n                    case where this test is negated). If unchecked, either the\n                    instance or definition may match for this test to pass.\n                    ',
                    tunable_type=bool,
                    default=True)),
            locked_args={'favorite_stack': None},
            default='preferred_object'),
        'negate':
        Tunable(
            description=
            '\n            If checked, the result of this test will be negated. Error cases,\n            like subject or target not being found or the subject not having a\n            favorites tracker, will always fail.\n            ',
            tunable_type=bool,
            default=False)
    }

    def get_expected_args(self):
        return {'subject': self.subject, 'target': self.target}

    def __call__(self, subject=None, target=None):
        if not (subject and target):
            logger.error(
                'Subject or Target not found while running a Favorites Test')
            return TestResult(False,
                              'Subject or Target was not found.',
                              tooltip=self.tooltip)
        if len(subject) > 1:
            logger.warn(
                'FavoritesTest is being called with more than one participant for subject. Only the first participant will be used.'
            )
        if len(target) > 1:
            logger.warn(
                'FavoritesTest is being called with more than one participant for target. Only the first participant will be used.'
            )
        sim = subject[0]
        obj = target[0]
        favorites_tracker = sim.sim_info.favorites_tracker
        if favorites_tracker is None:
            logger.error(
                "Trying to get a favorites tracker for Sim {} but they don't have one.",
                sim)
            return TestResult(False,
                              'Sim {} has no favorites tracker.',
                              sim,
                              tooltip=self.tooltip)
        if self.favorite_type is not None:
            if favorites_tracker.is_favorite(
                    self.favorite_type.tag, obj,
                    self.favorite_type.instance_must_match):
                if self.negate:
                    return TestResult(
                        False,
                        'Found favorite for Sim. Test is negated.',
                        tooltip=self.tooltip)
                return TestResult.TRUE
            if self.negate:
                return TestResult.TRUE
            return TestResult(False,
                              'Object {} is not the favorite for Sim {}',
                              obj,
                              sim,
                              tooltip=self.tooltip)
        if favorites_tracker.is_favorite_stack(obj):
            if self.negate:
                return TestResult(
                    False,
                    'Obj is part of a favorite stack for Sim. Test is negated.',
                    tooltip=self.tooltip)
            return TestResult.TRUE
        if self.negate:
            return TestResult.TRUE
        else:
            return TestResult(
                False,
                'Object {} is not part of a favorite stack for Sim {}',
                obj,
                sim,
                tooltip=self.tooltip)
class InfectedSituation(SubSituationOwnerMixin, SituationComplexCommon):
    INSTANCE_TUNABLES = {'infected_state': _InfectedState.TunableFactory(display_name='Infected State', tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP), 'possessed_start_time': TunableTimeOfDay(description='\n            Time of day Sims become possessed.\n            ', tuning_group=GroupNames.SITUATION), 'possessed_duration_hours': TestedSum.TunableFactory(description='\n            How long the possession lasts.\n            ', tuning_group=GroupNames.SITUATION), 'possessed_situation': Situation.TunableReference(description='\n            Possessed situation to place Sim in.\n            ', class_restrictions=('PossessedSituation',), tuning_group=GroupNames.SITUATION), 'default_job_and_role': TunableSituationJobAndRoleState(description='\n            The job/role the infected Sim will be in.\n            ', tuning_group=GroupNames.SITUATION), 'possessed_buff_tag': TunableTag(description='\n            Tag for buffs that can add the Possessed Mood through the Infection\n            System. Possessed status is refreshed when these buffs are added\n            or removed.\n            ', filter_prefixes=('Buff',)), 'possessed_buff_no_animate_tag': TunableTag(description='\n            Possession buffs with this tag will not play the start possession\n            animation.\n            ', filter_prefixes=('Buff',)), 'possession_time_buff': TunableBuffReference(description='\n            The buff to add to the Sim when it is the possessed start time.\n            ')}
    REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._start_possession_alarm = None
        self._end_possession_alarm = None
        self._possession_sources = []

    @classmethod
    def _states(cls):
        return (SituationStateData.from_auto_factory(0, cls.infected_state),)

    @classmethod
    def _get_tuned_job_and_default_role_state_tuples(cls):
        return ((cls.default_job_and_role.job, cls.default_job_and_role.role_state),)

    @classmethod
    def default_job(cls):
        pass

    @classproperty
    def situation_serialization_option(cls):
        return SituationSerializationOption.DONT

    def start_situation(self):
        super().start_situation()
        self._change_state(self.infected_state())
        sim_info = self._get_sim_info()
        for buff in sim_info.Buffs.get_all_buffs_with_tag(self.possessed_buff_tag):
            self._request_possession(buff.buff_type, animate_possession_override=False)
        now = services.time_service().sim_now
        time_span_day = create_time_span(days=1)
        start_day_time = self._get_possessed_start_day_time()
        time_span = now.time_till_next_day_time(start_day_time)
        self._start_possession_alarm = alarms.add_alarm(self, time_span, self._on_possession_start, repeating_time_span=time_span_day, repeating=True)
        end_day_time = self._get_possessed_end_day_time()
        sim_info.Buffs.on_buff_added.register(self._on_buff_added)
        sim_info.Buffs.on_buff_removed.register(self._on_buff_removed)
        in_possession_window = now.time_between_day_times(start_day_time, end_day_time)
        if in_possession_window:
            elapsed_time = create_time_span(days=1) - now.time_till_next_day_time(start_day_time)
            self._trigger_possession_time(elapsed_time=elapsed_time)

    def on_remove(self):
        sim_info = self._get_sim_info()
        sim_info.Buffs.on_buff_added.unregister(self._on_buff_added)
        sim_info.Buffs.on_buff_removed.unregister(self._on_buff_removed)
        super().on_remove()

    @property
    def sim_id(self):
        return self._guest_list.host_sim_id

    def _get_sim_info(self):
        sim_info = services.sim_info_manager().get(self.sim_id)
        return sim_info

    def _get_possessed_start_day_time(self):
        return self.possessed_start_time

    def _get_possessed_end_day_time(self):
        sim_info = self._get_sim_info()
        if sim_info is None:
            logger.error('Missing SimInfo for infected sim')
            return
        start_day_time = self._get_possessed_start_day_time()
        resolver = SingleSimResolver(sim_info)
        hours = self.possessed_duration_hours.get_modified_value(resolver)
        end_day_time = start_day_time + create_time_span(hours=hours)
        return end_day_time

    def _on_possession_start(self, _):
        self._trigger_possession_time()

    def _trigger_possession_time(self, elapsed_time=None):
        sim_info = self._get_sim_info()
        if sim_info is None:
            logger.error('Missing SimInfo for infected sim')
            return
        possession_buff = self.possession_time_buff
        sim_info.add_buff_from_op(possession_buff.buff_type, possession_buff.buff_reason)
        buff_commodity = sim_info.get_statistic(possession_buff.buff_type.commodity, add=False)
        if buff_commodity:
            resolver = SingleSimResolver(sim_info)
            hours = self.possessed_duration_hours.get_modified_value(resolver)
            buff_time = hours*60
            if elapsed_time is not None:
                buff_time -= elapsed_time.in_minutes()
            buff_commodity.set_value(buff_time)

    def _on_sub_situation_end(self, sub_situation_id):
        if services.current_zone().is_zone_shutting_down:
            return
        if self._possession_sources:
            sim_info = self._get_sim_info()
            for source in tuple(self._possession_sources):
                sim_info.remove_buff_by_type(source)

    def _start_possession_situation(self, animate_possession_override=None):
        guest_list = SituationGuestList(invite_only=True, host_sim_id=self.sim_id)
        animate_possession = services.current_zone().is_zone_running
        if animate_possession:
            if animate_possession_override is not None:
                animate_possession = animate_possession_override
        self._create_sub_situation(self.possessed_situation, guest_list=guest_list, user_facing=False, animate_possession=animate_possession)

    def _on_possession_sources_changed(self):
        sub_situations = self._get_sub_situations()
        if sub_situations:
            sub_situations[0].on_possession_sources_changed()

    def _request_possession(self, source, animate_possession_override=None):
        if source in self._possession_sources:
            logger.error('Redundant source: {}', source)
            return
        self._possession_sources.append(source)
        if not self._sub_situation_ids:
            self._start_possession_situation(animate_possession_override=animate_possession_override)
        self._on_possession_sources_changed()

    def _remove_possession_request(self, source):
        if source not in self._possession_sources:
            logger.error('Missing source: {}', source)
            return
        self._possession_sources.remove(source)
        self._on_possession_sources_changed()

    def _on_buff_added(self, buff_type, owner_sim_id):
        if self.possessed_buff_tag not in buff_type.tags:
            return
        animate = None
        if self.possessed_buff_no_animate_tag in buff_type.tags:
            animate = False
        self._request_possession(buff_type, animate_possession_override=animate)

    def _on_buff_removed(self, buff_type, owner_sim_id):
        if self.possessed_buff_tag not in buff_type.tags:
            return
        self._remove_possession_request(buff_type)

    def get_possession_source(self):
        sim_info = self._get_sim_info()
        if sim_info is None:
            return (None, None)
        buff_component = sim_info.Buffs
        longest_source = None
        buff_duration = None
        for source in self._possession_sources:
            buff = buff_component.get_buff_by_type(source)
            if buff is None:
                continue
            buff_commodity = buff.get_commodity_instance()
            if buff_commodity is None:
                longest_source = buff
                buff_duration = None
                break
            buff_value = buff_commodity.get_value()
            if not buff_duration is None:
                if buff_value > buff_duration:
                    buff_duration = buff_value
                    longest_source = buff
            buff_duration = buff_value
            longest_source = buff
        return (longest_source, buff_duration)
Exemple #22
0
class ZoneModifier(HasTunableReference,
                   metaclass=HashedTunedInstanceMetaclass,
                   manager=services.get_instance_manager(
                       sims4.resources.Types.ZONE_MODIFIER)):
    INSTANCE_TUNABLES = {
        'zone_modifier_locked':
        Tunable(
            description=
            '\n            Whether this is a locked trait that cannot be assigned/removed\n            through build/buy.\n            ',
            tunable_type=bool,
            default=False,
            export_modes=ExportModes.All,
            tuning_group=GroupNames.UI),
        'enter_lot_loot':
        TunableSet(
            description=
            '\n            Loot applied to Sims when they enter or spawn in on the lot while\n            this zone modifier is active.\n            \n            NOTE: The corresponding exit loot is not guaranteed to be given.\n            For example, if the Sim walks onto the lot, player switches to a\n            different zone, then summons that Sim, that Sim will bypass\n            getting the exit loot.\n            A common use case for exit lot loot is to remove buffs granted\n            by this zone_mod.  This case is already covered as buffs are \n            automatically removed if they are non-persistable (have no associated commodity)\n            ',
            tunable=LootActions.TunableReference(pack_safe=True),
            tuning_group=GroupNames.LOOT),
        'exit_lot_loot':
        TunableSet(
            description=
            '\n            Loot applied to Sims when they exit or spawn off of the lot while\n            this zone modifier is active.\n            \n            NOTE: This loot is not guaranteed to be given after the enter loot.\n            For example, if the Sim walks onto the lot, player switches to a\n            different zone, then summons that Sim, that Sim will bypass\n            getting the exit loot.\n            A common use case for exit lot loot is to remove buffs granted\n            by this zone_mod.  This case is already covered as buffs are \n            automatically removed if they are non-persistable (have no associated commodity)\n            ',
            tunable=LootActions.TunableReference(pack_safe=True),
            tuning_group=GroupNames.LOOT),
        'interaction_triggers':
        TunableList(
            description=
            '\n            A mapping of interactions to possible loots that can be applied\n            when an on-lot Sim executes them if this zone modifier is set.\n            ',
            tunable=ZoneInteractionTriggers.TunableFactory()),
        'schedule':
        ZoneModifierWeeklySchedule.TunableFactory(
            description=
            '\n            Schedule to be activated for this particular zone modifier.\n            '
        ),
        'household_actions':
        TunableList(
            description=
            '\n            Actions to apply to the household that owns this lot when this zone\n            modifier is set.\n            ',
            tunable=ZoneModifierHouseholdActionVariants(
                description=
                '\n                The action to apply to the household.\n                '
            )),
        'object_tag_to_actions':
        TunableMapping(
            description=
            '\n            Mapping of object tag to zone modifier from object actions. Objects \n            in this tuning can be buy objects, build objects (column, window, pool),\n            and materials (floor tiles, roof tiles, wallpaper).\n            \n            This is primarily intended for architectural elements such as wallpaper, \n            roof materials, windows will give effect to utilities and eco footprint.\n            \n            NOTE: The actions will only be applied if user enables the \n            "Architecture Affects Eco Living" option under Game Options.\n            ',
            key_type=TunableTag(
                description=
                '\n                The object tag that will be used to do actions.\n                '
            ),
            value_type=TunableList(
                description=
                '\n                The list of action to apply.\n                ',
                tunable=ZoneModifierFromObjectsActionVariants())),
        'prohibited_situations':
        OptionalTunable(
            description=
            '\n            Optionally define if this zone should prevent certain situations\n            from running or getting scheduled.\n            ',
            tunable=SituationIdentityTest.TunableFactory(
                description=
                '\n                Prevent a situation from running if it is one of the specified \n                situations or if it contains one of the specified tags.\n                '
            )),
        'venue_requirements':
        TunableVariant(
            description=
            '\n            Whether or not we use a blacklist or white list for the venue\n            requirements on this zone modifier.\n            ',
            allowed_venue_types=TunableSet(
                description=
                '\n                A list of venue types that this Zone Modifier can be placed on.\n                All other venue types are not allowed.\n                ',
                tunable=TunableReference(
                    description=
                    '\n                    A venue type that this Zone Modifier can be placed on.\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.VENUE),
                    pack_safe=True)),
            prohibited_venue_types=TunableSet(
                description=
                '\n                A list of venue types that this Zone Modifier cannot be placed on.\n                ',
                tunable=TunableReference(
                    description=
                    '\n                    A venue type that this Zone Modifier cannot be placed on.\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.VENUE),
                    pack_safe=True)),
            export_modes=ExportModes.All),
        'conflicting_zone_modifiers':
        TunableSet(
            description=
            '\n            Conflicting zone modifiers for this zone modifier. If the lot has any of the\n            specified zone modifiers, then it is not allowed to be equipped with this\n            one.\n            ',
            tunable=TunableReference(manager=services.get_instance_manager(
                sims4.resources.Types.ZONE_MODIFIER),
                                     pack_safe=True),
            export_modes=ExportModes.All),
        'additional_situations':
        SituationCurve.TunableFactory(
            description=
            "\n            An additional schedule of situations that can be added in addition\n            a situation scheduler's source tuning.\n            ",
            get_create_params={'user_facing': False}),
        'zone_wide_loot':
        ZoneModifierUpdateAction.TunableFactory(
            description=
            '\n            Loots applied when spawning into a zone with \n            this zone modifier. This loot is also applied to all sims, \n            objects, etc. in the zone when this zone modifier is added to a lot.\n            ',
            tuning_group=GroupNames.LOOT),
        'cleanup_loot':
        ZoneModifierUpdateAction.TunableFactory(
            description=
            '\n            Loots applied when this zone modifier is removed.\n            ',
            tuning_group=GroupNames.LOOT),
        'on_add_loot':
        ZoneModifierUpdateAction.TunableFactory(
            description=
            '\n            Loots applied when this zone modifier is added.\n            ',
            tuning_group=GroupNames.LOOT),
        'spin_up_lot_loot':
        ZoneModifierUpdateAction.TunableFactory(
            description=
            '\n            Loots applied when the zone spins up.\n            ',
            tuning_group=GroupNames.LOOT),
        'utility_supply_surplus_loot':
        TunableMapping(
            description=
            '\n            Loots applied when utility supply statistic change\n            from deficit to surplus.\n            ',
            key_type=TunableEnumEntry(
                description=
                '\n                The utility that we want to listen for supply change.\n                ',
                tunable_type=Utilities,
                default=Utilities.POWER),
            value_type=ZoneModifierUpdateAction.TunableFactory(
                description=
                '\n                Loots to apply.\n                '),
            tuning_group=GroupNames.LOOT),
        'utility_supply_deficit_loot':
        TunableMapping(
            description=
            '\n            Loots applied when utility supply statistic change\n            from surplus to deficit.\n            ',
            key_type=TunableEnumEntry(
                description=
                '\n                The utility that we want to listen for supply change.\n                ',
                tunable_type=Utilities,
                default=Utilities.POWER),
            value_type=ZoneModifierUpdateAction.TunableFactory(
                description=
                '\n                Loots to apply.\n                '),
            tuning_group=GroupNames.LOOT),
        'ignore_route_events_during_zone_spin_up':
        Tunable(
            description=
            "\n            Don't handle sim route events during zone spin up.  Useful for preventing\n            unwanted loot from being applied when enter_lot_loot runs situation blacklist tests.\n            If we require sims to retrieve loot on zone spin up, we can tune spin_up_lot_loot. \n            ",
            tunable_type=bool,
            default=False),
        'hide_screen_slam':
        Tunable(
            description=
            '\n            If checked, this zone modifier will not show the usual screen slam\n            when first applied.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.UI)
    }
    _obj_tag_id_to_count = None

    @classproperty
    def obj_tag_id_to_count(cls):
        return cls._obj_tag_id_to_count

    @classmethod
    def on_start_actions(cls):
        cls.register_interaction_triggers()

    @classmethod
    def on_spin_up_actions(cls, is_build_eco_effects_enabled):
        sim_spawner_service = services.sim_spawner_service()
        if not sim_spawner_service.is_registered_sim_spawned_callback(
                cls.zone_wide_loot.apply_to_sim):
            sim_spawner_service.register_sim_spawned_callback(
                cls.zone_wide_loot.apply_to_sim)
        cls.spin_up_lot_loot.apply_all_actions()
        cls.zone_wide_loot.apply_all_actions()
        cls.apply_object_actions(is_build_eco_effects_enabled)

    @classmethod
    def on_add_actions(cls, is_build_eco_effects_enabled):
        sim_spawner_service = services.sim_spawner_service()
        if not sim_spawner_service.is_registered_sim_spawned_callback(
                cls.zone_wide_loot.apply_to_sim):
            sim_spawner_service.register_sim_spawned_callback(
                cls.zone_wide_loot.apply_to_sim)
        cls.register_interaction_triggers()
        cls.start_household_actions()
        cls.on_add_loot.apply_all_actions()
        cls.zone_wide_loot.apply_all_actions()
        cls.apply_object_actions(is_build_eco_effects_enabled)

    @classmethod
    def on_stop_actions(cls):
        services.sim_spawner_service().unregister_sim_spawned_callback(
            cls.zone_wide_loot.apply_to_sim)
        cls.unregister_interaction_triggers()
        cls.stop_household_actions()
        cls.revert_object_actions()

    @classmethod
    def on_remove_actions(cls):
        services.sim_spawner_service().unregister_sim_spawned_callback(
            cls.zone_wide_loot.apply_to_sim)
        cls.unregister_interaction_triggers()
        cls.stop_household_actions()
        cls.cleanup_loot.apply_all_actions()
        cls.revert_object_actions()

    @classmethod
    def on_utility_supply_surplus(cls, utility):
        if utility in cls.utility_supply_surplus_loot:
            cls.utility_supply_surplus_loot[utility].apply_all_actions()

    @classmethod
    def on_utility_supply_deficit(cls, utility):
        if utility in cls.utility_supply_deficit_loot:
            cls.utility_supply_deficit_loot[utility].apply_all_actions()

    @classmethod
    def handle_event(cls, sim_info, event, resolver):
        if event not in InteractionTestEvents:
            return
        sim = sim_info.get_sim_instance()
        if sim is None or not sim.is_on_active_lot():
            return
        for trigger in cls.interaction_triggers:
            trigger.handle_interaction_event(sim_info, event, resolver)

    @classmethod
    def start_household_actions(cls):
        if not cls.household_actions:
            return
        household_id = services.owning_household_id_of_active_lot()
        if household_id is not None:
            for household_action in cls.household_actions:
                household_action.start_action(household_id)

    @classmethod
    def stop_household_actions(cls):
        if not cls.household_actions:
            return
        household_id = services.owning_household_id_of_active_lot()
        if household_id is not None:
            for household_action in cls.household_actions:
                household_action.stop_action(household_id)

    @classmethod
    def _on_build_objects_environment_score_update(cls):
        household = services.active_household()
        if household is None:
            return
        for sim in household.instanced_sims_gen(
                allow_hidden_flags=ALL_HIDDEN_REASONS):
            sim.on_build_objects_environment_score_update()

    @classmethod
    def apply_object_actions(cls, is_build_eco_effects_enabled):
        if not is_build_eco_effects_enabled:
            return
        if not cls.object_tag_to_actions:
            return
        object_tags = list(cls.object_tag_to_actions.keys())
        curr_obj_tag_id_to_count = services.active_lot(
        ).get_object_count_by_tags(object_tags)
        if cls._obj_tag_id_to_count is None:
            delta_obj_tag_id_to_count = curr_obj_tag_id_to_count
        else:
            delta_obj_tag_id_to_count = {
                key:
                curr_obj_tag_id_to_count[key] - cls._obj_tag_id_to_count[key]
                for key in curr_obj_tag_id_to_count
            }
        zone = services.current_zone()
        for (obj_tag_id, obj_count) in delta_obj_tag_id_to_count.items():
            if obj_count != 0:
                for action in cls.object_tag_to_actions[Tag(obj_tag_id)]:
                    success = action.apply(obj_count)
                    if not success:
                        continue
                    if action.action_type == ZoneModifierFromObjectsActionType.STATISTIC_CHANGE:
                        zone.zone_architectural_stat_effects[
                            action.stat.guid64] += action.get_value(obj_count)
        cls._on_build_objects_environment_score_update()
        cls._obj_tag_id_to_count = curr_obj_tag_id_to_count

    @classmethod
    def revert_object_actions(cls):
        if not cls._obj_tag_id_to_count:
            return
        zone = services.current_zone()
        for (obj_tag_id, obj_count) in cls._obj_tag_id_to_count.items():
            if obj_count != 0:
                for action in cls.object_tag_to_actions[Tag(obj_tag_id)]:
                    success = action.revert(obj_count)
                    if not success:
                        continue
                    if action.action_type == ZoneModifierFromObjectsActionType.STATISTIC_CHANGE:
                        zone.zone_architectural_stat_effects[
                            action.stat.guid64] -= action.get_value(obj_count)
        cls._on_build_objects_environment_score_update()
        cls._obj_tag_id_to_count = None

    @classmethod
    def register_interaction_triggers(cls):
        services.get_event_manager().register_tests(cls,
                                                    cls._get_trigger_tests())

    @classmethod
    def unregister_interaction_triggers(cls):
        services.get_event_manager().unregister_tests(cls,
                                                      cls._get_trigger_tests())

    @classmethod
    def _get_trigger_tests(cls):
        tests = list()
        for trigger in cls.interaction_triggers:
            tests.extend(trigger.get_trigger_tests())
        return tests

    @classmethod
    def is_situation_prohibited(cls, situation_type):
        if cls.prohibited_situations is None:
            return False
        return cls.prohibited_situations(situation=situation_type)
Exemple #23
0
class CASMenuItem(metaclass=HashedTunedInstanceMetaclass,
                  manager=services.get_instance_manager(Types.CAS_MENU_ITEM)):
    class RemoveActions(enum.Int):
        DISALLOW_REMOVAL = 0
        REMOVE_PART = 1
        REMOVE_PAINT_LAYER = 2

    INSTANCE_TUNABLES = {
        'icons':
        CASMenuItemIconSet(
            description=
            '\n            Default icons to use for the menu item.\n            ',
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.ClientBinary),
        'icon_overrides':
        TunableList(
            description=
            '\n            List of possible overrides for the icons for this menu item.\n            Each override will be evaluated in the order listed, the last\n            override to satisfy its criteria will be used.\n            ',
            tunable=TunableTuple(
                override_criteria=CASContextCriterionList(
                    description=
                    '\n                    Criteria to determine when this set of icons should be used.\n                    '
                ),
                icons=CASMenuItemIconSet(
                    description=
                    '\n                    Icons to use instead of the default icons.\n                    Note that you must specify all the desired icons here, even\n                    if they differ from the defaults.  For example, if you wish\n                    to only override the selected icon, you still need to specify\n                    the default icon here as well.\n                    '
                ),
                export_class_name='CASMenuItemIconOverride'),
            export_modes=ExportModes.ClientBinary,
            tuning_group=GroupNames.UI),
        'name':
        TunableLocalizedStringFactory(
            description='\n            Item name\n            ',
            export_modes=ExportModes.ClientBinary,
            tuning_group=GroupNames.UI),
        'actions':
        TunableTuple(
            description=
            '\n            Actions to perform when this menu item is selected.\n            ',
            change_menu_state=OptionalTunable(
                TunableTuple(
                    description=
                    '\n                MenuState to change into. This value must be supported by the\n                code, so this will most likely be provided by an engineer.\n                To be deprecated.  It is part of the old system.\n                ',
                    menu_type=TunableEnumEntry(
                        tunable_type=CASMenuState.MenuType,
                        default=CASMenuState.MenuType.NONE),
                    menu_mode=TunableEnumEntry(
                        tunable_type=CASMenuState.MenuMode,
                        default=CASMenuState.MenuMode.NONE),
                    menu_section=TunableEnumEntry(
                        tunable_type=CASMenuState.MenuSection,
                        default=CASMenuState.MenuSection.NONE),
                    menu_item=TunableEnumEntry(
                        tunable_type=CASMenuState.MenuItem,
                        default=CASMenuState.MenuItem.NONE),
                    export_class_name='CASMenuState')),
            display_ui_widget=OptionalTunable(
                Tunable(
                    description=
                    '\n                Name of a UI widget that should be shown when this item is selected.\n                ',
                    tunable_type=str,
                    default='')),
            export_class_name='CASMenuItemActions',
            export_modes=ExportModes.ClientBinary),
        'remove_action':
        TunableEnumEntry(
            description=
            '\n            What action to perform when the remove button is clicked, if present.\n            ',
            tunable_type=RemoveActions,
            default=RemoveActions.REMOVE_PART,
            export_modes=ExportModes.ClientBinary),
        'audio_component_name':
        Tunable(
            description=
            '\n            Optional name to associate with this item for the UI audio system.\n            ',
            tunable_type=str,
            default='',
            allow_empty=True,
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.ClientBinary),
        'disable_reasons':
        TunableList(
            description=
            '\n            List of possible conditions under which this menu item should appear disabled.\n            ',
            tunable=TunableTuple(
                criteria=CASContextCriterionList(
                    description=
                    '\n                    Criteria which defines when this menu item should be disabled for\n                    the given reason.\n                    '
                ),
                reason=TunableLocalizedStringFactory(
                    description=
                    '\n                    The reason for disabling this menu item.\n                    '
                ),
                export_class_name='CASMenuItemDisableReason'),
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.ClientBinary),
        'sim_click_activations':
        TunableList(
            description=
            '\n            Defines a list of areas on the sim that when clicked should cause\n            this menu item to become selected.\n            ',
            tunable=TunableTuple(
                criteria=CASContextCriteria(
                    description=
                    '\n                    Criteria to determine if this click source should be enabled.\n                    '
                ),
                click_source=TunableVariant(
                    description=
                    '\n                    The possible areas on which a sim might be clicked.\n                    \n                    Note: Clothing is only a valid option for pet sims.\n                    ',
                    locked_args={'clothing': True},
                    body_types=TunableEnumSet(
                        description=
                        '\n                        Areas corresponding to sim BodyTypes.\n                        ',
                        enum_type=BodyType,
                        enum_default=BodyType.NONE),
                    sim_regions=TunableEnumSet(
                        description=
                        '\n                        Areas corresponding to SimRegions.\n                        ',
                        enum_type=SimRegion,
                        enum_default=SimRegion.INVALID)),
                export_class_name='CASMenuItemSimClickActivation'),
            export_modes=ExportModes.ClientBinary),
        'part_tags':
        TunableSet(
            description=
            '\n            Tags to use for what parts to include in the displayed catalog.\n            (09/26/2017 - Currently only one tag is supported but will be expanded to multiple later.)\n            ',
            tunable=TunableTag(),
            maxlength=1,
            export_modes=ExportModes.ClientBinary)
    }
class FavoriteObjectSituationMixin:
    INSTANCE_TUNABLES = {'_favorite_objects': TunableList(description='\n            A list of favorites objects to give to Sims when they enter this\n            situation. These favorites will be removed from the Sim when the \n            situation ends.\n            ', tunable=TunableTuple(description='\n                Favorite data to add to the Sim.\n                ', favorite_tag=TunableTag(description='\n                    The tag for this favorite object.\n                    ', filter_prefixes=('func',)), potential_favorites=TunableSet(description='\n                    A set of potential objects. One of these will be chosen at\n                    random.\n                    ', tunable=TunableReference(description='\n                        The definition of the favorite object.\n                        ', manager=services.definition_manager()), minlength=1)), tuning_group=GroupNames.SITUATION)}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._favorite_types_added = defaultdict(set)

    def _on_set_sim_job(self, sim, job_type):
        super()._on_set_sim_job(sim, job_type)
        favorites_tracker = sim.sim_info.favorites_tracker
        if favorites_tracker is None:
            return
        for favorite_data in self._favorite_objects:
            favorite_tag = favorite_data.favorite_tag
            if favorites_tracker.has_favorite(favorite_tag):
                continue
            self._favorite_types_added[sim.id].add(favorite_tag)
            favorite_object = random.choice(list(favorite_data.potential_favorites))
            favorites_tracker.set_favorite(favorite_tag, obj_def_id=favorite_object.id)

    def _on_remove_sim_from_situation(self, sim):
        favorites = self._favorite_types_added.get(sim.id, None)
        if favorites:
            favorites_tracker = sim.sim_info.favorites_tracker
            for favorite in favorites:
                favorites_tracker.clear_favorite_type(favorite)
        super()._on_remove_sim_from_situation(sim)
Exemple #25
0
class CASTuning:
    CAS_BRANDED_TAG_DATA = TunableList(
        description=
        '\n        A list of CAS tag to data used to show a branded logo on the item\n        ',
        tunable=TunableTuple(
            description=
            '\n            Tuning for branded logo to use.\n            ',
            tag=TunableTag(
                description=
                '\n                Tag to use for the brand to be displayed\n                '
            ),
            icon=TunableIcon(
                description=
                '\n                Icon to be displayed on the item\n                '
            ),
            background_type=TunableEnumEntry(
                description=
                '\n                Background to be used for it\n                ',
                tunable_type=CASBrandedLogoBackground,
                default=CASBrandedLogoBackground.LIGHT),
            export_class_name='CasBrandedTagEntry'),
        export_modes=ExportModes.ClientBinary)
    CAS_SPECIES_PAINT_POSES = TunableMapping(
        description=
        '\n        A mapping of species type to data that is required for the paint pose ui\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            The species type that this entry applies to.\n            ',
            tunable_type=SpeciesExtended,
            default=SpeciesExtended.HUMAN,
            invalid_enums=(SpeciesExtended.INVALID, )),
        value_type=TunableList(
            description=
            '\n            A list of CasPaintPostTuples\n            ',
            tunable=TunableTuple(
                description=
                '\n                Data required for each UI Paint pose button.\n                ',
                icon=TunableIcon(
                    description=
                    '\n                    Icon to be displayed on the button for the pose\n                    ',
                    tuning_group=GroupNames.UI),
                icon_selected=TunableIcon(
                    description=
                    '\n                    Icon to be displayed on the button when the pose button is selected\n                    ',
                    tuning_group=GroupNames.UI),
                pose=TunableEnumEntry(
                    description=
                    '\n                    The pose to play when the button is pressed\n                    ',
                    tunable_type=CASPaintPose,
                    default=CASPaintPose.NONE),
                export_class_name='CasPaintPoseTuple')),
        export_modes=ExportModes.ClientBinary,
        tuple_name='CasPaintPoseKeyValue')
    CAS_VOICES_DATA = TunableMapping(
        description=
        '\n        A mapping of species type to data required for the personality panel ui.\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            The species type that this entry applies to.\n            ',
            tunable_type=SpeciesExtended,
            default=SpeciesExtended.HUMAN,
            invalid_enums=(SpeciesExtended.INVALID, )),
        value_type=TunableMapping(
            description=
            '\n            A mapping of age type to data required for displaying voices in the ui.\n            ',
            key_type=TunableEnumEntry(
                description=
                '\n                The age that this entry applies to.\n                ',
                tunable_type=Age,
                default=Age.ADULT),
            value_type=TunableList(
                description=
                '\n                a list of voice data for this species at this age.\n                ',
                tunable=TunableTuple(
                    description=
                    '\n                    data required to display this voice in the ui.\n                    ',
                    icon=TunableIcon(
                        description=
                        '\n                        Icon to be displayed as voice button.\n                        ',
                        tuning_group=GroupNames.UI),
                    icon_selected=TunableIcon(
                        description=
                        '\n                        Icon to be displayed as voice button when it is selected.\n                        ',
                        tuning_group=GroupNames.UI),
                    tooltip=TunableLocalizedString(
                        description=
                        '\n                        Localized name of this voice.\n                        '
                    ),
                    export_class_name='CasVoicesDataTuple')),
            tuple_name='CasVoicesAgeKeyValue'),
        export_modes=ExportModes.ClientBinary,
        tuple_name='CasVoicesSpeciesKeyValue')
    CAS_RANDOMIZE_FILTERS = TunableMapping(
        description=
        '\n        An Ordered list of randomization menu items that will appear in the randomization panel ui in CAS. \n        The list is filtered by the criteria on each menu item.\n        ',
        key_type=Tunable(
            description=
            '\n            An integer value used to set the specific order of the menu items\n            in the ui. The lower numbers are displayed first in the ui.\n            ',
            tunable_type=int,
            default=0),
        value_type=TunableTuple(
            description=
            '\n            A randomization menu item and its inclusion (and/or exclusion) criteria.\n            ',
            criteria=CASContextCriterionList(
                description=
                '\n                Use this menu item if all of the specified criteria are met.\n                '
            ),
            flags=TunableList(
                description=
                '\n                A list of randomization flags for this item.\n                ',
                tunable=TunableEnumEntry(
                    description=
                    '\n                    A randomization flag.\n                    ',
                    tunable_type=CASRandomizeFlag,
                    default=CASRandomizeFlag.RANDOMIZE_BY_MENUSTATE)),
            name=TunableLocalizedString(
                description=
                '\n                The name of this menu item displayed in the ui.\n                '
            ),
            required_flags=TunableList(
                description=
                '\n                A list of randomization flags that are required to be enabled \n                in order for this menu item to be enabled. \n                ',
                tunable=TunableEnumEntry(
                    description=
                    '\n                    A randomization flag.\n                    ',
                    tunable_type=CASRandomizeFlag,
                    default=CASRandomizeFlag.RANDOMIZE_BY_MENUSTATE)),
            export_class_name='CasRandomizeItemTuple'),
        tuple_name='CasRandomizeItemsKeyValue',
        export_modes=(ExportModes.ClientBinary, ))
    CAS_COPY_FILTERS = TunableList(
        description=
        '\n        An Ordered list of copy menu items that will appear in the randomization panel ui in CAS. \n        The list is filtered by the criteria on each menu item.\n        ',
        tunable=TunableTuple(
            description=
            '\n            A copy menu item and its inclusion (and/or exclusion) criteria.\n            ',
            criteria=CASContextCriterionList(
                description=
                '\n                Use this menu item if all of the specified criteria are met.\n                '
            ),
            flags=TunableList(
                description=
                '\n                A list of copy flags for this item.\n                ',
                tunable=TunableEnumEntry(
                    description=
                    '\n                    A copy flag.\n                    ',
                    tunable_type=CASRandomizeFlag,
                    default=CASRandomizeFlag.RANDOMIZE_BY_MENUSTATE)),
            name=TunableLocalizedString(
                description=
                '\n                The name of this menu item displayed in the ui.\n                '
            ),
            required_flags=TunableList(
                description=
                '\n                A list of copy flags that are required to be enabled \n                in order for this menu item to be enabled. \n                ',
                tunable=TunableEnumEntry(
                    description=
                    '\n                    A copy flag.\n                    ',
                    tunable_type=CASRandomizeFlag,
                    default=CASRandomizeFlag.RANDOMIZE_BY_MENUSTATE)),
            export_class_name='CasCopyItemEntry'),
        export_modes=(ExportModes.ClientBinary, ))
    CAS_ADD_SIM_MENU_DATA = TunableMapping(
        description=
        '\n        An ordered mapping of menu data used for the Add Sim portion of CAS.\n        ',
        key_name='index',
        key_type=Tunable(
            description=
            '\n            The order in which these entries should be added. 1 is first, 2 is\n            second, etc.\n            ',
            tunable_type=int,
            default=0),
        value_name='data',
        value_type=TunableTuple(
            description=
            '\n            Data associated with an add Sim button in CAS.\n            ',
            criteria=CASContextCriterionList(
                description=
                '\n                Only add this menu item if the criteria are met.\n                '
            ),
            parent_index=Tunable(
                description=
                '\n                The index of the parent entry if this is a child to another\n                entry in the list. 0 if this entry has no parent.\n                ',
                tunable_type=int,
                default=0),
            tooltip=TunableLocalizedString(
                description=
                '\n                The tooltip when hovering over this entry.\n                ',
                allow_none=True),
            icon=TunableResourceKey(
                description=
                '\n                The icon for this entry.\n                ',
                allow_none=True,
                pack_safe=True),
            icon_selected=TunableResourceKey(
                description=
                '\n                The icon when this entry is selected.\n                ',
                allow_none=True,
                pack_safe=True),
            audio_name=Tunable(
                description=
                '\n                The audio to play when this entry is selected.\n                ',
                tunable_type=str,
                default='',
                allow_empty=True),
            flair_name=Tunable(
                description=
                '\n                Flair to apply to this entry (for instance, god rays).\n                ',
                tunable_type=str,
                default='',
                allow_empty=True),
            tutorial_control_enum=TunableEnumEntry(
                description=
                '\n                The enum used for tutorial controls. UI_INVALID should be\n                used if this entry has no tutorial control.\n                ',
                tunable_type=TutorialTipUiElement,
                default=TutorialTipUiElement.UI_INVALID),
            action=TunableEnumEntry(
                description=
                '\n                The action to take when clicking this entry.\n                ',
                tunable_type=CASAddSimAction,
                default=CASAddSimAction.ACTION_NONE),
            species=TunableEnumEntry(
                description=
                "\n                The species for this entry. Species.INVALID indicates no\n                preference or it's not relevant to this menu entry.\n                ",
                tunable_type=SpeciesExtended,
                default=SpeciesExtended.INVALID),
            occult_type=TunableEnumFlags(
                description=
                '\n                The occult type for this entry, if any.\n                ',
                enum_type=OccultType,
                allow_no_flags=True),
            limit_genetics_species=TunableEnumSet(
                description=
                '\n                Species in this list will only be allowed through if the action\n                for this entry is GENETICS. This is very likely only going to be\n                used for pet genetics.\n                ',
                enum_type=SpeciesExtended,
                enum_default=SpeciesExtended.INVALID,
                allow_empty_set=True),
            export_class_name='CasAddSimMenuData'),
        tuple_name='CasAddSimMenuDataKeyValue',
        export_modes=(ExportModes.ClientBinary, ))
class OutfitChangeTags(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {'outfit_change_data': TunableList(description='\n            List of data corresponding at possible outfits and tests that will\n            generate change outfit affordances.\n            ', tunable=TunableTuple(description='\n                Outfits and tests that will generate the change outfit\n                interactions.\n                ', outfit_tag=TunableTag(description='\n                    Outfit tag that will generate the outfit change \n                    interactions.\n                    ', filter_prefixes=('Uniform', 'OutfitCategory', 'Style', 'Situation')), outfit_tests=TunableTestSet(description='\n                    Tests the Sim should pass to be able to change into the\n                    outfit.\n                    ')))}

    def outfit_affordances_gen(self, sim, target, affordance, **kwargs):
        resolver = SingleSimResolver(sim.sim_info)
        for outfit_data in self.outfit_change_data:
            if outfit_data.outfit_tests.run_tests(resolver):
                yield AffordanceObjectPair(affordance, target, affordance, None, pie_menu_cateogory=affordance.category, outfit_tags=(outfit_data.outfit_tag,), **kwargs)

    def get_outfit_tags(self):
        outfit_tags = set()
        for outfit_data in self.outfit_change_data:
            outfit_tags.add(outfit_data.outfit_tag)
        return outfit_tags

    def get_outfit_for_clothing_change(self, sim_info, outfit_change_category):
        return (outfit_change_category, 0)
class SpawnOnVehicleActionAffordance(HasTunableSingletonFactory,
                                     AutoFactoryInit):
    FACTORY_TUNABLES = {
        'vehicle_obj_tag':
        TunableTag(
            description=
            '\n            The tag to use to look up if the sim has a favorite vehicle\n            to use for the spawn action.\n            ',
            filter_prefixes=('Func', ))
    }

    @TunableFactory.factory_option
    def list_pack_safe(list_pack_safe=False):
        tuning_name = 'fallback_vehicle_def'
        description = "\n            The definition of the vehicle to spawn if the sim doesn't have\n            a favorite vehicle.\n        "
        if list_pack_safe:
            return {
                tuning_name:
                TunableReference(description=description,
                                 manager=services.definition_manager(),
                                 pack_safe=True)
            }
        return {
            tuning_name:
            TunablePackSafeReference(description=description,
                                     manager=services.definition_manager())
        }

    def __call__(self, sim):
        def _abort(vehicle_obj):
            sim.allow_opacity_change = True
            sim.fade_in()
            if vehicle_obj is not None:
                vehicle_obj.destroy()

        vehicle = None
        if sim.sim_info.favorites_tracker is not None:
            favorites_tracker = sim.sim_info.favorites_tracker
            definition_manager = services.definition_manager()
            vehicle_def_id = favorites_tracker.get_favorite_definition_id(
                self.vehicle_obj_tag)
            if vehicle_def_id is not None:
                vehicle_def = definition_manager.get(vehicle_def_id)
                if vehicle_def is not None:
                    vehicle = objects.system.create_object(vehicle_def)
        if vehicle is None:
            if self.fallback_vehicle_def is None:
                _abort(vehicle)
                return True
            vehicle = create_object(self.fallback_vehicle_def)
            if vehicle is None:
                _abort(vehicle)
                return True
        vehicle.set_household_owner_id(sim.household_id)
        starting_location = placement.create_starting_location(
            position=sim.position)
        fgl_context = placement.create_fgl_context_for_object(
            starting_location, vehicle)
        (position, orientation) = placement.find_good_location(fgl_context)
        if position is None or orientation is None:
            _abort(vehicle)
            return True
        vehicle.transform = sims4.math.Transform(position, orientation)
        result = vehicle.vehicle_component.push_drive_affordance(
            sim, priority=Priority.Critical)
        if result is None:
            _abort(vehicle)
            return True
        if result.interaction is None:
            logger.warn(
                "Vehicle's push drive affordance {} resulted in a None interaction. Result: {}.",
                vehicle.vehicle_component.drive_affordance,
                result,
                owner='jmorrow')
            _abort(vehicle)
            return True
        sim.fade_in()
        vehicle.claim()
        for situation in services.get_zone_situation_manager().get_all():
            if sim in situation.all_sims_in_situation_gen():
                situation.manage_vehicle(vehicle)
                break
        return True