Example #1
0
class _WaypointGeneratorPool(_WaypointGeneratorBase):
    FACTORY_TUNABLES = {
        'constraint_width':
        TunableRange(
            description=
            '\n            The width of the constraint created around the edge of the pool.\n            ',
            tunable_type=float,
            default=1.5,
            minimum=0),
        'ocean_constraint_radius':
        TunableRange(
            description=
            '\n            When in the ocean, the radius of the area around the nearest swim\n            portal to generate waypoints.\n            ',
            tunable_type=float,
            default=30,
            minimum=0,
            maximum=1000),
        'ocean_constraint_distance_past_swim_portal':
        TunableRange(
            description=
            '\n            When in the ocean, an offset away from the nearest swim portal to\n            center the area to generate waypoints.\n            ',
            tunable_type=float,
            default=0,
            minimum=0),
        'ocean_unique_goal_count':
        TunableRange(
            description=
            '\n            When in the ocean, the number of unique waypoints to generate.\n            ',
            tunable_type=int,
            default=10,
            minimum=0),
        'shuffle_waypoints':
        Tunable(
            description=
            '\n            If true, pool edge waypoint constraints will be shuffled and traversed in a random order.\n            If false, pool edge waypoint constraints will be traversed in counter-clockwise order.        \n            ',
            tunable_type=bool,
            default=True),
        'keep_away_from_edges':
        OptionalTunable(
            description=
            '\n            If enabled, turns on a constraint that forces sims away from the pool edges by a tuned distance.\n            ',
            tunable=Tunable(
                description=
                '\n                The distance from the pool edge.\n                ',
                tunable_type=float,
                default=0.25))
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        sim = self._context.sim
        self._routing_surface = routing.SurfaceIdentifier(
            self._routing_surface.primary_id,
            self._routing_surface.secondary_id,
            routing.SurfaceType.SURFACETYPE_POOL)
        position = self._target.position if self._target is not None else sim.position
        level = self._routing_surface.secondary_id
        self._start_constraint = None
        self._master_depth_constraint = None
        self._waypoint_constraints = []
        self.keep_away_constraint = None
        self._location_is_pool = build_buy.is_location_pool(position, level)
        if self._location_is_pool:
            pool_block_id = build_buy.get_block_id(sim.zone_id, position,
                                                   level - 1)
            pool = pool_utils.get_pool_by_block_id(pool_block_id)
            if pool is not None:
                pool_edge_constraints = pool.get_edge_constraint(
                    constraint_width=self.constraint_width,
                    inward_dir=True,
                    return_constraint_list=True)
                pool_edge_constraints = [
                    constraint.generate_geometry_only_constraint()
                    for constraint in pool_edge_constraints
                ]
                if self.keep_away_from_edges is not None:
                    bb_polys = build_buy.get_pool_polys(
                        pool_block_id, level - 1)
                    if len(bb_polys) > 0:
                        bb_poly = bb_polys[0]
                        _WaypointGeneratorPool._push_poly_inward(
                            bb_poly, self.keep_away_from_edges)
                        bb_poly.reverse()
                        keep_away_geom = sims4.geometry.RestrictedPolygon(
                            sims4.geometry.Polygon(bb_poly), ())
                        self.keep_away_constraint = Constraint(
                            routing_surface=pool.provided_routing_surface,
                            geometry=keep_away_geom)
                    else:
                        logger.error(
                            f'Pool Waypoint Generator: Pool polygon data unexpectedly empty while ${sim} was routing on a pool with id ${pool_block_id}.',
                            owner='jmorrow')
                    for i in range(len(pool_edge_constraints)):
                        pool_edge_constraints[i] = pool_edge_constraints[
                            i].intersect(self.keep_away_constraint)
                self._start_constraint = create_constraint_set(
                    pool_edge_constraints)
                self._waypoint_constraints = pool_edge_constraints

    def get_start_constraint(self):
        if self._start_constraint is not None:
            return self._start_constraint
        sim = self._context.sim
        position = self._target.position if self._target is not None else sim.position
        relative_offset_vector = Vector3(
            0, 0, self.ocean_constraint_distance_past_swim_portal)
        if self._target is not None and self._target.routing_surface is not None:
            routing_surface = self._target.routing_surface
        else:
            routing_surface = sim.routing_surface
        if routing_surface.type != routing.SurfaceType.SURFACETYPE_POOL:
            self._start_constraint = OceanStartLocationConstraint.create_simple_constraint(
                WaterDepthIntervals.SWIM,
                self.ocean_constraint_radius,
                sim,
                self._target,
                position,
                ideal_radius=self.constraint_width,
                ideal_radius_width=self.constraint_width,
                relative_offset_vector=relative_offset_vector)
        else:
            self._start_constraint = Circle(
                position,
                self.ocean_constraint_radius,
                routing_surface=self._routing_surface)
        self._master_depth_constraint = WaterDepthIntervalConstraint.create_water_depth_interval_constraint(
            sim, WaterDepthIntervals.SWIM)
        self._start_constraint = self._start_constraint.intersect(
            self._master_depth_constraint)
        return self._start_constraint

    def get_waypoint_constraints_gen(self, routing_agent, waypoint_count):
        if self._start_constraint is None:
            self.get_start_constraint()
        goals = []
        handles = self._start_constraint.get_connectivity_handles(
            routing_agent)
        for handle in handles:
            goals.extend(handle.get_goals(always_reject_invalid_goals=True))
        agent_radius = routing_agent.routing_component.pathplan_context.agent_radius
        ocean_goal_count = min(len(goals), self.ocean_unique_goal_count)
        for _ in range(ocean_goal_count):
            goal = random.choice(goals)
            break
            goals.remove(goal)
            constraint = Circle(goal.position,
                                agent_radius,
                                routing_surface=self._routing_surface)
            self._waypoint_constraints.append(
                constraint.intersect(self._master_depth_constraint))
        available_waypoint_count = len(self._waypoint_constraints)
        if not self._start_constraint is not None or (
                self._waypoint_constraints
                or not goals) or available_waypoint_count == 0:
            return
        use_pool_debug_visualizer = False and (
            routing.waypoints.waypoint_generator.enable_waypoint_visualization
            and self._location_is_pool)
        polygon_metadata = {}
        for i in range(waypoint_count):
            if not not i % available_waypoint_count == 0 and self.shuffle_waypoints:
                random.shuffle(self._waypoint_constraints)
            yield self._waypoint_constraints[i % available_waypoint_count]
            if use_pool_debug_visualizer:
                self._build_polygon_metadata_dictionary(
                    polygon_metadata,
                    self._waypoint_constraints[i % available_waypoint_count],
                    i)
        if not use_pool_debug_visualizer or use_pool_debug_visualizer:
            self._draw_pool_debugvis(polygon_metadata)

    def _draw_pool_debugvis(self, polygon_metadata):
        color_palette = [Color.WHITE, Color.BLUE, Color.GREEN, Color.MAGENTA]
        if routing.waypoints.waypoint_generator.enable_waypoint_visualization:
            with debugvis.Context(routing.waypoints.waypoint_generator.
                                  DEBUGVIS_WAYPOINT_LAYER_NAME) as layer:
                for entry in polygon_metadata.values():
                    position = entry[0]
                    waypoint_indices = entry[1]
                    layer.add_text_world(position, f'{waypoint_indices}')
                for (index,
                     constraint) in enumerate(self._waypoint_constraints):
                    polygon = constraint.geometry.polygon
                    layer.add_polygon(polygon,
                                      color=color_palette[index % 4],
                                      altitude=0.1)
                if self.keep_away_from_edges is not None:
                    polygon = self.keep_away_constraint.geometry.polygon
                    layer.add_polygon(polygon, color=Color.BLACK, altitude=0.1)

    def _build_polygon_metadata_dictionary(self, polygon_metadata, constraint,
                                           waypoint_index):
        compound_polygon = constraint.geometry.polygon
        if isinstance(compound_polygon, CompoundPolygon):
            for polygon in compound_polygon:
                if len(polygon) > 0:
                    key = polygon
                    if key not in polygon_metadata:
                        center = sum(polygon, Vector3.ZERO()) / len(polygon)
                        polygon_metadata[key] = (center, [])
                    waypoint_indices = polygon_metadata[key][1]
                    waypoint_indices.append(waypoint_index)
                else:
                    sim = self._context.sim
                    logger.error(
                        f'Pool Waypoint Generator: Polygon unexpectedly contains no vertices while drawing debug visuals of ${sim}"s route"',
                        owner='jmorrow')
        else:
            sim = self._context.sim
            logger.error(
                f'Pool Waypoint Generator: Constraint geometry in unexpected format while drawing debug visuals of ${sim}"s route."',
                owner='jmorrow')

    @staticmethod
    def _push_poly_inward(verts, amt):
        for i in range(1, len(verts)):
            _WaypointGeneratorPool._push_edge_inward(verts, i - 1, i, amt)
        _WaypointGeneratorPool._push_edge_inward(verts, i, 0, amt)

    @staticmethod
    def _push_edge_inward(verts, start, stop, amt):
        along = amt * sims4.math.vector_normalize(verts[stop] - verts[start])
        inward = sims4.math.vector3_rotate_axis_angle(
            along, sims4.math.PI / 2, sims4.math.Vector3.Y_AXIS())
        verts[start] += inward
        verts[stop] += inward
class HouseholdFundsInterestLootOp(BaseLootOperation):
    __qualname__ = 'HouseholdFundsInterestLootOp'
    FACTORY_TUNABLES = {'description': '\n            This loot will deliver interest income to the current Household for their current funds,\n            based on the percentage tuned against total held. \n        ', 'interest_rate': Tunable(description='\n            The percentage of interest to apply to current funds.\n            ', tunable_type=int, default=0), 'notification': OptionalTunable(description='\n            If enabled, this notification will display when this interest payment is made.\n            Token 0 is the Sim - i.e. {0.SimFirstName}\n            Token 1 is the interest payment amount - i.e. {1.Money}\n            ', tunable=UiDialogNotification.TunableFactory())}

    def __init__(self, interest_rate, notification, **kwargs):
        super().__init__(**kwargs)
        self._interest_rate = interest_rate
        self._notification = notification

    def _apply_to_subject_and_target(self, subject, target, resolver):
        pay_out = int(subject.household.funds.money*self._interest_rate*FLOAT_TO_PERCENT)
        subject.household.funds.add(pay_out, Consts_pb2.TELEMETRY_INTERACTION_REWARD, self._get_object_from_recipient(subject))
        if self._notification is not None:
            dialog = self._notification(subject, resolver)
            dialog.show_dialog(event_id=self._notification.factory.DIALOG_MSG_TYPE, additional_tokens=(pay_out,))
Example #3
0
class OutfitTest(HasTunableSingletonFactory, AutoFactoryInit, BaseTest):
    OUTFIT_CURRENT = 0
    OUTFIT_PREVIOUS = 1
    TEST_CAN_ADD = 0
    TEST_CANNOT_ADD = 1

    class _OutfitCategoryFromEnum(HasTunableSingletonFactory, AutoFactoryInit):
        FACTORY_TUNABLES = {
            'outfit_category':
            TunableEnumEntry(
                description=
                '\n                The outfit category for which we must be able to add an outfit.\n                ',
                tunable_type=OutfitCategory,
                default=OutfitCategory.EVERYDAY)
        }

        def get_expected_args(self):
            return {}

        def get_outfit_category(self, **kwargs):
            return self.outfit_category

    class _OutfitCategoryFromParticipant(HasTunableSingletonFactory,
                                         AutoFactoryInit):
        FACTORY_TUNABLES = {
            'participant':
            TunableEnumEntry(
                description=
                '\n                The participant whose current outfit will determine the\n                resulting outfit category.\n                ',
                tunable_type=ParticipantTypeSingle,
                default=ParticipantTypeSingle.Actor)
        }

        def get_expected_args(self):
            return {'outfit_category_targets': self.participant}

        def get_outfit_category(self, outfit_category_targets=(), **kwargs):
            outfit_category_target = next(iter(outfit_category_targets), None)
            if outfit_category_target is not None:
                outfit = outfit_category_target.get_current_outfit()
                return outfit[0]

    FACTORY_TUNABLES = {
        'participant':
        TunableEnumEntry(
            description=
            '\n            The participant against which to run this outfit test.\n            ',
            tunable_type=ParticipantType,
            default=ParticipantType.Actor),
        'outfit':
        TunableVariant(
            description=
            '\n            The outfit to use for the blacklist/whitelist tests.\n            ',
            locked_args={
                'current_outfit': OUTFIT_CURRENT,
                'previous_outfits': OUTFIT_PREVIOUS
            },
            default='current_outfit'),
        'blacklist_outfits':
        TunableEnumSet(
            description=
            "\n            If the specified participant's outfit matches any of these\n            categories, the test will fail.\n            ",
            enum_type=OutfitCategory),
        'whitelist_outfits':
        TunableEnumSet(
            description=
            "\n            If set, then the participant's outfit must match any of these\n            entries, or the test will fail.\n            ",
            enum_type=OutfitCategory),
        'outfit_addition_test':
        OptionalTunable(
            description=
            '\n            If enabled, then the test will verify whether or not the specified\n            participant can add an outfit to a specific category.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Tunables controlling the nature of this test.\n                ',
                outfit_category=TunableVariant(
                    description=
                    '\n                    Define the outfit category for which we need to test addition.\n                    ',
                    from_enum=_OutfitCategoryFromEnum.TunableFactory(),
                    from_participant=_OutfitCategoryFromParticipant.
                    TunableFactory(),
                    default='from_enum'),
                test_type=TunableVariant(
                    description=
                    '\n                    The condition to test for.\n                    ',
                    locked_args={
                        'can_add': TEST_CAN_ADD,
                        'cannot_add': TEST_CANNOT_ADD
                    },
                    default='can_add')))
    }

    def get_expected_args(self):
        expected_args = {'test_targets': self.participant}
        if self.outfit_addition_test is not None:
            expected_args.update(
                self.outfit_addition_test.outfit_category.get_expected_args())
        return expected_args

    @cached_test
    def __call__(self, test_targets=(), **kwargs):
        for target in test_targets:
            if self.outfit == self.OUTFIT_CURRENT:
                outfit = target.get_current_outfit()
            elif self.outfit == self.OUTFIT_PREVIOUS:
                outfit = target.get_previous_outfit()
            if any(outfit[0] == blacklist_category
                   for blacklist_category in self.blacklist_outfits):
                return TestResult(
                    False,
                    '{} is wearing a blacklisted outfit category',
                    target,
                    tooltip=self.tooltip)
            if self.whitelist_outfits and not any(
                    outfit[0] == whitelist_category
                    for whitelist_category in self.whitelist_outfits):
                return TestResult(
                    False,
                    '{} is not wearing any whitelisted outfit category',
                    target,
                    tooltip=self.tooltip)
            outfit_addition_test = self.outfit_addition_test
            if outfit_addition_test is not None:
                outfit_category = outfit_addition_test.outfit_category.get_outfit_category(
                    **kwargs)
                outfits = target.get_outfits()
                outfits_in_category = outfits.get_outfits_in_category(
                    outfit_category)
                if outfit_addition_test.test_type == self.TEST_CAN_ADD:
                    if outfits_in_category is not None and len(
                            outfits_in_category
                    ) >= get_maximum_outfits_for_category(outfit_category):
                        return TestResult(
                            False,
                            '{} cannot add a new {} outfit, but is required to be able to',
                            target,
                            outfit_category,
                            tooltip=self.tooltip)
                elif outfit_addition_test.test_type == self.TEST_CANNOT_ADD:
                    if not outfits_in_category is None:
                        if len(outfits_in_category
                               ) < get_maximum_outfits_for_category(
                                   outfit_category):
                            return TestResult(
                                False,
                                '{} can add a new {} outfit, but is required not to not be able to',
                                target,
                                outfit_category,
                                tooltip=self.tooltip)
                    return TestResult(
                        False,
                        '{} can add a new {} outfit, but is required not to not be able to',
                        target,
                        outfit_category,
                        tooltip=self.tooltip)
        return TestResult.TRUE
Example #4
0
class TravelInteraction(SuperInteraction):
    __qualname__ = 'TravelInteraction'
    INSTANCE_TUNABLES = {
        'travel_xevt':
        OptionalTunable(
            description=
            '\n            If enabled, specify an xevent at which the Sim will disappear from\n            the world.\n            ',
            tunable=Tunable(
                description=
                '\n                The xevent at which the Sim will disappear from the world.\n                ',
                tunable_type=int,
                needs_tuning=False,
                default=100))
    }

    @classmethod
    def _define_supported_postures(cls):
        return STAND_NO_CARRY_NO_SURFACE_POSTURE_MANIFEST

    def __init__(self, aop, context, **kwargs):
        super().__init__(aop, context, **kwargs)
        self.from_zone_id = kwargs['from_zone_id']
        self.to_zone_id = kwargs['to_zone_id']
        self.on_complete_callback = kwargs['on_complete_callback']
        self.on_complete_context = kwargs['on_complete_context']
        self.force_save_and_destroy_sim = True

    def _setup_gen(self, timeline):
        if self.travel_xevt is not None:

            def on_travel_visuals(*_, **__):
                self.sim.remove_from_client()
                event_handler.release()

            event_handler = self.animation_context.register_event_handler(
                on_travel_visuals, handler_id=self.travel_xevt)
        result = yield super()._setup_gen(timeline)
        return result

    def run_pre_transition_behavior(self, *args, **kwargs):
        result = super().run_pre_transition_behavior(*args, **kwargs)
        if result:
            self.sim.set_allow_route_instantly_when_hitting_marks(False)
        return result

    def _run_interaction_gen(self, timeline):
        self.save_and_destroy_sim(False, self.sim.sim_info)

    def save_and_destroy_sim(self, on_reset, sim_info):
        if services.current_zone().is_zone_shutting_down:
            return
        from_zone_id = self.from_zone_id
        to_zone_id = self.to_zone_id
        callback = self.on_complete_callback
        context = self.on_complete_context

        def notify_travel_service():
            if services.travel_service().has_pending_travel(sim_info.account):
                travel_service.on_travel_interaction_succeeded(
                    sim_info, from_zone_id, to_zone_id, callback, context)
            if not sim_info.is_npc:
                services.client_manager().get_first_client(
                ).send_selectable_sims_update()

        try:
            logger.debug('Saving sim during TravelInteraction for {}',
                         sim_info)
            sim_info.inject_into_inactive_zone(self.to_zone_id)
            save_success = sim_info.save_sim()
            while not save_success:
                logger.error('Failure saving during TravelInteraction for {}',
                             sim_info)
        finally:
            logger.debug('Destroying sim {}', sim_info)
            self.force_save_and_destroy_sim = False
            if on_reset:
                if self.sim is not None:
                    services.object_manager().remove(self.sim)
                notify_travel_service()
            elif self.sim is not None:
                self.sim.schedule_destroy_asap(
                    source=self, cause='Destroying sim on travel.')
Example #5
0
class PickTypeTest(HasTunableSingletonFactory, AutoFactoryInit, BaseTest):
    FACTORY_TUNABLES = {
        'whitelist':
        TunableSet(
            description=
            '\n            A set of pick types that will pass the test if the pick type\n            matches any of them.\n            ',
            tunable=TunableEnumEntry(
                description='\n                A pick type.\n                ',
                tunable_type=PickType,
                default=PickType.PICK_NONE)),
        'blacklist':
        TunableSet(
            description=
            '\n            A set of pick types that will fail the test if the pick type\n            matches any of them.\n            ',
            tunable=TunableEnumEntry(
                description='\n                A pick type.\n                ',
                tunable_type=PickType,
                default=PickType.PICK_NONE)),
        'terrain_tags':
        OptionalTunable(
            description=
            '\n            If checked, will verify the location of the test is currently on\n            one of the tuned terrain tags.\n            ',
            disabled_name="Don't_Test",
            tunable=TunableEnumSet(
                description=
                '\n                A set of terrain tags. Only one of these tags needs to be\n                present at this location. Although it is not tunable, there\n                is a threshold weight underneath which a terrain tag will\n                not appear to be present.\n                ',
                enum_type=TerrainTag,
                enum_default=TerrainTag.INVALID))
    }

    @cached_test
    def __call__(self, context=None):
        if context is None:
            return TestResult(
                False,
                'Interaction Context is None. Make sure this test is Tuned on an Interaction.'
            )
        pick_info = context.pick
        if pick_info is None:
            return TestResult(
                False,
                'PickTerrainTest cannot run without a valid pick info from the Interaction Context.'
            )
        pick_type = pick_info.pick_type
        if self.whitelist and pick_type not in self.whitelist:
            return TestResult(
                False, 'Pick type {} not in whitelist {}'.format(
                    pick_type, self.whitelist))
        if pick_type in self.blacklist:
            return TestResult(
                False,
                'Pick type {} in blacklist {}'.format(pick_type,
                                                      self.blacklist))
        if self.terrain_tags is not None:
            position = pick_info.location
            if not terrain.is_terrain_tag_at_position(
                    position.x,
                    position.z,
                    self.terrain_tags,
                    level=pick_info.routing_surface.secondary_id):
                return TestResult(False,
                                  'Pick does not have required terrain tag.',
                                  tooltip=self.tooltip)
        return TestResult.TRUE
Example #6
0
class Reward(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.REWARD)):
    __qualname__ = 'Reward'
    INSTANCE_SUBCLASSES_ONLY = True
    INSTANCE_TUNABLES = {'name': TunableLocalizedString(description='\n            The display name for this reward.\n            ', export_modes=ExportModes.All), 'reward_description': TunableLocalizedString(description='\n            Description for this reward.\n            ', export_modes=ExportModes.All), 'icon': TunableResourceKey(description='\n            The icon image for this reward.\n            ', default='PNG:missing_image', resource_types=sims4.resources.CompoundTypes.IMAGE, export_modes=ExportModes.All), 'tests': TunableTestSet(description='\n            A series of tests that must pass in order for reward to be available.\n            '), 'rewards': TunableList(TunableVariant(description='\n                The gifts that will be given for this reward. They can be either\n                a specific reward or a random reward, in the form of a list of\n                specific rewards.\n                ', specific_reward=TunableSpecificReward(), random_reward=TunableList(TunableRandomReward()))), 'notification': OptionalTunable(description='\n            If enabled, this notification will show when the sim/household receives this reward.\n            ', tunable=TunableUiDialogNotificationSnippet())}

    @classmethod
    def give_reward(cls, sim_info):
        raise NotImplementedError

    @classmethod
    def try_show_notification(cls, sim_info):
        if cls.notification is not None:
            dialog = cls.notification(sim_info, SingleSimResolver(sim_info))
            dialog.show_dialog()

    @classmethod
    def is_valid(cls, sim_info):
        if not cls.tests.run_tests(SingleSimResolver(sim_info)):
            return False
        for reward in cls.rewards:
            if not isinstance(reward, tuple):
                return reward.valid_reward(sim_info)
            for each_reward in reward:
                while not each_reward.reward.valid_reward(sim_info):
                    return False
            return True
class GameRules(metaclass=TunedInstanceMetaclass,
                manager=services.get_instance_manager(
                    sims4.resources.Types.GAME_RULESET)):
    __qualname__ = 'GameRules'
    INSTANCE_TUNABLES = {
        'game_name':
        TunableLocalizedStringFactory(
            description='\n            Name of the game.\n            ',
            default=1860708663),
        'teams_per_game':
        TunableInterval(
            description=
            '\n            An interval specifying the number of teams allowed per game.\n            \n            Joining Sims are put on a new team if the maximum number of teams\n            has not yet been met, otherwise they are put into the team with the\n            fewest number of players.\n            ',
            tunable_type=int,
            default_lower=2,
            default_upper=2,
            minimum=1),
        'players_per_game':
        TunableInterval(
            description=
            '\n            An interval specifying the number of players allowed per game.\n            \n            If the maximum number of players has not been met, Sims can\n            continue to join a game.  Joining Sims are put on a new team if the\n            maximum number of teams as specified in the "teams_per_game"\n            tunable has not yet been met, otherwise they are put into the team\n            with the fewest number of players.\n            ',
            tunable_type=int,
            default_lower=2,
            default_upper=2,
            minimum=1),
        'players_per_turn':
        TunableRange(
            description=
            '\n            An integer specifying number of players from the active team who\n            take their turn at one time.\n            ',
            tunable_type=int,
            default=1,
            minimum=1),
        'initial_state':
        ObjectStateValue.TunableReference(
            description=
            "\n            The game's starting object state.\n            "),
        'score_info':
        TunableTuple(
            description=
            "\n            Tunables that affect the game's score.\n            ",
            winning_score=Tunable(
                description=
                '\n                An integer value specifying at what score the game will end.\n                ',
                tunable_type=int,
                default=100),
            score_increase=TunableInterval(
                description=
                '\n                An interval specifying the minimum and maximum score increases\n                possible in one turn. A random value in this interval will be\n                generated each time score loot is given.\n                ',
                tunable_type=int,
                default_lower=35,
                default_upper=50,
                minimum=0),
            skill_level_bonus=Tunable(
                description=
                "\n                A bonus number of points based on the Sim's skill level in the\n                relevant_skill tunable that will be added to score_increase.\n                \n                ex: If this value is 2 and the Sim receiving score has a\n                relevant skill level of 4, they will receive 8 (2 * 4) extra\n                points.\n                ",
                tunable_type=float,
                default=2),
            relevant_skill=Skill.TunableReference(
                description=
                "\n                The skill relevant to this game.  Each Sim's proficiency in\n                this skill will effect the score increase they get.\n                "
            ),
            use_effective_skill_level=Tunable(
                description=
                '\n                If checked, we will use the effective skill level rather than\n                the actual skill level of the relevant_skill tunable.\n                ',
                tunable_type=bool,
                default=True),
            progress_stat=Statistic.TunableReference(
                description=
                '\n                The statistic that advances the progress state of this game.\n                '
            )),
        'clear_score_on_player_join':
        Tunable(
            description=
            '\n            Tunable that, when checked, will clear the game score when a player joins.\n            \n            This essentially resets the game.\n            ',
            tunable_type=bool,
            default=False),
        'alternate_target_object':
        OptionalTunable(
            description=
            '\n            Tunable that, when enabled, means the game should create an alternate object\n            in the specified slot on setup that will be modified as the game goes on\n            and destroyed when the game ends.\n            ',
            tunable=TunableTuple(
                target_game_object=TunableReference(
                    description=
                    '\n                    The definition of the object that will be created/destroyed/altered\n                    by the game.\n                    ',
                    manager=services.definition_manager()),
                parent_slot=TunableVariant(
                    description=
                    '\n                    The slot on the parent object where the target_game_object object should go. This\n                    may be either the exact name of a bone on the parent object or a\n                    slot type, in which case the first empty slot of the specified type\n                    in which the child object fits will be used.\n                    ',
                    by_name=Tunable(
                        description=
                        '\n                        The exact name of a slot on the parent object in which the target\n                        game object should go.  \n                        ',
                        tunable_type=str,
                        default='_ctnm_'),
                    by_reference=TunableReference(
                        description=
                        '\n                        A particular slot type in which the target game object should go.  The\n                        first empty slot of this type found on the parent will be used.\n                        ',
                        manager=services.get_instance_manager(
                            sims4.resources.Types.SLOT_TYPE)))))
    }
Example #8
0
class PaymentElement(XevtTriggeredElement):
    FACTORY_TUNABLES = {
        'payment':
        TunablePaymentSnippet(),
        'display_only':
        Tunable(
            description=
            "\n            A PaymentElement marked as display_only will affect an affordance's\n            display name (by appending the Simoleon cost in parentheses), but\n            will not deduct funds when run.\n            ",
            tunable_type=bool,
            default=False),
        'include_in_total':
        Tunable(
            description=
            "\n            This should normally be set, but in cases where multiple payment\n            elements are tuned in separate outcomes, they will be all be summed\n            up to tally up the total cost of the interaction.\n            \n            In those cases, only set this to True for the 'definitive' cost.\n            ",
            tunable_type=bool,
            default=True),
        'insufficient_funds_behavior':
        TunableTuple(
            description=
            "\n            The behavior to define if we can succeed the payment if the\n            household doesn't have enough money.\n            ",
            allow_payment_succeed=Tunable(
                description=
                '\n                If True, the payment element will still return True if there is\n                not enough fund. Otherwise return False.\n                ',
                tunable_type=bool,
                default=False),
            notification=OptionalTunable(
                description=
                "\n                The notification about what the game will do if household\n                doesn't have enough fund.\n                ",
                tunable=TunableUiDialogNotificationSnippet()))
    }

    @classmethod
    def on_affordance_loaded_callback(cls,
                                      affordance,
                                      payment_element,
                                      object_tuning_id=DEFAULT):
        if not payment_element.include_in_total:
            return

        def get_simoleon_delta(interaction,
                               target=DEFAULT,
                               context=DEFAULT,
                               **interaction_parameters):
            interaction_resolver = interaction.get_resolver(
                target=target, context=context, **interaction_parameters)
            return payment_element.payment.get_simoleon_delta(
                interaction_resolver)

        def get_cost_upper_bound(funds_source=DEFAULT, context=DEFAULT):
            return payment_element.payment.payment_source.max_funds(
                context.sim)

        affordance.register_simoleon_delta_callback(
            get_simoleon_delta, object_tuning_id=object_tuning_id)
        affordance.register_upper_limit_callback(get_cost_upper_bound)
        affordance.register_cost_gain_strings_callbacks(
            payment_element.payment.get_cost_string,
            payment_element.payment.get_gain_string)

    def _do_behavior(self):
        if self.display_only:
            return True
        sim = self.interaction.sim
        resolver = self.interaction.get_resolver()
        if self.payment.try_deduct_payment(
                resolver, sim, self.try_show_insufficient_funds_notification):
            return True
        return self.insufficient_funds_behavior.allow_payment_succeed

    def try_show_insufficient_funds_notification(self):
        if self.insufficient_funds_behavior.notification is not None:
            sim = self.interaction.sim
            resolver = self.interaction.get_resolver()
            dialog = self.insufficient_funds_behavior.notification(
                sim, resolver)
            dialog.show_dialog()
Example #9
0
 def __init__(self, reload_dependent=False, allow_none=False, **kwargs):
     super().__init__(buff_type=TunablePackSafeReference(manager=services.buff_manager(), description='Buff that will get added to sim.', allow_none=allow_none, reload_dependent=reload_dependent), buff_reason=OptionalTunable(description='\n                            If set, specify a reason why the buff was added.\n                            ', tunable=TunableLocalizedString(description='\n                                The reason the buff was added. This will be displayed in the\n                                buff tooltip.\n                                ')), **kwargs)
Example #10
0
class UiDialogNotification(UiDialog):
    DIALOG_MSG_TYPE = Consts_pb2.MSG_UI_NOTIFICATION_SHOW

    class UiDialogNotificationExpandBehavior(enum.Int):
        USER_SETTING = 0
        FORCE_EXPAND = 1

    class UiDialogNotificationUrgency(enum.Int):
        DEFAULT = 0
        URGENT = 1

    class UiDialogNotificationLevel(enum.Int):
        PLAYER = 0
        SIM = 1

    class UiDialogNotificationVisualType(enum.Int):
        INFORMATION = 0
        SPEECH = 1
        SPECIAL_MOMENT = 2

    FACTORY_TUNABLES = {
        'expand_behavior':
        TunableEnumEntry(
            description=
            "\n            Specify the notification's expand behavior.\n            ",
            tunable_type=UiDialogNotificationExpandBehavior,
            default=UiDialogNotificationExpandBehavior.USER_SETTING),
        'urgency':
        TunableEnumEntry(
            description=
            "\n            Specify the notification's urgency.\n            ",
            tunable_type=UiDialogNotificationUrgency,
            default=UiDialogNotificationUrgency.DEFAULT),
        'information_level':
        TunableEnumEntry(
            description=
            "\n            Specify the notification's information level.\n            ",
            tunable_type=UiDialogNotificationLevel,
            default=UiDialogNotificationLevel.SIM),
        'visual_type':
        TunableEnumEntry(
            description=
            "\n            Specify the notification's visual treatment.\n            ",
            tunable_type=UiDialogNotificationVisualType,
            default=UiDialogNotificationVisualType.INFORMATION),
        'primary_icon_response':
        OptionalTunable(
            description=
            '\n            If enabled, associate a response to clicking the primary icon.\n            ',
            tunable=get_defualt_ui_dialog_response(
                description=
                '\n                The response associated to the primary icon.\n                '
            )),
        'secondary_icon_response':
        OptionalTunable(
            description=
            '\n            If enabled, associate a response to clicking the secondary icon.\n            ',
            tunable=get_defualt_ui_dialog_response(
                description=
                '\n                The response associated to the secondary icon.\n                '
            )),
        'participant':
        OptionalTunable(
            description=
            "\n            This field is deprecated. Please use 'icon' instead.\n            ",
            tunable=TunableEnumEntry(tunable_type=ParticipantType,
                                     default=ParticipantType.TargetSim),
            deprecated=True)
    }

    def distribute_dialog(self, dialog_type, dialog_msg, **kwargs):
        distributor = Distributor.instance()
        notification_op = GenericProtocolBufferOp(
            Operation.UI_NOTIFICATION_SHOW, dialog_msg)
        owner = self.owner
        if owner is not None and owner.valid_for_distribution:
            distributor.add_op(owner, notification_op)
        else:
            distributor.add_op_with_no_owner(notification_op)

    def build_msg(self,
                  additional_tokens=(),
                  icon_override=DEFAULT,
                  event_id=None,
                  career_args=None,
                  **kwargs):
        if icon_override is DEFAULT:
            if self.participant is not None:
                participant = self._resolver.get_participant(self.participant)
                if participant is not None:
                    icon_override = IconInfoData(obj_instance=participant)
        msg = super().build_msg(icon_override=icon_override,
                                additional_tokens=additional_tokens,
                                **kwargs)
        msg.dialog_type = Dialog_pb2.UiDialogMessage.NOTIFICATION
        notification_msg = msg.Extensions[
            Dialog_pb2.UiDialogNotification.dialog]
        notification_msg.expand_behavior = self.expand_behavior
        notification_msg.criticality = self.urgency
        notification_msg.information_level = self.information_level
        notification_msg.visual_type = self.visual_type
        if career_args is not None:
            notification_msg.career_args = career_args
        if self.primary_icon_response is not None:
            self._build_response_arg(self.primary_icon_response,
                                     notification_msg.primary_icon_response,
                                     **kwargs)
        if self.secondary_icon_response is not None:
            self._build_response_arg(self.secondary_icon_response,
                                     notification_msg.secondary_icon_response,
                                     **kwargs)
        return msg
Example #11
0
class FestivalContestSubmitElement(XevtTriggeredElement):
    FACTORY_TUNABLES = {
        'success_notification_by_rank':
        TunableList(
            description=
            '\n            Notifications displayed if submitted object is large enough to be ranked in\n            the contest. Index refers to the place that the player is in currently.\n            1st, 2nd, 3rd, etc.\n            ',
            tunable=UiDialogNotification.TunableFactory(),
            tuning_group=GroupNames.UI),
        'unranked_notification':
        OptionalTunable(
            description=
            '\n            If enabled, notification displayed if submitted object is not large enough to rank in\n            the contest. \n            ',
            tunable=TunableUiDialogNotificationSnippet(
                description=
                '\n                The notification that will appear when the submitted object does not rank.\n                '
            ),
            tuning_group=GroupNames.UI)
    }

    def _do_behavior(self):
        resolver = self.interaction.get_resolver()
        running_contests = services.drama_scheduler_service(
        ).get_running_nodes_by_drama_node_type(DramaNodeType.FESTIVAL)
        for contest in running_contests:
            if hasattr(contest, 'festival_contest_tuning'):
                if contest.festival_contest_tuning is None:
                    continue
                if contest.is_during_pre_festival():
                    continue
                obj = self.interaction.get_participant(
                    ParticipantType.PickedObject)
                if obj is None:
                    logger.error('{} does not have PickedObject participant',
                                 resolver)
                    return False
                sim = self.interaction.sim
                if sim is None:
                    logger.error('{} does not have sim participant', resolver)
                    return False
                return self._enter_object_into_contest(contest, sim, obj,
                                                       resolver)
        logger.error('{} no valid active Contest', resolver)
        return False

    def _enter_object_into_contest(self, contest, sim, obj, resolver):
        weight_statistic = contest.festival_contest_tuning._weight_statistic
        weight_tracker = obj.get_tracker(weight_statistic)
        if weight_tracker is None:
            logger.error('{} picked object does not have weight stat {}',
                         resolver, weight_statistic)
            return False
        if contest.festival_contest_tuning._destroy_object_on_submit and not self._destroy_object(
                contest, sim, obj, resolver):
            return False
        elif not self._add_score(contest, sim, obj, resolver):
            return False
        return True

    def _destroy_object(self, contest, sim, obj, resolver):
        obj.make_transient()
        return True

    def _add_score(self, contest, sim, obj, resolver):
        weight_statistic = contest.festival_contest_tuning._weight_statistic
        weight_tracker = obj.get_tracker(weight_statistic)
        if weight_tracker is None:
            logger.error('{} picked object does not have weight stat {}',
                         resolver, weight_statistic)
            return False
        rank = contest.add_score(sim.id, obj.id,
                                 weight_tracker.get_value(weight_statistic))
        if rank is not None:
            if rank >= len(self.success_notification_by_rank):
                return False
            notification = self.success_notification_by_rank[rank]
            dialog = notification(sim, target_sim_id=sim.id, resolver=resolver)
            dialog.show_dialog()
        elif self.unranked_notification is not None:
            dialog = self.unranked_notification(sim,
                                                target_sim_id=sim.id,
                                                resolver=resolver)
            dialog.show_dialog()
        return True
Example #12
0
class SituationGoal(SituationGoalDisplayMixin,
                    metaclass=HashedTunedInstanceMetaclass,
                    manager=services.get_instance_manager(
                        sims4.resources.Types.SITUATION_GOAL)):
    INSTANCE_SUBCLASSES_ONLY = True
    IS_TARGETED = False
    INSTANCE_TUNABLES = {
        '_pre_tests':
        TunableSituationGoalPreTestSet(
            description=
            '\n            A set of tests on the player sim and environment that all must\n            pass for the goal to be given to the player. e.g. Player Sim\n            has cooking skill level 7.\n            ',
            tuning_group=GroupNames.TESTS),
        '_post_tests':
        TunableSituationGoalPostTestSet(
            description=
            '\n            A set of tests that must all pass when the player satisfies the\n            goal_test for the goal to be consider completed. e.g. Player\n            has Drunk Buff when Kissing another sim at Night.\n            ',
            tuning_group=GroupNames.TESTS),
        '_cancel_on_travel':
        Tunable(
            description=
            '\n            If set, this situation goal will cancel (technically, complete\n            with score overridden to 0 so that situation score is not\n            progressed) if situation changes zone.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.TESTS),
        '_environment_pre_tests':
        TunableSituationGoalEnvironmentPreTestSet(
            description=
            '\n            A set of sim independent pre tests.\n            e.g. There are five desks.\n            ',
            tuning_group=GroupNames.TESTS),
        'role_tags':
        TunableSet(
            TunableEnumEntry(Tag, Tag.INVALID),
            description=
            '\n            This goal will only be given to Sims in SituationJobs or Role\n            States marked with one of these tags.\n            '
        ),
        '_cooldown':
        TunableSimMinute(
            description=
            '\n            The cooldown of this situation goal.  Goals that have been\n            completed will not be chosen again for the amount of time that\n            is tuned.\n            ',
            default=600,
            minimum=0),
        '_iterations':
        Tunable(
            description=
            '\n             Number of times the player must perform the action to complete the goal\n             ',
            tunable_type=int,
            default=1),
        '_score':
        Tunable(
            description=
            '\n            The number of points received for completing the goal.\n            ',
            tunable_type=int,
            default=10),
        'score_on_iteration_complete':
        OptionalTunable(
            description=
            '\n            If enabled then we will add an amount of score to the situation\n            with every iteration of the situation goal completing.\n            ',
            tunable=Tunable(
                description=
                '\n                An amount of score that should be applied when an iteration\n                completes.\n                ',
                tunable_type=int,
                default=10)),
        '_pre_goal_loot_list':
        TunableList(
            description=
            '\n            A list of pre-defined loot actions that will applied to every\n            sim in the situation when this situation goal is started.\n             \n            Do not use this loot list in an attempt to undo changes made by\n            the RoleStates to the sim. For example, do not attempt\n            to remove buffs or commodities added by the RoleState.\n            ',
            tunable=SituationGoalLootActions.TunableReference()),
        '_goal_loot_list':
        TunableList(
            description=
            '\n            A list of pre-defined loot actions that will applied to every\n            sim in the situation when this situation goal is completed.\n             \n            Do not use this loot list in an attempt to undo changes made by\n            the RoleStates to the sim. For example, do not attempt\n            to remove buffs or commodities added by the RoleState.\n            ',
            tunable=SituationGoalLootActions.TunableReference()),
        'noncancelable':
        Tunable(
            description=
            '\n            Checking this box will prevent the player from canceling this goal in the whim system.',
            tunable_type=bool,
            default=False),
        'time_limit':
        Tunable(
            description=
            '\n            Timeout (in Sim minutes) for Sim to complete this goal. The default state of 0 means\n            time is unlimited. If the goal is not completed in time, any tuned penalty loot is applied.',
            tunable_type=int,
            default=0),
        'penalty_loot_list':
        TunableList(
            description=
            '\n            A list of pre-defined loot actions that will applied to the Sim who fails\n            to complete this goal within the tuned time limit.\n            ',
            tunable=SituationGoalLootActions.TunableReference()),
        'goal_awarded_notification':
        OptionalTunable(
            description=
            '\n            If enabled, this goal will have a notification associated with it.\n            It is up to whatever system awards the goal (e.g. the Whim system)\n            to display the notification when necessary.\n            ',
            tunable=TunableUiDialogNotificationSnippet()),
        'goal_completion_notification':
        OptionalTunable(tunable=UiDialogNotification.TunableFactory(
            description=
            '\n                A TNS that will fire when this situation goal is completed.\n                '
        )),
        'goal_completion_notification_and_modal_target':
        OptionalTunable(
            description=
            '\n            If enabled then we will use the tuned situation job to pick a\n            random sim in the owning situation with that job to be the target\n            sim of the notification and modal dialog.\n            ',
            tunable=TunableReference(
                description=
                '\n                The situation job that will be used to find a sim in the owning\n                situation to be the target sim.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.SITUATION_JOB))),
        'audio_sting_on_complete':
        TunableResourceKey(
            description=
            '\n            The sound to play when this goal is completed.\n            ',
            resource_types=(sims4.resources.Types.PROPX, ),
            default=None,
            allow_none=True,
            tuning_group=GroupNames.AUDIO),
        'goal_completion_modal_dialog':
        OptionalTunable(tunable=UiDialogOk.TunableFactory(
            description=
            '\n                A modal dialog that will fire when this situation goal is\n                completed.\n                '
        )),
        'visible_minor_goal':
        Tunable(
            description=
            '\n            Whether or not this goal should be displayed in the minor goals\n            list if this goal is for a player facing situation.\n            ',
            tunable_type=bool,
            default=True,
            tuning_group=GroupNames.UI),
        'display_type':
        TunableEnumEntry(
            description=
            '\n            How this goal is presented in user-facing situations.\n            ',
            tunable_type=SituationGoalDisplayType,
            default=SituationGoalDisplayType.NORMAL,
            tuning_group=GroupNames.UI)
    }

    @classmethod
    def can_be_given_as_goal(cls, actor, situation, **kwargs):
        if actor is not None:
            resolver = event_testing.resolver.DataResolver(
                actor.sim_info, None)
            result = cls._pre_tests.run_tests(resolver)
            if not result:
                return result
        else:
            resolver = GlobalResolver()
        environment_test_result = cls._environment_pre_tests.run_tests(
            resolver)
        if not environment_test_result:
            return environment_test_result
        return TestResult.TRUE

    def __init__(self,
                 sim_info=None,
                 situation=None,
                 goal_id=0,
                 count=0,
                 locked=False,
                 completed_time=None,
                 secondary_sim_info=None,
                 **kwargs):
        self._sim_info = sim_info
        self._secondary_sim_info = secondary_sim_info
        self._situation = situation
        self.id = goal_id
        self._on_goal_completed_callbacks = CallableList()
        self._completed_time = completed_time
        self._count = count
        self._locked = locked
        self._score_override = None
        self._goal_status_override = None
        self._setup = False

    def setup(self):
        self._setup = True

    def destroy(self):
        self.decommision()
        self._sim_info = None
        self._situation = None

    def decommision(self):
        if self._setup:
            self._decommision()

    def _decommision(self):
        self._on_goal_completed_callbacks.clear()

    def create_seedling(self):
        actor_id = 0 if self._sim_info is None else self._sim_info.sim_id
        target_sim_info = self.get_required_target_sim_info()
        target_id = 0 if target_sim_info is None else target_sim_info.sim_id
        secondary_target_id = 0 if self._secondary_sim_info is None else self._secondary_sim_info.sim_id
        seedling = situations.situation_serialization.GoalSeedling(
            type(self), actor_id, target_id, secondary_target_id, self._count,
            self._locked, self._completed_time)
        return seedling

    def register_for_on_goal_completed_callback(self, listener):
        self._on_goal_completed_callbacks.append(listener)

    def unregister_for_on_goal_completed_callback(self, listener):
        self._on_goal_completed_callbacks.remove(listener)

    def get_gsi_name(self):
        if self._iterations <= 1:
            return self.__class__.__name__
        return '{} {}/{}'.format(self.__class__.__name__, self._count,
                                 self._iterations)

    def on_goal_offered(self):
        if self._situation is None:
            return
        for sim in self._situation.all_sims_in_situation_gen():
            resolver = sim.get_resolver()
            for loots in self._pre_goal_loot_list:
                for loot in loots.goal_loot_actions:
                    loot.apply_to_resolver(resolver)

    def _display_goal_completed_dialogs(self):
        actor_sim_info = services.active_sim_info()
        target_sim_info = None
        if self.goal_completion_notification_and_modal_target is not None:
            possible_sims = list(
                self._situation.all_sims_in_job_gen(
                    self.goal_completion_notification_and_modal_target))
            if possible_sims:
                target_sim_info = random.choice(possible_sims)
            if target_sim_info is None:
                return
        resolver = DoubleSimResolver(actor_sim_info, target_sim_info)
        if self.goal_completion_notification is not None:
            notification = self.goal_completion_notification(actor_sim_info,
                                                             resolver=resolver)
            notification.show_dialog()
        if self.goal_completion_modal_dialog is not None:
            dialog = self.goal_completion_modal_dialog(actor_sim_info,
                                                       resolver=resolver)
            dialog.show_dialog()

    def _on_goal_completed(self, start_cooldown=True):
        if start_cooldown:
            self._completed_time = services.time_service().sim_now
        loot_sims = (self._sim_info, ) if self._situation is None else tuple(
            self._situation.all_sims_in_situation_gen())
        for loots in self._goal_loot_list:
            for loot in loots.goal_loot_actions:
                for sim in loot_sims:
                    loot.apply_to_resolver(sim.get_resolver())
        self._display_goal_completed_dialogs()
        with situations.situation_manager.DelayedSituationDestruction():
            self._on_goal_completed_callbacks(self, True)

    def _on_iteration_completed(self):
        self._on_goal_completed_callbacks(self, False)

    def force_complete(self,
                       target_sim=None,
                       score_override=None,
                       start_cooldown=True):
        self._score_override = score_override
        self._count = self._iterations
        self._on_goal_completed(start_cooldown=start_cooldown)

    def _valid_event_sim_of_interest(self, sim_info):
        return self._sim_info is None or self._sim_info is sim_info

    def handle_event(self, sim_info, event, resolver):
        if not self._valid_event_sim_of_interest(sim_info):
            return
        if self._run_goal_completion_tests(sim_info, event, resolver):
            self._count += 1
            if self._count >= self._iterations:
                self._on_goal_completed()
            else:
                self._on_iteration_completed()

    def _run_goal_completion_tests(self, sim_info, event, resolver):
        return self._post_tests.run_tests(resolver)

    def should_autocomplete_on_load(self, previous_zone_id):
        if self._cancel_on_travel:
            zone_id = services.current_zone_id()
            if previous_zone_id != zone_id:
                return True
        return False

    def get_actual_target_sim_info(self):
        pass

    @property
    def sim_info(self):
        return self._sim_info

    def get_required_target_sim_info(self):
        pass

    def get_secondary_sim_info(self):
        return self._secondary_sim_info

    @property
    def created_time(self):
        pass

    @property
    def completed_time(self):
        return self._completed_time

    def is_on_cooldown(self):
        if self._completed_time is None:
            return False
        time_since_last_completion = services.time_service(
        ).sim_now - self._completed_time
        return time_since_last_completion < interval_in_sim_minutes(
            self._cooldown)

    def get_localization_tokens(self):
        target_sim_info = self.get_required_target_sim_info()
        return (self._numerical_token, self._sim_info, target_sim_info,
                self._secondary_sim_info)

    def get_display_name(self):
        display_name = self.display_name
        if display_name is not None:
            return display_name(*self.get_localization_tokens())

    def get_display_tooltip(self):
        display_tooltip = self.display_tooltip
        if display_tooltip is not None:
            return display_tooltip(*self.get_localization_tokens())

    @property
    def score(self):
        if self._score_override is not None:
            return self._score_override
        return self._score

    @property
    def goal_status_override(self):
        return self._goal_status_override

    @property
    def completed_iterations(self):
        return self._count

    @property
    def max_iterations(self):
        return self._iterations

    @property
    def _numerical_token(self):
        return self.max_iterations

    @property
    def locked(self):
        return self._locked

    def toggle_locked_status(self):
        self._locked = not self._locked

    def validate_completion(self):
        if self._completed_time is not None:
            return
        if self.completed_iterations < self.max_iterations:
            return
        self.force_complete()

    def show_goal_awarded_notification(self):
        if self.goal_awarded_notification is None:
            return
        icon_override = IconInfoData(icon_resource=self.display_icon)
        secondary_icon_override = IconInfoData(obj_instance=self._sim_info)
        notification = self.goal_awarded_notification(self._sim_info)
        notification.show_dialog(
            additional_tokens=self.get_localization_tokens(),
            icon_override=icon_override,
            secondary_icon_override=secondary_icon_override)
Example #13
0
class AutonomyModifier:
    __qualname__ = 'AutonomyModifier'
    STATISTIC_RESTRICTIONS = (statistics.commodity.Commodity, statistics.statistic.Statistic, statistics.skill.Skill)
    FACTORY_TUNABLES = {'description': "\n            An encapsulation of a modification to Sim behavior.  These objects\n            are passed to the autonomy system to affect things like scoring,\n            which SI's are available, etc.\n            ", 'super_affordance_compatibility': TunableAffordanceFilterSnippet(description='\n            Tune this to provide suppression to certain affordances when an object has\n            this autonomy modifier.\n            EX: Tune this to exclude all on the buff for the maid to prevent\n                other sims from trying to chat with the maid while the maid is\n                doing her work.\n            To tune if this restriction is for autonomy only, etc, see\n            super_affordance_suppression_mode.\n            Note: This suppression will also apply to the owning sim! So if you\n                prevent people from autonomously interacting with the maid, you\n                also prevent the maid from doing self interactions. To disable\n                this, see suppress_self_affordances.\n            '), 'super_affordance_suppression_mode': TunableEnumEntry(description='\n            Setting this defines how to apply the settings tuned in Super Affordance Compatibility.', tunable_type=SuperAffordanceSuppression, default=SuperAffordanceSuppression.AUTONOMOUS_ONLY), 'super_affordance_suppress_on_add': Tunable(description='\n            If checked, then the suppression rules will be applied when the\n            modifier is added, potentially canceling interactions the owner is\n            running.\n            ', tunable_type=bool, default=False), 'suppress_self_affordances': Tunable(description="\n            If checked, the super affordance compatibility tuned for this \n            autonomy modifier will also apply to the sim performing self\n            interactions.\n            \n            If not checked, we will not do super_affordance_compatibility checks\n            if the target of the interaction is the same as the actor.\n            \n            Ex: Tune the maid's super_affordance_compatibility to exclude all\n                so that other sims will not chat with the maid. But disable\n                suppress_self_affordances so that the maid can still perform\n                interactions on herself (such as her No More Work interaction\n                that tells her she's finished cleaning).\n            ", tunable_type=bool, default=True), 'score_multipliers': TunableMapping(description='\n                Mapping of statistics to multipliers values to the autonomy\n                scores.  EX: giving motive_bladder a multiplier value of 2 will\n                make it so that that motive_bladder is scored twice as high as\n                it normally would be.\n                ', key_type=TunableReference(services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=STATISTIC_RESTRICTIONS, description='\n                    The stat the multiplier will apply to.\n                    '), value_type=Tunable(float, 1, description='\n                    The autonomy score multiplier for the stat.  Multiplies\n                    autonomy scores by the tuned value.\n                    ')), 'static_commodity_score_multipliers': TunableMapping(description='\n                Mapping of statistics to multipliers values to the autonomy\n                scores.  EX: giving motive_bladder a multiplier value of 2 will\n                make it so that that motive_bladder is scored twice as high as\n                it normally would be.\n                ', key_type=TunableReference(services.get_instance_manager(sims4.resources.Types.STATIC_COMMODITY), description='\n                    The static commodity the multiplier will apply to.\n                    '), value_type=Tunable(float, 1, description='\n                    The autonomy score multiplier for the static commodity.  Multiplies\n                    autonomy scores by the tuned value.\n                    ')), 'relationship_score_multiplier_with_buff_on_target': TunableMapping(description="\n                Mapping of buffs to multipliers.  The buff must exist on the TARGET sim.\n                If it does, this value will be multiplied into the relationship score.\n                \n                Example: The make children desire to socialize with children, you can add \n                this autonomy modifier to the child's age buff.  You can then map it with \n                a key to the child buff to apply a positive multiplier.  An alternative \n                would be to create a mapping to every other age and apply a multiplier that \n                is smaller than 1.\n                ", key_type=TunableReference(services.get_instance_manager(sims4.resources.Types.BUFF), description='\n                    The buff that the target sim must have to apply this multiplier.\n                    '), value_type=Tunable(float, 1, description='\n                    The multiplier to apply.\n                    ')), 'locked_stats': TunableList(TunableReference(services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=STATISTIC_RESTRICTIONS, description='\n                    The stat the modifier will apply to.\n                    '), description='\n                List of the stats we locked from this modifier.  Locked stats\n                are set to their maximum values and then no longer allowed to\n                decay.\n                '), 'decay_modifiers': CommodityDecayModifierMapping(description='\n                Statistic to float mapping for decay modifiers for\n                statistics.  All decay modifiers are multiplied together along\n                with the decay rate.\n                '), 'skill_tag_modifiers': TunableMapping(description='\n                The skill_tag to float mapping of skill modifiers.  Skills with\n                these tags will have their amount gained multiplied by the\n                sum of all the tuned values.\n                ', key_type=TunableEnumEntry(tag.Tag, tag.Tag.INVALID, description='\n                    What skill tag to apply the modifier on.\n                    '), value_type=Tunable(float, 0)), 'commodities_to_add': TunableList(TunableReference(services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=statistics.commodity.Commodity), description='\n                Commodites that are added while this autonomy modifier is\n                active.  These commodities are removed when the autonomy\n                modifier is removed.\n                '), 'only_scored_stats': OptionalTunable(TunableList(TunableReference(services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=STATISTIC_RESTRICTIONS), description='\n                    List of statistics that will only be considered when doing\n                    autonomy.\n                    '), tuning_filter=FilterTag.EXPERT_MODE, description="\n                If enabled, the sim in this role state will consider ONLY these\n                stats when doing autonomy. EX: for the maid, only score\n                commodity_maidrole_clean so she doesn't consider doing things\n                that she shouldn't care about.\n                "), 'only_scored_static_commodities': OptionalTunable(TunableList(StaticCommodity.TunableReference(), description='\n                    List of statistics that will only be considered when doing\n                    autonomy.\n                    '), tuning_filter=FilterTag.EXPERT_MODE, description='\n                If enabled, the sim in this role state will consider ONLY these\n                static commodities when doing autonomy. EX: for walkbys, only\n                consider the ringing the doorbell\n                '), 'stat_use_multiplier': TunableMapping(description='\n                List of stats and multiplier to affect their increase-decrease.\n                All stats on this list whenever they get modified (e. by a \n                constant modifier on an interaction, an interaction result...)\n                will apply the multiplier to their modified values. \n                e. A toilet can get a multiplier to decrease the repair rate\n                when its used, for this we would tune the commodity\n                brokenness and the multiplier 0.5 (to decrease its effect)\n                This tunable multiplier will affect the object statistics\n                not the ones for the sims interacting with it.\n                ', key_type=TunableReference(services.get_instance_manager(sims4.resources.Types.STATISTIC), class_restrictions=STATISTIC_RESTRICTIONS, description='\n                    The stat the multiplier will apply to.\n                    '), value_type=TunableTuple(description='\n                    Float value to apply to the statistic whenever its\n                    affected.  Greater than 1.0 if you want to increase.\n                    Less than 1.0 if you want a decrease (>0.0). \n                    A value of 0 is considered invalid and is skipped.\n                    ', multiplier=Tunable(description='\n                        Float value to apply to the statistic whenever its\n                        affected.  Greater than 1.0 if you want to increase.\n                        Less than 1.0 if you want a decrease (>0.0). \n                        A value of 0 is considered invalid and is skipped.\n                        ', tunable_type=float, default=1.0), apply_direction=TunableEnumEntry(StatisticChangeDirection, StatisticChangeDirection.BOTH, description='\n                        Direction on when the multiplier should work on the \n                        statistic.  For example a decrease on an object \n                        brokenness rate, should not increase the time it takes to \n                        repair it.\n                        '))), 'relationship_multipliers': TunableMapping(description='\n                List of relationship tracks and multiplier to affect their\n                increase or decrease of track value. All stats on this list\n                whenever they get modified (e. by a constant modifier on an\n                interaction, an interaction result...) will apply the\n                multiplier to their modified values. e.g. A LTR_Friendship_Main\n                can get a multiplier to decrease the relationship decay when\n                interacting with someone with a given trait, for this we would\n                tune the relationship track LTR_Friendship_Main and the\n                multiplier 0.5 (to decrease its effect)\n                ', key_type=relationships.relationship_track.RelationshipTrack.TunableReference(description='\n                    The Relationship track the multiplier will apply to.\n                    '), value_type=TunableTuple(description="\n                    Float value to apply to the statistic whenever it's\n                    affected.  Greater than 1.0 if you want to increase.\n                    Less than 1.0 if you want a decrease (>0.0).\n                    ", multiplier=Tunable(tunable_type=float, default=1.0), apply_direction=TunableEnumEntry(description='\n                        Direction on when the multiplier should work on the \n                        statistic.  For example a decrease on an object \n                        brokenness rate, should not increase the time it takes to \n                        repair it.\n                        ', tunable_type=StatisticChangeDirection, default=StatisticChangeDirection.BOTH))), 'off_lot_autonomy_rule': OptionalTunable(TunableVariant(description="\n                The rules to apply for how autonomy handle on-lot and off-lot\n                targets.\n                \n                DEFAULT:\n                    Sims will behave according to their default behavior.  Off-\n                    lot sims who are outside the lot's tolerance will not\n                    autonomously perform interactions on the lot.  Sims will\n                    only autonomously perform off-lot interactions within their\n                    off-lot radius.\n                ON_LOT_ONLY:\n                    Sims will only consider targets on the active lot.  They\n                    will ignore the off lot radius and off lot tolerance\n                    settings.\n                OFF_LOT_ONLY:\n                    Sims will only consider targets that are off the active lot.\n                    They will ignore the off lot tolerance settings, but they\n                    will respect the off lot radius.\n                UNLIMITED:\n                    Sims will consider all objects regardless of on/off lot\n                    status.\n                ", default_behavior=TunableTuple(description="\n                    Sims will behave according to their default behavior.  Off-\n                    lot sims who are outside the lot's tolerance will not\n                    autonomously perform interactions on the lot.  Sims will\n                    only autonomously perform off-lot interactions within their\n                    off-lot radius.\n                    ", locked_args={'rule': OffLotAutonomyRules.DEFAULT}, tolerance=Tunable(description='\n                        This is how many meters the Sim can be off of the lot while still being \n                        considered on the lot for the purposes of autonomy.  For example, if \n                        this is set to 5, the sim can be 5 meters from the edge of the lot and \n                        still consider all the objects on the lot for autonomy.  If the sim were \n                        to step 6 meters from the lot, the sim would be considered off the lot \n                        and would only score off-lot objects that are within the off lot radius.\n                        \n                        Note: If this value is set to anything below 0, it will use the global \n                        default in autonomy.autonomy_modes.OFF_LOT_TOLERANCE.\n                        ', tunable_type=float, default=-1), radius=Tunable(description='\n                        The radius around the sim in which he will consider off-lot objects.  If it is \n                        0, the Sim will not consider off-lot objects at all.  This is not recommended \n                        since it will keep them from running any interactions unless they are already \n                        within the tolerance for that lot (set with Off Lot Tolerance).\n                        \n                        Note: If this value is less than zero, the range is considered infinite.  The \n                        sim will consider every off-lot object.\n                        ', tunable_type=float, default=0)), on_lot_only=TunableTuple(description='\n                    Sims will only consider targets on the active lot.\n                    ', locked_args={'rule': OffLotAutonomyRules.ON_LOT_ONLY, 'tolerance': 0, 'radius': 0}), off_lot_only=TunableTuple(description='\n                    Sims will only consider targets that are off the active lot. \n                    ', locked_args={'rule': OffLotAutonomyRules.OFF_LOT_ONLY, 'tolerance': 0}, radius=Tunable(description='\n                        The radius around the sim in which he will consider off-lot objects.  If it is \n                        0, the Sim will not consider off-lot objects at all.  This is not recommended \n                        since it will keep them from running any interactions unless they are already \n                        within the tolerance for that lot (set with Off Lot Tolerance).\n                        \n                        Note: If this value is less than zero, the range is considered infinite.  The \n                        sim will consider every off-lot object.\n                        ', tunable_type=float, default=-1)), unlimited=TunableTuple(description='\n                    Sims will consider all objects regardless of on/off lot\n                    status.\n                    ', locked_args={'rule': OffLotAutonomyRules.UNLIMITED, 'tolerance': 0, 'radius': 0}), default='default_behavior')), 'override_convergence_value': OptionalTunable(description="\n            If enabled it will set a new convergence value to the tuned\n            statistics.  The decay of those statistics will start moving\n            toward the new convergence value.\n            Convergence value will apply as long as these modifier is active,\n            when modifier is removed, convergence value will return to default\n            tuned value.\n            As a tuning restriction when this modifier gets removed we will \n            reset the convergence to its original value.  This means that we \n            don't support two states at the same time overwriting convergence\n            so we should'nt tune multiple convergence overrides on the same \n            object.\n            ", tunable=TunableMapping(description='\n                Mapping of statistic to new convergence value.\n                ', key_type=Commodity.TunableReference(), value_type=Tunable(description='\n                    Value to which the statistic should convert to.\n                    ', tunable_type=int, default=0)), disabled_name='Use_default_convergence', enabled_name='Set_new_convergence_value'), 'subject': TunableVariant(description='\n            Specifies to whom this autonomy modifier will apply.\n            - Apply to owner: Will apply the modifiers to the object or sim who \n            is triggering the modifier.  \n            e.g Buff will apply the modifiers to the sim when he gets the buff.  \n            An object will apply the modifiers to itself when it hits a state.\n            - Apply to interaction participant:  Will save the modifiers to \n            be only triggered when the object/sim who holds the modifier \n            is on an interaction.  When the interaction starts the the subject\n            tuned will get the modifiers during the duration of the interaction. \n            e.g A sim with modifiers to apply on an object will only trigger \n            when the sim is interactin with an object.\n            ', apply_on_interaction_to_participant=OptionalTunable(TunableEnumFlags(description='\n                    Subject on which the modifiers should apply.  When this is set\n                    it will mean that the autonomy modifiers will trigger on a \n                    subect different than the object where they have been added.\n                    e.g. a shower ill have hygiene modifiers that have to affect \n                    the Sim ', enum_type=ParticipantType, default=ParticipantType.Object)), default='apply_to_owner', locked_args={'apply_to_owner': False})}

    def __init__(self, score_multipliers=None, static_commodity_score_multipliers=None, relationship_score_multiplier_with_buff_on_target=None, super_affordance_compatibility=None, super_affordance_suppression_mode=SuperAffordanceSuppression.AUTONOMOUS_ONLY, suppress_self_affordances=False, super_affordance_suppress_on_add=False, locked_stats=set(), decay_modifiers=None, statistic_modifiers=None, skill_tag_modifiers=None, commodities_to_add=(), only_scored_stats=None, only_scored_static_commodities=None, stat_use_multiplier=None, relationship_multipliers=None, off_lot_autonomy_rule=None, override_convergence_value=None, subject=None, exclusive_si=None):
        self._super_affordance_compatibility = super_affordance_compatibility
        self._super_affordance_suppression_mode = super_affordance_suppression_mode
        self._suppress_self_affordances = suppress_self_affordances
        self._super_affordance_suppress_on_add = super_affordance_suppress_on_add
        self._score_multipliers = score_multipliers
        self._locked_stats = set(locked_stats)
        self._decay_modifiers = decay_modifiers
        self._statistic_modifiers = statistic_modifiers
        self._relationship_score_multiplier_with_buff_on_target = relationship_score_multiplier_with_buff_on_target
        self._skill_tag_modifiers = skill_tag_modifiers
        self._commodities_to_add = commodities_to_add
        self._stat_use_multiplier = stat_use_multiplier
        self._relationship_multipliers = relationship_multipliers
        self._off_lot_autonomy_rule = off_lot_autonomy_rule
        self._subject = subject
        self._override_convergence_value = override_convergence_value
        self._exclusive_si = exclusive_si
        self._skill_tag_modifiers = {}
        if skill_tag_modifiers:
            for (skill_tag, skill_tag_modifier) in skill_tag_modifiers.items():
                skill_modifier = SkillTagMultiplier(skill_tag_modifier, StatisticChangeDirection.INCREASE)
                self._skill_tag_modifiers[skill_tag] = skill_modifier
        if static_commodity_score_multipliers:
            if self._score_multipliers is not None:
                self._score_multipliers = FrozenAttributeDict(self._score_multipliers, static_commodity_score_multipliers)
            else:
                self._score_multipliers = static_commodity_score_multipliers
        self._static_commodity_score_multipliers = static_commodity_score_multipliers
        self._only_scored_stat_types = None
        if only_scored_stats is not None:
            self._only_scored_stat_types = []
            self._only_scored_stat_types.extend(only_scored_stats)
        if only_scored_static_commodities is not None:
            if self._only_scored_stat_types is None:
                self._only_scored_stat_types = []
            self._only_scored_stat_types.extend(only_scored_static_commodities)

    def __repr__(self):
        return standard_auto_repr(self)

    @property
    def exclusive_si(self):
        return self._exclusive_si

    def affordance_suppressed(self, sim, aop_or_interaction, user_directed=DEFAULT):
        user_directed = aop_or_interaction.is_user_directed if user_directed is DEFAULT else user_directed
        if not self._suppress_self_affordances and aop_or_interaction.target == sim:
            return False
        affordance = aop_or_interaction.affordance
        if self._super_affordance_compatibility is None:
            return False
        if user_directed and self._super_affordance_suppression_mode == SuperAffordanceSuppression.AUTONOMOUS_ONLY:
            return False
        if not user_directed and self._super_affordance_suppression_mode == SuperAffordanceSuppression.USER_DIRECTED:
            return False
        return not self._super_affordance_compatibility(affordance)

    def locked_stats_gen(self):
        for stat in self._locked_stats:
            yield stat

    def get_score_multiplier(self, stat_type):
        if self._score_multipliers is not None and stat_type in self._score_multipliers:
            return self._score_multipliers[stat_type]
        return 1

    def get_stat_multiplier(self, stat_type, participant_type):
        if self._stat_use_multiplier is None:
            return 1
        if self._subject == participant_type and stat_type in self._stat_use_multiplier:
            return self._stat_use_multiplier[stat_type].multiplier
        return 1

    @property
    def subject(self):
        return self._subject

    @property
    def statistic_modifiers(self):
        return self._statistic_modifiers

    @property
    def statistic_multipliers(self):
        return self._stat_use_multiplier

    @property
    def relationship_score_multiplier_with_buff_on_target(self):
        return self._relationship_score_multiplier_with_buff_on_target

    @property
    def relationship_multipliers(self):
        return self._relationship_multipliers

    @property
    def decay_modifiers(self):
        return self._decay_modifiers

    @property
    def skill_tag_modifiers(self):
        return self._skill_tag_modifiers

    @property
    def commodities_to_add(self):
        return self._commodities_to_add

    @property
    def override_convergence(self):
        return self._override_convergence_value

    def is_locked(self, stat_type):
        if self._locked_stats and stat_type in self._locked_stats:
            return True
        return False

    def is_scored(self, stat_type):
        if self._only_scored_stat_types is None or stat_type in self._only_scored_stat_types:
            return True
        return False

    @property
    def off_lot_autonomy_rule(self):
        return self._off_lot_autonomy_rule

    @property
    def super_affordance_suppress_on_add(self):
        return self._super_affordance_suppress_on_add
Example #14
0
class Season(HasTunableReference,
             metaclass=HashedTunedInstanceMetaclass,
             manager=services.get_instance_manager(
                 sims4.resources.Types.SEASON)):
    INSTANCE_TUNABLES = {
        'season_icon':
        TunableIcon(
            description="\n            The season's icon.\n            ",
            export_modes=ExportModes.All,
            tuning_group=GroupNames.UI),
        'season_name':
        TunableLocalizedString(
            description="\n            The season's name.\n            ",
            export_modes=ExportModes.All,
            tuning_group=GroupNames.UI),
        'season_length_content':
        TunableMapping(
            description=
            '\n            A mapping of season length option to the content contained within.\n            ',
            key_type=TunableEnumEntry(tunable_type=SeasonLength,
                                      default=SeasonLength.NORMAL),
            value_type=SeasonalContent.TunableFactory()),
        'screen_slam':
        OptionalTunable(
            description=
            '\n            If enabled, trigger this Screen Slam when transitioning to this season.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The screenslam to trigger, and hour of the day when it should\n                appear to the users.\n                ',
                slam=TunableScreenSlamSnippet(),
                trigger_time=TunableTimeOfDay(default_hour=6))),
        'whim_set':
        OptionalTunable(
            description=
            '\n            If enabled then this season will offer a whim set to the Sim\n            when it is that season.\n            ',
            tunable=TunableReference(
                description=
                '\n                A whim set that is active when this season is active.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.ASPIRATION),
                class_restrictions=('ObjectivelessWhimSet', )))
    }

    def __init__(self, start_time, **kwargs):
        super().__init__(**kwargs)
        self._start_time = start_time
        self._length_option = None
        self._length_span = None
        self._content_data = None
        self._mid_season_begin = None
        self._absolute_mid = None
        self._late_season_begin = None
        self._end_of_season = None

    def __contains__(self, date_and_time):
        return self._start_time <= date_and_time < self._end_of_season

    @property
    def info(self):
        holiday_formatted = '\n\t'.join([
            '{} on {}'.format(holiday.__name__, day_of_season.date_and_time)
            for (holiday, day_of_season) in self.get_holiday_dates()
        ])
        return 'Resource: {}\nLength: {}\nStart: {}\n\tMid-Season Period: {}\n\tAbsolute Mid-Season: {}\n\tLate-Season Period: {}\nEnd: {}\nHolidays:\n\t{}'.format(
            self.__class__, self._length_span, self._start_time,
            self._mid_season_begin, self._absolute_mid,
            self._late_season_begin, self._end_of_season, holiday_formatted)

    @property
    def start_time(self):
        return self._start_time

    @property
    def length(self):
        return self._length_span

    @property
    def end_time(self):
        return self._end_of_season

    @property
    def midpoint_time(self):
        return self._absolute_mid

    def get_date_at_season_progress(self, progress):
        progress = clamp(0, progress, 1)
        return self._start_time + self._length_span * progress

    def get_position(self, date_and_time):
        return date_and_time - self._start_time

    def get_segment(self, date_and_time):
        if not self._verify_in_season(date_and_time):
            return
        if date_and_time < self._mid_season_begin:
            return SeasonSegment.EARLY
        if date_and_time >= self._late_season_begin:
            return SeasonSegment.LATE
        return SeasonSegment.MID

    def get_progress(self, date_and_time):
        if not self._verify_in_season(date_and_time):
            return
        current_ticks = self.get_position(date_and_time).in_ticks()
        total_ticks = self._length_span.in_ticks()
        return current_ticks / total_ticks

    def get_screen_slam_trigger_time(self):
        if self.screen_slam is None:
            return
        return self._start_time.time_of_next_day_time(
            self.screen_slam.trigger_time)

    def _verify_in_season(self, date_and_time):
        within_season = date_and_time in self
        if not within_season:
            seasons_logger.error(
                'Provided time {} is not within the current season, which is from {} to {}',
                date_and_time, self._start_time, self._end_of_season)
        return within_season

    def set_length_option(self, length_option):
        if self._length_option == length_option:
            return
        self._length_option = length_option
        self._length_span = SeasonsTuning.SEASON_LENGTH_OPTIONS[length_option](
        )
        self._calculate_important_dates()

    def _calculate_important_dates(self):
        self._content_data = self.season_length_content[self._length_option]
        self._mid_season_begin = self._start_time + self._content_data.segments.early_season_length(
        )
        self._absolute_mid = self.get_date_at_season_progress(0.5)
        self._late_season_begin = self._start_time + (
            self._length_span -
            self._content_data.segments.late_season_length())
        self._end_of_season = self._start_time + self._length_span

    def get_holiday_dates(self):
        holidays_in_season = []
        for (holiday, season_times) in self._content_data.holidays.items():
            holidays_in_season.extend(
                iter((holiday, day_of_season(self._start_time))
                     for day_of_season in season_times))
        return holidays_in_season

    def get_all_holiday_data(self):
        holidays_data = []
        for season_length in SeasonLength:
            for (
                    holiday, season_times
            ) in self.season_length_content[season_length].holidays.items():
                holidays_data.extend(
                    iter((season_length, holiday,
                          day(date_and_time.DATE_AND_TIME_ZERO).day_of_season)
                         for day in season_times))
        return holidays_data

    def get_holidays(self, season_length):
        return set(self._content_data.holidays.keys())
Example #15
0
class CareerTone(AwayAction):
    INSTANCE_TUNABLES = {
        'dominant_tone_loot_actions':
        TunableList(
            description=
            '\n            Loot to apply at the end of a work period if this tone ran for the\n            most amount of time out of all tones.\n            ',
            tunable=TunableReference(
                manager=services.get_instance_manager(
                    sims4.resources.Types.ACTION),
                class_restrictions=('LootActions', 'RandomWeightedLoot'))),
        'performance_multiplier':
        Tunable(
            description=
            '\n            Performance multiplier applied to work performance gain.\n            ',
            tunable_type=float,
            default=1),
        'periodic_sim_filter_loot':
        TunableList(
            description=
            '\n            Loot to apply periodically to between the working Sim and other\n            Sims, specified via a Sim filter.\n            \n            Example Usages:\n            -Gain relationship with 2 coworkers every hour.\n            -Every hour, there is a 5% chance of meeting a new coworker.\n            ',
            tunable=TunableTuple(
                chance=SuccessChance.TunableFactory(
                    description=
                    '\n                    Chance per hour of loot being applied.\n                    '
                ),
                sim_filter=TunableSimFilter.TunableReference(
                    description=
                    '\n                    Filter for specifying who to set at target Sims for loot\n                    application.\n                    '
                ),
                max_sims=OptionalTunable(
                    description=
                    '\n                    If enabled and the Sim filter finds more than the specified\n                    number of Sims, the loot will only be applied a random\n                    selection of this many Sims.\n                    ',
                    tunable=TunableRange(tunable_type=int,
                                         default=1,
                                         minimum=1)),
                loot=LootActions.TunableReference(
                    description=
                    '\n                    Loot actions to apply to the two Sims. The Sim in the \n                    career is Actor, and if Targeted is enabled those Sims\n                    will be TargetSim.\n                    '
                )))
    }
    runtime_commodity = None

    @classmethod
    def _tuning_loaded_callback(cls):
        if cls.runtime_commodity is not None:
            return
        commodity = RuntimeCommodity.generate(cls.__name__)
        commodity.decay_rate = 0
        commodity.convergence_value = 0
        commodity.remove_on_convergence = True
        commodity.visible = False
        commodity.max_value_tuning = date_and_time.SECONDS_PER_WEEK
        commodity.min_value_tuning = 0
        commodity.initial_value = 0
        commodity._time_passage_fixup_type = CommodityTimePassageFixupType.DO_NOT_FIXUP
        cls.runtime_commodity = commodity

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._update_alarm_handle = None
        self._last_update_time = None

    def run(self, callback):
        super().run(callback)
        self._last_update_time = services.time_service().sim_now
        time_span = clock.interval_in_sim_minutes(
            Career.CAREER_PERFORMANCE_UPDATE_INTERVAL)
        self._update_alarm_handle = alarms.add_alarm(
            self,
            time_span,
            lambda alarm_handle: self._update(),
            repeating=True)

    def stop(self):
        if self._update_alarm_handle is not None:
            alarms.cancel_alarm(self._update_alarm_handle)
            self._update_alarm_handle = None
        self._update()
        super().stop()

    def _update(self):
        career = self.sim_info.career_tracker.get_at_work_career()
        if career is None:
            logger.error(
                'CareerTone {} trying to update performance when Sim {} not at work',
                self,
                self.sim_info,
                owner='tingyul')
            return
        if career._upcoming_gig is not None and career._upcoming_gig.odd_job_tuning is not None:
            return
        now = services.time_service().sim_now
        elapsed = now - self._last_update_time
        self._last_update_time = now
        career.apply_performance_change(elapsed, self.performance_multiplier)
        career.resend_career_data()
        resolver = SingleSimResolver(self.sim_info)
        for entry in self.periodic_sim_filter_loot:
            chance = entry.chance.get_chance(resolver) * elapsed.in_hours()
            if random.random() > chance:
                continue
            services.sim_filter_service().submit_filter(
                entry.sim_filter,
                self._sim_filter_loot_response,
                callback_event_data=entry,
                requesting_sim_info=self.sim_info,
                gsi_source_fn=self.get_sim_filter_gsi_name)

    def get_sim_filter_gsi_name(self):
        return str(self)

    def _sim_filter_loot_response(self, filter_results, callback_event_data):
        entry = callback_event_data
        if entry.max_sims is None:
            targets = tuple(result.sim_info for result in filter_results)
        else:
            sample_size = min(len(filter_results), entry.max_sims)
            targets = tuple(
                result.sim_info
                for result in random.sample(filter_results, sample_size))
        for target in targets:
            resolver = DoubleSimResolver(self.sim_info, target)
            entry.loot.apply_to_resolver(resolver)

    def apply_dominant_tone_loot(self):
        resolver = self.get_resolver()
        for loot in self.dominant_tone_loot_actions:
            loot.apply_to_resolver(resolver)
Example #16
0
class ParticipantRanInteractionTest(event_testing.test_base.BaseTest):
    __qualname__ = 'ParticipantRanInteractionTest'
    UNIQUE_TARGET_TRACKING_AVAILABLE = True
    UNIQUE_POSTURE_TRACKING_AVAILABLE = True
    TAG_CHECKLIST_TRACKING_AVAILABLE = True
    USES_EVENT_DATA = True
    FACTORY_TUNABLES = {
        'description':
        'Check to see if the Sim ran an affordance as a particular actor',
        'participant':
        TunableEnumEntry(
            ParticipantType,
            ParticipantType.Actor,
            description=
            'This is the role the sim in question should be to pass.'),
        'affordances':
        TunableList(
            TunableReference(services.affordance_manager()),
            description=
            "The Sim must have run either any affordance or have a proxied affordance in this list or an interaction matching one of the tags in this tunable's Tags field."
        ),
        'interaction_outcome':
        OptionalTunable(
            TunableEnumEntry(OutcomeResult, OutcomeResult.NONE),
            description='participant type this interaction had to be run as.'),
        'running_time':
        OptionalTunable(
            TunableSimMinute(
                description=
                '\n            Amount of time in sim minutes that this interaction needs to\n            have been running for for this test to pass true.  This is not\n            a cumulative time that this particular interaction has been\n            run running, it is only counts consecutive time.  In addition,\n            this is checked at the end of an interaction and will not\n            trigger an objective when it would be completed.\n            ',
                default=10,
                minimum=0)),
        'target_filters':
        TunableTuple(
            description=
            '\n            Restrictions on the target of this interaction.\n            ',
            object_tags=OptionalTunable(
                description=
                '\n                Object tags for limiting test success to a subset of target \n                objects.\n                ',
                tunable=TunableTuple(
                    description=
                    '\n                    Target object tags and how they are tested.\n                    ',
                    tag_set=TunableSet(
                        description=
                        '\n                        A set of tags to test the target object for.\n                        ',
                        tunable=TunableEnumEntry(
                            description=
                            '\n                            A tag to test the target object for.\n                            ',
                            tunable_type=Tag,
                            default=Tag.INVALID)),
                    test_type=TunableEnumEntry(
                        description=
                        '\n                        How to test the tags in the tag set against the \n                        target object.\n                        ',
                        tunable_type=TagTestType,
                        default=TagTestType.CONTAINS_ANY_TAG_IN_SET)))),
        'tags':
        TunableSet(
            TunableEnumEntry(Tag, Tag.INVALID),
            description=
            'The Sim must have run either an interaction matching one of these Tags or an affordance from the list of Affordances in this tunable.'
        ),
        'test_event':
        TunableEnumEntry(
            description=
            '\n            The event that we want to trigger this instance of the tuned\n            test on.\n            InteractionStart: Triggers when the interaction starts.\n            InteractionComplete: Triggers when the interaction ends.\n            InteractionUpdate: Triggers on a 15 sim minute cadence from the\n            start of the interaction.  If the interaction ends before a cycle\n            is up it does not trigger.  Do not use this for short interactions\n            as it has a possibility of never getting an update for an\n            interaction.\n            ',
            tunable_type=InteractionTestEvents,
            default=InteractionTestEvents.InteractionComplete),
        'consider_cancelled_as_failure':
        Tunable(
            bool,
            True,
            description=
            '\n            If True, test will consider the interaction outcome to be Failure if canceled by the user.\n            '
        )
    }

    def __init__(self, participant, affordances, interaction_outcome,
                 running_time, target_filters, tags, test_event,
                 consider_cancelled_as_failure, **kwargs):
        super().__init__(**kwargs)
        self.participant_type = participant
        self.affordances = affordances
        self.interaction_outcome = interaction_outcome
        if running_time is not None:
            self.running_time = interval_in_sim_minutes(running_time)
        else:
            self.running_time = None
        self.tags = tags
        self.object_tags = target_filters.object_tags
        if test_event == InteractionTestEvents.InteractionUpdate:
            self.test_events = (test_event,
                                InteractionTestEvents.InteractionComplete)
        else:
            self.test_events = (test_event, )
        self.consider_cancelled_as_failure = consider_cancelled_as_failure

    def get_expected_args(self):
        return {
            'sims': event_testing.test_events.SIM_INSTANCE,
            'interaction': event_testing.test_events.FROM_EVENT_DATA
        }

    @cached_test
    def __call__(self, sims=None, interaction=None):
        if interaction is None:
            return TestResult(
                False,
                'No interaction found, this is normal during zone load.')
        for sim_info in sims:
            participant_type = interaction.get_participant_type(
                sim_info.get_sim_instance(
                    allow_hidden_flags=ALL_HIDDEN_REASONS))
            if participant_type is None:
                return TestResult(
                    False,
                    'Failed participant check: {} is not an instanced sim.',
                    sim_info)
            if participant_type != self.participant_type:
                return TestResult(False, 'Failed participant check: {} != {}',
                                  participant_type, self.participant_type)
            tag_match = len(self.tags & interaction.get_category_tags()
                            ) > 0 if self.tags else False
            if not tag_match and not (
                    interaction.affordance in self.affordances
                    or hasattr(interaction.affordance, 'proxied_affordance')
                    and interaction.affordance.proxied_affordance
                    in self.affordances):
                return TestResult(False,
                                  'Failed affordance check: {} not in {}',
                                  interaction.affordance, self.affordances)
            if self.object_tags is not None and not self.target_matches_object_tags(
                    interaction):
                return TestResult(
                    False,
                    "Target of interaction didn't match object tag requirement."
                )
            if self.interaction_outcome is not None:
                if self.consider_cancelled_as_failure and interaction.has_been_user_canceled and self.interaction_outcome != OutcomeResult.FAILURE:
                    return TestResult(
                        False,
                        'Failed outcome check: interaction canceled by user treated as Failure'
                    )
                if self.interaction_outcome == OutcomeResult.SUCCESS:
                    if interaction.outcome_result == OutcomeResult.FAILURE:
                        return TestResult(
                            False,
                            'Failed outcome check: interaction({}) failed when OutcomeResult Success or None required.',
                            interaction.affordance)
                        if self.interaction_outcome != interaction.outcome_result:
                            return TestResult(
                                False,
                                'Failed outcome check: interaction({}) result {} not {}',
                                interaction.affordance,
                                interaction.outcome_result,
                                self.interaction_outcome)
                elif self.interaction_outcome != interaction.outcome_result:
                    return TestResult(
                        False,
                        'Failed outcome check: interaction({}) result {} not {}',
                        interaction.affordance, interaction.outcome_result,
                        self.interaction_outcome)
            elif self.consider_cancelled_as_failure and interaction.has_been_user_canceled:
                return TestResult(
                    False,
                    'Failed outcome check: interaction canceled by user treated as Failure'
                )
            running_time = interaction.consecutive_running_time_span
            while self.running_time is not None and running_time < self.running_time:
                return TestResult(False, 'Failed hours check: {} < {}',
                                  running_time, self.running_time)
        return TestResult.TRUE

    def get_test_events_to_register(self):
        return ()

    def get_custom_event_registration_keys(self):
        keys = []
        for test_event in self.test_events:
            keys.extend([(test_event, affordance)
                         for affordance in self.affordances])
            keys.extend([(test_event, tag) for tag in self.tags])
        return keys

    def get_target_id(self, sims=None, interaction=None, id_type=None):
        if interaction is None or interaction.target is None:
            return
        if id_type == TargetIdTypes.DEFAULT or id_type == TargetIdTypes.DEFINITION:
            if interaction.target.is_sim:
                return interaction.target.id
            return interaction.target.definition.id
        if id_type == TargetIdTypes.INSTANCE:
            return interaction.target.id
        if id_type == TargetIdTypes.HOUSEHOLD:
            if not interaction.target.is_sim:
                logger.error(
                    'Unique target ID type: {} is not supported for test: {} with an object as target.',
                    id_type, self)
                return
            return interaction.target.household.id

    def get_posture_id(self, sims=None, interaction=None):
        if interaction is None or interaction.sim is None or interaction.sim.posture is None:
            return
        return interaction.sim.posture.guid64

    def get_tags(self, sims=None, interaction=None):
        if interaction is None:
            return ()
        return interaction.interaction_category_tags

    def tuning_is_valid(self):
        return len(self.tags) != 0 or len(self.affordances) != 0

    def target_matches_object_tags(self, interaction=None):
        if interaction is None or interaction.target is None or interaction.target.is_sim:
            return False
        object_id = interaction.target.definition.id
        target_object_tags = set(build_buy.get_object_all_tags(object_id))
        if self.object_tags.test_type == TagTestType.CONTAINS_ANY_TAG_IN_SET:
            return target_object_tags & self.object_tags.tag_set
        if self.object_tags.test_type == TagTestType.CONTAINS_ALL_TAGS_IN_SET:
            return target_object_tags & self.object_tags.tag_set == self.object_tags.tag_set
        if self.object_tags.test_type == TagTestType.CONTAINS_NO_TAGS_IN_SET:
            return not target_object_tags & self.object_tags.tag_set
        return False
Example #17
0
class PlayAudioMixin:
    INSTANCE_SUBCLASSES_ONLY = True
    INSTANCE_TUNABLES = {
        'play_multiple_clips':
        Tunable(
            description=
            '\n            If true, the Sim will continue playing until the interaction is\n            cancelled or exit conditions are met. \n            ',
            needs_tuning=False,
            tunable_type=bool,
            default=False),
        'music_styles':
        TunableList(
            description=
            '\n            List of music styles that are available for this interaction.\n            ',
            tunable=MusicStyle.TunableReference(
                description=
                '\n                A music style available for this interaction.\n                ',
                pack_safe=True)),
        'use_buffer':
        Tunable(
            description=
            "\n            If true, this interaction will add the buffer tuned on the music\n            track to the length of the track.  This is tunable because some\n            interactions, like Practice, use shorter audio clips that don't\n            require the buffer.\n            ",
            needs_tuning=False,
            tunable_type=bool,
            default=True),
        'instrument_participant':
        TunableEnumEntry(
            description=
            '\n            The participant that the music will play on.\n            ',
            tunable_type=ParticipantTypeSingle,
            default=ParticipantTypeSingle.Object),
        'audio_start_event':
        Tunable(
            description=
            '\n            The script event to listen for from animation so we know when to\n            start the music and vocals.\n            ',
            tunable_type=int,
            default=100),
        'audio_stop_event':
        Tunable(
            description=
            '\n            The script event to listen for from animation so we know when to\n            stop the music and vocals.\n            ',
            tunable_type=int,
            default=101),
        'mouthpiece_target':
        OptionalTunable(
            description=
            '\n            The participant of mine that mouthpieces must target as their mouthpiece\n            target.  e.g. if they are targeting the actor sim of this interaction, \n            their mouthpiece target would be targetsim, my mouthpiece target \n            would be actor.  If all of us are targeting a certain object then\n            both would be object.\n            ',
            tunable=TunableEnumEntry(
                description=
                '\n                The participant of mine that mouthpieces must target.\n                ',
                tunable_type=ParticipantTypeSingle,
                default=ParticipantTypeSingle.Object))
    }

    def __init__(self,
                 aop,
                 context,
                 track=None,
                 pie_menu_category=None,
                 unlockable_name=None,
                 **kwargs):
        super().__init__(aop, context, **kwargs)
        self._track = track
        self.pie_menu_category = pie_menu_category
        self._unlockable_name = unlockable_name
        self._sound_alarm = None
        self._sound = None
        self._vocals = {}

    def build_basic_content(self, sequence=(), **kwargs):
        self.store_event_handler(self._create_sound_alarm,
                                 self.audio_start_event)
        self.store_event_handler(self._cancel_sound_alarm,
                                 self.audio_stop_event)
        sequence = super().build_basic_content(sequence, **kwargs)
        return build_critical_section_with_finally(
            sequence, self._cancel_sound_alarm_no_data)

    def _get_mouthpiece_interaction(self, mouthpiece_object, sim):
        for interaction in sim.get_all_running_and_queued_interactions():
            if isinstance(interaction, PlayAudioMouthpieceSuperInteraction):
                if not interaction.is_finishing:
                    if interaction.is_mouthpiece_target(mouthpiece_object):
                        return interaction

    def _get_required_sims(self, *args, **kwargs):
        required_sims = super()._get_required_sims(*args, **kwargs)
        mouthpiece_target = None if self.mouthpiece_target is None else self.get_participant(
            self.mouthpiece_target)
        if mouthpiece_target is not None:
            for (musician, _) in self._musicians_and_vocals_gen():
                if self._get_mouthpiece_interaction(mouthpiece_target,
                                                    musician) is not None:
                    required_sims.add(musician)
        return required_sims

    def _create_sound_alarm(self, event_data, *args, **kwargs):
        if event_data is not None and event_data.event_data[
                'event_actor_id'] != self.sim.id:
            return
        if self._track is None:
            logger.error('Could not find a music track to play for {}',
                         self,
                         owner='rmccord')
            return
        track_length = self._get_track_length()
        if self._sound_alarm is None:
            self._sound_alarm = alarms.add_alarm(self, track_length,
                                                 self._sound_alarm_callback)
        if self._sound is None and self._track.music_clip is not None:
            instrument = self._get_instrument()
            if instrument is not None:
                self._sound = PlaySound(instrument,
                                        self._track.music_clip.instance)
                self._sound.start()
            else:
                logger.error('Instrument is None for participant {} in {}',
                             self.instrument_participant,
                             self,
                             owner='rmccord')
        mouthpiece_target = None if self.mouthpiece_target is None else self.get_participant(
            self.mouthpiece_target)
        for (musician, vocal_track) in self._musicians_and_vocals_gen():
            interaction = None
            if musician.is_sim and musician is not self.sim and mouthpiece_target is not None:
                interaction = self._get_mouthpiece_interaction(
                    mouthpiece_target, musician)
                if interaction is None:
                    continue
                interaction.set_mouthpiece_owner(self)
                liability = self.get_liability(
                    CANCEL_INTERACTION_ON_EXIT_LIABILITY)
                if liability is None:
                    liability = CancelInteractionsOnExitLiability()
                    self.add_liability(CANCEL_INTERACTION_ON_EXIT_LIABILITY,
                                       liability)
                liability.add_cancel_entry(musician, interaction)
            vocal = PlaySound(musician,
                              vocal_track.vocal_clip.instance,
                              is_vox=True)
            vocal.start()
            self._vocals[musician] = (vocal, interaction)

    def _sound_alarm_callback(self, handle):
        if self.play_multiple_clips:
            self._cancel_sound_alarm(None)
            styles = self._get_music_styles()
            self._track = PlayAudioMixin._get_next_track(
                styles, self.sim, self.get_resolver())
            self._create_sound_alarm(None)
        else:
            self.cancel(
                FinishingType.NATURAL,
                cancel_reason_msg=
                'Sound alarm triggered and the song finished naturally.')

    def stop_mouthpiece(self, sim):
        if sim in self._vocals:
            (vocal, interaction) = self._vocals.pop(sim)
            vocal.stop()
            if interaction is not None:
                self.get_liability(
                    CANCEL_INTERACTION_ON_EXIT_LIABILITY).remove_cancel_entry(
                        sim, interaction)

    def _cancel_sound_alarm_no_data(self, *args, **kwargs):
        self._cancel_sound_alarm(None)

    def _cancel_sound_alarm(self, event_data, *args, **kwargs):
        if event_data is not None and event_data.event_data[
                'event_actor_id'] != self.sim.id:
            return
        if self._sound_alarm is not None:
            alarms.cancel_alarm(self._sound_alarm)
            self._sound_alarm = None
        if self._sound is not None:
            self._sound.stop()
            self._sound = None
        liability = self.get_liability(CANCEL_INTERACTION_ON_EXIT_LIABILITY)
        for (vocal, interaction) in self._vocals.values():
            vocal.stop()
            if interaction is not None:
                interaction.set_mouthpiece_owner(None)
                liability.remove_cancel_entry(interaction.sim, interaction)
        self._vocals.clear()

    def _get_track_length(self):
        real_seconds = self._track.length
        if self.use_buffer:
            real_seconds += self._track.buffer
        interval = clock.interval_in_real_seconds(real_seconds)
        return interval

    def _get_instrument(self):
        return self.get_participant(self.instrument_participant)

    def _musicians_and_vocals_gen(self):
        resolver = self.get_resolver()
        for (participant_type, vocal_tracks) in self._track.vocals.items():
            for participant in resolver.get_participants(participant_type):
                participant = participant.get_sim_instance(
                    allow_hidden_flags=ALL_HIDDEN_REASONS_EXCEPT_UNINITIALIZED)
                if not isinstance(
                        participant,
                        sims.sim_info.SimInfo) or participant is None:
                    logger.warn('Musician participant {} is None for {}',
                                participant_type,
                                self,
                                owner='rmccord')
                else:
                    for track in vocal_tracks:
                        if track.tests.run_tests(resolver):
                            yield (participant, track)
                            break

    def _get_music_styles(self):
        return self.music_styles

    @staticmethod
    def _get_skill_level(skill_type, sim):
        skill = sim.get_statistic(skill_type, add=False)
        if skill is not None:
            return skill.get_user_value()
        elif skill_type.can_add(sim):
            return skill_type.get_user_value()
        return 0

    @staticmethod
    def _get_next_track(styles, sim, resolver):
        valid_tracks = []
        styles = set(styles)
        for (skill_type,
             level_to_tracks) in MusicStyle.tracks_by_skill.items():
            skill_level = PlayAudioMixin._get_skill_level(skill_type, sim)
            for track in level_to_tracks[skill_level]:
                if not styles & MusicStyle.styles_for_track[track]:
                    continue
                valid_tracks.append(track)
        sim_mood = sim.get_mood()
        valid_mood_tracks = [
            track for track in valid_tracks if sim_mood in track.moods
        ]
        if not valid_mood_tracks and not valid_tracks:
            return
        to_consider = valid_mood_tracks or valid_tracks
        random.shuffle(to_consider)
        for track in to_consider:
            if track.check_for_unlock and sim.sim_info.unlock_tracker is not None and sim.sim_info.unlock_tracker.is_unlocked(
                    track):
                return track
            if not track.check_for_unlock:
                if track.tests.run_tests(resolver):
                    return track

    @classmethod
    def _has_tracks(cls, sim, resolver):
        has_tracker = sim.sim_info.unlock_tracker is not None
        styles = set(cls.music_styles)
        for (skill_type,
             level_to_tracks) in MusicStyle.tracks_by_skill.items():
            skill_level = PlayAudioMixin._get_skill_level(skill_type, sim)
            for track in level_to_tracks[skill_level]:
                if not styles & MusicStyle.styles_for_track[track]:
                    continue
                if track.check_for_unlock and has_tracker and sim.sim_info.unlock_tracker.is_unlocked(
                        track):
                    return True
                if not track.check_for_unlock and track.tests.run_tests(
                        resolver):
                    return True
        return False

    @classmethod
    def _verify_tuning_callback(cls):
        if cls.is_super:
            for affordance in cls._content_sets.all_affordances_gen():
                if isinstance(affordance, PlayAudioMixin):
                    logger.error(
                        '{} references another PlayAudio interaction: {} in its content set. This will not properly work as the clip events will collide with one another.',
                        cls, affordance)
Example #18
0
class SimoleonsEarnedTest(event_testing.test_base.BaseTest):
    __qualname__ = 'SimoleonsEarnedTest'
    test_events = (event_testing.test_events.TestEvent.SimoleonsEarned, )
    USES_EVENT_DATA = True
    FACTORY_TUNABLES = {
        'description':
        'Require the participant(s) to (each) earn a specific amount of Simoleons for a skill or tag on an object sold.',
        'event_type_to_test':
        TunableVariant(
            skill_to_test=SkillTestFactory(),
            tags_to_test=TagSetTestFactory(),
            description='Test a skill for an event or tags on an object.'),
        'threshold':
        TunableThreshold(description='Amount in Simoleons required to pass'),
        'household_fund_threshold':
        OptionalTunable(
            description=
            '\n            Restricts test success based on household funds.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Household fund threshold and moment of evaluation.\n                ',
                threshold=TunableThreshold(
                    description=
                    '\n                    Amount of simoleons in household funds required to pass.\n                    '
                ),
                test_before_earnings=Tunable(
                    description=
                    '\n                    If True, threshold will be evaluated before funds were \n                    updated with earnings.\n                    ',
                    tunable_type=bool,
                    default=False)))
    }

    def __init__(self, event_type_to_test, threshold, household_fund_threshold,
                 **kwargs):
        super().__init__(**kwargs)
        self.event_type_to_test = event_type_to_test
        self.threshold = threshold
        self.household_fund_threshold = household_fund_threshold

    def get_expected_args(self):
        return {
            'sims': event_testing.test_events.SIM_INSTANCE,
            'amount': event_testing.test_events.FROM_EVENT_DATA,
            'skill_used': event_testing.test_events.FROM_EVENT_DATA,
            'tags': event_testing.test_events.FROM_EVENT_DATA
        }

    @cached_test
    def __call__(self, sims=None, amount=None, skill_used=None, tags=None):
        if amount is None:
            return TestResultNumeric(
                False,
                'SimoleonsEarnedTest: amount is none, valid during zone load.',
                current_value=0,
                goal_value=self.threshold.value,
                is_money=True)
        if not self.threshold.compare(amount):
            return TestResultNumeric(
                False,
                'SimoleonsEarnedTest: not enough Simoleons earned.',
                current_value=amount,
                goal_value=self.threshold.value,
                is_money=True)
        if not (self.event_type_to_test is not None
                and self.event_type_to_test(skill_used, tags)):
            return TestResult(
                False,
                '\n                    SimoleonsEarnedTest: the skill used to earn Simoleons does\n                    not match the desired skill or tuned tags do not match\n                    object tags.\n                    '
            )
        if self.household_fund_threshold is not None:
            for sim_info in sims:
                household = services.household_manager().get_by_sim_id(
                    sim_info.sim_id)
                if household is None:
                    return TestResult(False,
                                      "Couldn't find household for sim {}",
                                      sim_info)
                household_funds = household.funds.money
                if self.household_fund_threshold.test_before_earnings:
                    household_funds -= amount
                while not self.household_fund_threshold.threshold.compare(
                        household_funds):
                    return TestResult(
                        False,
                        'Threshold test on household funds failed for sim {}',
                        sim_info)
        return TestResult.TRUE

    def goal_value(self):
        return self.threshold.value
class PregnancyTracker(SimInfoTracker):
    PREGNANCY_COMMODITY_MAP = TunableMapping(
        description=
        '\n        The commodity to award if conception is successful.\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            Species these commodities are intended for.\n            ',
            tunable_type=Species,
            default=Species.HUMAN,
            invalid_enums=(Species.INVALID, )),
        value_type=TunableReference(
            description=
            '\n            The commodity reference controlling pregnancy.\n            ',
            pack_safe=True,
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC)))
    PREGNANCY_TRAIT = TunableReference(
        description=
        '\n        The trait that all pregnant Sims have during pregnancy.\n        ',
        manager=services.trait_manager())
    PREGNANCY_ORIGIN_TRAIT_MAPPING = TunableMapping(
        description=
        '\n        A mapping from PregnancyOrigin to a set of traits to be added at the\n        start of the pregnancy, and removed at the end of the pregnancy.\n        ',
        key_type=PregnancyOrigin,
        value_type=TunableTuple(
            description=
            '\n            A tuple of the traits that should be added/removed with a pregnancy\n            that has this origin, and the content pack they are associated with.\n            ',
            traits=TunableSet(
                description=
                '\n                The traits to be added/removed.\n                ',
                tunable=Trait.TunablePackSafeReference()),
            pack=TunableEnumEntry(
                description=
                '\n                The content pack associated with this set of traits. If the pack\n                is uninstalled, the pregnancy will be auto-completed.\n                ',
                tunable_type=Pack,
                default=Pack.BASE_GAME)))
    PREGNANCY_RATE = TunableRange(
        description='\n        The rate per Sim minute of pregnancy.\n        ',
        tunable_type=float,
        default=0.001,
        minimum=EPSILON)
    MULTIPLE_OFFSPRING_CHANCES = TunableList(
        description=
        '\n        A list defining the probabilities of multiple births.\n        ',
        tunable=TunableTuple(
            size=Tunable(
                description=
                '\n                The number of offspring born.\n                ',
                tunable_type=int,
                default=1),
            weight=Tunable(
                description=
                '\n                The weight, relative to other outcomes.\n                ',
                tunable_type=float,
                default=1),
            npc_dialog=UiDialogOk.TunableFactory(
                description=
                '\n                A dialog displayed when a NPC Sim gives birth to an offspring\n                that was conceived by a currently player-controlled Sim. The\n                dialog is specifically used when this number of offspring is\n                generated.\n                \n                Three tokens are passed in: the two parent Sims and the\n                offspring\n                ',
                locked_args={'text_tokens': None}),
            modifiers=TunableMultiplier.TunableFactory(
                description=
                '\n                A tunable list of test sets and associated multipliers to apply\n                to the total chance of this number of potential offspring.\n                '
            ),
            screen_slam_one_parent=OptionalTunable(
                description=
                '\n                Screen slam to show when only one parent is available.\n                Localization Tokens: Sim A - {0.SimFirstName}\n                ',
                tunable=TunableScreenSlamSnippet()),
            screen_slam_two_parents=OptionalTunable(
                description=
                '\n                Screen slam to show when both parents are available.\n                Localization Tokens: Sim A - {0.SimFirstName}, Sim B -\n                {1.SimFirstName}\n                ',
                tunable=TunableScreenSlamSnippet())))
    MONOZYGOTIC_OFFSPRING_CHANCE = TunablePercent(
        description=
        '\n        The chance that each subsequent offspring of a multiple birth has the\n        same genetics as the first offspring.\n        ',
        default=50)
    GENDER_CHANCE_STAT = TunableReference(
        description=
        '\n        A commodity that determines the chance that an offspring is female. The\n        minimum value guarantees the offspring is male, whereas the maximum\n        value guarantees it is female.\n        ',
        manager=services.statistic_manager())
    BIRTHPARENT_BIT = RelationshipBit.TunableReference(
        description=
        '\n        The bit that is added on the relationship from the Sim to any of its\n        offspring.\n        '
    )
    AT_BIRTH_TESTS = TunableGlobalTestSet(
        description=
        '\n        Tests to run between the pregnant sim and their partner, at the time of\n        birth. If any test fails, the the partner sim will not be set as the\n        other parent. This is intended to prevent modifications to the partner\n        sim during the time between impregnation and birth that would make the\n        partner sim an invalid parent (age too young, relationship incestuous, etc).\n        '
    )
    PREGNANCY_ORIGIN_MODIFIERS = TunableMapping(
        description=
        '\n        Define any modifiers that, given the origination of the pregnancy,\n        affect certain aspects of the generated offspring.\n        ',
        key_type=TunableEnumEntry(
            description=
            '\n            The origin of the pregnancy.\n            ',
            tunable_type=PregnancyOrigin,
            default=PregnancyOrigin.DEFAULT,
            pack_safe=True),
        value_type=TunableTuple(
            description=
            '\n            The aspects of the pregnancy modified specifically for the specified\n            origin.\n            ',
            default_relationships=TunableTuple(
                description=
                '\n                Override default relationships for the parents.\n                ',
                father_override=OptionalTunable(
                    description=
                    '\n                    If set, override default relationships for the father.\n                    ',
                    tunable=TunableEnumEntry(
                        description=
                        '\n                        The default relationships for the father.\n                        ',
                        tunable_type=DefaultGenealogyLink,
                        default=DefaultGenealogyLink.FamilyMember)),
                mother_override=OptionalTunable(
                    description=
                    '\n                    If set, override default relationships for the mother.\n                    ',
                    tunable=TunableEnumEntry(
                        description=
                        '\n                        The default relationships for the mother.\n                        ',
                        tunable_type=DefaultGenealogyLink,
                        default=DefaultGenealogyLink.FamilyMember))),
            trait_entries=TunableList(
                description=
                '\n                Sets of traits that might be randomly applied to each generated\n                offspring. Each group is individually randomized.\n                ',
                tunable=TunableTuple(
                    description=
                    '\n                    A set of random traits. Specify a chance that a trait from\n                    the group is selected, and then specify a set of traits.\n                    Only one trait from this group may be selected. If the\n                    chance is less than 100%, no traits could be selected.\n                    ',
                    chance=TunablePercent(
                        description=
                        '\n                        The chance that a trait from this set is selected.\n                        ',
                        default=100),
                    traits=TunableList(
                        description=
                        '\n                        The set of traits that might be applied to each\n                        generated offspring. Specify a weight for each trait\n                        compared to other traits in the same set.\n                        ',
                        tunable=TunableTuple(
                            description=
                            '\n                            A weighted trait that might be applied to the\n                            generated offspring. The weight is relative to other\n                            entries within the same set.\n                            ',
                            weight=Tunable(
                                description=
                                '\n                                The relative weight of this trait compared to\n                                other traits within the same set.\n                                ',
                                tunable_type=float,
                                default=1),
                            trait=Trait.TunableReference(
                                description=
                                '\n                                A trait that might be applied to the generated\n                                offspring.\n                                ',
                                pack_safe=True)))))))

    def __init__(self, sim_info):
        self._sim_info = sim_info
        self._clear_pregnancy_data()
        self._completion_callback_listener = None
        self._completion_alarm_handle = None

    @property
    def account(self):
        return self._sim_info.account

    @property
    def is_pregnant(self):
        if self._seed:
            return True
        return False

    @property
    def offspring_count(self):
        return max(len(self._offspring_data), 1)

    @property
    def offspring_count_override(self):
        return self._offspring_count_override

    @offspring_count_override.setter
    def offspring_count_override(self, value):
        self._offspring_count_override = value

    def _get_parent(self, sim_id):
        sim_info_manager = services.sim_info_manager()
        if sim_id in sim_info_manager:
            return sim_info_manager.get(sim_id)

    def get_parents(self):
        if self._parent_ids:
            parent_a = self._get_parent(self._parent_ids[0])
            parent_b = self._get_parent(self._parent_ids[1]) or parent_a
            return (parent_a, parent_b)
        return (None, None)

    def get_partner(self):
        (owner, partner) = self.get_parents()
        if partner is not owner:
            return partner

    def start_pregnancy(self,
                        parent_a,
                        parent_b,
                        pregnancy_origin=PregnancyOrigin.DEFAULT):
        if self.is_pregnant:
            return
        if not parent_a.incest_prevention_test(parent_b):
            return
        self._seed = random.randint(1, MAX_UINT32)
        self._parent_ids = (parent_a.id, parent_b.id)
        self._offspring_data = []
        self._origin = pregnancy_origin
        self.enable_pregnancy()

    def enable_pregnancy(self):
        if self.is_pregnant:
            if not self._is_enabled:
                pregnancy_commodity_type = self.PREGNANCY_COMMODITY_MAP.get(
                    self._sim_info.species)
                tracker = self._sim_info.get_tracker(pregnancy_commodity_type)
                pregnancy_commodity = tracker.get_statistic(
                    pregnancy_commodity_type, add=True)
                pregnancy_commodity.add_statistic_modifier(self.PREGNANCY_RATE)
                threshold = sims4.math.Threshold(pregnancy_commodity.max_value,
                                                 operator.ge)
                self._completion_callback_listener = tracker.create_and_add_listener(
                    pregnancy_commodity.stat_type, threshold,
                    self._on_pregnancy_complete)
                if threshold.compare(pregnancy_commodity.get_value()):
                    self._on_pregnancy_complete()
                tracker = self._sim_info.get_tracker(self.GENDER_CHANCE_STAT)
                tracker.add_statistic(self.GENDER_CHANCE_STAT)
                self._sim_info.add_trait(self.PREGNANCY_TRAIT)
                traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get(
                    self._origin)
                if traits_pack_tuple is not None:
                    for trait in traits_pack_tuple.traits:
                        self._sim_info.add_trait(trait)
                self._is_enabled = True

    def _on_pregnancy_complete(self, *_, **__):
        if not self.is_pregnant:
            return
        if self._sim_info.is_npc:
            current_zone = services.current_zone()
            if not current_zone.is_zone_running or self._sim_info.is_instanced(
                    allow_hidden_flags=ALL_HIDDEN_REASONS):
                if self._completion_alarm_handle is None:
                    self._completion_alarm_handle = alarms.add_alarm(
                        self,
                        clock.interval_in_sim_minutes(1),
                        self._on_pregnancy_complete,
                        repeating=True,
                        cross_zone=True)
            else:
                self._create_and_name_offspring()
                self._show_npc_dialog()
                self.clear_pregnancy()

    def complete_pregnancy(self):
        services.get_event_manager().process_event(
            TestEvent.OffspringCreated,
            sim_info=self._sim_info,
            offspring_created=self.offspring_count)
        for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES:
            if tuning_data.size == self.offspring_count:
                (parent_a, parent_b) = self.get_parents()
                if parent_a is parent_b:
                    screen_slam = tuning_data.screen_slam_one_parent
                else:
                    screen_slam = tuning_data.screen_slam_two_parents
                if screen_slam is not None:
                    screen_slam.send_screen_slam_message(
                        self._sim_info, parent_a, parent_b)
                break

    def _clear_pregnancy_data(self):
        self._seed = 0
        self._parent_ids = []
        self._offspring_data = []
        self._offspring_count_override = None
        self._origin = PregnancyOrigin.DEFAULT
        self._is_enabled = False

    def clear_pregnancy_visuals(self):
        if self._sim_info.pregnancy_progress:
            self._sim_info.pregnancy_progress = 0

    def clear_pregnancy(self):
        pregnancy_commodity_type = self.PREGNANCY_COMMODITY_MAP.get(
            self._sim_info.species)
        tracker = self._sim_info.get_tracker(pregnancy_commodity_type)
        if tracker is not None:
            stat = tracker.get_statistic(pregnancy_commodity_type, add=True)
            if stat is not None:
                stat.set_value(stat.min_value)
                stat.remove_statistic_modifier(self.PREGNANCY_RATE)
            if self._completion_callback_listener is not None:
                tracker.remove_listener(self._completion_callback_listener)
                self._completion_callback_listener = None
        tracker = self._sim_info.get_tracker(self.GENDER_CHANCE_STAT)
        if tracker is not None:
            tracker.remove_statistic(self.GENDER_CHANCE_STAT)
        if self._sim_info.has_trait(self.PREGNANCY_TRAIT):
            self._sim_info.remove_trait(self.PREGNANCY_TRAIT)
        traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get(
            self._origin)
        if traits_pack_tuple is not None:
            for trait in traits_pack_tuple.traits:
                if self._sim_info.has_trait(trait):
                    self._sim_info.remove_trait(trait)
        if self._completion_alarm_handle is not None:
            alarms.cancel_alarm(self._completion_alarm_handle)
            self._completion_alarm_handle = None
        self.clear_pregnancy_visuals()
        self._clear_pregnancy_data()

    def _create_and_name_offspring(self, on_create=None):
        self.create_offspring_data()
        for offspring_data in self.get_offspring_data_gen():
            offspring_data.first_name = self._get_random_first_name(
                offspring_data)
            sim_info = self.create_sim_info(offspring_data)
            if on_create is not None:
                on_create(sim_info)

    def validate_partner(self):
        impregnator = self.get_partner()
        if impregnator is None:
            return
        resolver = DoubleSimResolver(self._sim_info, impregnator)
        if not self.AT_BIRTH_TESTS.run_tests(resolver):
            self._parent_ids = (self._sim_info.id, self._sim_info.id)

    def create_sim_info(self, offspring_data):
        self.validate_partner()
        (parent_a, parent_b) = self.get_parents()
        sim_creator = SimCreator(age=offspring_data.age,
                                 gender=offspring_data.gender,
                                 species=offspring_data.species,
                                 first_name=offspring_data.first_name,
                                 last_name=offspring_data.last_name)
        household = self._sim_info.household
        zone_id = household.home_zone_id
        (sim_info_list, _) = SimSpawner.create_sim_infos(
            (sim_creator, ),
            household=household,
            account=self.account,
            zone_id=zone_id,
            generate_deterministic_sim=True,
            creation_source='pregnancy')
        sim_info = sim_info_list[0]
        sim_info.world_id = services.get_persistence_service(
        ).get_world_id_from_zone(zone_id)
        for trait in tuple(sim_info.trait_tracker.personality_traits):
            sim_info.remove_trait(trait)
        for trait in offspring_data.traits:
            sim_info.add_trait(trait)
        sim_info.apply_genetics(parent_a,
                                parent_b,
                                seed=offspring_data.genetics)
        sim_info.resend_extended_species()
        sim_info.resend_physical_attributes()
        default_track_overrides = {}
        mother = parent_a if parent_a.gender == Gender.FEMALE else parent_b
        father = parent_a if parent_a.gender == Gender.MALE else parent_b
        if self._origin in self.PREGNANCY_ORIGIN_MODIFIERS:
            father_override = self.PREGNANCY_ORIGIN_MODIFIERS[
                self._origin].default_relationships.father_override
            if father_override is not None:
                default_track_overrides[father] = father_override
            mother_override = self.PREGNANCY_ORIGIN_MODIFIERS[
                self._origin].default_relationships.mother_override
            if mother_override is not None:
                default_track_overrides[mother] = mother_override
        self.initialize_sim_info(
            sim_info,
            parent_a,
            parent_b,
            default_track_overrides=default_track_overrides)
        self._sim_info.relationship_tracker.add_relationship_bit(
            sim_info.id, self.BIRTHPARENT_BIT)
        return sim_info

    @staticmethod
    def initialize_sim_info(sim_info,
                            parent_a,
                            parent_b,
                            default_track_overrides=None):
        sim_info.add_parent_relations(parent_a, parent_b)
        if sim_info.household is not parent_a.household:
            parent_a.household.add_sim_info_to_household(sim_info)
        sim_info.set_default_relationships(
            reciprocal=True, default_track_overrides=default_track_overrides)
        services.sim_info_manager().set_default_genealogy(
            sim_infos=(sim_info, ))
        parent_generation = max(
            parent_a.generation,
            parent_b.generation if parent_b is not None else 0)
        sim_info.generation = parent_generation + 1 if sim_info.is_played_sim else parent_generation
        services.get_event_manager().process_event(TestEvent.GenerationCreated,
                                                   sim_info=sim_info)
        client = services.client_manager().get_client_by_household_id(
            sim_info.household_id)
        if client is not None:
            client.add_selectable_sim_info(sim_info)
        parent_b_sim_id = parent_b.sim_id if parent_b is not None else 0
        RelgraphService.relgraph_add_child(parent_a.sim_id, parent_b_sim_id,
                                           sim_info.sim_id)

    @classmethod
    def select_traits_for_offspring(cls,
                                    offspring_data,
                                    parent_a,
                                    parent_b,
                                    num_traits,
                                    origin=PregnancyOrigin.DEFAULT,
                                    random=random):
        traits = []
        personality_trait_slots = num_traits

        def _add_trait_if_possible(selected_trait):
            nonlocal personality_trait_slots
            if selected_trait in traits:
                return False
            if any(t.is_conflicting(selected_trait) for t in traits):
                return False
            if selected_trait.is_personality_trait:
                if not personality_trait_slots:
                    return False
                personality_trait_slots -= 1
            traits.append(selected_trait)
            return True

        if origin in cls.PREGNANCY_ORIGIN_MODIFIERS:
            trait_entries = cls.PREGNANCY_ORIGIN_MODIFIERS[
                origin].trait_entries
            for trait_entry in trait_entries:
                if random.random() >= trait_entry.chance:
                    continue
                selected_trait = pop_weighted(
                    [(t.weight, t.trait) for t in trait_entry.traits
                     if t.trait.is_valid_trait(offspring_data)],
                    random=random)
                if selected_trait is not None:
                    _add_trait_if_possible(selected_trait)
        if parent_a is not None:
            if parent_b is not None:
                for inherited_trait_entries in parent_a.trait_tracker.get_inherited_traits(
                        parent_b):
                    selected_trait = pop_weighted(
                        list(inherited_trait_entries), random=random)
                    if selected_trait is not None:
                        _add_trait_if_possible(selected_trait)
        if not personality_trait_slots:
            return traits
        personality_traits = get_possible_traits(offspring_data)
        random.shuffle(personality_traits)
        while True:
            current_trait = personality_traits.pop()
            if _add_trait_if_possible(current_trait):
                break
            if not personality_traits:
                return traits
        if not personality_trait_slots:
            return traits
        traits_a = set(parent_a.trait_tracker.personality_traits)
        traits_b = set(parent_b.trait_tracker.personality_traits)
        shared_parent_traits = list(
            traits_a.intersection(traits_b) - set(traits))
        random.shuffle(shared_parent_traits)
        while personality_trait_slots:
            while shared_parent_traits:
                current_trait = shared_parent_traits.pop()
                if current_trait in personality_traits:
                    personality_traits.remove(current_trait)
                did_add_trait = _add_trait_if_possible(current_trait)
                if did_add_trait:
                    if not personality_trait_slots:
                        return traits
        remaining_parent_traits = list(
            traits_a.symmetric_difference(traits_b) - set(traits))
        random.shuffle(remaining_parent_traits)
        while personality_trait_slots:
            while remaining_parent_traits:
                current_trait = remaining_parent_traits.pop()
                if current_trait in personality_traits:
                    personality_traits.remove(current_trait)
                did_add_trait = _add_trait_if_possible(current_trait)
                if did_add_trait:
                    if not personality_trait_slots:
                        return traits
        while personality_trait_slots:
            while personality_traits:
                current_trait = personality_traits.pop()
                _add_trait_if_possible(current_trait)
        return traits

    def create_offspring_data(self):
        r = random.Random()
        r.seed(self._seed)
        if self._offspring_count_override is not None:
            offspring_count = self._offspring_count_override
        else:
            offspring_count = pop_weighted([
                (p.weight *
                 p.modifiers.get_multiplier(SingleSimResolver(self._sim_info)),
                 p.size) for p in self.MULTIPLE_OFFSPRING_CHANCES
            ],
                                           random=r)
        offspring_count = min(self._sim_info.household.free_slot_count + 1,
                              offspring_count)
        species = self._sim_info.species
        age = self._sim_info.get_birth_age()
        aging_data = AgingTuning.get_aging_data(species)
        num_personality_traits = aging_data.get_personality_trait_count(age)
        self._offspring_data = []
        for offspring_index in range(offspring_count):
            if offspring_index and r.random(
            ) < self.MONOZYGOTIC_OFFSPRING_CHANCE:
                gender = self._offspring_data[offspring_index - 1].gender
                genetics = self._offspring_data[offspring_index - 1].genetics
            else:
                gender_chance_stat = self._sim_info.get_statistic(
                    self.GENDER_CHANCE_STAT)
                if gender_chance_stat is None:
                    gender_chance = 0.5
                else:
                    gender_chance = (gender_chance_stat.get_value() -
                                     gender_chance_stat.min_value) / (
                                         gender_chance_stat.max_value -
                                         gender_chance_stat.min_value)
                gender = Gender.FEMALE if r.random(
                ) < gender_chance else Gender.MALE
                genetics = r.randint(1, MAX_UINT32)
            last_name = SimSpawner.get_last_name(self._sim_info.last_name,
                                                 gender, species)
            offspring_data = PregnancyOffspringData(age,
                                                    gender,
                                                    species,
                                                    genetics,
                                                    last_name=last_name)
            (parent_a, parent_b) = self.get_parents()
            offspring_data.traits = self.select_traits_for_offspring(
                offspring_data,
                parent_a,
                parent_b,
                num_personality_traits,
                origin=self._origin)
            self._offspring_data.append(offspring_data)

    def get_offspring_data_gen(self):
        for offspring_data in self._offspring_data:
            yield offspring_data

    def _get_random_first_name(self, offspring_data):
        tries_left = 10

        def is_valid(first_name):
            nonlocal tries_left
            if not first_name:
                return False
            tries_left -= 1
            if tries_left and any(sim.first_name == first_name
                                  for sim in self._sim_info.household):
                return False
            elif any(sim.first_name == first_name
                     for sim in self._offspring_data):
                return False
            return True

        first_name = None
        while not is_valid(first_name):
            first_name = SimSpawner.get_random_first_name(
                offspring_data.gender, offspring_data.species)
        return first_name

    def assign_random_first_names_to_offspring_data(self):
        for offspring_data in self.get_offspring_data_gen():
            offspring_data.first_name = self._get_random_first_name(
                offspring_data)

    def _show_npc_dialog(self):
        for tuning_data in self.MULTIPLE_OFFSPRING_CHANCES:
            if tuning_data.size == self.offspring_count:
                npc_dialog = tuning_data.npc_dialog
                if npc_dialog is not None:
                    for parent in self.get_parents():
                        if parent is None:
                            logger.error(
                                'Pregnancy for {} has a None parent for IDs {}. Please file a DT with a save attached.',
                                self._sim_info, ','.join(
                                    str(parent_id)
                                    for parent_id in self._parent_ids))
                            return
                        parent_instance = parent.get_sim_instance()
                        if parent_instance is not None:
                            if parent_instance.client is not None:
                                additional_tokens = list(
                                    itertools.chain(self.get_parents(),
                                                    self._offspring_data))
                                dialog = npc_dialog(
                                    parent_instance,
                                    DoubleSimResolver(additional_tokens[0],
                                                      additional_tokens[1]))
                                dialog.show_dialog(
                                    additional_tokens=additional_tokens)
                return

    def save(self):
        data = SimObjectAttributes_pb2.PersistablePregnancyTracker()
        data.seed = self._seed
        data.origin = self._origin
        data.parent_ids.extend(self._parent_ids)
        return data

    def load(self, data):
        self._seed = int(data.seed)
        try:
            self._origin = PregnancyOrigin(data.origin)
        except KeyError:
            self._origin = PregnancyOrigin.DEFAULT
        self._parent_ids.clear()
        self._parent_ids.extend(data.parent_ids)

    def refresh_pregnancy_data(self, on_create=None):
        if not self.is_pregnant:
            self.clear_pregnancy()
            return
        traits_pack_tuple = self.PREGNANCY_ORIGIN_TRAIT_MAPPING.get(
            self._origin)
        if traits_pack_tuple is not None and not is_available_pack(
                traits_pack_tuple.pack):
            self._create_and_name_offspring(on_create=on_create)
            self.clear_pregnancy()
        self.enable_pregnancy()

    def on_lod_update(self, old_lod, new_lod):
        if new_lod == SimInfoLODLevel.MINIMUM:
            self.clear_pregnancy()
class BaseFestivalState(HasTunableFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        '_situations':
        TunableList(
            description=
            '\n            The different Situations that should be running at this time.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The Tunables for a single entry in this list.  Each entry\n                includes a minimum number of situations that we want to have\n                running from this entry and a list of weighted Situations.\n                We try and ensure a minimum number of the situations specified\n                within the Situations list will exist.\n                ',
                number_of_situations=TunableRange(
                    description=
                    '\n                    The number of situations that we want to have running from\n                    this entry.  This is the Minimum number of situations that\n                    we try and maintain.  If the number of situations exceeds\n                    this we will not destroy situations to reduce ourself to\n                    this value.\n                    ',
                    tunable_type=int,
                    default=1,
                    minimum=1),
                object_tag_requirement=OptionalTunable(
                    description=
                    '\n                    If enabled then we will cap the number of situations\n                    created by this entry at either the minimum of objects\n                    created for this situation by tuned tag or the Number of\n                    Situations tunable.\n                    ',
                    tunable=TunableEnumEntry(
                        description=
                        '\n                        A specific tag that an object on this lot must have for this\n                        situation to be allowed to start.\n                        ',
                        tunable_type=Tag,
                        default=Tag.INVALID,
                        invalid_enums=(Tag.INVALID, ))),
                situations=TunableList(
                    description=
                    '\n                    A weighted list of situations that can be chosen for this\n                    state in the festival.\n                    ',
                    tunable=TunableTuple(
                        description=
                        '\n                        A pair between a weight and a situation that can be\n                        chosen.\n                        ',
                        weight=TunableRange(
                            description=
                            '\n                            Weight for each of the different situations that\n                            can be chosen.\n                            ',
                            tunable_type=float,
                            default=1,
                            minimum=1),
                        situation=TunableReference(
                            description=
                            '\n                            The situation that can be chosen.  We will run any\n                            tests that GPEs have added to determine if this\n                            situation has been valid before actually selecting\n                            it.\n                            ',
                            manager=services.get_instance_manager(
                                sims4.resources.Types.SITUATION),
                            class_restrictions=(
                                BaseGenericFestivalSituation, ))))))
    }

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

    CHECK_SITUATIONS_ALARM_TIME = 10
    CHECK_SITUATION_ALARM_KEY = 'check_situation_alarm'

    def __init__(self, owner, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._owner = owner
        self._check_situations_alarm_handle = None
        self._alarms = {}

    @classproperty
    def key(cls):
        raise NotImplementedError

    def _test_situation(self, situation):
        return situation.situation_meets_starting_requirements()

    def _create_required_number_of_situations(self):
        situation_manager = services.get_zone_situation_manager()
        running_situations = [
            type(situation)
            for situation in self._owner.get_running_festival_situations()
        ]
        for situation_entry in self._situations:
            number_of_situations_running = 0
            for situation_type_entry in situation_entry.situations:
                number_of_situations_running += running_situations.count(
                    situation_type_entry.situation)
            if situation_entry.object_tag_requirement is None:
                required_situations = situation_entry.number_of_situations
            else:
                tagged_objects = 0
                for obj in self._owner.get_all_layer_created_objects():
                    if build_buy.get_object_has_tag(
                            obj.definition.id,
                            situation_entry.object_tag_requirement):
                        tagged_objects += 1
                required_situations = min(tagged_objects,
                                          situation_entry.number_of_situations)
            if number_of_situations_running < required_situations:
                possible_situations = [
                    (situation_type_entry.weight,
                     situation_type_entry.situation)
                    for situation_type_entry in situation_entry.situations
                    if self._test_situation(situation_type_entry.situation)
                ]
                if possible_situations:
                    for _ in range(situation_entry.number_of_situations -
                                   number_of_situations_running):
                        situation = weighted_random_item(possible_situations)
                        guest_list = situation.get_predefined_guest_list()
                        if guest_list is None:
                            guest_list = SituationGuestList(invite_only=True)
                        situation_id = situation_manager.create_situation(
                            situation,
                            guest_list=guest_list,
                            spawn_sims_during_zone_spin_up=True,
                            user_facing=False)
                        self._owner._add_created_situation(situation_id)

    def _create_situations_callback(self, _):
        self._create_required_number_of_situations()

    def on_state_activated(self, reader=None, preroll_time_override=None):
        self.schedule_alarm(self.CHECK_SITUATION_ALARM_KEY,
                            self.CHECK_SITUATIONS_ALARM_TIME,
                            self._create_situations_callback,
                            repeating=True,
                            should_persist=False,
                            reader=reader)

    def on_state_deactivated(self):
        for alarm_data in self._alarms.values():
            alarms.cancel_alarm(alarm_data.alarm_handle)

    def on_layer_loaded(self, conditional_layer):
        pass

    def on_layer_objects_destroyed(self, conditional_layer):
        pass

    def save(self, writer):
        for (alarm_key, alarm_data) in self._alarms.items():
            if alarm_data.should_persist:
                writer.write_float(
                    alarm_key,
                    alarm_data.alarm_handle.get_remaining_time().in_minutes())

    def schedule_alarm(self,
                       alarm_key,
                       alarm_time,
                       callback,
                       repeating=False,
                       should_persist=True,
                       reader=None):
        if should_persist:
            if reader is not None:
                alarm_time = reader.read_float(alarm_key, alarm_time)
        alarm_handle = alarms.add_alarm(self,
                                        create_time_span(minutes=alarm_time),
                                        callback,
                                        repeating=repeating)
        self._alarms[alarm_key] = FestivalAlarmData(should_persist,
                                                    alarm_handle)

    def _run_preroll(self):
        self._create_required_number_of_situations()

    def _get_fake_preroll_time(self):
        pass

    def _preroll_end_of_state(self):
        raise NotImplementedError

    def preroll(self, time_to_preroll):
        if time_to_preroll is None:
            return
        self._run_preroll()
        time_spent = self._get_fake_preroll_time()
        if time_spent is None:
            return TimeSpan(0)
        time_left = time_to_preroll - time_spent
        if time_left > TimeSpan.ZERO:
            self._preroll_end_of_state()
        return time_left
class WalksStyleBehavior(HasTunableSingletonFactory, AutoFactoryInit):
    SWIMMING_WALKSTYLES = TunableList(
        description=
        '\n        The exhaustive list of walkstyles allowed while Sims are swimming. If a\n        Sim has a request for a walkstyle that is not supported, the first\n        element is used as a replacement.\n        ',
        tunable=TunableWalkstyle(pack_safe=True))
    WALKSTYLE_COST = TunableMapping(
        description=
        '\n        Associate a specific walkstyle to a statistic cost before the walkstyle\n        can be activated.\n        ',
        key_type=TunableWalkstyle(
            description=
            '\n            The walkstyle that should have a specified cost when triggered.\n            ',
            pack_safe=True),
        value_type=TunableTuple(
            description=
            '\n            Cost data of the specified walkstyle.\n            ',
            walkstyle_cost_statistic=TunableReference(
                description=
                '\n                The statistic we are operating on when the walkstyle is\n                triggered.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.STATISTIC),
                pack_safe=True),
            cost=TunableRange(
                description=
                '\n                When the walkstyle is triggered during a route, this is the\n                cost that will be substracted from the specified statistic. \n                ',
                tunable_type=int,
                default=1,
                minimum=0)))
    WALKSTYLES_OVERRIDE_TELEPORT = TunableList(
        description=
        '\n        Any walkstyles found here will be able to override the teleport styles\n        if they are specified.\n        ',
        tunable=TunableWalkstyle(pack_safe=True))
    FACTORY_TUNABLES = {
        'carry_walkstyle_behavior':
        OptionalTunable(
            description=
            '\n            Define the walkstyle the Sim plays whenever they are being carried\n            by another Sim.\n            \n            If this is set to "no behavior", the Sim will not react to the\n            parent\'s walkstyle at all. They will play no walkstyle, and rely on\n            posture idles to animate.\n            \n            If this is set, Sims have the ability to modify their walkstyle\n            whenever their parent is routing.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Specify how this Sim behaves when being carried by another Sim.\n                ',
                default_carry_walkstyle=TunableWalkstyle(
                    description=
                    '\n                    Unless an override is specified, this is the walkstyle\n                    applied to the Sim whenever they are being carried.\n                    '
                ),
                carry_walkstyle_overrides=TunableMapping(
                    description=
                    '\n                    Define carry walkstyle overrides. For instance, we might\n                    want to specify a different carry walkstyle if the parent is\n                    frantically running, for example.\n                    ',
                    key_type=TunableWalkstyle(
                        description=
                        '\n                        The walkstyle that this carry walkstyle override applies to.\n                        ',
                        pack_safe=True),
                    value_type=TunableWalkstyle(
                        description=
                        '\n                        The carry walkstyle override.\n                        ',
                        pack_safe=True))),
            enabled_name='Apply_Carry_Walkstyle',
            disabled_name='No_Behavior'),
        'combo_walkstyle_replacements':
        TunableList(
            description=
            '\n            The prioritized list of the combo walkstyle replacement rules. We\n            use this list to decide if a Sim should use a combo walk style based\n            on the the highest priority walkstyle request, and other walkstyles\n            that might affect the replacement based on the key combo rules.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The n->1 mapping of walkstyle replacement. \n                ',
                key_combo_list=TunableList(
                    description=
                    '\n                    The list of the walkstyles used as key combos. If the\n                    current highest priority walkstyle exists in this list, and\n                    the Sim has every other walkstyle in the key list, then we\n                    replace this with the result walkstyle tuned in the tuple.\n                    ',
                    tunable=TunableWalkstyle(pack_safe=True)),
                result=TunableWalkstyle(
                    description=
                    '\n                    The mapped combo walkstyle.\n                    ',
                    pack_safe=True))),
        'default_walkstyle':
        TunableWalkstyle(
            description=
            '\n            The underlying walkstyle for this Sim. This is most likely going to\n            be overridden by the CAS walkstyle, emotional walkstyles, buff\n            walkstyles, etc...\n            '
        ),
        'run_allowed_flags':
        TunableEnumFlags(
            description=
            "\n            Define where the Sim is allowed to run. Certain buffs might suppress\n            a Sim's ability to run.\n            ",
            enum_type=WalkStyleRunAllowedFlags,
            default=WalkStyleRunAllowedFlags.RUN_ALLOWED_OUTDOORS,
            allow_no_flags=True),
        'run_disallowed_walkstyles':
        TunableList(
            description=
            "\n            A set of walkstyles that would never allow a Sim to run, i.e., if\n            the Sim's requested walkstyle is in this set, they will not run,\n            even to cover great distances.\n            ",
            tunable=TunableWalkstyle(pack_safe=True)),
        'run_required_total_distance':
        TunableRange(
            description=
            '\n            For an entire route, the minimum distance required for Sim to run.\n            ',
            tunable_type=float,
            minimum=0,
            default=20),
        'run_required_segment_distance':
        TunableRange(
            description=
            '\n            For a specific route segment, the minimum distance required for the\n            Sim to run.\n            ',
            tunable_type=float,
            minimum=0,
            default=10),
        'run_walkstyle':
        TunableWalkstyle(
            description=
            '\n            The walkstyle to use when this Sim is supposed to be running.\n            '
        ),
        'wading_walkstyle':
        OptionalTunable(
            description=
            '\n            If enabled, the routing agent will play a different walkstyle when\n            walking through water.\n            ',
            tunable=TunableWalkstyle(
                description=
                '\n                The walkstyle to use when wading through water.\n                '
            )),
        'wading_walkstyle_buff':
        OptionalTunable(
            description=
            '\n            A buff which, if tuned, will be on the sim if the sim is currently\n            in wading level water.\n            ',
            tunable=TunableReference(manager=services.get_instance_manager(
                sims4.resources.Types.BUFF))),
        'short_walkstyle':
        TunableWalkstyle(
            description=
            '\n            The walkstyle to use when Sims are routing over a distance shorter\n            than the one defined in "Short Walkstyle Distance" or any of the\n            overrides.\n            \n            This value is used if no override is tuned in "Short Walkstyle Map".\n            '
        ),
        'short_walkstyle_distance':
        TunableRange(
            description=
            "\n            Any route whose distance is less than this value will request the\n            short version of the Sim's current walkstyle.\n            ",
            tunable_type=float,
            minimum=0,
            default=7),
        'short_walkstyle_distance_override_map':
        TunableMapping(
            description=
            "\n            If a Sim's current walkstyle is any of the ones specified in here,\n            use the associated value to determine if the short version of the\n            walkstyle is to be requested.\n            ",
            key_type=TunableWalkstyle(
                description=
                '\n                The walkstyle that this distance override applies to.\n                ',
                pack_safe=True),
            value_type=TunableRange(
                description=
                "\n                Any route whose distance is less than this value will request\n                the short version of the Sim's current walkstyle, provided the\n                Sim's current walkstyle is the associated walkstyle.\n                ",
                tunable_type=float,
                minimum=0,
                default=7)),
        'short_walkstyle_map':
        TunableMapping(
            description=
            '\n            Associate a specific short version of a walkstyle to walkstyles.\n            ',
            key_type=TunableWalkstyle(
                description=
                '\n                The walkstyle that this short walkstyle mapping applies to.\n                ',
                pack_safe=True),
            value_type=TunableWalkstyle(
                description=
                '\n                The short version of the associated walkstyle.\n                ',
                pack_safe=True))
    }

    def _get_walkstyle_overrides(self, actor):
        if actor.is_sim:
            return tuple(buff.walkstyle_behavior_override
                         for buff in actor.get_active_buff_types()
                         if buff.walkstyle_behavior_override is not None)
        return ()

    def _apply_run_walkstyle_to_path(self,
                                     actor,
                                     path,
                                     walkstyle_overrides,
                                     time_offset=None):
        run_allowed_flags = self.run_allowed_flags
        for walkstyle_override in walkstyle_overrides:
            run_allowed_flags |= walkstyle_override.additional_run_flags
        for walkstyle_override in walkstyle_overrides:
            run_allowed_flags &= ~walkstyle_override.removed_run_flags
        if not run_allowed_flags:
            return
        run_required_total_distance = sims4.math.safe_max(
            (override for override in walkstyle_overrides
             if override.run_required_total_distance is not None),
            key=operator.attrgetter('walkstyle_behavior_priority'),
            default=self).run_required_total_distance
        if path.length() < run_required_total_distance:
            return
        run_required_segment_distance = sims4.math.safe_max(
            (override for override in walkstyle_overrides
             if override.run_required_segment_distance is not None),
            key=operator.attrgetter('walkstyle_behavior_priority'),
            default=self).run_required_segment_distance
        path_nodes = list(path.nodes)
        all_path_node_data = []
        for (start_node, end_node) in zip(path_nodes, path_nodes[1:]):
            switch_routing_surface = start_node.routing_surface_id != end_node.routing_surface_id
            is_outside = start_node.portal_id == 0 and get_block_id_for_node(
                start_node) == 0
            route_key = (switch_routing_surface, is_outside)
            all_path_node_data.append((route_key, start_node, end_node))
        run_walkstyle = self.get_run_walkstyle(actor)
        for ((_, is_outside),
             path_node_data) in itertools.groupby(all_path_node_data,
                                                  key=operator.itemgetter(0)):
            if is_outside and not run_allowed_flags & WalkStyleRunAllowedFlags.RUN_ALLOWED_OUTDOORS:
                continue
            if not is_outside and not run_allowed_flags & WalkStyleRunAllowedFlags.RUN_ALLOWED_INDOORS:
                continue
            path_node_data = list(path_node_data)
            segment_length = sum(
                (sims4.math.Vector3(*start_node.position) -
                 sims4.math.Vector3(*end_node.position)).magnitude_2d()
                for (_, start_node, end_node) in path_node_data)
            if segment_length < run_required_segment_distance:
                continue
            for (_, path_node, _) in path_node_data:
                if not time_offset is None:
                    if path_node.time >= time_offset:
                        path_node.walkstyle = run_walkstyle
                path_node.walkstyle = run_walkstyle

    def check_for_wading(self, sim, *_, **__):
        routing_component = sim.routing_component
        if not routing_component.last_route_has_wading_nodes and not routing_component.wading_buff_handle:
            return
        wading_interval = OceanTuning.get_actor_wading_interval(sim)
        if wading_interval is None:
            return
        water_height = get_water_depth_at_location(sim.location)
        if water_height in wading_interval:
            if routing_component.wading_buff_handle is None:
                routing_component.wading_buff_handle = sim.add_buff(
                    self.wading_walkstyle_buff)
        elif routing_component.wading_buff_handle is not None:
            sim.remove_buff(routing_component.wading_buff_handle)
            routing_component.wading_buff_handle = None

    def _apply_wading_walkstyle_to_path(self,
                                        actor,
                                        path,
                                        default_walkstyle,
                                        time_offset=None):
        if actor.is_sim and actor.sim_info.is_ghost:
            return False
        wading_interval = OceanTuning.get_actor_wading_interval(actor)
        if wading_interval is None:
            return False
        wading_walkstyle = self.get_wading_walkstyle(actor)
        if wading_walkstyle is None:
            return False

        def get_node_water_height(path_node):
            return get_water_depth(path_node.position[0],
                                   path_node.position[2],
                                   path_node.routing_surface_id.secondary_id)

        path_nodes = list(path.nodes)
        start_wading = get_node_water_height(path_nodes[0]) in wading_interval
        end_wading = get_node_water_height(path_nodes[-1]) in wading_interval
        if not start_wading and not end_wading:
            return False
        path_contains_wading = False
        for (start_node, end_node) in zip(path_nodes, path_nodes[1:]):
            if time_offset is not None and end_node.time < time_offset:
                continue
            if start_node.routing_surface_id.type == SurfaceType.SURFACETYPE_POOL:
                continue
            if start_node.portal_object_id != 0:
                continue
            start_wading = get_node_water_height(start_node) in wading_interval
            end_wading = get_node_water_height(end_node) in wading_interval
            if not start_wading and not end_wading:
                continue
            is_wading = start_wading
            if is_wading:
                start_node.walkstyle = wading_walkstyle
                path_contains_wading = True
            nodes_to_add = []
            for (transform, routing_surface,
                 time) in path.get_location_data_along_segment_gen(
                     start_node.index, end_node.index, time_step=0.3):
                should_wade = get_water_depth(
                    transform.translation[0],
                    transform.translation[2]) in wading_interval
                if is_wading and not should_wade:
                    is_wading = False
                    nodes_to_add.append(
                        (Location(transform.translation, transform.orientation,
                                  routing_surface), time, 0, default_walkstyle,
                         0, 0, end_node.index))
                elif not is_wading:
                    if should_wade:
                        is_wading = True
                        nodes_to_add.append(
                            (Location(transform.translation,
                                      transform.orientation, routing_surface),
                             time, 0, wading_walkstyle, 0, 0, end_node.index))
                        path_contains_wading = True
            running_index_offset = 0
            for (loc, time, node_type, walkstyle, portal_obj_id, portal_id,
                 index) in nodes_to_add:
                node_index = index + running_index_offset
                path.nodes.add_node(loc, time, node_type, walkstyle,
                                    portal_obj_id, portal_id, node_index)
                node = path.nodes[node_index]
                node.is_procedural = False
                running_index_offset += 1
        return path_contains_wading

    def apply_walkstyle_to_path(self, actor, path, time_offset=None):
        gsi_archiver = None
        can_archive = gsi_handlers.walkstyle_handlers.archiver.enabled and actor.is_sim
        if can_archive:
            gsi_archiver = WalkstyleGSIArchiver(actor)
        walkstyle = self.get_walkstyle_for_path(actor, path, gsi_archiver)
        if can_archive:
            gsi_archiver.gsi_archive_entry()
        path_nodes = list(path.nodes)
        for path_node in path_nodes:
            if not time_offset is None:
                if path_node.time >= time_offset:
                    path_node.walkstyle = walkstyle
            path_node.walkstyle = walkstyle
        walkstyle_overrides = self._get_walkstyle_overrides(actor)
        if walkstyle not in self.run_disallowed_walkstyles:
            self._apply_run_walkstyle_to_path(actor,
                                              path,
                                              walkstyle_overrides,
                                              time_offset=time_offset)
        if actor.is_sim:
            actor.routing_component.last_route_has_wading_nodes = self._apply_wading_walkstyle_to_path(
                actor, path, walkstyle, time_offset=time_offset)
        return walkstyle

    def get_combo_replacement(self, highest_priority_walkstyle,
                              walkstyle_list):
        for combo_tuple in self.combo_walkstyle_replacements:
            key_combo_list = combo_tuple.key_combo_list
            if highest_priority_walkstyle in key_combo_list:
                if all(ws in walkstyle_list for ws in key_combo_list):
                    return combo_tuple

    def get_combo_replaced_walkstyle(self, highest_priority_walkstyle,
                                     walkstyle_list):
        combo_tuple = self.get_combo_replacement(highest_priority_walkstyle,
                                                 walkstyle_list)
        if combo_tuple is not None:
            return combo_tuple.result

    def get_default_walkstyle(self, actor, gsi_archiver=None):
        walkstyle = actor.get_cost_valid_walkstyle(
            WalksStyleBehavior.WALKSTYLE_COST)
        walkstyle_list = actor.get_walkstyle_list()
        replaced_walkstyle = self.get_combo_replaced_walkstyle(
            walkstyle, walkstyle_list)
        if gsi_archiver is not None:
            gsi_archiver.default_walkstyle = walkstyle
            gsi_archiver.combo_replacement_walkstyle_found = replaced_walkstyle
        if replaced_walkstyle is not None:
            walkstyle = replaced_walkstyle
        return walkstyle

    def get_short_walkstyle(self, walkstyle, actor):
        short_walkstyle = self._get_property_override(actor, 'short_walkstyle')
        return self.short_walkstyle_map.get(walkstyle, short_walkstyle)

    def get_run_walkstyle(self, actor):
        run_walkstyle = self._get_property_override(actor, 'run_walkstyle')
        return run_walkstyle

    def get_wading_walkstyle(self, actor):
        wading_walkstyle = self._get_property_override(actor,
                                                       'wading_walkstyle')
        return wading_walkstyle

    def supports_wading_walkstyle_buff(self, actor):
        return self.wading_walkstyle_buff is not None and OceanTuning.get_actor_wading_interval(
            actor)

    def _get_property_override(self, actor, property_name):
        overrides = self._get_walkstyle_overrides(actor)
        override = sims4.math.safe_max(
            (override for override in overrides
             if getattr(override, property_name) is not None),
            key=operator.attrgetter('walkstyle_behavior_priority'),
            default=self)
        property_value = getattr(override, property_name)
        return property_value

    def _apply_walkstyle_cost(self, actor, walkstyle):
        walkstyle_cost = WalksStyleBehavior.WALKSTYLE_COST.get(walkstyle, None)
        if walkstyle_cost is not None:
            stat_instance = actor.get_stat_instance(
                walkstyle_cost.walkstyle_cost_statistic)
            if stat_instance is None:
                logger.error(
                    'Statistic {}, not found on Sim {} for walkstyle cost',
                    walkstyle_cost.walkstyle_cost_statistic,
                    actor,
                    owner='camilogarcia')
                return
            stat_instance.add_value(-walkstyle_cost.cost)

    def get_walkstyle_for_path(self, actor, path, gsi_archiver=None):
        walkstyle = self.get_default_walkstyle(actor, gsi_archiver)
        if gsi_archiver is not None:
            gsi_archiver.walkstyle_requests = actor.routing_component.get_walkstyle_requests(
            )
        short_walk_distance = self.short_walkstyle_distance_override_map.get(
            walkstyle, self.short_walkstyle_distance)
        if path.length() < short_walk_distance:
            walkstyle = self.get_short_walkstyle(walkstyle, actor)
            if gsi_archiver is not None:
                gsi_archiver.default_walkstyle_replaced_by_short_walkstyle = walkstyle
        if actor.is_sim:
            if actor.in_pool and walkstyle not in self.SWIMMING_WALKSTYLES:
                walkstyle = self.SWIMMING_WALKSTYLES[0]
                if gsi_archiver is not None:
                    gsi_archiver.default_walkstyle_replaced_by_swimming_walkstyle = walkstyle
                return walkstyle
            else:
                posture = actor.posture
                if posture.mobile and posture.compatible_walkstyles and walkstyle not in posture.compatible_walkstyles:
                    walkstyle = posture.compatible_walkstyles[0]
                    if gsi_archiver is not None:
                        gsi_archiver.default_walkstyle_replaced_by_posture_walkstyle = walkstyle
                    return walkstyle
        return walkstyle
Example #22
0
class DebugSetupLotInteraction(TerrainImmediateSuperInteraction):
    INSTANCE_TUNABLES = {
        'setup_lot_destroy_old_objects':
        Tunable(bool,
                False,
                description=
                'Destroy objects previously created by this interaction.'),
        'setup_lot_objects':
        TunableList(
            TunableTuple(
                definition=TunableReference(definition_manager()),
                position=TunableVector2(Vector2.ZERO()),
                angle=TunableRange(int, 0, -360, 360),
                children=TunableList(
                    TunableTuple(
                        definition=TunableReference(
                            definition_manager(),
                            description=
                            'The child object to create.  It will appear in the first available slot in which it fits, subject to additional restrictions specified in the other values of this tuning.'
                        ),
                        part_index=OptionalTunable(
                            Tunable(
                                int,
                                0,
                                description=
                                'If specified, restrict slot selection to the given part index.'
                            )),
                        bone_name=OptionalTunable(
                            Tunable(
                                str,
                                '_ctnm_chr_',
                                description=
                                'If specified, restrict slot selection to one with this exact bone name.'
                            )),
                        slot_type=OptionalTunable(
                            TunableReference(
                                manager=services.get_instance_manager(
                                    Types.SLOT_TYPE),
                                description=
                                'If specified, restrict slot selection to ones that support this type of slot.'
                            )),
                        init_state_values=TunableList(
                            description=
                            '\n                                List of states the children object will be set to.\n                                ',
                            tunable=TunableStateValueReference()))),
                init_state_values=TunableList(
                    description=
                    '\n                    List of states the created object will be pushed to.\n                    ',
                    tunable=TunableStateValueReference())))
    }
    _zone_to_cls_to_created_objects = WeakKeyDictionary()

    @classproperty
    def destroy_old_objects(cls):
        return cls.setup_lot_destroy_old_objects

    @classproperty
    def created_objects(cls):
        created_objects = cls._zone_to_cls_to_created_objects.setdefault(
            services.current_zone(), {})
        return setdefault_callable(created_objects, cls, WeakSet)

    def _run_interaction_gen(self, timeline):
        with supress_posture_graph_build():
            if self.destroy_old_objects:
                while self.created_objects:
                    obj = self.created_objects.pop()
                    obj.destroy(
                        source=self,
                        cause='Destroying old objects in setup debug lot.')
            position = self.context.pick.location
            self.spawn_objects(position)
        return True
        yield

    def _create_object(self,
                       definition_id,
                       position=Vector3.ZERO(),
                       orientation=Quaternion.IDENTITY(),
                       level=0,
                       owner_id=0):
        obj = objects.system.create_object(definition_id)
        if obj is not None:
            transform = Transform(position, orientation)
            location = Location(transform, self.context.pick.routing_surface)
            obj.location = location
            obj.set_household_owner_id(owner_id)
            self.created_objects.add(obj)
        return obj

    def spawn_objects(self, position):
        root = sims4.math.Vector3(position.x, position.y, position.z)
        zone = services.current_zone()
        lot = zone.lot
        owner_id = lot.owner_household_id
        if not self.contained_in_lot(lot, root):
            closest_point = self.find_nearest_point_on_lot(lot, root)
            if closest_point is None:
                return False
            radius = (self.top_right_pos -
                      self.bottom_left_pos).magnitude_2d() / 2
            root = closest_point + sims4.math.vector_normalize(
                sims4.math.vector_flatten(lot.center) -
                closest_point) * (radius + 1)
            if not self.contained_in_lot(lot, root):
                sims4.log.warn(
                    'Placement',
                    "Placed the lot objects but the entire bounding box isn't inside the lot. This is ok. If you need them to be inside the lot run the interaction again at a diffrent location."
                )

        def _generate_vector(offset_x, offset_z):
            ground_obj = services.terrain_service.terrain_object()
            ret_vector = sims4.math.Vector3(root.x + offset_x, root.y,
                                            root.z + offset_z)
            ret_vector.y = ground_obj.get_height_at(ret_vector.x, ret_vector.z)
            return ret_vector

        def _generate_quat(rot):
            return sims4.math.Quaternion.from_axis_angle(
                rot, sims4.math.Vector3(0, 1, 0))

        for info in self.setup_lot_objects:
            new_pos = _generate_vector(info.position.x, info.position.y)
            new_rot = _generate_quat(sims4.math.PI / 180 * info.angle)
            new_obj = self._create_object(info.definition,
                                          new_pos,
                                          new_rot,
                                          owner_id=owner_id)
            if new_obj is None:
                sims4.log.error('SetupLot', 'Unable to create object: {}',
                                info)
            else:
                for state_value in info.init_state_values:
                    new_obj.set_state(state_value.state, state_value)
                for child_info in info.children:
                    slot_owner = new_obj
                    if child_info.part_index is not None:
                        for obj_part in new_obj.parts:
                            if obj_part.subroot_index == child_info.part_index:
                                slot_owner = obj_part
                                break
                    bone_name_hash = None
                    if child_info.bone_name is not None:
                        bone_name_hash = hash32(child_info.bone_name)
                    slot_type = None
                    if child_info.slot_type is not None:
                        slot_type = child_info.slot_type
                    for runtime_slot in slot_owner.get_runtime_slots_gen(
                            slot_types={slot_type},
                            bone_name_hash=bone_name_hash):
                        if runtime_slot.is_valid_for_placement(
                                definition=child_info.definition):
                            break
                    else:
                        sims4.log.error(
                            'SetupLot',
                            'Unable to find slot for child object: {}',
                            child_info)
                    child = self._create_object(child_info.definition,
                                                owner_id=owner_id)
                    if child is None:
                        sims4.log.error('SetupLot',
                                        'Unable to create child object: {}',
                                        child_info)
                    else:
                        runtime_slot.add_child(child)
                        for state_value in child_info.init_state_values:
                            child.set_state(state_value.state, state_value)

    def contained_in_lot(self, lot, root):
        self.find_corner_points(root)
        return True

    def find_corner_points(self, root):
        max_x = 0
        min_x = 0
        max_z = 0
        min_z = 0
        for info in self.setup_lot_objects:
            if info.position.x > max_x:
                max_x = info.position.x
            if info.position.x < min_x:
                min_x = info.position.x
            if info.position.y > max_z:
                max_z = info.position.y
            if info.position.y < min_z:
                min_z = info.position.y
        self.top_right_pos = sims4.math.Vector3(root.x + max_x, root.y,
                                                root.z + max_z)
        self.bottom_right_pos = sims4.math.Vector3(root.x + max_x, root.y,
                                                   root.z + min_z)
        self.top_left_pos = sims4.math.Vector3(root.x + min_x, root.y,
                                               root.z + max_z)
        self.bottom_left_pos = sims4.math.Vector3(root.x + min_x, root.y,
                                                  root.z + min_z)

    def find_nearest_point_on_lot(self, lot, root):
        lot_corners = lot.corners
        segments = [(lot_corners[0], lot_corners[1]),
                    (lot_corners[1], lot_corners[2]),
                    (lot_corners[2], lot_corners[3]),
                    (lot_corners[3], lot_corners[1])]
        dist = 0
        closest_point = None
        for segment in segments:
            new_point = sims4.math.get_closest_point_2D(segment, root)
            new_distance = (new_point - root).magnitude()
            if dist == 0:
                dist = new_distance
                closest_point = new_point
            elif new_distance < dist:
                dist = new_distance
                closest_point = new_point
        return closest_point
Example #23
0
class FormationTypeFollow(FormationTypeBase):
    ATTACH_NODE_COUNT = 3
    ATTACH_NODE_RADIUS = 0.25
    ATTACH_NODE_ANGLE = math.PI
    ATTACH_NODE_FLAGS = 4
    RAYTRACE_HEIGHT = 1.5
    RAYTRACE_RADIUS = 0.1
    FACTORY_TUNABLES = {
        'formation_offsets':
        TunableList(
            description=
            '\n            A list of offsets, relative to the master, that define where slaved\n            Sims are positioned.\n            ',
            tunable=TunableVector2(default=Vector2.ZERO()),
            minlength=1),
        'formation_constraints':
        TunableList(
            description=
            '\n            A list of constraints that slaved Sims must satisfy any time they\n            run interactions while in this formation. This can be a geometric\n            constraint, for example, that ensures Sims are always placed within\n            a radius or cone of their slaved position.\n            ',
            tunable=TunableConstraintVariant(
                constraint_locked_args={'multi_surface': True},
                circle_locked_args={'require_los': False},
                disabled_constraints={'spawn_points', 'relative_circle'})),
        '_route_length_interval':
        TunableInterval(
            description=
            '\n            Sims are slaved in formation only if the route is within this range\n            amount, in meters.\n            \n            Furthermore, routes shorter than the minimum\n            will not interrupt behavior (e.g. a socializing Sim will not force\n            dogs to get up and move around).\n            \n            Also routes longer than the maximum will make the slaved sim  \n            instantly position next to their master\n            (e.g. if a leashed dog gets too far from the owner, we place it next to the owner).\n            ',
            tunable_type=float,
            default_lower=1,
            default_upper=20,
            minimum=0),
        'fgl_on_routes':
        TunableTuple(
            description=
            '\n            Data associated with the FGL Context on following slaves.\n            ',
            slave_should_face_master=Tunable(
                description=
                '\n                If enabled, the Slave should attempt to face the master at the end\n                of routes.\n                ',
                tunable_type=bool,
                default=False),
            height_tolerance=OptionalTunable(
                description=
                '\n                If enabled than we will set the height tolerance in FGL.\n                ',
                tunable=TunableRange(
                    description=
                    '\n                    The height tolerance piped to FGL.\n                    ',
                    tunable_type=float,
                    default=0.035,
                    minimum=0,
                    maximum=1)))
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._attachment_chain = []
        formation_count = self.master.get_routing_slave_data_count(
            self._formation_cls)
        self._formation_offset = self.formation_offsets[formation_count]
        self._setup_right_angle_connections()
        self._offset = Vector3.ZERO()
        for attachment_info in self._attachment_chain:
            self._offset.x = self._offset.x + attachment_info.parent_offset.x - attachment_info.offset.x
            self._offset.z = self._offset.z + attachment_info.parent_offset.y - attachment_info.offset.y
        self._slave_constraint = None
        self._slave_lock = None
        self._final_transform = None

    @classproperty
    def routing_type(cls):
        return FormationRoutingType.FOLLOW

    @property
    def offset(self):
        return self._formation_offset

    @property
    def slave_attachment_type(self):
        return Routing_pb2.SlaveData.SLAVE_FOLLOW_ATTACHMENT

    @staticmethod
    def get_max_slave_count(tuned_factory):
        return len(tuned_factory._tuned_values.formation_offsets)

    @property
    def route_length_minimum(self):
        return self._route_length_interval.lower_bound

    @property
    def route_length_maximum(self):
        return self._route_length_interval.upper_bound

    def attachment_info_gen(self):
        yield from self._attachment_chain

    def on_master_route_start(self):
        self._build_routing_slave_constraint()
        self._lock_slave()
        if self._slave.is_sim:
            for si in self._slave.get_all_running_and_queued_interactions():
                if si.transition is not None:
                    if si.transition is not self.master.transition_controller:
                        si.transition.derail(DerailReason.CONSTRAINTS_CHANGED,
                                             self._slave)

    def on_master_route_end(self):
        self._build_routing_slave_constraint()
        if self._slave.is_sim:
            for si in self._slave.get_all_running_and_queued_interactions():
                if si.transition is not None:
                    if si.transition is not self.master.transition_controller:
                        si.transition.derail(DerailReason.CONSTRAINTS_CHANGED,
                                             self._slave)
        self._unlock_slave()
        self._final_transform = None

    def _lock_slave(self):
        self._slave_lock = self._slave.add_work_lock(self)

    def _unlock_slave(self):
        self._slave.remove_work_lock(self)

    def _build_routing_slave_constraint(self):
        self._slave_constraint = ANYWHERE
        for constraint in self.formation_constraints:
            constraint = constraint.create_constraint(
                self._slave,
                target=self._master,
                target_position=self._master.intended_position)
            self._slave_constraint = self._slave_constraint.intersect(
                constraint)

    def get_routing_slave_constraint(self):
        if self._slave_constraint is None or not self._slave_constraint.valid:
            self._build_routing_slave_constraint()
        return self._slave_constraint

    def _add_attachment_node(self, parent_offset: Vector2, offset: Vector2,
                             radius, angle_constraint, flags, node_type):
        attachment_node = _RoutingFormationAttachmentNode(
            parent_offset, offset, radius, angle_constraint, flags, node_type)
        self._attachment_chain.append(attachment_node)

    def find_good_location_for_slave(self, master_location):
        restrictions = []
        fgl_kwargs = {}
        fgl_flags = 0
        fgl_tuning = self.fgl_on_routes
        slave_position = master_location.transform.transform_point(
            self._offset)
        orientation = master_location.transform.orientation
        routing_surface = master_location.routing_surface
        if routing_surface is None:
            master_parent = master_location.parent
            if master_parent:
                routing_surface = master_parent.routing_surface
        if self.slave.is_sim or isinstance(self.slave, StubActor):
            (min_water_depth, max_water_depth
             ) = OceanTuning.make_depth_bounds_safe_for_surface_and_sim(
                 routing_surface, self.slave)
        else:
            min_water_depth = None
            max_water_depth = None
        (min_water_depth, max_water_depth
         ) = OceanTuning.make_depth_bounds_safe_for_surface_and_sim(
             routing_surface,
             self.master,
             min_water_depth=min_water_depth,
             max_water_depth=max_water_depth)
        fgl_kwargs.update({
            'min_water_depth': min_water_depth,
            'max_water_depth': max_water_depth
        })
        if fgl_tuning.height_tolerance is not None:
            fgl_kwargs['height_tolerance'] = fgl_tuning.height_tolerance
        if fgl_tuning.slave_should_face_master:
            restrictions.append(
                RelativeFacingRange(master_location.transform.translation, 0))
            fgl_kwargs.update({
                'raytest_radius':
                self.RAYTRACE_RADIUS,
                'raytest_start_offset':
                self.RAYTRACE_HEIGHT,
                'raytest_end_offset':
                self.RAYTRACE_HEIGHT,
                'ignored_object_ids': {self.master.id, self.slave.id},
                'raytest_start_point_override':
                master_location.transform.translation
            })
            fgl_flags = FGLSearchFlag.SHOULD_RAYTEST
            orientation_offset = sims4.math.angle_to_yaw_quaternion(
                sims4.math.vector3_angle(
                    sims4.math.vector_normalize(self._offset)))
            orientation = Quaternion.concatenate(orientation,
                                                 orientation_offset)
        starting_location = placement.create_starting_location(
            position=slave_position,
            orientation=orientation,
            routing_surface=routing_surface)
        if self.slave.is_sim:
            fgl_flags |= FGLSearchFlagsDefaultForSim
            fgl_context = placement.create_fgl_context_for_sim(
                starting_location,
                self.slave,
                search_flags=fgl_flags,
                restrictions=restrictions,
                **fgl_kwargs)
        else:
            fgl_flags |= FGLSearchFlagsDefault
            footprint = self.slave.get_footprint()
            master_position = master_location.position if hasattr(
                master_location,
                'position') else master_location.transform.translation
            fgl_context = FindGoodLocationContext(
                starting_location,
                object_id=self.slave.id,
                object_footprints=(footprint, )
                if footprint is not None else None,
                search_flags=fgl_flags,
                restrictions=restrictions,
                connectivity_group_override_point=master_position,
                **fgl_kwargs)
        (new_position,
         new_orientation) = placement.find_good_location(fgl_context)
        if new_position is None or new_orientation is None:
            logger.warn(
                'No good location found for {} after slaved in a routing formation headed to {}.',
                self.slave,
                starting_location,
                owner='rmccord')
            return sims4.math.Transform(
                Vector3(*starting_location.position),
                Quaternion(*starting_location.orientation))
        new_position.y = services.terrain_service.terrain_object(
        ).get_routing_surface_height_at(new_position.x, new_position.z,
                                        master_location.routing_surface)
        final_transform = sims4.math.Transform(new_position, new_orientation)
        return final_transform

    def on_release(self):
        self._unlock_slave()

    def _setup_right_angle_connections(self):
        formation_offset_x = Vector2(self._formation_offset.x / 6.0, 0.0)
        formation_offset_y = Vector2(0.0, self._formation_offset.y)
        for _ in range(self.ATTACH_NODE_COUNT):
            self._add_attachment_node(
                formation_offset_x, formation_offset_x * -1,
                self.ATTACH_NODE_RADIUS, 0, self.ATTACH_NODE_FLAGS,
                RoutingFormationFollowType.NODE_TYPE_FOLLOW_LEADER)
        self._setup_direct_connections(formation_offset_y)

    def _setup_direct_connections(self, formation_offset):
        formation_vector_magnitude = formation_offset.magnitude()
        normalized_offset = formation_offset / formation_vector_magnitude
        attachment_node_step = formation_vector_magnitude / (
            (self.ATTACH_NODE_COUNT - 1) * 2)
        attachment_vector = normalized_offset * attachment_node_step
        for i in range(0, self.ATTACH_NODE_COUNT - 1):
            flags = self.ATTACH_NODE_FLAGS
            if i == self.ATTACH_NODE_COUNT - 2:
                flags = 5
            self._add_attachment_node(
                attachment_vector, attachment_vector * -1,
                self.ATTACH_NODE_RADIUS, self.ATTACH_NODE_ANGLE, flags,
                RoutingFormationFollowType.NODE_TYPE_CHAIN)

    def should_slave_for_path(self, path):
        path_length = path.length() if path is not None else MAX_INT32
        final_path_node = path.nodes[-1]
        final_position = sims4.math.Vector3(*final_path_node.position)
        final_orientation = sims4.math.Quaternion(*final_path_node.orientation)
        routing_surface = final_path_node.routing_surface_id
        final_position.y = services.terrain_service.terrain_object(
        ).get_routing_surface_height_at(final_position.x, final_position.z,
                                        routing_surface)
        final_transform = sims4.math.Transform(final_position,
                                               final_orientation)
        slave_position = final_transform.transform_point(self._offset)
        slave_position.y = services.terrain_service.terrain_object(
        ).get_routing_surface_height_at(slave_position.x, slave_position.z,
                                        routing_surface)
        final_dist_sq = (slave_position -
                         self.slave.position).magnitude_squared()
        if path_length >= self.route_length_minimum or final_dist_sq >= self.route_length_minimum * self.route_length_minimum:
            return True
        return False

    def build_routing_slave_pb(self, slave_pb, path=None):
        starting_location = path.final_location if path is not None else self.master.intended_location
        slave_transform = self.find_good_location_for_slave(starting_location)
        slave_loc = slave_pb.final_location_override
        (slave_loc.translation.x, slave_loc.translation.y,
         slave_loc.translation.z) = slave_transform.translation
        (slave_loc.orientation.x, slave_loc.orientation.y,
         slave_loc.orientation.z,
         slave_loc.orientation.w) = slave_transform.orientation
        self._final_transform = slave_transform

    def update_slave_position(self,
                              master_transform,
                              master_orientation,
                              routing_surface,
                              distribute=True,
                              path=None,
                              canceled=False):
        master_transform = sims4.math.Transform(master_transform.translation,
                                                master_orientation)
        if distribute and not canceled:
            slave_transform = self._final_transform if self._final_transform is not None else self.slave.transform
            slave_position = slave_transform.translation
        else:
            slave_position = master_transform.transform_point(self._offset)
            slave_transform = sims4.math.Transform(slave_position,
                                                   master_orientation)
        slave_route_distance_sqrd = (self._slave.position -
                                     slave_position).magnitude_squared()
        if path is not None and path.length(
        ) < self.route_length_minimum and slave_route_distance_sqrd < self.route_length_minimum * self.route_length_minimum:
            return
        slave_too_far_from_master = False
        if slave_route_distance_sqrd > self.route_length_maximum * self.route_length_maximum:
            slave_too_far_from_master = True
        if distribute and not slave_too_far_from_master:
            self._slave.move_to(routing_surface=routing_surface,
                                transform=slave_transform)
        else:
            location = self.slave.location.clone(
                routing_surface=routing_surface, transform=slave_transform)
            self.slave.set_location_without_distribution(location)
Example #24
0
class DebugCreateSimWithGenderAndAgeInteraction(
        TerrainImmediateSuperInteraction):
    INSTANCE_TUNABLES = {
        'gender':
        TunableEnumEntry(
            description=
            '\n            The gender of the Sim to be created.\n            ',
            tunable_type=Gender,
            default=Gender.MALE),
        'age':
        TunableEnumEntry(
            description=
            '\n            The age of the Sim to be created.\n            ',
            tunable_type=Age,
            default=Age.ADULT),
        'species':
        TunableEnumEntry(
            description=
            '\n            The species of the Sim to be created.\n            ',
            tunable_type=Species,
            default=Species.HUMAN,
            invalid_enums=(Species.INVALID, )),
        'breed_picker':
        OptionalTunable(
            description=
            '\n            Breed picker to use if using a non-human species.\n            \n            If disabled, breed will be random.\n            ',
            tunable=TunableReference(
                manager=services.get_instance_manager(
                    sims4.resources.Types.INTERACTION),
                class_restrictions=('BreedPickerSuperInteraction', ),
                allow_none=True))
    }

    def _run_interaction_gen(self, timeline):
        if self.species == Species.HUMAN or self.breed_picker is None:
            position = self.context.pick.location
            routing_surface = self.context.pick.routing_surface
            actor_sim_info = self.sim.sim_info
            household = actor_sim_info.household if self.age == Age.BABY else None
            sim_creator = sims.sim_spawner.SimCreator(age=self.age,
                                                      gender=self.gender,
                                                      species=self.species)
            (sim_info_list, _) = sims.sim_spawner.SimSpawner.create_sim_infos(
                (sim_creator, ),
                household=household,
                account=actor_sim_info.account,
                zone_id=actor_sim_info.zone_id,
                creation_source='cheat: DebugCreateSimInteraction',
                is_debug=True)
            sim_info = sim_info_list[0]
            if sim_info.age == Age.BABY:
                PregnancyTracker.initialize_sim_info(sim_info, actor_sim_info,
                                                     None)
                create_and_place_baby(sim_info,
                                      position=position,
                                      routing_surface=routing_surface)
            else:
                sims.sim_spawner.SimSpawner.spawn_sim(sim_info,
                                                      sim_position=position,
                                                      is_debug=True)
        else:
            self.sim.push_super_affordance(self.breed_picker,
                                           self.target,
                                           self.context,
                                           picked_object=self.target,
                                           age=self.age,
                                           gender=self.gender,
                                           species=self.species)
        return True
        yield
Example #25
0
class PickTerrainTest(HasTunableSingletonFactory, AutoFactoryInit, BaseTest):
    FACTORY_TUNABLES = {
        'terrain_location':
        TunableEnumEntry(
            description='\n             Terrain type to find.\n             ',
            tunable_type=PickTerrainType,
            default=PickTerrainType.ANYWHERE),
        'terrain_feature':
        OptionalTunable(
            description=
            '\n            Tune this if you want to require a floor feature to be present\n            ',
            tunable=TunableEnumEntry(tunable_type=FloorFeatureType,
                                     default=FloorFeatureType.BURNT)),
        'terrain_feature_radius':
        Tunable(
            description=
            '\n            The radius to look for the floor feature, if one is tuned in terrain_feature\n            ',
            tunable_type=float,
            default=2.0)
    }

    @cached_test
    def __call__(self, context=None):
        if context is None:
            return TestResult(
                False,
                'Interaction Context is None. Make sure this test is Tuned on an Interaction.'
            )
        pick_info = context.pick
        if pick_info is None:
            return TestResult(
                False,
                'PickTerrainTest cannot run without a valid pick info from the Interaction Context.'
            )
        if pick_info.pick_type not in PICK_TRAVEL:
            return TestResult(
                False,
                'Attempting to run a PickTerrainTest with a pick that has an invalid type.'
            )
        if self.terrain_feature is not None:
            zone_id = services.current_zone_id()
            if not build_buy.find_floor_feature(
                    zone_id, self.terrain_feature, pick_info.location,
                    pick_info.routing_surface.secondary_id,
                    self.terrain_feature_radius):
                return TestResult(
                    False,
                    'Location does not have the required floor feature.')
        if self.terrain_location == PickTerrainType.ANYWHERE:
            return TestResult.TRUE
        on_lot = services.current_zone().lot.is_position_on_lot(
            pick_info.location)
        if self.terrain_location == PickTerrainType.ON_LOT:
            if on_lot:
                return TestResult.TRUE
            return TestResult(False, 'Pick Terrain is not ON_LOT as expected.')
        if self.terrain_location == PickTerrainType.OFF_LOT:
            if not on_lot:
                return TestResult.TRUE
            return TestResult(False,
                              'Pick Terrain is not OFF_LOT as expected.')
        current_zone_id = services.current_zone().id
        other_zone_id = pick_info.get_zone_id_from_pick_location()
        if self.terrain_location == PickTerrainType.ON_OTHER_LOT:
            if not on_lot and other_zone_id is not None and other_zone_id != current_zone_id:
                return TestResult.TRUE
            return TestResult(False,
                              'Pick Terrain is not ON_OTHER_LOT as expected.')
        if self.terrain_location == PickTerrainType.NO_LOT:
            if other_zone_id is None:
                return TestResult.TRUE
            return TestResult(
                False, 'Pick Terrain is is on a valid lot, but not expected.')
        in_street = is_position_in_street(pick_info.location)
        if self.terrain_location == PickTerrainType.IN_STREET:
            if in_street:
                return TestResult.TRUE
            return TestResult(False,
                              'Pick Terrain is not IN_STREET as expected.')
        if self.terrain_location == PickTerrainType.OFF_STREET:
            if not in_street:
                return TestResult.TRUE
            return TestResult(
                False, 'Pick Terrain is in the street, but not expected.')
        if self.terrain_location == PickTerrainType.IS_OUTSIDE:
            is_outside = is_location_outside(pick_info.location,
                                             pick_info.level)
            if is_outside:
                return TestResult.TRUE
            return TestResult(False, 'Pick Terrain is not outside')
        return TestResult.TRUE
class AffordanceReferenceScoringModifier(BaseGameEffectModifier):
    FACTORY_TUNABLES = {
        'content_score_bonus':
        Tunable(
            description=
            '\n            When determine content score for affordances and afforance matches\n            tuned here, content score is increased by this amount.\n            ',
            tunable_type=int,
            default=0),
        'success_modifier':
        TunablePercent(
            description=
            '\n            Amount to adjust percent success chance. For example, tuning 10%\n            will increase success chance by 10% over the base success chance.\n            Additive with other buffs.\n            ',
            default=0,
            minimum=-100),
        'affordances':
        TunableList(
            description=
            '\n            A list of affordances that will be compared against.\n            ',
            tunable=TunableReference(manager=services.affordance_manager())),
        'affordance_lists':
        TunableList(
            description=
            '\n            A list of affordance snippets that will be compared against.\n            ',
            tunable=snippets.TunableAffordanceListReference()),
        'interaction_category_tags':
        TunableSet(
            description=
            '\n            This attribute is used to test for affordances that contain any of the tags in this set.\n            ',
            tunable=TunableEnumEntry(
                description=
                '\n                These tag values are used for testing interactions.\n                ',
                tunable_type=Tag,
                default=Tag.INVALID)),
        'interaction_category_blacklist_tags':
        TunableSet(
            description=
            '\n            Any interaction with a tag in this set will NOT be modiified.\n            Affects display name on a per interaction basis.\n            ',
            tunable=TunableEnumEntry(
                description=
                '\n                These tag values are used for testing interactions.\n                ',
                tunable_type=Tag,
                default=Tag.INVALID)),
        'pie_menu_parent_name':
        OptionalTunable(
            description=
            '\n            If enabled, we will insert the name into this parent string\n            in the pie menu.  Only affected by test and blacklist tags\n            (for performance reasons)\n            ',
            tunable=TunableLocalizedStringFactory(
                description=
                '\n                A string to wrap the normal interaction name.  Token 0 is actor,\n                Token 1 is the normal name.\n                '
            )),
        'new_pie_menu_icon':
        TunableIconAllPacks(
            description=
            "\n            Icon to put on interactions that pass test (interaction resolver)\n            and don't match blacklist tags.\n            ",
            allow_none=True),
        'basic_extras':
        TunableBasicExtras(
            description=
            '\n            Basic extras to add to interactions that match. \n            '
        ),
        'test':
        event_testing.tests.TunableTestSet(
            description=
            '\n            The test to run to see if the display_name should be\n            overridden. Ors of Ands.\n            '
        )
    }

    def __init__(self,
                 content_score_bonus=0,
                 success_modifier=0,
                 affordances=(),
                 affordance_lists=(),
                 interaction_category_tags=set(),
                 interaction_category_blacklist_tags=set(),
                 pie_menu_parent_name=None,
                 new_pie_menu_icon=None,
                 basic_extras=(),
                 test=None):
        super().__init__(GameEffectType.AFFORDANCE_MODIFIER)
        self._score_bonus = content_score_bonus
        self._success_modifier = success_modifier
        self._affordances = affordances
        self._affordance_lists = affordance_lists
        self._interaction_category_tags = interaction_category_tags
        self._interaction_category_blacklist_tags = interaction_category_blacklist_tags
        self._pie_menu_parent_name = pie_menu_parent_name
        self._new_pie_menu_icon = new_pie_menu_icon
        self._basic_extras = basic_extras
        self._test = test

    def is_type(self, affordance, resolver):
        if affordance is not None:
            if affordance.interaction_category_tags & self._interaction_category_blacklist_tags:
                return False
            if affordance in self._affordances:
                return True
            for affordances in self._affordance_lists:
                if affordance in affordances:
                    return True
            if affordance.interaction_category_tags & self._interaction_category_tags:
                return True
            elif self._test:
                result = False
                try:
                    result = self._test.run_tests(resolver)
                except:
                    pass
                if result:
                    return True
        if self._test:
            result = False
            try:
                result = self._test.run_tests(resolver)
            except:
                pass
            if result:
                return True
        return False

    def get_score_for_type(self, affordance, resolver):
        if self.is_type(affordance, resolver):
            return self._score_bonus
        return 0

    def get_success_for_type(self, affordance, resolver):
        if self.is_type(affordance, resolver):
            return self._success_modifier
        return 0

    def get_new_pie_menu_icon_and_parent_name_for_type(self, affordance,
                                                       resolver):
        if self.is_type(affordance, resolver):
            return (self._new_pie_menu_icon, self._pie_menu_parent_name,
                    self._interaction_category_blacklist_tags)
        return (None, None, None)

    def get_basic_extras_for_type(self, affordance, resolver):
        if self.is_type(affordance, resolver):
            return self._basic_extras
        return []

    def debug_affordances_gen(self):
        for affordance in self._affordances:
            yield affordance.__name__
        for affordnace_snippet in self._affordance_lists:
            yield affordnace_snippet.__name__
Example #27
0
class OutfitBodyTypeTest(HasTunableSingletonFactory, AutoFactoryInit,
                         BaseTest):
    FACTORY_TUNABLES = {
        'subject':
        TunableEnumEntry(
            description=
            '\n            The Sim we want to test the body type outfit for.\n            ',
            tunable_type=ParticipantType,
            default=ParticipantType.Actor),
        'outfit_override':
        OptionalTunable(
            description=
            "\n            If enabled, specify a particular outfit to check the body types of.\n            Otherwise we check the subject's current outfit.\n            ",
            tunable=TunableTuple(
                description=
                '\n                The outfit we want to check the body types of.\n                ',
                outfit_category=TunableEnumEntry(
                    description=
                    '\n                    The outfit category.\n                    ',
                    tunable_type=OutfitCategory,
                    default=OutfitCategory.EVERYDAY),
                index=Tunable(
                    description=
                    '\n                    The outfit index.\n                    ',
                    tunable_type=int,
                    default=0))),
        'body_types':
        TunableWhiteBlackList(
            description=
            '\n            The allowed and disallowed body types required to pass this test.\n            All CAS parts of the subject will be used to determine success or\n            failure.\n            ',
            tunable=TunableEnumEntry(
                description=
                '\n                The body type we want the CAS part to support or not support.\n                ',
                tunable_type=BodyType,
                default=BodyType.FULL_BODY,
                invalid_enums=BodyType.NONE))
    }

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

    @cached_test
    def __call__(self, subjects, *args, **kwargs):
        for subject in subjects:
            if subject is None or not subject.is_sim:
                return TestResult(False,
                                  'OutfitBodyTypeTest cannot test {}.',
                                  subject,
                                  tooltip=self.tooltip)
            outfit_category_and_index = subject.get_current_outfit(
            ) if self.outfit_override is None else (
                self.outfit_override.outfit_category,
                self.outfit_override.index)
            if not subject.has_outfit(outfit_category_and_index):
                return TestResult(
                    False,
                    'OutfitBodyTypeTest cannot test {} since they do not have the requested outfit {}.',
                    subject,
                    outfit_category_and_index,
                    tooltip=self.tooltip)
            outfit = subject.get_outfit(*outfit_category_and_index)
            if not self.body_types.test_collection(outfit.body_types):
                return TestResult(
                    False,
                    'OutfitBodyTypeTest subject {} failed list of body types for outfit {}.',
                    subject,
                    outfit_category_and_index,
                    tooltip=self.tooltip)
        return TestResult.TRUE
Example #28
0
class CareerPickerSuperInteraction(PickerSingleChoiceSuperInteraction):
    class CareerPickerFilter(HasTunableSingletonFactory, AutoFactoryInit):
        def is_valid(self, inter_cls, inter_inst, target, context, career,
                     **kwargs):
            raise NotImplementedError

    class CareerPickerFilterAll(CareerPickerFilter):
        def is_valid(self, inter_cls, inter_inst, target, context, career,
                     **kwargs):
            return True

    class CareerPickerFilterWhitelist(CareerPickerFilter):
        FACTORY_TUNABLES = {
            'whitelist':
            TunableEnumSet(
                description=
                '\n                Only careers of this category are allowed. If this set is\n                empty, then no careers are allowed.\n                ',
                enum_type=CareerCategory)
        }

        def is_valid(self, inter_cls, inter_inst, target, context, career,
                     **kwargs):
            return career.career_category in self.whitelist

    class CareerPickerFilterBlacklist(CareerPickerFilter):
        FACTORY_TUNABLES = {
            'blacklist':
            TunableEnumSet(
                description=
                '\n                Careers of this category are not allowed. All others are\n                allowed.\n                ',
                enum_type=CareerCategory)
        }

        def is_valid(self, inter_cls, inter_inst, target, context, career,
                     **kwargs):
            return career.career_category not in self.blacklist

    class CareerPickerFilterTested(CareerPickerFilter):
        FACTORY_TUNABLES = {
            'tests':
            event_testing.tests.TunableTestSet(
                description=
                '\n                A set of tests that are run against the prospective careers. At least\n                one test must pass in order for the prospective career to show. All\n                careers will pass if there are no tests. PickedItemId is the \n                participant type for the prospective career.\n                '
            )
        }

        def is_valid(self, inter_cls, inter_inst, target, context, career,
                     **kwargs):
            if inter_inst:
                interaction_parameters = inter_inst.interaction_parameters.copy(
                )
            else:
                interaction_parameters = kwargs.copy()
            interaction_parameters['picked_item_ids'] = {career.guid64}
            resolver = InteractionResolver(inter_cls,
                                           inter_inst,
                                           target=target,
                                           context=context,
                                           **interaction_parameters)
            if not self.tests.run_tests(resolver):
                return False
            return True

    INSTANCE_TUNABLES = {
        'continuation':
        OptionalTunable(
            description=
            '\n            If enabled, you can tune a continuation to be pushed. PickedItemId\n            will be the id of the selected career.\n            ',
            tunable=TunableContinuation(
                description=
                '\n                If specified, a continuation to push with the chosen career.\n                '
            ),
            tuning_group=GroupNames.PICKERTUNING),
        'career_filter':
        TunableVariant(
            description=
            '\n            Which career types to show.\n            ',
            all=CareerPickerFilterAll.TunableFactory(),
            blacklist=CareerPickerFilterBlacklist.TunableFactory(),
            whitelist=CareerPickerFilterWhitelist.TunableFactory(),
            tested=CareerPickerFilterTested.TunableFactory(),
            default='all',
            tuning_group=GroupNames.PICKERTUNING)
    }

    @flexmethod
    def _valid_careers_gen(cls, inst, target, context, **kwargs):
        sim = context.sim
        if sim is None:
            return
        yield from (career for career in sim.sim_info.careers.values()
                    if cls.career_filter.is_valid(cls, inst, target, context,
                                                  career, **kwargs))

    @classmethod
    def has_valid_choice(cls, target, context, **kwargs):
        return any(cls._valid_careers_gen(target, context, **kwargs))

    def _run_interaction_gen(self, timeline):
        self._show_picker_dialog(self.sim, target_sim=self.target)
        return True
        yield

    @flexmethod
    def picker_rows_gen(cls, inst, target, context, **kwargs):
        inst_or_cls = inst if inst is not None else cls
        for career in inst_or_cls._valid_careers_gen(target, context,
                                                     **kwargs):
            track = career.current_track_tuning
            row = ObjectPickerRow(name=track.get_career_name(context.sim),
                                  icon=track.icon,
                                  row_description=track.get_career_description(
                                      context.sim),
                                  tag=career)
            yield row

    def on_choice_selected(self, choice_tag, **kwargs):
        career = choice_tag
        if career is not None and self.continuation is not None:
            picked_item_set = set()
            picked_item_set.add(career.guid64)
            self.interaction_parameters['picked_item_ids'] = picked_item_set
            self.push_tunable_continuation(self.continuation,
                                           picked_item_ids=picked_item_set)
Example #29
0
class Skill(HasTunableReference,
            statistics.continuous_statistic_tuning.TunedContinuousStatistic,
            metaclass=HashedTunedInstanceMetaclass,
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC)):
    __qualname__ = 'Skill'
    SKILL_LEVEL_LIST = TunableMapping(
        key_type=TunableEnumEntry(SkillLevelType, SkillLevelType.MAJOR),
        value_type=TunableList(
            Tunable(int, 0),
            description=
            'The level boundaries for skill type, specified as a delta from the previous value'
        ),
        export_modes=ExportModes.All)
    SKILL_EFFECTIVENESS_GAIN = TunableMapping(
        key_type=TunableEnumEntry(SkillEffectiveness,
                                  SkillEffectiveness.STANDARD),
        value_type=TunableCurve(),
        description='Skill gain points based on skill effectiveness.')
    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            Localized name of this Statistic\n            ',
            export_modes=ExportModes.All),
        '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))),
        'weight':
        Tunable(
            description=
            "\n            The weight of the Skill with regards to autonomy.  It's ignored \n            for the purposes of sorting stats, but it's applied when scoring \n            the actual statistic operation for the SI.\n            ",
            tunable_type=float,
            default=0.5),
        'skill_level_type':
        TunableEnumEntry(
            description='\n            Skill level list to use.\n            ',
            tunable_type=SkillLevelType,
            default=SkillLevelType.MAJOR,
            export_modes=ExportModes.All),
        'locked_description':
        TunableLocalizedString(
            description=
            "\n            The skill description when it's locked.\n            ",
            export_modes=ExportModes.All),
        'skill_description':
        TunableLocalizedString(
            description=
            "\n            The skill's normal description.\n            ",
            export_modes=ExportModes.All),
        'is_default':
        Tunable(
            description=
            '\n            Whether Sim will default has this skill.\n            ',
            tunable_type=bool,
            default=False),
        'genders':
        TunableSet(
            description=
            '\n            Skill allowed gender, empty set means not specified\n            ',
            tunable=TunableEnumEntry(tunable_type=sim_info_types.Gender,
                                     default=None,
                                     export_modes=ExportModes.All)),
        'ages':
        TunableSet(
            description=
            '\n            Skill allowed ages, empty set means not specified\n            ',
            tunable=TunableEnumEntry(tunable_type=sim_info_types.Age,
                                     default=None,
                                     export_modes=ExportModes.All)),
        'entitlement':
        TunableEntitlement(
            description=
            '\n            Entitlement required to use this skill.\n            '
        ),
        'icon':
        TunableResourceKey(
            description=
            '\n            Icon to be displayed for the Skill.\n            ',
            default='PNG:missing_image',
            resource_types=sims4.resources.CompoundTypes.IMAGE,
            export_modes=ExportModes.All),
        'tags':
        TunableList(
            description=
            '\n            The associated categories of the skill\n            ',
            tunable=TunableEnumEntry(tunable_type=tag.Tag,
                                     default=tag.Tag.INVALID)),
        'priority':
        Tunable(
            description=
            '\n            Skill priority.  Higher priority skill will trump other skills when\n            being displayed on the UI side. When a sim gains multiple skills at\n            the same time only the highest priority one will display a progress\n            bar over its head.\n            ',
            tunable_type=int,
            default=1,
            export_modes=ExportModes.All),
        '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),
        'next_level_teaser':
        TunableList(
            description=
            '\n            Tooltip which describes what the next level entails.\n            ',
            tunable=TunableLocalizedString(),
            export_modes=(ExportModes.ClientBinary, )),
        '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(),
                    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(),
            export_modes=ExportModes.All),
        'stat_asm_param':
        TunableStatAsmParam.TunableFactory(),
        '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),
            class_restrictions=('Tutorial', )),
        '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', )))
    }
    REMOVE_INSTANCE_TUNABLES = ('min_value_tuning', 'max_value_tuning',
                                'decay_rate', '_default_convergence_value')

    def __init__(self, tracker):
        super().__init__(tracker, self.initial_value)
        self._delta_enabled = True
        self._callback_handle = None
        if self.tracker.owner.is_simulating:
            self.on_initial_startup()
        self._max_level_update_sent = False

    def on_initial_startup(self):
        if self.tracker.owner.is_selectable:
            self.refresh_level_up_callback()

    def on_remove(self, on_destroy=False):
        super().on_remove(on_destroy=on_destroy)
        self._destory_callback_handle()

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

    @caches.cached
    def get_user_value(self):
        return super(Skill, self).get_user_value()

    def set_value(self,
                  value,
                  *args,
                  from_load=False,
                  interaction=None,
                  **kwargs):
        old_value = self.get_value()
        super().set_value(value, *args, **kwargs)
        self.get_user_value.cache.clear()
        if not from_load:
            new_value = self.get_value()
            new_level = self.convert_to_user_value(value)
            if old_value == self.initial_value and old_value != new_value:
                sim_info = self._tracker._owner
                services.get_event_manager().process_event(
                    test_events.TestEvent.SkillLevelChange,
                    sim_info=sim_info,
                    statistic=self.stat_type)
            old_level = self.convert_to_user_value(old_value)
            if old_level < new_level:
                self._apply_multipliers_to_continuous_statistics()

    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)
        self.get_user_value.cache.clear()
        if interaction is not None:
            self.on_skill_updated(telemhook, old_value, self.get_value(),
                                  interaction.affordance.__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()
        self.get_user_value.cache.clear()
        new_value = self._value
        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)
            sim_info = self._tracker._owner
            services.get_event_manager().process_event(
                test_events.TestEvent.SkillLevelChange,
                sim_info=sim_info,
                statistic=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_value < new_value and old_level < new_level:
            if self._tracker is not None:
                self._tracker.notify_watchers(self.stat_type, self._value,
                                              self._value)

    def on_skill_updated(self, telemhook, old_value, new_value,
                         affordance_name):
        owner_sim = self._tracker._owner
        if owner_sim.is_selectable:
            with telemetry_helper.begin_hook(skill_telemetry_writer,
                                             telemhook,
                                             sim=owner_sim) 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._show_level_notification(skill_level)

    def _destory_callback_handle(self):
        if self._callback_handle is not None:
            self.remove_callback(self._callback_handle)
            self._callback_handle = None

    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.add_callback(
            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=sim_info) as hook:
            hook.write_guid(TELEMETRY_FIELD_SKILL_ID, self.guid64)
            hook.write_int(TELEMETRY_FIELD_SKILL_LEVEL, new_level)
        if sim_info.account is not None:
            services.social_service.post_skill_message(sim_info, self,
                                                       old_level, new_level)
        self._show_level_notification(new_level)
        services.get_event_manager().process_event(
            test_events.TestEvent.SkillLevelChange,
            sim_info=sim_info,
            statistic=self.stat_type)

    def _show_level_notification(self, skill_level):
        sim_info = self._tracker._owner
        if not sim_info.is_npc:
            level_data = self.level_data.get(skill_level)
            if level_data is not None:
                tutorial_id = None
                if self.tutorial is not None and 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=(self.icon, None),
                                         secondary_icon_override=(None,
                                                                  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)

    @classproperty
    def skill_type(cls):
        return cls

    @classproperty
    def remove_on_convergence(cls):
        return False

    @classmethod
    def can_add(cls, owner, force_add=False, **kwargs):
        if force_add:
            return True
        if cls.genders and owner.gender not in cls.genders:
            return False
        if cls.ages and owner.age not in cls.ages:
            return False
        if cls.entitlement is None:
            return True
        if owner.is_npc:
            return False
        return mtx.has_entitlement(cls.entitlement)

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

    @classmethod
    def get_max_skill_value(cls):
        level_list = cls.get_level_list()
        return sum(level_list)

    @classmethod
    def get_skill_value_for_level(cls, level):
        level_list = cls.get_level_list()
        if level > len(level_list):
            logger.error('Level {} out of bounds', level)
            return 0
        return sum(level_list[:level])

    @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

    @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')

    @classmethod
    def convert_to_user_value(cls, value):
        if not cls.get_level_list():
            return 0
        current_value = value
        for (level, level_threshold) in enumerate(cls.get_level_list()):
            current_value -= level_threshold
            while 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 _get_level_bounds(cls, level):
        level_list = cls.get_level_list()
        level_min = sum(level_list[:level])
        if level < cls.max_level:
            level_max = sum(level_list[:level + 1])
        else:
            level_max = sum(level_list)
        return (level_min, level_max)

    def _get_next_level_bound(self):
        level = self.convert_to_user_value(self._value)
        (_, level_max) = self._get_level_bounds(level)
        return level_max

    @property
    def reached_max_level(self):
        max_value = self.get_max_skill_value()
        if self.get_value() >= max_value:
            return True
        return False

    @property
    def should_send_update(self):
        if not self.reached_max_level:
            return True
        if not self._max_level_update_sent:
            self._max_level_update_sent = True
            return True
        return False

    @classproperty
    def is_skill(cls):
        return True

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

    @classmethod
    def create_skill_update_msg(cls, sim_id, stat_value):
        if not cls.convert_to_user_value(stat_value) > 0:
            return
        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

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

    @classproperty
    def valid_for_stat_testing(cls):
        return True
class _WaypointGeneratorBase(HasTunableFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {'mobile_posture_override': OptionalTunable(description='\n            If enabled, the mobile posture specified would require the sim to\n            be in this posture to begin the route. This allows us to make the\n            Sim Swim or Ice Skate instead of walk/run.\n            ', tunable=MobilePosture.TunableReference(description='\n                The mobile posture we want to use.\n                ')), '_loops': TunableRange(description='\n            The number of loops we want to perform per route.\n            ', tunable_type=int, default=1, minimum=1), 'use_provided_routing_surface': Tunable(description="\n            If enabled, we will use the target's provided routing surface if it\n            has one.\n            ", tunable_type=bool, default=False)}

    def __init__(self, context, target, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._context = context
        self._target = target if target is not None else context.sim
        provided_routing_surface = self._target.provided_routing_surface
        if provided_routing_surface is not None and self.use_provided_routing_surface:
            self._routing_surface = provided_routing_surface
        else:
            self._routing_surface = self._target.routing_location.routing_surface
        self._water_constraint = dict()

    @property
    def loops(self):
        return self._loops

    def get_water_constraint(self, min_water_depth=None, max_water_depth=None):
        water_constraint_key = (min_water_depth, max_water_depth)
        if water_constraint_key in self._water_constraint:
            return self._water_constraint[water_constraint_key]
        if self._context.sim is not None:
            (min_water_depth, max_water_depth) = OceanTuning.make_depth_bounds_safe_for_surface_and_sim(self._routing_surface, self._context.sim, min_water_depth, max_water_depth)
        if self._target is not None:
            if self._target is not self._context.sim:
                (min_water_depth, max_water_depth) = OceanTuning.make_depth_bounds_safe_for_surface_and_sim(self._routing_surface, self._target, min_water_depth, max_water_depth)
        if self.is_for_vehicle:
            wading_interval = TunedInterval(0.1, 0.1)
            (min_water_depth, max_water_depth) = OceanTuning.make_depth_bounds_safe_for_surface(self._routing_surface, wading_interval, min_water_depth, max_water_depth)
        if min_water_depth is None and max_water_depth is None:
            constraint = ANYWHERE
        else:
            constraint = Constraint(min_water_depth=min_water_depth, max_water_depth=max_water_depth)
        self._water_constraint[water_constraint_key] = constraint
        return constraint

    def apply_water_constraint(self, constraint_list):
        if not constraint_list:
            return constraint_list
        water_constraint = self.get_water_constraint()
        if water_constraint is not ANYWHERE:
            orig_list = constraint_list
            constraint_list = []
            for orig_constraint in orig_list:
                constraint_list.append(orig_constraint.intersect(water_constraint))
        return constraint_list

    def get_start_constraint(self):
        raise NotImplementedError

    def clean_up(self):
        pass

    def get_waypoint_constraints_gen(self, routing_agent, waypoint_count):
        raise NotImplementedError

    def get_posture_constraint(self):
        return get_mobile_posture_constraint(posture=self.mobile_posture_override, target=self._target)

    @property
    def is_for_vehicle(self):
        return self.mobile_posture_override is not None and self.mobile_posture_override.is_vehicle