示例#1
0
class CareerAttendenceTest(event_testing.test_base.BaseTest):
    __qualname__ = 'CareerAttendenceTest'
    test_events = (event_testing.test_events.TestEvent.WorkdayComplete, )
    USES_DATA_OBJECT = True
    USES_EVENT_DATA = True
    FACTORY_TUNABLES = {
        'description':
        'After a work day completes, did your sim work a desired of hours, earn a tuned amount (total over lifetime),                            at a specific or any career. Note: any career (leaving career untuned) means it checks against total of all of them.',
        'career_to_test':
        TunableReference(manager=services.get_instance_manager(
            sims4.resources.Types.CAREER)),
        'career_category':
        TunableEnumEntry(
            careers.career_tuning.CareerCategory,
            careers.career_tuning.CareerCategory.Invalid,
            description=
            'Category the specified career is required to be in order to pass validation'
        ),
        'simoleons_earned':
        TunableThreshold(description='Amount in Simoleons required to pass'),
        'hours_worked':
        TunableThreshold(description='Amount in hours required to pass')
    }

    def __init__(self, career_to_test, career_category, simoleons_earned,
                 hours_worked, **kwargs):
        super().__init__(**kwargs)
        self.career_to_test = career_to_test
        self.simoleons_earned = simoleons_earned
        self.hours_worked = hours_worked
        self.career_category = career_category

    def get_expected_args(self):
        return {
            'career': event_testing.test_events.FROM_EVENT_DATA,
            'data': event_testing.test_events.FROM_DATA_OBJECT,
            'objective_guid64': event_testing.test_events.OBJECTIVE_GUID64
        }

    @cached_test
    def __call__(self, career=None, data=None, objective_guid64=None):
        if career is None:
            return TestResult(
                False, 'Career provided is None, valid during zone load.')
        total_money_made = 0
        total_time_worked = 0
        if not isinstance(career, self.career_to_test):
            return TestResult(False, '{} does not match tuned value {}',
                              career, self.career_to_test)
        career_data = data.get_career_data(career)
        total_money_made = career_data.get_money_earned()
        total_time_worked = career_data.get_hours_worked()
        relative_start_values = data.get_starting_values(objective_guid64)
        money = 0
        time = 1
        total_money_made -= relative_start_values[money]
        total_time_worked -= relative_start_values[time]
        if not (self.career_to_test is not None
                and relative_start_values is not None
                and self.simoleons_earned.compare(total_money_made)):
            return TestResultNumeric(
                False,
                'CareerAttendenceTest: not the desired amount of Simoleons.',
                current_value=total_money_made,
                goal_value=self.simoleons_earned.value,
                is_money=True)
        if not self.hours_worked.compare(total_time_worked):
            return TestResultNumeric(
                False,
                'CareerAttendenceTest: not the desired amount of time worked.',
                current_value=total_time_worked,
                goal_value=self.hours_worked.value,
                is_money=False)
        return TestResult.TRUE

    def save_relative_start_values(self, objective_guid64, data_object):
        if self.career_to_test is not None:
            return
        career_name = self.career_to_test.__name__
        start_money = data_object.get_career_data_by_name(
            career_name).get_money_earned()
        start_time = data_object.get_career_data_by_name(
            career_name).get_hours_worked()
        data_object.set_starting_values(objective_guid64,
                                        [start_money, start_time])
class CarryableComponent(
        Component,
        HasTunableFactory,
        AutoFactoryInit,
        component_name=objects.components.types.CARRYABLE_COMPONENT):
    class _CarryableAllowedHands(HasTunableSingletonFactory, AutoFactoryInit):
        FACTORY_TUNABLES = {
            'biped_allowed_hands':
            TunableVariant(locked_args={
                'both': (Hand.LEFT, Hand.RIGHT),
                'left_only': (Hand.LEFT, ),
                'right_only': (Hand.RIGHT, )
            },
                           default='both'),
            'quadruped_allowed_hands':
            TunableVariant(locked_args={
                'both': (Hand.LEFT, Hand.RIGHT),
                'mouth_only': (Hand.RIGHT, ),
                'back_only': (Hand.LEFT, )
            },
                           default='mouth_only')
        }

        def get_allowed_hands(self, sim):
            if sim is None:
                return self.biped_allowed_hands
            return sim.get_allowed_hands_type(self)

    class _CarryableTransitionConstraint(HasTunableSingletonFactory,
                                         AutoFactoryInit):
        FACTORY_TUNABLES = {
            'constraint_mobile':
            TunableList(
                description=
                '\n                The constraint to use when the Sim is in a mobile posture.\n                ',
                tunable=TunableGeometricConstraintVariant(
                    disabled_constraints={
                        'spawn_points', 'relative_circle', 'current_position'
                    })),
            'constraint_non_mobile':
            TunableList(
                description=
                '\n                The constraint to use when the Sim is not in a mobile posture.\n                ',
                tunable=TunableGeometricConstraintVariant(
                    disabled_constraints={
                        'spawn_points', 'relative_circle', 'current_position'
                    }))
        }

    DEFAULT_GEOMETRIC_TRANSITION_CONSTRAINT = _CarryableTransitionConstraint.TunableFactory(
        description=
        '\n        Unless specifically overridden, the constraint to use when transitioning\n        into and out of a carry for any carryable object.\n        '
    )
    DEFAULT_GEOMETRIC_TRANSITION_LARGE = TunableRange(
        description=
        '\n        This is a large transition distance. This is used by:\n            * TYAE humans picking up any pet\n        ',
        tunable_type=float,
        default=0.7,
        minimum=0)
    DEFAULT_GEOMETRIC_TRANSITION_MEDIUM = TunableRange(
        description=
        '\n        This is a medium transition distance. This is used by:\n            * TYAE humans picking up P humans\n        ',
        tunable_type=float,
        default=0.6,
        minimum=0)
    DEFAULT_GEOMETRIC_TRANSITION_SMALL = TunableRange(
        description=
        '\n        This is a small transition distance. This is used by:\n            * C humans picking up AE cats and AE small dogs\n        ',
        tunable_type=float,
        default=0.503,
        minimum=0)
    DEFAULT_GEOMETRIC_TRANSITION_TINY = TunableRange(
        description=
        '\n        This is a tiny transition distance. This is used by:\n            * C humans picking up C cats and C dogs and small dogs\n        ',
        tunable_type=float,
        default=0.419,
        minimum=0)
    DEFAULT_CARRY_AFFORDANCES = TunableList(
        description=
        '\n        The list of default carry affordances.\n        ',
        tunable=TunableReference(manager=services.get_instance_manager(
            sims4.resources.Types.INTERACTION)))
    PUT_IN_INVENTORY_AFFORDANCE = TunableReference(
        description=
        '\n        The affordance used by carryable component to put objects in inventory.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.INTERACTION))
    PUT_DOWN_HERE_AFFORDANCE = TunableReference(
        description=
        '\n        The affordance used by carryable component to put down here via the\n        PutDownLiability liability.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.INTERACTION))
    PUT_DOWN_ANYWHERE_AFFORDANCE = TunableReference(
        description=
        '\n        The affordance used by carryable component to put down objects anywhere\n        via the PutDownLiability liability.\n        ',
        manager=services.get_instance_manager(
            sims4.resources.Types.INTERACTION))
    FACTORY_TUNABLES = {
        'put_down_tuning':
        TunableVariant(
            description=
            '\n            Define how Sims prefer to put this object down.\n            ',
            reference=TunablePutDownStrategySpeciesMapping(
                description=
                '\n                Define specific costs for all the possible locations this object\n                could be put down. Also, define, if necessary, custom\n                interactions to put the object down.\n                \n                If this is not specified, and the default is used, the most\n                appropriate put down strategy is used. This allows the object to\n                be put down in most places (floor, inventory, slots). Also,\n                species might specify their own custom behavior.\n                '
            ),
            locked_args={'use_default': None},
            default='use_default'),
        'state_based_put_down_tuning':
        TunableMapping(
            description=
            '\n            A mapping from a state value to a putdownstrategy. If the owning\n            object is in any of the states tuned here, it will use that state\'s\n            associated putdownstrategy in place of the one putdownstrategy tuned\n            in the "put_down_tuning" field. If the object is in multiple states\n            listed in this mapping, the behavior is undefined.\n            ',
            key_type=TunableReference(
                description=
                '\n                The state value this object must be in in order to use the\n                associated putdownstrategy.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.OBJECT_STATE)),
            value_type=
            TunableVariant(reference=TunablePutDownStrategySpeciesMapping(
                description=
                '\n                    Tuning for how to score where a Sim might want to set an\n                    object down.\n                    '
            )),
            key_name='State',
            value_name='PutDownStrategy'),
        'carry_affordances':
        OptionalTunable(tunable=TunableList(tunable=TunableReference(
            description=
            '\n                    The versions of the HoldObject affordance that this object\n                    supports.\n                    ',
            manager=services.affordance_manager())),
                        disabled_name='use_default_affordances',
                        enabled_name='use_custom_affordances'),
        'provided_affordances':
        TunableProvidedAffordances(
            description=
            '\n            Affordances that are generated when a Sim holding this object\n            selects another object to interact with. The generated interactions\n            target the selected object, and have their carry target set to the\n            component\'s owner.\n            \n            By default, this is applied to Sims. e.g.: The "Drink" interaction\n            provides "Make Toast" on other Sims.\n            \n            Optionally, you can specify a tag to have the interaction appear on\n            other objects. e.g.: The "Hold Puppy" interaction might unlock "Put\n            Down Here" on sofas.\n                \n            Target defaults to the object selected (Invalid). Carry Target is\n            locked to this object being carried. If is_linked is\n            checked, the affordance will be linked to the interaction\n            carrying this object.\n            ',
            class_restrictions=('SuperInteraction', ),
            locked_args={
                'allow_self': False,
                'target': ParticipantType.Object,
                'carry_target': ParticipantType.CarriedObject
            }),
        'constraint_pick_up':
        OptionalTunable(
            description=
            '\n            A list of constraints that must be fulfilled in order to\n            interact with this object.\n            ',
            tunable=TunableList(tunable=TunableConstraintVariant(
                description=
                '\n                    A constraint that must be fulfilled in order to\n                    interact with this object.\n                    '
            ))),
        'allowed_hands_data':
        _CarryableAllowedHands.TunableFactory(),
        'holster_while_routing':
        Tunable(
            description=
            '\n            If True, the Sim will holster the object before routing and\n            unholster when the route is complete.\n            ',
            tunable_type=bool,
            default=False),
        'holster_compatibility':
        TunableAffordanceFilterSnippet(
            description=
            '\n            Define interactions for which holstering this object is\n            explicitly disallowed.\n            \n            e.g. The Scythe is tuned to be holster-incompatible with\n            sitting, meaning that Sims will holster the Sctyhe when sitting.\n            '
        ),
        'unholster_when_routing':
        TunableUnholsterWhileRoutingBehaviorVariant(),
        'prefer_owning_sim_inventory_when_not_on_home_lot':
        Tunable(
            description=
            "\n            If checked, this object will highly prefer to be put into the\n            owning Sim's inventory when being put down by the owning Sim on\n            a lot other than their home lot.\n            \n            Certain objects, like consumables, should be exempt from this.\n            ",
            tunable_type=bool,
            default=True),
        'is_valid_posture_graph_object':
        Tunable(
            description=
            '\n            If this is checked, this object is allowed to provide postures and\n            expand in the posture graph, despite its ability to be carried.\n            \n            Normally, for the performance reasons, carryable objects are not\n            posture providing objects. The preferred way of interacting with a\n            carryable object is marking it as a carry requirement in the ASM.\n            \n            However, there are objects for which this is not possible. For\n            example, Nesting Blocks are carryable, but toddlers interact with\n            them while in the "Sit On Ground" posture, which must be provided by\n            the object\'s parts.\n            \n            This field should be used with caution, since posture graph\n            generation is one of our slowest systems. Do not enable it on\n            objects such as food or drinks, since the world is bound to have\n            many of them.\n            ',
            tunable_type=bool,
            default=False),
        'portal_key_mask_flags':
        TunableEnumFlags(
            description=
            "\n            Any flag tuned here will be kept on the Sim routing context who's\n            picking up this object.  This will allow a Sim to pickup some\n            type of objects and still be allowed to transition through\n            some portals while carrying an object.\n            ",
            enum_type=PortalFlags,
            allow_no_flags=True),
        'reslot_plumbbob':
        OptionalTunable(
            description=
            '\n            If tuned, the plumbbob will be repositioned when this item is carried.\n            Reslot will always go away when sim stops carrying the object.\n            ',
            tunable=TunableReslotPlumbbob()),
        'defer_putdown':
        Tunable(
            description=
            '\n            If true, the put down will be deferred to the end of the route. \n            If false, put down will be done at the start of the route. This\n            should be the default behavior. \n            ',
            tunable_type=bool,
            default=False),
        'put_down_height_tolerance':
        TunableRange(
            description=
            '\n            Maximum height tolerance on the terrain we will use for the \n            placement of this object when asking FGL to find a spot on the\n            floor.\n            Having a high value here will make it so an object can be placed\n            in a terrain spot with a high slope that might result with\n            clipping depending on the width of the object.  The way this will\n            work will be using the object footprint, if the edges are at a\n            height higher than the height tolerance, then the location will\n            not be valid.\n            ',
            tunable_type=float,
            default=1.1,
            minimum=0)
    }

    def __init__(self, owner, **kwargs):
        super().__init__(owner, **kwargs)
        self._attempted_putdown = False
        self._attempted_alternative_putdown = False
        self._cached_put_down_strategy = None

    @property
    def attempted_putdown(self):
        return self._attempted_putdown

    @property
    def attempted_alternative_putdown(self):
        return self._attempted_alternative_putdown

    @property
    def ideal_slot_type_set(self):
        put_down_strategy = self.get_put_down_strategy()
        return put_down_strategy.ideal_slot_type_set

    @componentmethod
    def get_carry_object_posture(self):
        if self.carry_affordances is None:
            return CarryPostureStaticTuning.POSTURE_CARRY_OBJECT
        return self.carry_affordances[0].provided_posture_type

    @componentmethod_with_fallback(lambda *_, **__: ())
    def get_allowed_hands(self, sim):
        return self.allowed_hands_data.get_allowed_hands(sim)

    @componentmethod_with_fallback(lambda *_, **__: 0)
    def get_portal_key_make_for_carry(self):
        return self.portal_key_mask_flags

    @componentmethod
    def should_unholster(self, *args, **kwargs):
        return self.unholster_when_routing.should_unholster(*args, **kwargs)

    @componentmethod
    def get_put_down_strategy(self, parent=DEFAULT):
        if self._cached_put_down_strategy is None:
            parent = self.owner.parent if parent is DEFAULT else parent
            species = parent.species if parent is not None else Species.HUMAN
            for (state_value, put_down_strategy
                 ) in self.state_based_put_down_tuning.items():
                if self.owner.state_value_active(state_value):
                    self._cached_put_down_strategy = put_down_strategy.get(
                        species)
                    break
            else:
                if self.put_down_tuning is not None:
                    self._cached_put_down_strategy = self.put_down_tuning.get(
                        species)
            if self._cached_put_down_strategy is None:
                put_down_strategy = parent.get_default_put_down_strategy()
                self._cached_put_down_strategy = put_down_strategy
        return self._cached_put_down_strategy

    def _get_carry_transition_distance_for_sim(self, sim):
        carry_sim = self.owner.sim_info
        carrying_sim = sim.sim_info
        if carry_sim.is_toddler:
            return self.DEFAULT_GEOMETRIC_TRANSITION_MEDIUM
        if carrying_sim.is_teen_or_older:
            return self.DEFAULT_GEOMETRIC_TRANSITION_LARGE
        if carry_sim.is_teen_or_older:
            return self.DEFAULT_GEOMETRIC_TRANSITION_SMALL
        return self.DEFAULT_GEOMETRIC_TRANSITION_TINY

    def _get_adjusted_circle_constraint(self, sim, constraint):
        if not self.owner.is_sim:
            return constraint
        if not isinstance(constraint, TunedCircle):
            return constraint
        ideal_radius_override = self._get_carry_transition_distance_for_sim(
            sim)
        constraint = copy.copy(constraint)
        constraint.ideal_radius_width = constraint.ideal_radius_width / constraint.ideal_radius * ideal_radius_override
        constraint.radius = constraint.radius / constraint.ideal_radius * ideal_radius_override
        constraint.ideal_radius = ideal_radius_override
        return constraint

    @componentmethod
    def get_carry_transition_constraint(self,
                                        sim,
                                        position,
                                        routing_surface,
                                        cost=0,
                                        mobile=True):
        constraints = self.DEFAULT_GEOMETRIC_TRANSITION_CONSTRAINT
        constraints = constraints.constraint_mobile if mobile else constraints.constraint_non_mobile
        final_constraint = Anywhere()
        for constraint in constraints:
            if mobile:
                constraint = self._get_adjusted_circle_constraint(
                    sim, constraint)
            final_constraint = final_constraint.intersect(
                constraint.create_constraint(None,
                                             None,
                                             target_position=position,
                                             routing_surface=routing_surface))
        final_constraint = final_constraint.generate_constraint_with_cost(cost)
        final_constraint = final_constraint._copy(_multi_surface=True)
        return final_constraint

    @componentmethod
    def get_pick_up_constraint(self, sim):
        if self.constraint_pick_up is None:
            return
        final_constraint = Anywhere()
        for constraint in self.constraint_pick_up:
            constraint = self._get_adjusted_circle_constraint(sim, constraint)
            constraint = constraint.create_constraint(sim, target=self.owner)
            final_constraint = final_constraint.intersect(constraint)
        final_constraint = final_constraint._copy(_multi_surface=True)
        return final_constraint

    @componentmethod
    def get_provided_aops_gen(self, target, context, **kwargs):
        for provided_affordance_data in self.provided_affordances:
            if not provided_affordance_data.affordance.is_affordance_available(
                    context=context):
                continue
            if not provided_affordance_data.object_filter.is_object_valid(
                    target):
                continue
            if provided_affordance_data.affordance.is_social and not target.is_sim:
                affordance = provided_affordance_data.affordance
                interaction_target = self.owner
                preferred_objects = (target, )
            else:
                affordance = CarryTargetInteraction.generate(
                    provided_affordance_data.affordance, self.owner)
                interaction_target = target
                preferred_objects = ()
            depended_on_si = None
            parent = self.owner.parent
            if parent is not None:
                if parent.is_sim:
                    carry_posture = parent.posture_state.get_carry_posture(
                        self.owner)
                    if carry_posture is not None:
                        if provided_affordance_data.is_linked:
                            depended_on_si = carry_posture.source_interaction
            yield from affordance.potential_interactions(
                interaction_target,
                context,
                depended_on_si=depended_on_si,
                preferred_objects=preferred_objects,
                **kwargs)

    def component_super_affordances_gen(self, **kwargs):
        if self.carry_affordances is None:
            affordances = self.DEFAULT_CARRY_AFFORDANCES
        else:
            affordances = self.carry_affordances
        for affordance in affordances:
            yield affordance

    def component_interactable_gen(self):
        yield self

    def on_state_changed(self, state, old_value, new_value, from_init):
        if new_value in self.state_based_put_down_tuning or old_value in self.state_based_put_down_tuning:
            self._cached_put_down_strategy = None

    def component_reset(self, reset_reason):
        self.reset_put_down_count()

    @componentmethod
    def get_initial_put_down_position(self, carrying_sim=None):
        carrying_sim = carrying_sim or self.owner.parent
        if carrying_sim is None:
            return (self.owner.position, self.owner.routing_surface)
        additional_put_down_distance = carrying_sim.posture.additional_put_down_distance
        position = carrying_sim.position + carrying_sim.forward * (
            carrying_sim.object_radius + additional_put_down_distance)
        sim_los_constraint = carrying_sim.lineofsight_component.constraint
        if not sims4.geometry.test_point_in_compound_polygon(
                position, sim_los_constraint.geometry.polygon):
            position = carrying_sim.position
        return (position, carrying_sim.routing_surface)

    @componentmethod
    def get_put_down_aop(self,
                         interaction,
                         context,
                         alternative_multiplier=1,
                         own_inventory_multiplier=1,
                         object_inventory_multiplier=DEFAULT,
                         in_slot_multiplier=DEFAULT,
                         on_floor_multiplier=1,
                         visibility_override=None,
                         display_name_override=None,
                         additional_post_run_autonomy_commodities=None,
                         add_putdown_liability=False,
                         **kwargs):
        sim = interaction.sim
        owner = self.owner
        if owner.transient:
            return self._get_destroy_aop(sim, **kwargs)
        put_down_strategy = self.get_put_down_strategy(parent=sim)
        if object_inventory_multiplier is DEFAULT:
            object_inventory_multiplier = sim.get_put_down_object_inventory_cost_override(
            )
        if in_slot_multiplier is DEFAULT:
            in_slot_multiplier = sim.get_put_down_slot_cost_override()
        slot_types_and_costs = self.get_slot_types_and_costs(
            multiplier=in_slot_multiplier)
        (terrain_transform,
         terrain_routing_surface) = self._get_terrain_transform(interaction)
        objects = self._get_objects_with_inventory(interaction)
        objects = [obj for obj in objects if obj.can_access_for_putdown(sim)]
        if put_down_strategy.floor_cost is not None and on_floor_multiplier is not None:
            world_cost = put_down_strategy.floor_cost * on_floor_multiplier
        else:
            world_cost = None
        if put_down_strategy.inventory_cost is not None and own_inventory_multiplier is not None:
            sim_inventory_cost = put_down_strategy.inventory_cost * own_inventory_multiplier
        else:
            sim_inventory_cost = None
        if put_down_strategy.object_inventory_cost is not None and object_inventory_multiplier is not None:
            object_inventory_cost = put_down_strategy.object_inventory_cost * object_inventory_multiplier
        else:
            object_inventory_cost = None
        if not put_down_strategy.affordances:
            self._attempted_alternative_putdown = True
        if not self._attempted_alternative_putdown or self.owner.is_sim:
            self._attempted_alternative_putdown = True
            scored_aops = []
            for scored_aop in self._gen_affordance_score_and_aops(
                    interaction,
                    slot_types_and_costs=slot_types_and_costs,
                    world_cost=world_cost,
                    sim_inventory_cost=sim_inventory_cost,
                    object_inventory_cost=object_inventory_cost,
                    terrain_transform=terrain_transform,
                    terrain_routing_surface=terrain_routing_surface,
                    objects_with_inventory=objects,
                    visibility_override=visibility_override,
                    display_name_override=display_name_override,
                    additional_post_run_autonomy_commodities=
                    additional_post_run_autonomy_commodities,
                    multiplier=alternative_multiplier,
                    add_putdown_liability=add_putdown_liability):
                if scored_aop.aop.test(context):
                    scored_aops.append(scored_aop)
            if scored_aops:
                scored_aops.sort(key=operator.itemgetter(0))
                return scored_aops[-1].aop
        affordance = CarryableComponent.PUT_DOWN_ANYWHERE_AFFORDANCE
        if add_putdown_liability:
            liabilities = ((PutDownLiability.LIABILITY_TOKEN,
                            PutDownLiability(self.owner)), )
        else:
            liabilities = ()
        aop = AffordanceObjectPair(
            affordance,
            self.owner,
            affordance,
            None,
            slot_types_and_costs=slot_types_and_costs,
            world_cost=world_cost,
            sim_inventory_cost=sim_inventory_cost,
            object_inventory_cost=object_inventory_cost,
            terrain_transform=terrain_transform,
            terrain_routing_surface=terrain_routing_surface,
            objects_with_inventory=objects,
            visibility_override=visibility_override,
            display_name_override=display_name_override,
            additional_post_run_autonomy_commodities=
            additional_post_run_autonomy_commodities,
            liabilities=liabilities,
            **kwargs)
        self._attempted_putdown = True
        return aop

    def _gen_affordance_score_and_aops(
            self,
            interaction,
            slot_types_and_costs,
            world_cost,
            sim_inventory_cost,
            object_inventory_cost,
            terrain_transform,
            terrain_routing_surface,
            objects_with_inventory,
            visibility_override,
            display_name_override,
            additional_post_run_autonomy_commodities,
            multiplier=1,
            add_putdown_liability=False):
        put_down_strategy = self.get_put_down_strategy()
        for affordance in put_down_strategy.affordances:
            if add_putdown_liability:
                liabilities = ((PutDownLiability.LIABILITY_TOKEN,
                                PutDownLiability(self.owner)), )
            else:
                liabilities = ()
            aop = AffordanceObjectPair(
                affordance,
                self.owner,
                affordance,
                None,
                slot_types_and_costs=slot_types_and_costs,
                world_cost=world_cost,
                sim_inventory_cost=sim_inventory_cost,
                object_inventory_cost=object_inventory_cost,
                terrain_transform=terrain_transform,
                terrain_routing_surface=terrain_routing_surface,
                objects_with_inventory=objects,
                visibility_override=visibility_override,
                display_name_override=display_name_override,
                additional_post_run_autonomy_commodities=
                additional_post_run_autonomy_commodities,
                liabilities=liabilities)
            yield ScoredAOP(multiplier, aop)

    def _get_cost_for_slot_type(self, slot_type):
        put_down_strategy = self.get_put_down_strategy()
        if slot_type in self.owner.ideal_slot_types:
            return put_down_strategy.preferred_slot_cost
        return put_down_strategy.normal_slot_cost

    def get_slot_types_and_costs(self, multiplier=1):
        slot_types_and_costs = []
        for slot_type in self.owner.all_valid_slot_types:
            cost = self._get_cost_for_slot_type(slot_type)
            if cost is not None and multiplier is not None:
                cost *= multiplier
            else:
                cost = None
            slot_types_and_costs.append((slot_type, cost))
        return slot_types_and_costs

    def _get_terrain_transform(self, interaction):
        if not self.owner.is_sim and self.owner.footprint_component is None:
            return (None, None)
        else:
            sim = interaction.sim
            put_down_position = interaction.interaction_parameters.get(
                'put_down_position')
            put_down_routing_surface = interaction.interaction_parameters.get(
                'put_down_routing_surface')
            if put_down_position is None:
                (starting_position, starting_routing_surface
                 ) = self.get_initial_put_down_position(carrying_sim=sim)
            else:
                starting_position = put_down_position
                starting_routing_surface = put_down_routing_surface
            starting_location = placement.create_starting_location(
                position=starting_position,
                orientation=sim.orientation,
                routing_surface=starting_routing_surface)
            if self.owner.is_sim:
                search_flags = FGLSearchFlagsDefaultForSim | FGLSearchFlag.STAY_IN_CURRENT_BLOCK
                fgl_context_fn = functools.partial(
                    placement.create_fgl_context_for_sim,
                    search_flags=search_flags)
            else:
                search_flags = FGLSearchFlag.STAY_IN_CURRENT_BLOCK | FGLSearchFlag.SHOULD_TEST_ROUTING | FGLSearchFlag.CALCULATE_RESULT_TERRAIN_HEIGHTS | FGLSearchFlag.DONE_ON_MAX_RESULTS | FGLSearchFlag.SHOULD_TEST_BUILDBUY
                fgl_context_fn = functools.partial(
                    placement.create_fgl_context_for_object,
                    search_flags=search_flags)
            MAX_PUTDOWN_STEPS = 8
            MAX_PUTDOWN_DISTANCE = 10
            fgl_context = fgl_context_fn(
                starting_location,
                self.owner,
                max_steps=MAX_PUTDOWN_STEPS,
                max_distance=MAX_PUTDOWN_DISTANCE,
                height_tolerance=self.put_down_height_tolerance)
            (position, orientation) = placement.find_good_location(fgl_context)
            if position is not None:
                put_down_transform = sims4.math.Transform(
                    position, orientation)
                return (put_down_transform, starting_routing_surface)
        return (None, None)

    def _get_objects_with_inventory(self, interaction):
        objects = []
        inventory_item = self.owner.inventoryitem_component
        if inventory_item is not None:
            if CarryableComponent.PUT_IN_INVENTORY_AFFORDANCE is not None:
                for obj in inventory_item.valid_object_inventory_gen():
                    objects.append(obj)
        return objects

    def _get_destroy_aop(self, sim, **kwargs):
        affordance = CarryableComponent.PUT_DOWN_HERE_AFFORDANCE
        return AffordanceObjectPair(affordance,
                                    self.owner,
                                    affordance,
                                    None,
                                    put_down_transform=None,
                                    **kwargs)

    def reset_put_down_count(self):
        self._attempted_alternative_putdown = False
        self._attempted_putdown = False
        self._cached_put_down_strategy = None

    def on_object_carry(self, actor, *_, **__):
        if self.reslot_plumbbob is not None:
            reslot_plumbbob(actor, self.reslot_plumbbob)

    def on_object_uncarry(self, actor, *_, **__):
        if self.reslot_plumbbob is not None:
            unslot_plumbbob(actor)
示例#3
0
class RoleState(HasDependentTunableReference,
                role.role_state_base.RoleStateBase,
                metaclass=HashedTunedInstanceMetaclass,
                manager=services.get_instance_manager(
                    sims4.resources.Types.ROLE_STATE)):
    INSTANCE_TUNABLES = {
        '_role_priority':
        TunableEnumEntry(
            RolePriority,
            RolePriority.NORMAL,
            description=
            '\n                The priority of this role state.  All the role states with the\n                same priority will all be applied together.  The highest group\n                of priorities is considered the active ones.\n                '
        ),
        '_buffs':
        TunableList(
            buffs.tunable.TunableBuffReference(pack_safe=True),
            description=
            '\n                Buffs that will be added to sim when role is active.\n                '
        ),
        '_off_lot_autonomy_buff':
        buffs.tunable.TunableBuffReference(
            description=
            'A buff that\n            prevents autonomy from considering some objects based on the\n            location of the object (e.g. on lot, off lot, within a radius of the\n            sim).\n             \n            In the buff set: Game Effect Modifiers->Autonomy Modifier->Off Lot\n            Autonomy Rule.\n            ',
            allow_none=True),
        'tags':
        TunableSet(
            TunableEnumEntry(Tag, Tag.INVALID),
            description=
            '\n                Tags for the role state for checking role states against a set\n                of tags rather than against a list of role states.\n                '
        ),
        'role_affordances':
        TunableList(
            description=
            "\n            A list of affordances that are available on the Sim in this Role\n            State.\n            \n            e.g: when a Maid is in the working Role State, he or she will have\n            the 'Dismiss' and 'Fire' affordances available in the Pie Menu.\n            ",
            tunable=TunableReference(manager=services.affordance_manager(),
                                     class_restrictions=('SuperInteraction', ),
                                     pack_safe=True)),
        'role_target_affordances':
        TunableList(
            description=
            '\n            A list of affordances that are available on other Sims when the\n            actor Sim is in this Role State.\n            \n            e.g. a Sim in a specific Role State could have an "Invite to\n            Situation" interaction available when bringing up other Sims\' Pie\n            Menus.\n            ',
            tunable=TunableReference(manager=services.affordance_manager(),
                                     class_restrictions=('SuperInteraction', ),
                                     pack_safe=True)),
        'preroll_affordances':
        TunableList(
            description=
            '\n            A list of affordances that are available for sims to consider when\n            running pre-roll. Objects related to role can specify preroll\n            autonomy, but there are some roles that may not have an object\n            associated with it\n            \n            e.g. Romance guru in romance festival preroll to an attractor point.\n            ',
            tunable=TunableReference(manager=services.affordance_manager(),
                                     class_restrictions=('SuperInteraction', ),
                                     pack_safe=True)),
        '_on_activate':
        TunableVariant(
            description=
            '\n                Select the autonomy behavior when this role state becomes active on the sim.\n                disabled: Take no action.\n                autonomy_ping: We explicitly force an autonomy ping on the sim.\n                push_affordance: Push the specific affordance on the sim.\n                ',
            locked_args={'disabled': None},
            autonomy_ping=DoAutonomyPingFromRole.TunableFactory(),
            parameterized_autonomy_ping=DoParameterizedAutonomyPingFromRole.
            TunableFactory(),
            push_affordance=PushAffordanceFromRole.TunableFactory(),
            default='disabled'),
        '_portal_disallowance_tags':
        TunableSet(
            description=
            '\n                A set of tags that define what the portal disallowance tags of\n                this role state are.  Portals that include any of these\n                disallowance tags are considered locked for sims that have this\n                role state.\n                ',
            tunable=TunableEnumWithFilter(
                description=
                '\n                    A single portal disallowance tag.\n                    ',
                tunable_type=tag.Tag,
                default=tag.Tag.INVALID,
                filter_prefixes=tag.PORTAL_DISALLOWANCE_PREFIX)),
        '_allow_npc_routing_on_active_lot':
        Tunable(
            description=
            '\n                If True, then npc in this role will be allowed to route on the\n                active lot.\n                If False, then npc in this role will not be allowed to route on the\n                active lot, unless they are already on the lot when the role\n                state is activated.\n                \n                This flag is ignored for player sims and npcs who live on the\n                active lot.\n                \n                e.g. ambient walkby sims should not be routing on the active lot\n                because that is rude.\n                ',
            tunable_type=bool,
            default=True),
        '_autonomy_state_override':
        OptionalTunable(
            description=
            '\n            If tuned, will force role sims into a specific autonomy state.\n            Please consult your GPE partner before using this.\n            ',
            tunable=TunableEnumEntry(tunable_type=AutonomyState,
                                     default=AutonomyState.LIMITED_ONLY,
                                     invalid_enums=(AutonomyState.MEDIUM, )),
            tuning_filter=FilterTag.EXPERT_MODE),
        '_crafting_process_override':
        TunableEnumEntry(
            description=
            '\n                The override option of who to assign ownership of objects made\n                by Sims in this role state.\n                ',
            tunable_type=RoleStateCraftingOwnershipOverride,
            default=RoleStateCraftingOwnershipOverride.NO_OVERRIDE),
        'always_active':
        Tunable(
            description=
            "\n                If set to True, this role will always be allowed to be active\n                when set on a Sim, regardless of whether or not it is \n                lower priority than the Sim's other currently active roles. \n                Use for roles that are important but retuning priority for it \n                and/or other roles isn't feasible.\n                \n                Consult a GPE before you set this to True.\n                This is not to be used lightly and there may be other options\n                like situation exclusivity that can be explored before you\n                go down this route.\n                \n                e.g. Sim is possessed which runs at HIGH priority.\n                Sim wants to go visit an NPC residential lot, which places\n                Sim in NORMAL priority Role_UngreetedPlayerVisitingNPC, which\n                sets portal disallowance and adds specific buffs.\n                \n                We actually want Role_UngreetedPlayerVisitingNPC to run\n                even though the role priority is now HIGH, because \n                otherwise a possessed Sim visiting an NPC would magically\n                be able to route through homes because portal disallowance\n                is removed.\n                ",
            tunable_type=bool,
            default=False)
    }

    @classmethod
    def _verify_tuning_callback(cls):
        for buff_ref in cls.buffs:
            if buff_ref is None:
                logger.error(
                    '{} has empty buff in buff list. Please fix tuning.', cls)
            elif buff_ref.buff_type._temporary_commodity_info is not None:
                logger.error(
                    '{} has a buff {} that has a temporary commodity.', cls,
                    buff_ref.buff_type)

    @classproperty
    def role_priority(cls):
        return cls._role_priority

    @classproperty
    def buffs(cls):
        return cls._buffs

    @classproperty
    def off_lot_autonomy_buff(cls):
        return cls._off_lot_autonomy_buff

    @classproperty
    def role_specific_affordances(cls):
        return cls.role_affordances

    @classproperty
    def allow_npc_routing_on_active_lot(cls):
        return cls._allow_npc_routing_on_active_lot

    @classproperty
    def autonomy_state_override(cls):
        return cls._autonomy_state_override

    @classproperty
    def on_activate(cls):
        return cls._on_activate

    @classproperty
    def portal_disallowance_tags(cls):
        return cls._portal_disallowance_tags

    @classproperty
    def has_full_permissions(cls):
        current_venue = services.get_current_venue()
        if current_venue and current_venue.allow_rolestate_routing_on_navmesh:
            return True
        return not cls._portal_disallowance_tags and cls._allow_npc_routing_on_active_lot

    def _get_target_for_push_affordance(self,
                                        situation_target,
                                        situation=None,
                                        role_affordance_target=None):
        if situation_target == SituationAffordanceTarget.NO_TARGET:
            return
        if situation_target == SituationAffordanceTarget.CRAFTED_OBJECT:
            return role_affordance_target
        if situation_target == SituationAffordanceTarget.TARGET_OBJECT and situation is not None:
            return situation.get_target_object()
        if situation_target == SituationAffordanceTarget.CREATED_OBJECT and situation is not None:
            return situation.get_created_object()
        logger.error(
            'Unable to resolve target when trying to push affordance on role state {} activate. requested target type was {}',
            self, self._on_activate.target)

    @classproperty
    def active_household_crafting_override(cls):
        return cls._crafting_process_override == RoleStateCraftingOwnershipOverride.ACTIVE_HOUSEHOLD

    @classproperty
    def lot_owner_crafting_override(cls):
        return cls._crafting_process_override == RoleStateCraftingOwnershipOverride.LOT_OWNER
示例#4
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
示例#5
0
class RabbitHole(HasTunableReference,
                 metaclass=HashedTunedInstanceMetaclass,
                 manager=services.get_instance_manager(
                     sims4.resources.Types.RABBIT_HOLE)):
    INSTANCE_TUNABLES = {
        'affordance':
        TunableReference(
            description=
            ' \n            The rabbit hole affordance. This affordance must have a tuned rabbit\n            hole liability and must use a rabbit hole exit condition.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.INTERACTION)),
        'away_action':
        OptionalTunable(
            description=
            '\n            If tuned, an away action for the rabbit holed sim info to run. If\n            not tuned, no away actions will be started.\n            ',
            tunable=TunableReference(manager=services.get_instance_manager(
                sims4.resources.Types.AWAY_ACTION)),
            enabled_by_default=True),
        'go_home_and_attend':
        OptionalTunable(
            description=
            '"\n            If tuned, this affordance will run when a sim needs to go home to\n            attend a rabbit hole. If not tuned, the sim will use the generic\n            travel. This only needs to be tuned in cases where we need special\n            travel behavior (like different constraints).\n            ',
            tunable=TunableReference(
                manager=services.get_instance_manager(
                    sims4.resources.Types.INTERACTION),
                class_restrictions=('GoHomeTravelInteraction', ))),
        'loot_list':
        TunableList(
            description=
            "\n            Loots to apply to rabbit holed sim after they leave the \n            rabbit hole. Won't be applied if the rabbit hole is cancelled.\n            ",
            tunable=TunableReference(manager=services.get_instance_manager(
                sims4.resources.Types.ACTION),
                                     class_restrictions=('LootActions', ),
                                     pack_safe=True)),
        'exit_conditions':
        TunableList(
            description=
            '\n            A list of exit conditions for this rabbit hole. When exit\n            conditions are met then the rabbit hole ends.\n            ',
            tunable=TunableTuple(
                conditions=TunableList(
                    description=
                    '\n                    A list of conditions that all must be satisfied for the\n                    group to be considered satisfied.\n                    ',
                    tunable=TunableRabbitHoleCondition(
                        description=
                        '\n                        A condition that must be satisfied.\n                        '
                    )),
                tests=TunableTestSet(
                    description=
                    '\n                    A set of tests. If these tests do not pass, this condition\n                    will not be attached.\n                    '
                ))),
        'time_tracking_policy':
        TunableEnumEntry(
            description=
            "\n            This option determines how a rabbit hole will keep track of \n            duration:\n            COUNT_ALL_TIME - This rabbit hole's duration will begin when this\n            rabbit hole is first pushed. This should be used if the rabbit\n            hole's duration is supposed to point to a specific time. For\n            instance, if I know a sim has an audition between 1pm-2pm, I will\n            push them into a rabbit hole at 1pm with a duration of 1 hour. Now\n            imagine my sim is busy at class till 1:45pm. When they are done \n            with class, they should go to the audition. At this point, there\n            should be 15 minutes left in the audition and not 1 hour left. This\n            is because we decided to COUNT_ALL_TIME for the audition rabbit \n            hole.\n            COUNT_ACTIVE_TIME - This rabbit hole's duration will begin when the\n            sim enters it. Continuing from the above example, the audition\n            rabbit hole would end at 2:45pm and not 2:00pm if it had been tuned\n            to COUNT_ACTIVE_TIME since it only became active at 1:45pm.\n            ",
            tunable_type=RabbitHoleTimingPolicy,
            default=RabbitHoleTimingPolicy.COUNT_ACTIVE_TIME),
        'tested_affordances':
        TunableList(
            description=
            "\n            A list of test sets to run to choose the affordance to do for this\n            rabbit hole. If an affordance is found from this list, the sim will be\n            instantiated into this zone if not already and pushed to do the found\n            affordance, so tests should fail out if you do not want a sim to move\n            zones.\n            \n            If no affordance is found from this list that pass the\n            tests, normal rabbit hole affordance behavior will take over, running\n            either 'affordance' if at home or 'go_home_and_attend' if not at home.\n            \n            These tests are run when Sim is being added to a rabbit hole and also\n            on zone spin-up to check if we need to bring this Sim into the new zone to\n            put them into the rabbit hole in the new zone.\n            ",
            tunable=TunableTuple(
                tests=TunableTestSet(
                    description=
                    '\n                    A set of tests that if passed will make this the affordance that is\n                    run for the rabbit hole.\n                    '
                ),
                affordance=TunableReference(
                    description=
                    '\n                    The rabbit hole affordance for this test set. This affordance must have a tuned rabbit\n                    hole liability and must use a rabbit hole exit condition. \n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.INTERACTION))))
    }

    def __init__(self,
                 sim_id,
                 rabbit_hole_id=None,
                 starting_phase=RabbitHolePhase.STARTING,
                 picked_skill=None):
        self.rabbit_hole_id = rabbit_hole_id or id_generator.generate_object_id(
        )
        self.sim_id = sim_id
        self.alarm_handle = None
        self.callbacks = CallableList()
        self.linked_rabbit_holes = []
        self.picked_skill = picked_skill
        self.ignore_travel_cancel_callbacks = False
        self.current_phase = starting_phase
        self._selected_affordance = None
        self.time_remaining_on_load = None

    @property
    def sim(self):
        return services.sim_info_manager().get(self.sim_id)

    @property
    def target(self):
        pass

    @flexmethod
    def get_participant(cls,
                        inst,
                        participant_type=ParticipantType.Actor,
                        **kwargs):
        inst_or_cl = inst if inst is not None else cls
        participants = inst_or_cl.get_participants(
            participant_type=participant_type, **kwargs)
        if not participants:
            return
        if len(participants) > 1:
            raise ValueError('Too many participants returned for {}!'.format(
                participant_type))
        return next(iter(participants))

    @flexmethod
    def get_participants(cls, inst, participant_type, *args, **kwargs):
        if inst:
            sim_info = inst.sim
            if participant_type is ParticipantType.Actor:
                return (sim_info, )
            else:
                if participant_type is ParticipantType.Lot:
                    (services.get_zone(sim_info.zone_id,
                                       allow_uninstantiated_zones=True), )
                if participant_type is ParticipantType.PickedStatistic:
                    return (inst.picked_skill, )

    def is_valid_to_restore(self, sim_info):
        return True

    def save(self, rabbit_hole_data):
        if self.alarm_handle is not None:
            rabbit_hole_data.time_remaining = self.alarm_handle.get_remaining_time(
            ).in_ticks()
        picked_stat = self.get_participant(ParticipantType.PickedStatistic)
        if picked_stat is not None:
            rabbit_hole_data.picked_stat_id = picked_stat.guid64
        rabbit_hole_data.phase = self.current_phase

    def load(self, rabbit_hole_data):
        if rabbit_hole_data.HasField('time_remaining'):
            self.time_remaining_on_load = date_and_time.TimeSpan(
                rabbit_hole_data.time_remaining)
        if rabbit_hole_data.HasField('picked_stat_id'):
            self.picked_skill = services.get_instance_manager(
                sims4.resources.Types.STATISTIC).get(
                    rabbit_hole_data.picked_stat_id)
        if rabbit_hole_data.HasField('phase'):
            self.current_phase = RabbitHolePhase(rabbit_hole_data.phase)
        else:
            self.current_phase = RabbitHolePhase.ACTIVE

    def on_restore(self):
        self._selected_affordance = None
        self.ignore_travel_cancel_callbacks = False

    def on_activate(self):
        pass

    def on_remove(self, canceled=False):
        self.callbacks(canceled=canceled)

    def select_affordance(self):
        if self._selected_affordance is not None:
            return self._selected_affordance
        sim_info = services.sim_info_manager().get(self.sim_id)
        resolver = SingleSimResolver(sim_info)
        for tested_affordance_tuning in self.tested_affordances:
            if tested_affordance_tuning.tests.run_tests(resolver):
                self._selected_affordance = tested_affordance_tuning.affordance
                return tested_affordance_tuning.affordance
        if sim_info.is_at_home:
            self._selected_affordance = self.affordance
            return self.affordance

    def select_travel_affordance(self):
        return self.go_home_and_attend

    def set_expiration_alarm(self, callback):
        if self.time_tracking_policy is RabbitHoleTimingPolicy.NO_TIME_LIMIT:
            logger.error(
                "Expiration timer is trying to be set for a rabbit hole {} that doesn't support it.",
                self)
            return
        if self.alarm_handle is not None:
            time_remaining = self.alarm_handle.get_remaining_time()
            self.alarm_handle = alarms.add_alarm(self,
                                                 time_remaining,
                                                 callback,
                                                 cross_zone=True)
            return
        if self.time_remaining_on_load is not None:
            self.alarm_handle = alarms.add_alarm(self,
                                                 self.time_remaining_on_load,
                                                 callback,
                                                 cross_zone=True)
            return
        else:
            duration = self._get_duration()
            if duration is not None:
                self.alarm_handle = alarms.add_alarm(self,
                                                     duration,
                                                     callback,
                                                     cross_zone=True)
                return

    def _get_duration(self):
        affordance = self.select_affordance()
        if affordance is not None:
            for conditional_action in affordance.basic_content.conditional_actions:
                for condition in conditional_action.conditions:
                    if hasattr(
                            condition._tuned_values, 'min_time') and hasattr(
                                condition._tuned_values, 'max_time'):
                        min_time = condition._tuned_values.min_time
                        max_time = condition._tuned_values.max_time
                        tuned_minutes = random.uniform(min_time, max_time)
                        return date_and_time.create_time_span(
                            minutes=tuned_minutes)
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)))))
    }
示例#7
0
class OutfitMixin:
    __qualname__ = 'OutfitMixin'
    INSTANCE_TUNABLES = {
        'outfit_category_map':
        TunableMapping(
            key_type=TunableEnumEntry(
                OutfitCategory,
                OutfitCategory.EVERYDAY,
                description='The outfit category to pull outfit indexes from'),
            value_type=TunableReference(
                services.get_instance_manager(
                    sims4.resources.Types.PIE_MENU_CATEGORY),
                description=
                'Pie menu category so we can display a submenu for each outfit category'
            )),
        'current_outfit_tooltip':
        TunableLocalizedStringFactory(
            description=
            'Greyed out tooltip that displays if the sim is currently wearing the selected outfit'
        ),
        'outfit_and_index_interaction_name':
        TunableLocalizedStringFactory(
            description=
            'A string that concatenates the outfit category localized string and the number of the outfit category index'
        )
    }

    def __init__(self,
                 aop,
                 context,
                 pie_menu_category=None,
                 outfit_category=None,
                 outfit_index=None,
                 **kwargs):
        super().__init__(aop, context, **kwargs)
        self.pie_menu_category = pie_menu_category
        self.outfit_category = outfit_category
        self.outfit_index = outfit_index

    @flexmethod
    def get_pie_menu_category(cls,
                              inst,
                              pie_menu_category=None,
                              **interaction_parameters):
        if inst is not None:
            return inst.pie_menu_category
        return pie_menu_category

    @staticmethod
    def _get_interaction_name(cls, outfit_category, outfit_index):
        localized_string = SimOutfits.OUTFIT_CATEGORY_TUNING.get(
            outfit_category).localized_category
        if localized_string is not None:
            return cls.outfit_and_index_interaction_name(
                localized_string(), outfit_index + 1)

    @flexmethod
    def _get_name(cls,
                  inst,
                  target=DEFAULT,
                  context=DEFAULT,
                  outfit_category=None,
                  outfit_index=None,
                  **interaction_parameters):
        if inst is not None:
            return cls._get_interaction_name(cls, inst.outfit_category,
                                             inst.outfit_index)
        return cls._get_interaction_name(cls, outfit_category, outfit_index)

    @classmethod
    def _shared_test(cls, sim, outfit_category, outfit_index):
        requested_outfit = (outfit_category, outfit_index)
        if sim.sim_info.get_current_outfit() == requested_outfit:
            return TestResult(False,
                              'Already in requested outfit',
                              tooltip=cls.current_outfit_tooltip)
        return TestResult.TRUE

    @classmethod
    def _shared_potential_interactions(cls, sim, target, **kwargs):
        if sim is None:
            return
        outfit_category_map = cls.outfit_category_map
        for outfit_category in outfit_category_map:
            pie_menu_category = outfit_category_map[outfit_category]
            outfits = sim.sim_info.sim_outfits.outfits_in_category(
                outfit_category)
            index = 0
            while outfits is not None:
                while True:
                    for _ in outfits:
                        yield AffordanceObjectPair(
                            cls,
                            target,
                            cls,
                            None,
                            pie_menu_category=pie_menu_category,
                            outfit_category=outfit_category,
                            outfit_index=index,
                            **kwargs)
                        index += 1
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 CleanupObjectsFestivalState(BaseFestivalState):
    FACTORY_TUNABLES = {
        '_conditional_layers':
        TunableList(
            description=
            '\n            A list of layers to be destroyed.  Each one will load one after\n            another.\n            ',
            tunable=TunableReference(
                description=
                '\n                The conditional layer that will be destroyed.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.CONDITIONAL_LAYER)),
            unique_entries=True)
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._layers_to_destroy = []

    def _get_next_state(self):
        raise NotImplementedError

    def _destroy_layers(self):
        self._layers_to_destroy = list(self._conditional_layers)
        if self._layers_to_destroy:
            for conditional_layer in tuple(self._layers_to_destroy):
                self._owner.remove_layer_objects(conditional_layer)
        else:
            self._next_state_or_destroy()

    def on_layer_objects_destroyed(self, conditional_layer):
        super().on_layer_objects_destroyed(conditional_layer)
        if self._owner._prerolling:
            return
        self._layers_to_destroy.remove(conditional_layer)
        if not self._layers_to_destroy:
            self._next_state_or_destroy()

    def _next_state_or_destroy(self):
        next_state = self._get_next_state()
        if next_state is not None:
            self._owner.change_state(next_state)
        else:
            self._owner._ready_for_destruction = True
            self._owner.self_destruct()

    def on_state_activated(self, reader=None, preroll_time_override=None):
        self._owner.run_lot_cleanup()
        super().on_state_activated(reader=reader,
                                   preroll_time_override=preroll_time_override)
        self._destroy_layers()

    def _run_preroll(self):
        for conditional_layer in self._layers_to_destroy:
            self._owner.remove_layer_objects(conditional_layer)
        super()._run_preroll()

    def _get_fake_preroll_time(self):
        return TimeSpan.ZERO

    def _preroll_end_of_state(self):
        next_state = self._get_next_state()
        if next_state is not None:
            self._owner.change_state(next_state)
        else:
            self._owner.self_destruct()
示例#10
0
class StatisticTransferOp(StatisticOperation):
    __qualname__ = 'StatisticTransferOp'
    FACTORY_TUNABLES = {
        'statistic_donor':
        TunableEnumEntry(
            description=
            '\n            The owner of the statistic we are transferring the value from.\n            ',
            tunable_type=ParticipantType,
            default=ParticipantType.TargetSim),
        'transferred_stat':
        TunableReference(
            description=
            '\n            The statistic whose value to transfer.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC)),
        'transfer_type':
        TunableEnumEntry(
            description=
            '\n            Type of statistic transfer to use.\n            ',
            tunable_type=TransferType,
            default=TransferType.ADDITIVE)
    }

    def __init__(self,
                 statistic_donor=None,
                 transferred_stat=None,
                 transfer_type=None,
                 **kwargs):
        super().__init__(**kwargs)
        self._statistic_donor = statistic_donor
        self._transferred_stat = transferred_stat
        self._transfer_type = transfer_type
        self._donors = None

    def __repr__(self):
        if self.stat is not None:
            return '<{}: {} transfer>'.format(self.__class__.__name__,
                                              self.stat.__name__)
        return '<{}: Stat is None in StatisticTransferOp>'.format(
            self.__class__.__name__)

    def get_value(self, obj=None, interaction=None, sims=None):
        return self.stat.get_value()

    def _apply_to_subject_and_target(self, subject, target, resolver):
        self._donors = resolver.get_participants(self._statistic_donor)
        super()._apply_to_subject_and_target(subject, target, resolver)

    def _apply(self, tracker, interaction=None):
        donors = self._donors if self._donors is not None else interaction.get_participants(
            self._statistic_donor)
        for donor in donors:
            transfer_value = donor.statistic_tracker.get_value(
                self._transferred_stat)
            if self._transfer_type == TransferType.ADDITIVE:
                tracker.add_value(self.stat,
                                  transfer_value,
                                  interaction=interaction)
            elif self._transfer_type == TransferType.SUBTRACTIVE:
                tracker.add_value(self.stat,
                                  -transfer_value,
                                  interaction=interaction)
            else:
                while self._transfer_type == TransferType.REPLACEMENT:
                    tracker.set_value(self.stat,
                                      transfer_value,
                                      interaction=interaction)
示例#11
0
class RelationshipOperation(StatisticOperation, BaseTargetedLootOperation):
    __qualname__ = 'RelationshipOperation'

    @staticmethod
    def _verify_tunable_callback(instance_class,
                                 tunable_name,
                                 source,
                                 relationship_operation=None,
                                 **tuned_values):
        if relationship_operation.target_participant_type is None or relationship_operation.target_participant_type == ParticipantType.Invalid:
            logger.error(
                'Relationship Operation: {} has no Target Participant Type tuned.',
                instance_class)

    FACTORY_TUNABLES = {
        'track':
        TunableReference(
            description='\n            The track to be manipulated.',
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC),
            class_restrictions='RelationshipTrack'),
        'track_range':
        TunableInterval(
            description=
            '\n            The relationship track must > lower_bound and <= upper_bound for\n            the operation to apply.',
            tunable_type=float,
            default_lower=-101,
            default_upper=100),
        'locked_args': {
            'advertise': False
        },
        'verify_tunable_callback':
        _verify_tunable_callback
    }
    DEFAULT_PARTICIPANT_ARGUMENTS = {
        'subject_participant_type_options': {
            'description':
            '\n            The owner Sim for this relationship change. Relationship is\n            applied to all Sims in this list, to all Sims in the Target\n            Participant Type list\n            ',
            'use_flags_enum': True
        },
        'target_participant_type_options': {
            'description':
            "\n            The target Sim for this relationship change. Any\n            relationship that would be given to 'self' is discarded.\n            ",
            'use_flags_enum': True
        }
    }

    def __init__(self, track_range=None, track=DEFAULT, **kwargs):
        super().__init__(**kwargs)
        self._track_range = track_range
        self._track = track
        if self._track is None:
            self._track = DEFAULT
        self._loot_type = LootType.RELATIONSHIP

    def get_stat(self, interaction, source=None, target=None):
        if source is None:
            actors = interaction.get_participants(self.subject)
            source = next(iter(actors))
        if target is None:
            targets = interaction.get_participants(
                self.target_participant_type)
            for potential_target in targets:
                while potential_target is not source:
                    target = potential_target
                    break
        if target is None:
            return
        if isinstance(target, int):
            target_sim_id = target
        else:
            target_sim_id = target.sim_id
        return source.sim_info.relationship_tracker.get_relationship_track(
            target_sim_id, self._track, True)

    def _get_interval(self, aop):
        return StatisticOperation.STATIC_CHANGE_INTERVAL

    def _apply_to_subject_and_target(self, subject, target, resolver):
        source_sim_info = self._get_sim_info_from_participant(subject)
        if not source_sim_info:
            return
        target_sim_info = self._get_sim_info_from_participant(target)
        if not target_sim_info:
            return
        self._apply_to_sim_info(source_sim_info, target_sim_info.sim_id,
                                resolver.interaction)
        self._apply_to_sim_info(target_sim_info, source_sim_info.sim_id,
                                resolver.interaction)

    def _get_sim_info_from_participant(self, participant):
        if isinstance(participant, int):
            sim_info_manager = services.sim_info_manager()
            if sim_info_manager is None:
                return
            sim_info = sim_info_manager.get(participant)
        else:
            sim_info = getattr(participant, 'sim_info', participant)
        if sim_info is None:
            logger.error(
                'Could not get Sim Info from {0} in StatisticAddRelationship loot op.',
                participant)
        return sim_info

    def _apply_to_sim_info(self, source_sim_info, target_sim_id, interaction):
        if self._track is DEFAULT:
            self._track = RelationshipGlobalTuning.REL_INSPECTOR_TRACK
        rel_stat = source_sim_info.relationship_tracker.get_relationship_track(
            target_sim_id, self._track, True)
        if rel_stat is not None:
            self._maybe_apply_op(rel_stat.tracker,
                                 source_sim_info,
                                 interaction=interaction)

    def _maybe_apply_op(self, tracker, target_sim, **kwargs):
        value = tracker.get_value(self._track)
        if self._track_range.lower_bound < value <= self._track_range.upper_bound:
            self._apply(tracker, target_sim, **kwargs)
示例#12
0
class StatisticOperation(BaseLootOperation):
    __qualname__ = 'StatisticOperation'
    STATIC_CHANGE_INTERVAL = 1
    DISPLAY_TEXT = TunableLocalizedStringFactory(
        description=
        '\n        A string displaying the amount that this stat operation awards. It will\n        be provided two tokens: the statistic name and the value change.\n        '
    )
    DEFAULT_PARTICIPANT_ARGUMENTS = {
        'subject_participant_type_options': {
            'description':
            '\n            The owner of the stat that we are operating on.\n            ',
            'use_flags_enum': True
        }
    }
    FACTORY_TUNABLES = {
        'stat':
        TunableReference(
            description='\n            The statistic we are operating on.',
            manager=services.get_instance_manager(
                sims4.resources.Types.STATISTIC)),
        'advertise':
        Tunable(
            description=
            '\n            This statistic operation should advertise to autonomy.  This only\n            advertises if the statistic operation is used as part of Periodic\n            Statistic Change.\n            ',
            tunable_type=bool,
            needs_tuning=True,
            default=True)
    }

    def __init__(self, stat=None, **kwargs):
        super().__init__(**kwargs)
        self._stat = stat
        self._ad_multiplier = 1
        self._loot_type = LootType.GENERIC
        if self._stat is not None and issubclass(self._stat, Skill):
            self._loot_type = LootType.SKILL

    def __repr__(self):
        return '<{} {} {}>'.format(
            type(self).__name__, self.stat, self.subject)

    @property
    def stat(self):
        return self._stat

    @property
    def loot_type(self):
        return self._loot_type

    @property
    def ad_multiplier(self):
        return self._ad_multiplier

    def modify_ad_multiplier(self, multiplier):
        pass

    def _apply_to_subject_and_target(self, subject, target, resolver):
        stat = self.get_stat(None)
        if not subject.is_locked(stat):
            tracker = subject.get_tracker(stat)
            self._apply(tracker, interaction=resolver.interaction)

    def _apply(self, tracker, interaction=None):
        raise NotImplementedError

    def get_value(self, obj=None, interaction=None, sims=None):
        raise NotImplementedError

    def _attempt_to_get_real_stat_value(self, obj, interaction):
        if obj is None and interaction is not None:
            obj = interaction.get_participant(ParticipantType.Actor)
        if obj is not None:
            stat_value = obj.get_stat_value(self.stat)
            if stat_value is not None:
                return stat_value
        return self.stat.default_value

    def _get_interval(self, aop):
        return aop.super_affordance.approximate_duration

    def get_fulfillment_rate(self, interaction):
        if not self._advertise:
            return 0
        value = self.get_value(interaction=interaction)
        if interaction.target is not None:
            value *= interaction.target.get_stat_multiplier(
                self.stat, self.subject)
        interval = self._get_interval(interaction)
        if interval <= 0:
            logger.error(
                'Tuning error: affordance interval should be greater than 0 (defaulting to 1)'
            )
            interval = 1
        score = value / interval
        return score

    def _get_display_text(self):
        if self.stat.stat_name is not None:
            value = self.get_value()
            if value:
                return self.DISPLAY_TEXT(*self._get_display_text_tokens())

    def _get_display_text_tokens(self):
        return (self.stat.stat_name, self.get_value())
示例#13
0
class BuffForAmountOfTimeTest(event_testing.test_base.BaseTest):
    __qualname__ = 'BuffForAmountOfTimeTest'
    test_events = (TestEvent.BuffEndedEvent, TestEvent.BuffUpdateEvent)
    USES_DATA_OBJECT = True
    USES_EVENT_DATA = True

    class BuffTestType(enum.Int):
        __qualname__ = 'BuffForAmountOfTimeTest.BuffTestType'
        ANY_SINGLE_BUFF = 0
        SUM_OF_BUFFS = 1

    FACTORY_TUNABLES = {
        'description':
        'Test for the total amount of time that this buff has been on sims on this account.',
        'buff_to_check':
        TunableList(
            TunableReference(services.get_instance_manager(
                sims4.resources.Types.BUFF),
                             description='Buff checked for this test.')),
        'length_of_time':
        TunableSimMinute(
            1,
            description=
            'The total length of time that should be checked against.'),
        'buff_test_type':
        TunableEnumEntry(
            description=
            '\n            The type determines how to handle multiple buffs in the list. "Any\n            single buff" will test for the time threshold in each listed buff\n            and return true if one meets it. "Sum of buffs" will add the time\n            stored for each buff and test against the total. Note that using\n            SUM OF BUFFS will accumulate time for all buffs in the list and\n            does not separate out overlaps. So if two buffs in the list are on\n            the Sim, time will accumulate twice as much during that period.\n            ',
            tunable_type=BuffTestType,
            default=BuffTestType.ANY_SINGLE_BUFF)
    }

    def __init__(self, buff_to_check, length_of_time, buff_test_type,
                 **kwargs):
        super().__init__(**kwargs)
        self.buffs_to_check = buff_to_check
        self.length_of_time = interval_in_sim_minutes(length_of_time)
        self.buff_test_type = buff_test_type

    def get_expected_args(self):
        return {
            'buff': event_testing.test_events.FROM_EVENT_DATA,
            'data_object': event_testing.test_events.FROM_DATA_OBJECT,
            'objective_guid64': event_testing.test_events.OBJECTIVE_GUID64
        }

    @cached_test
    def __call__(self, buff=None, data_object=None, objective_guid64=None):
        if buff is None:
            return TestResult(
                False, 'Buff provided is None, valid during zone load.')
        if buff not in self.buffs_to_check:
            return TestResult(
                False,
                'Buff provided is not among the buffs you are looking for.')
        buff_uptime = TimeSpan(0)
        if self.buff_test_type == self.BuffTestType.SUM_OF_BUFFS:
            for buff_tuning in self.buffs_to_check:
                buff_uptime += data_object.get_total_buff_uptime(buff_tuning)
        else:
            buff_uptime = data_object.get_total_buff_uptime(buff)
        relative_start_value = data_object.get_starting_values(
            objective_guid64)
        if relative_start_value is not None:
            ticks = 0
            buff_uptime -= TimeSpan(relative_start_value[ticks])
        if buff_uptime >= self.length_of_time:
            return TestResult.TRUE
        run_time = self.length_of_time.in_hours() - (self.length_of_time -
                                                     buff_uptime).in_hours()
        return TestResultNumeric(
            False,
            'BuffForAmountOfTimeTest: Buff has not existed long enough.',
            current_value=run_time,
            goal_value=self.length_of_time.in_hours(),
            is_money=False)

    def save_relative_start_values(self, objective_guid64, data_object):
        buff_uptime = TimeSpan(0)
        for buff_tuning in self.buffs_to_check:
            buff_uptime += data_object.get_total_buff_uptime(buff_tuning)
        data_object.set_starting_values(objective_guid64,
                                        [buff_uptime.in_ticks()])

    def tuning_is_valid(self):
        return len(self.buffs_to_check) > 0

    def goal_value(self):
        return self.length_of_time.in_hours()
示例#14
0
class TotalRelationshipBitTest(event_testing.test_base.BaseTest):
    __qualname__ = 'TotalRelationshipBitTest'
    test_events = (TestEvent.AddRelationshipBit, )
    USES_DATA_OBJECT = True
    FACTORY_TUNABLES = {
        'description':
        'Gate availability by a relationship status.',
        'use_current_relationships':
        Tunable(
            bool,
            False,
            description=
            'Use the current number of relationships held at this bit rather than the total number ever had.'
        ),
        'relationship_bits':
        TunableSet(
            TunableReference(
                services.relationship_bit_manager(),
                description='The relationship bit that will be checked.',
                class_restrictions='RelationshipBit')),
        'num_relations':
        TunableThreshold(
            description=
            'Number of Sims with specified relationships required to pass.')
    }

    def __init__(self, use_current_relationships, relationship_bits,
                 num_relations, **kwargs):
        super().__init__(**kwargs)
        self.use_current_relationships = use_current_relationships
        self.relationship_bits = relationship_bits
        self.num_relations = num_relations

    def get_expected_args(self):
        return {
            'data_object': event_testing.test_events.FROM_DATA_OBJECT,
            'objective_guid64': event_testing.test_events.OBJECTIVE_GUID64
        }

    @cached_test
    def __call__(self, data_object=None, objective_guid64=None):
        current_relationships = 0
        for relationship_bit in self.relationship_bits:
            if self.use_current_relationships:
                current_relationships += data_object.get_current_total_relationships(
                    relationship_bit)
            else:
                current_relationships += data_object.get_total_relationships(
                    relationship_bit)
        relative_start_value = data_object.get_starting_values(
            objective_guid64)
        if relative_start_value is not None:
            relations = 0
            current_relationships -= relative_start_value[relations]
        if not self.num_relations.compare(current_relationships):
            return TestResultNumeric(
                False,
                'TotalRelationshipBitTest: Not enough relationships.',
                current_value=current_relationships,
                goal_value=self.num_relations.value,
                is_money=False)
        return TestResult.TRUE

    def save_relative_start_values(self, objective_guid64, data_object):
        current_relationships = 0
        for relationship_bit in self.relationship_bits:
            if self.use_current_relationships:
                current_relationships += data_object.get_current_total_relationships(
                    relationship_bit)
            else:
                current_relationships += data_object.get_total_relationships(
                    relationship_bit)
        data_object.set_starting_values(objective_guid64,
                                        [current_relationships])

    def tuning_is_valid(self):
        if self.relationship_bits:
            return True
        return False

    def goal_value(self):
        return self.num_relations.value
示例#15
0
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()
示例#16
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
class GameComponent(Component, component_name=types.GAME_COMPONENT):
    __qualname__ = 'GameComponent'
    _PLAYERS = 0
    _SCORE = 1
    _NEXT_PLAYER = 2
    FACTORY_TUNABLES = {
        'description':
        'Manage information about the games that the attached object can play.',
        'games':
        TunableList(TunableReference(manager=services.get_instance_manager(
            sims4.resources.Types.GAME_RULESET)),
                    description='The games that the attached object can play.')
    }

    def __init__(self, owner, games, **kwargs):
        super().__init__(owner)
        self.owner = owner
        self.games = games
        self._teams = []
        self.active_sims = []
        self.active_team = None
        self.winning_team = None
        self.current_game = None
        self.current_target = None
        self.has_started = False
        self.requires_setup = False
        self.target_object = None

    @property
    def number_of_players(self):
        return sum(len(team[self._PLAYERS]) for team in self._teams)

    @property
    def number_of_teams(self):
        return len(self._teams)

    def get_team_name(self, team_number):
        return 'Team #' + str(team_number + 1)

    def is_joinable(self, sim=None):
        if self.current_game is None or self.number_of_players < self.current_game.players_per_game.upper_bound:
            if sim is None:
                return True
            for team in self._teams:
                while sim in team[self._PLAYERS]:
                    return False
            return True
        return False

    @property
    def game_state_dirty(self):
        if self.target_object is None:
            return False
        if self.current_game.initial_state is not None and not self.target_object.state_component.state_value_active(
                self.current_game.initial_state):
            return True
        return False

    @property
    def game_has_ended(self):
        if self.current_game is not None and self.winning_team is None:
            return False
        return True

    @property
    def progress_stat(self):
        max_score = max(team[self._SCORE] for team in self._teams)
        progress = max_score / self.current_game.score_info.winning_score
        progress *= self.current_game.score_info.progress_stat.max_value_tuning
        return progress

    def get_game_target(self, actor_sim=None):
        if self.number_of_teams <= 1 or self.active_team is None:
            return
        if actor_sim is None:
            actor_team = self.active_team
        else:
            for (actor_team, team) in enumerate(self._teams):
                while actor_sim in team[self._PLAYERS]:
                    break
            return
        random_team = random.randrange(self.number_of_teams - 1)
        if random_team >= actor_team:
            random_team += 1
        random_sim = random.choice(self._teams[random_team][self._PLAYERS])
        return random_sim

    def _build_active_sims(self):
        del self.active_sims[:]
        (self.active_sims, next_player) = self._generate_active_sims()
        self._teams[self.active_team][self._NEXT_PLAYER] = next_player

    def _generate_active_sims(self):
        temporary_active_sims = []
        team = self._teams[self.active_team][self._PLAYERS]
        next_player = self._teams[self.active_team][self._NEXT_PLAYER]
        next_player %= len(team)
        i = 0
        while i < self.current_game.players_per_turn:
            temporary_active_sims.append(team[next_player])
            i += 1
            next_player += 1
            next_player %= len(team)
        return (temporary_active_sims, next_player)

    def _rebalance_teams(self):
        excess_index = None
        starvation_index = None
        min_value = int(self.number_of_players / self.number_of_teams)
        i = 0
        for team in self._teams:
            team_length = len(team[self._PLAYERS])
            if excess_index is None and team_length > min_value:
                excess_index = i
            elif team_length < min_value:
                starvation_index = i
            if excess_index is not None and starvation_index is not None:
                self._teams[starvation_index][self._PLAYERS].append(
                    self._teams[excess_index][self._PLAYERS].pop())
                break
            i += 1
        if starvation_index is not None and excess_index is None:
            logger.error(
                'Unable to re-balance teams. No excess index index found.',
                owner='tastle')
        if gsi_handlers.game_component_handlers.game_log_archiver.enabled:
            gsi_handlers.game_component_handlers.archive_game_log_entry(
                self.target_object, 'Rebalanced teams.')

    def clear_scores(self):
        for team in self._teams:
            team[self._SCORE] = 0
        self.winning_team = None
        if gsi_handlers.game_component_handlers.game_log_archiver.enabled:
            gsi_handlers.game_component_handlers.archive_game_log_entry(
                self.target_object, 'Cleared all scores.')

    def add_team(self, sims):
        if self.current_game is None:
            logger.error('Cannot add a team when no game is running.',
                         owner='tastle')
            return
        if self.number_of_teams >= self.current_game.teams_per_game.upper_bound:
            logger.error(
                'Cannot add a team to a game that already has the maximum number of allowed teams.',
                owner='tastle')
            return
        self._teams.append([sims, 0, 0])
        if gsi_handlers.game_component_handlers.game_log_archiver.enabled:
            team_name = self.get_team_name(len(self._teams) - 1)
            team_str = 'Added team: ' + team_name
            gsi_handlers.game_component_handlers.archive_game_log_entry(
                self.target_object, team_str)

    def add_player(self, sim):
        if self.current_game is None:
            logger.error('Cannot add a player when no game is running.',
                         owner='tastle')
            return
        if self.number_of_players >= self.current_game.players_per_game.upper_bound:
            logger.error(
                'Cannot add any players to a game that already has the maximum number of allowed players.',
                owner='tastle')
            return
        if gsi_handlers.game_component_handlers.game_log_archiver.enabled:
            player_str = 'Added player: ' + str(sim)
            gsi_handlers.game_component_handlers.archive_game_log_entry(
                self.target_object, player_str)
        if self.game_state_dirty and not self.has_started:
            self.requires_setup = True
        if self.number_of_teams < self.current_game.teams_per_game.upper_bound:
            self.add_team([sim])
            return
        previous_number_of_players = len(self._teams[0][self._PLAYERS])
        for team in reversed(self._teams):
            while len(team[self._PLAYERS]) <= previous_number_of_players:
                team[self._PLAYERS].append(sim)
                return
        self._teams[0][self._PLAYERS].append(sim)
        if self.current_game.clear_score_on_player_join:
            self.clear_scores()

    def remove_player(self, sim):
        for team in self._teams:
            if sim not in team[self._PLAYERS]:
                pass
            if gsi_handlers.game_component_handlers.game_log_archiver.enabled:
                player_str = 'Removed player: ' + str(sim)
                gsi_handlers.game_component_handlers.archive_game_log_entry(
                    self.target_object, player_str)
            team[self._PLAYERS].remove(sim)
            if self.winning_team is None:
                self._rebalance_teams()
            if not team[self._PLAYERS]:
                self._teams.remove(team)
            if not self.has_started or self.current_game is not None and self.number_of_players < self.current_game.players_per_game.lower_bound:
                self.has_started = False
                self.active_team = None
                del self.active_sims[:]
                if self.game_state_dirty:
                    self.requires_setup = True
            if not self.number_of_teams:
                self.end_game()
            elif self.active_team is not None and self.active_team <= self.number_of_teams:
                self.active_team = random.randrange(self.number_of_teams)
                self._build_active_sims()
            break

    def is_sim_turn(self, sim):
        if self.active_team is not None and self.can_play(
        ) and sim in self.active_sims:
            return True
        return False

    def can_play(self):
        if self.current_game is None:
            return False
        team_len = self.number_of_teams
        player_len = self.number_of_players
        teams_per_game = self.current_game.teams_per_game
        if not teams_per_game.lower_bound <= team_len <= teams_per_game.upper_bound:
            return False
        players_per_game = self.current_game.players_per_game
        if not players_per_game.lower_bound <= player_len <= players_per_game.upper_bound:
            return False
        return True

    def take_turn(self, sim=None):
        if gsi_handlers.game_component_handlers.game_log_archiver.enabled and self.active_team is not None:
            team_name = self.get_team_name(self.active_team)
            turn_str = str(
                sim
            ) + ' (' + team_name + ') ' + 'just finished taking their turn'
            gsi_handlers.game_component_handlers.archive_game_log_entry(
                self.target_object, turn_str)
        if not self.can_play():
            return False
        if sim and sim in self.active_sims:
            self.active_sims.remove(sim)
        if self.active_sims:
            return False
        if self.active_team is None:
            self.clear_scores()
            self.active_team = random.randrange(self.number_of_teams)
            self.has_started = True
        self._build_active_sims()
        return True

    def set_current_game(self, game):
        if self.current_game is not None:
            self.end_game()
        self.current_game = game
        if self.current_game.alternate_target_object is None:
            self.target_object = self.owner
        if gsi_handlers.game_component_handlers.game_log_archiver.enabled:
            game_str = 'Setting current game to ' + str(self.current_game)
            gsi_handlers.game_component_handlers.archive_game_log_entry(
                self.target_object, game_str)
            target_str = 'Target Object is ' + str(self.target_object)
            gsi_handlers.game_component_handlers.archive_game_log_entry(
                self.target_object, target_str)

    def increase_score(self, sim):
        if self.target_object is None:
            return
        for (team_number, team) in enumerate(self._teams):
            if sim not in team[self._PLAYERS]:
                pass
            if self.active_team is not None and team_number != self.active_team:
                return
            score_info = self.current_game.score_info
            score_increase = sims4.random.uniform(
                score_info.score_increase.lower_bound,
                score_info.score_increase.upper_bound)
            relevant_skill = score_info.relevant_skill
            if relevant_skill is not None:
                if score_info.use_effective_skill_level:
                    skill_level = sim.get_effective_skill_level(relevant_skill)
                else:
                    skill = sim.get_stat_instance(relevant_skill)
                    skill_level = skill if skill is not None else 0
                score_increase += score_info.skill_level_bonus * skill_level
            team[self._SCORE] += score_increase
            if gsi_handlers.game_component_handlers.game_log_archiver.enabled:
                team_name = self.get_team_name(team_number)
                increase_str = str(sim) + ' scored ' + str(
                    score_increase) + ' points for ' + team_name
                gsi_handlers.game_component_handlers.archive_game_log_entry(
                    self.target_object, increase_str)
                score_str = 'Score for ' + team_name + ' is now ' + str(
                    team[self._SCORE]) + ' / ' + str(score_info.winning_score)
                gsi_handlers.game_component_handlers.archive_game_log_entry(
                    self.target_object, score_str)
            if team[self._SCORE] >= score_info.winning_score:
                self.winning_team = team[self._PLAYERS]
                if gsi_handlers.game_component_handlers.game_log_archiver.enabled:
                    team_name = self.get_team_name(team_number)
                    win_str = team_name + ' has won the game'
                    gsi_handlers.game_component_handlers.archive_game_log_entry(
                        self.target_object, win_str)
            if score_info.progress_stat is not None:
                self.target_object.statistic_tracker.set_value(
                    score_info.progress_stat, self.progress_stat)
        logger.error(
            'The given Sim {} is not a member of any team, so we cannot increase its score.',
            sim,
            owner='tastle')

    def end_game(self):
        if gsi_handlers.game_component_handlers.game_log_archiver.enabled:
            game_over_str = 'Game ' + str(self.current_game) + ' has ended'
            gsi_handlers.game_component_handlers.archive_game_log_entry(
                self.target_object, game_over_str)
        if self.target_object is not None and self.target_object is not self.owner:
            self.target_object.fade_and_destroy()
        self.target_object = None
        self.current_game = None
        self.active_team = None
        self.winning_team = None
        self.has_started = False
        del self._teams[:]
        del self.active_sims[:]

    def setup_game(self):
        self.requires_setup = False
        if self.target_object is not None:
            return
        if gsi_handlers.game_component_handlers.game_log_archiver.enabled:
            setup_str = 'Game ' + str(self.current_game) + ' has been set up'
            gsi_handlers.game_component_handlers.archive_game_log_entry(
                self.target_object, setup_str)
        self.clear_scores()
        slot_hash = None
        alternate_target_object = self.current_game.alternate_target_object
        parent_slot = alternate_target_object.parent_slot
        if isinstance(parent_slot, str):
            slot_hash = sims4.hash_util.hash32(parent_slot)
        for child in get_child_objects(self.owner):
            while child.definition is alternate_target_object.target_game_object:
                slot = child.parent_slot
                if slot_hash is not None:
                    if slot_hash == slot.slot_name_hash:
                        self.target_object = child
                        return
                        if parent_slot in slot.slot_types:
                            self.target_object = child
                            return
                elif parent_slot in slot.slot_types:
                    self.target_object = child
                    return
        created_object = create_object(
            alternate_target_object.target_game_object)
        self.target_object = created_object
        self.owner.slot_object(parent_slot=parent_slot,
                               slotting_object=created_object)
示例#18
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
示例#19
0
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
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__
示例#21
0
class RemoveTraitLootOp(BaseLootOperation):
    __qualname__ = 'RemoveTraitLootOp'
    FACTORY_TUNABLES = {'description': '\n            This loot will remove the specified trait\n            ', 'trait': TunableReference(description='\n            The trait to be removed.\n            ', manager=services.get_instance_manager(sims4.resources.Types.TRAIT))}

    def __init__(self, trait, **kwargs):
        super().__init__(**kwargs)
        self._trait = trait

    def _apply_to_subject_and_target(self, subject, target, resolver):
        subject.trait_tracker.remove_trait(self._trait)
示例#22
0
class WhimsTracker(SimInfoTracker):
    MAX_GOALS = 2
    EMOTIONAL_WHIM_PRIORITY = 1

    class WhimAwardTypes(enum.Int):
        MONEY = 0
        BUFF = 1
        OBJECT = 2
        TRAIT = 3
        CASPART = 4

    SATISFACTION_STORE_ITEMS = TunableMapping(
        description=
        '\n        A list of Sim based Tunable Rewards offered from the Satisfaction Store.\n        ',
        key_type=TunableReference(
            description='\n            The reward to offer.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.REWARD),
            pack_safe=True),
        value_type=TunableTuple(
            description=
            '\n            A collection of data about this reward.\n            ',
            cost=Tunable(tunable_type=int, default=100),
            award_type=TunableEnumEntry(WhimAwardTypes, WhimAwardTypes.MONEY)))
    WHIM_THRASHING_CHANCE = TunablePercent(
        description=
        '\n        The tunable percent chance that the activation of a whimset will try\n        and cancel a whim of a lower whimset priority as long as that whim is\n        not locked, and not on the anti thrashing cooldown.\n        ',
        default=50)
    WHIM_ANTI_THRASHING_TIME = TunableSimMinute(
        description=
        '\n        The amount of time in sim minutes that a whim will not be overwritten\n        by another whimset becoming active.  This is essentially a period of\n        time after a whim becomes active that it is considered locked.\n        ',
        default=5)

    @classproperty
    def max_whims(cls):
        return WhimsTracker.MAX_GOALS + 1

    @classproperty
    def emotional_whim_index(cls):
        return WhimsTracker.MAX_GOALS

    def __init__(self, sim_info):
        self._sim_info = sim_info
        self._goal_id_generator = uid.UniqueIdGenerator(1)
        self._active_whimsets_data = {}
        self._active_whims = [_ActiveWhimData() for _ in range(self.max_whims)]
        self._hidden = False
        self._cooldown_alarms = {}
        self._whim_goal_proto = None
        self._completed_goals = {}
        self._test_results_map = {}
        self._goals_dirty = True
        self._score_multipliers = []

    def start_whims_tracker(self):
        self._offer_whims()

    def activate_whimset_from_objective_completion(self, whimset):
        self._activate_whimset(whimset)
        self._try_and_thrash_whims(whimset.activated_priority)

    def validate_goals(self):
        sim = self._sim_info.get_sim_instance()
        if sim is None:
            return
        for whim_data in self._active_whims:
            whim = whim_data.whim
            if whim is None:
                continue
            required_sim_info = whim.get_required_target_sim_info()
            if not whim.can_be_given_as_goal(
                    sim, None, inherited_target_sim_info=required_sim_info):
                self._remove_whim(whim,
                                  TelemetryWhimEvents.NO_LONGER_AVAILABLE)
        self._offer_whims()

    def whims_and_parents_gen(self):
        for whim_data in self._active_whims:
            if whim_data.whim is None:
                continue
            yield (whim_data.whim, whim_data.whimset)

    def get_active_whimsets(self):
        whim_sets = set(self._active_whimsets_data.keys())
        if self._sim_info.primary_aspiration is not None and self._sim_info.primary_aspiration.whim_set is not None:
            whim_sets.add(self._sim_info.primary_aspiration.whim_set)
        current_venue = services.get_current_venue()
        if current_venue.whim_set is not None:
            whim_sets.add(current_venue.whim_set)
        for trait in self._sim_info.trait_tracker:
            if trait.whim_set is not None:
                whim_sets.add(trait.whim_set)
        season_service = services.season_service()
        if season_service is not None:
            season_content = season_service.season_content
            if season_content.whim_set is not None:
                whim_sets.add(season_content.whim_set)
        object_manager = services.object_manager()
        whim_sets.update(object_manager.active_whim_sets)
        zone_director = services.venue_service().get_zone_director()
        open_street_director = zone_director.open_street_director
        if open_street_director is not None and open_street_director.whim_set:
            whim_sets.add(open_street_director.whim_set)
        return whim_sets

    def get_active_whim_data(self):
        return tuple(self._active_whims)

    def get_whimset_target(self, whimset):
        whimset_data = self._active_whimsets_data.get(whimset)
        if whimset_data is None:
            return
        return whimset_data.target

    def get_emotional_whimset(self):
        return self._sim_mood().whim_set

    def refresh_emotion_whim(self):
        emotional_whim = self._active_whims[self.emotional_whim_index].whim
        if emotional_whim is not None:
            self._remove_whim(emotional_whim,
                              TelemetryWhimEvents.NO_LONGER_AVAILABLE)
        self._offer_whims()

    def get_priority(self, whimset):
        return whimset.get_priority(self._sim_info)

    def clean_up(self):
        for whim_data in self._active_whims:
            whim = whim_data.whim
            if whim is not None:
                whim.destroy()
            if whim_data.anti_thrashing_alarm_handle is not None:
                alarms.cancel_alarm(whim_data.anti_thrashing_alarm_handle)
        self._active_whims = [_ActiveWhimData() for _ in range(self.max_whims)]
        for alarm_handle in self._cooldown_alarms.values():
            alarms.cancel_alarm(alarm_handle)
        self._cooldown_alarms.clear()
        self._test_results_map.clear()
        self._active_whimsets_data.clear()

    def refresh_whim(self, whim_type):
        whim = self._get_whim_by_whim_type(whim_type)
        if whim is None:
            logger.error(
                'Trying to refresh whim type {} when there are no active whims of that type.',
                whim_type)
            return
        self._remove_whim(whim, TelemetryWhimEvents.CANCELED)
        self._offer_whims(prohibited_whims={whim_type})

    def toggle_whim_lock(self, whim_type):
        whim = self._get_whim_by_whim_type(whim_type)
        if whim is None:
            logger.error(
                'Trying to toggle the locked status of whim type {} when there are no active whims of that type.',
                whim_type)
            return
        whim.toggle_locked_status()
        self._goals_dirty = True
        self._send_goals_update()

    def hide_whims(self):
        if self._hidden:
            logger.error('Trying to hide whims when they are already hidden.')
            return
        self._hidden = True
        self._goals_dirty = True
        self._send_goals_update()

    def show_whims(self, reset=False):
        if not self._hidden:
            logger.error("Trying to show whims when they aren't hidden.")
            return
        self._hidden = False
        self._goals_dirty = True
        if reset:
            self.refresh_whims()
        self._send_goals_update()

    def refresh_whims(self):
        prohibited_whims = set()
        for whim_data in self._active_whims:
            whim = whim_data.whim
            if whim is not None:
                if whim.locked:
                    continue
                prohibited_whims.add(type(whim))
                self._remove_whim(whim, TelemetryWhimEvents.CANCELED)
        self._offer_whims(prohibited_whims=prohibited_whims)

    def add_score_multiplier(self, multiplier):
        self._score_multipliers.append(multiplier)
        self._goals_dirty = True
        self._send_goals_update()

    def get_score_multiplier(self):
        return reduce(operator.mul, self._score_multipliers, 1)

    def get_score_for_whim(self, score):
        return int(score * self.get_score_multiplier())

    def remove_score_multiplier(self, multiplier):
        if multiplier in self._score_multipliers:
            self._score_multipliers.remove(multiplier)
        self._goals_dirty = True
        self._send_goals_update()

    def on_zone_unload(self):
        if not game_services.service_manager.is_traveling:
            return
        self._whim_goal_proto = GameplaySaveData_pb2.WhimsetTrackerData()
        self.save_whims_info_to_proto(self._whim_goal_proto,
                                      copy_existing=False)
        self.clean_up()

    def purchase_whim_award(self, reward_guid64):
        reward_instance = services.get_instance_manager(
            sims4.resources.Types.REWARD).get(reward_guid64)
        award = reward_instance
        cost = self.SATISFACTION_STORE_ITEMS[reward_instance].cost
        if self._sim_info.get_whim_bucks() < cost:
            logger.debug(
                'Attempting to purchase a whim award with insufficient funds: Cost: {}, Funds: {}',
                cost, self._sim_info.get_whim_bucks())
            return
        self._sim_info.add_whim_bucks(-cost,
                                      SetWhimBucks.PURCHASED_REWARD,
                                      source=reward_guid64)
        award.give_reward(self._sim_info)

    def send_satisfaction_reward_list(self):
        msg = Sims_pb2.SatisfactionRewards()
        for (reward, data) in self.SATISFACTION_STORE_ITEMS.items():
            reward_msg = Sims_pb2.SatisfactionReward()
            reward_msg.reward_id = reward.guid64
            reward_msg.cost = data.cost
            reward_msg.affordable = True if data.cost <= self._sim_info.get_whim_bucks(
            ) else False
            reward_msg.available = reward.is_valid(self._sim_info)
            reward_msg.type = data.award_type
            unavailable_tooltip = reward.get_unavailable_tooltip(
                self._sim_info)
            if unavailable_tooltip is not None:
                reward_msg.unavailable_tooltip = unavailable_tooltip
            msg.rewards.append(reward_msg)
        msg.sim_id = self._sim_info.id
        distributor = Distributor.instance()
        distributor.add_op_with_no_owner(
            GenericProtocolBufferOp(Operation.SIM_SATISFACTION_REWARDS, msg))

    def cache_whim_goal_proto(self, whim_tracker_proto, skip_load=False):
        if skip_load:
            return
        if self._sim_info.is_npc:
            return
        if self._sim_info.whim_tracker is None:
            return
        self._whim_goal_proto = GameplaySaveData_pb2.WhimsetTrackerData()
        self._whim_goal_proto.CopyFrom(whim_tracker_proto)

    def load_whims_info_from_proto(self):
        if self._sim_info.is_npc:
            return
        if self._whim_goal_proto is None:
            return
        for whim_data in self._active_whims:
            whim = whim_data.whim
            if whim is not None:
                self._remove_whim(whim, None)
        if len(self._whim_goal_proto.active_whims) > self.max_whims:
            logger.error(
                'More whims saved than the max number of goals allowed')
        aspiration_manager = services.get_instance_manager(
            sims4.resources.Types.ASPIRATION)
        sim_info_manager = services.sim_info_manager()
        for active_whim_msg in self._whim_goal_proto.active_whims:
            if not active_whim_msg.HasField('index'):
                continue
            whimset = aspiration_manager.get(active_whim_msg.whimset_guid)
            if whimset is None:
                logger.info(
                    'Trying to load unavailable ASPIRATION resource: {}',
                    active_whim_msg.whimset_guid)
            else:
                goal_seed = GoalSeedling.deserialize_from_proto(
                    active_whim_msg.goal_data)
                if goal_seed is None:
                    continue
                target_sim_info = None
                if goal_seed.target_id:
                    target_sim_info = sim_info_manager.get(goal_seed.target_id)
                    if target_sim_info is None:
                        continue
                else:
                    secondary_sim_info = None
                    if goal_seed.secondary_target_id:
                        secondary_sim_info = sim_info_manager.get(
                            goal_seed.secondary_target_id)
                        if secondary_sim_info is None:
                            continue
                    else:
                        whim_index = active_whim_msg.index
                        goal = goal_seed.goal_type(
                            sim_info=self._sim_info,
                            goal_id=self._goal_id_generator(),
                            inherited_target_sim_info=target_sim_info,
                            secondary_sim_info=secondary_sim_info,
                            count=goal_seed.count,
                            reader=goal_seed.reader,
                            locked=goal_seed.locked)
                        goal.setup()
                        goal.register_for_on_goal_completed_callback(
                            self._on_goal_completed)
                        whim_data = self._active_whims[whim_index]
                        whim_data.whim = goal
                        whim_data.whimset = whimset
                        self._create_anti_thrashing_cooldown(whim_data)
                        self._goals_dirty = True
                        logger.info('Whim {} loaded.', goal_seed.goal_type)
        self._whim_goal_proto = None
        self._send_goals_update()

    def save_whims_info_to_proto(self, whim_tracker_proto, copy_existing=True):
        if self._sim_info.is_npc:
            return
        if copy_existing and self._whim_goal_proto is not None:
            whim_tracker_proto.CopyFrom(self._whim_goal_proto)
            return
        for (index, active_whim_data) in enumerate(self._active_whims):
            active_whim = active_whim_data.whim
            if active_whim is None:
                continue
            with ProtocolBufferRollback(
                    whim_tracker_proto.active_whims) as active_whim_msg:
                active_whim_msg.whimset_guid = active_whim_data.whimset.guid64
                active_whim_msg.index = index
                goal_seed = active_whim.create_seedling()
                goal_seed.finalize_creation_for_save()
                goal_seed.serialize_to_proto(active_whim_msg.goal_data)

    def debug_activate_whimset(self, whimset, chained):
        if not whimset.update_on_load:
            return
        self._activate_whimset(whimset)
        self._try_and_thrash_whims(whimset.activated_priority)

    def debug_activate_whim(self, whim):
        whim_data = self._active_whims[0]
        if whim_data.whim is not None:
            self._remove_whim(whim_data.whim, TelemetryWhimEvents.CANCELED)
        goal = whim(sim_info=self._sim_info, goal_id=self._goal_id_generator())
        goal.setup()
        goal.register_for_on_goal_completed_callback(self._on_goal_completed)
        goal.show_goal_awarded_notification()
        whim_data.whim = goal
        whim_data.whimset = next(iter(self._active_whimsets_data.keys()))
        self._create_anti_thrashing_cooldown(whim_data)
        self._goals_dirty = True
        self._send_goals_update()

    def debug_offer_whim_from_whimset(self, whimset):
        if whimset.update_on_load:
            self._activate_whimset(whimset)
        whim_data = self._active_whims[0]
        if whim_data.whim is not None:
            self._remove_whim(whim_data.whim, TelemetryWhimEvents.CANCELED)
        goal = self._create_whim(whimset, set())
        goal.setup()
        goal.register_for_on_goal_completed_callback(self._on_goal_completed)
        goal.show_goal_awarded_notification()
        whim_data.whim = goal
        whim_data.whimset = whimset
        self._create_anti_thrashing_cooldown(whim_data)
        self._goals_dirty = True
        self._send_goals_update()

    @property
    def _whims_needed(self):
        return self.max_whims - sum(1 for whim_info in self._active_whims
                                    if whim_info.whim is not None)

    @property
    def _sim_mood(self):
        return self._sim_info.get_mood()

    def _get_currently_active_whim_types(self):
        return {
            type(whim_data.whim)
            for whim_data in self._active_whims if whim_data.whim is not None
        }

    def _get_currently_used_whimsets(self):
        return {
            whim_data.whimset
            for whim_data in self._active_whims
            if whim_data.whimset is not None
        }

    def _get_whimsets_on_cooldown(self):
        return set(self._cooldown_alarms.keys())

    def _get_whim_data(self, whim):
        for whim_data in self._active_whims:
            if whim is whim_data.whim:
                return whim_data

    def _get_whim_by_whim_type(self, whim_type):
        for whim_data in self._active_whims:
            if isinstance(whim_data.whim, whim_type):
                return whim_data.whim

    def _get_target_for_whimset(self, whimset):
        if whimset.force_target is None:
            whimset_data = self._active_whimsets_data.get(whimset)
            if whimset_data is not None:
                return whimset_data.target
            return
        else:
            return whimset.force_target(self._sim_info)

    def _deactivate_whimset(self, whimset):
        if whimset not in self._active_whimsets_data:
            return
        logger.info('Deactivating Whimset {}', whimset)
        if whimset.cooldown_timer > 0:

            def _cooldown_ended(_):
                if whimset in self._cooldown_alarms:
                    del self._cooldown_alarms[whimset]

            self._cooldown_alarms[whimset] = alarms.add_alarm(
                self, create_time_span(minutes=whimset.cooldown_timer),
                _cooldown_ended)
        if whimset.timeout_retest is not None:
            resolver = event_testing.resolver.SingleSimResolver(self._sim_info)
            if resolver(whimset.timeout_retest.objective_test):
                self._activate_whimset(whimset)
                return
        del self._active_whimsets_data[whimset]
        if self._sim_info.aspiration_tracker is not None:
            self._sim_info.aspiration_tracker.reset_milestone(whimset)
        self._sim_info.remove_statistic(whimset.priority_commodity)

    def _activate_whimset(self, whimset, target=None, chained=False):
        if chained:
            new_priority = whimset.chained_priority
        else:
            new_priority = whimset.activated_priority
        if new_priority == 0:
            return
        self._sim_info.set_stat_value(whimset.priority_commodity,
                                      new_priority,
                                      add=True)
        whimset_data = self._active_whimsets_data.get(whimset)
        if whimset_data is None:
            stat = self._sim_info.get_stat_instance(whimset.priority_commodity)
            threshold = Threshold(whimset.priority_commodity.convergence_value,
                                  operator.le)

            def remove_active_whimset(_):
                self._deactivate_whimset(whimset)

            callback_data = stat.create_and_add_callback_listener(
                threshold, remove_active_whimset)
            self._active_whimsets_data[whimset] = _ActiveWhimsetData(
                target, callback_data)
            stat.decay_enabled = True
            logger.info('Setting whimset {} to active at priority {}.',
                        whimset, new_priority)
        else:
            logger.info(
                'Setting whimset {} which is already active to new priority value {}.',
                whimset, new_priority)

    def _remove_whim(self, whim, telemetry_event):
        whim.decommision()
        whim_data = self._get_whim_data(whim)
        whim_data.whim = None
        whim_data.whimset = None
        if whim_data.anti_thrashing_alarm_handle is not None:
            alarms.cancel_alarm(whim_data.anti_thrashing_alarm_handle)
            whim_data.anti_thrashing_alarm_handle = None
        if telemetry_event is not None:
            with telemetry_helper.begin_hook(writer,
                                             TELEMETRY_HOOK_WHIM_EVENT,
                                             sim_info=self._sim_info) as hook:
                hook.write_int(TELEMETRY_WHIM_EVENT_TYPE, telemetry_event)
                hook.write_guid(TELEMETRY_WHIM_GUID, whim.guid64)
        logger.info('Whim {} removed from whims tracker.', whim)
        self._goals_dirty = True

    def _on_goal_completed(self, whim, whim_completed):
        if not whim_completed:
            self._goals_dirty = True
            self._send_goals_update()
            return
        whim_data = self._get_whim_data(whim)
        parent_whimset = whim_data.whimset
        whim_type = type(whim)
        self._completed_goals[whim_type] = (whim, parent_whimset)
        inherited_target_sim_info = whim.get_actual_target_sim_info()
        self._remove_whim(whim, TelemetryWhimEvents.COMPLETED)
        services.get_event_manager().process_event(
            test_events.TestEvent.WhimCompleted,
            sim_info=self._sim_info,
            whim_completed=whim)
        should_deactivate_parent_whimset = parent_whimset.deactivate_on_completion
        highest_chained_priority = 0
        for set_to_chain in parent_whimset.connected_whim_sets:
            if set_to_chain is parent_whimset:
                should_deactivate_parent_whimset = False
            if set_to_chain.chained_priority > highest_chained_priority:
                highest_chained_priority = set_to_chain.chained_priority
            self._activate_whimset(set_to_chain,
                                   target=inherited_target_sim_info,
                                   chained=True)
        connected_whimsets = parent_whimset.connected_whims.get(whim)
        if connected_whimsets is not None:
            for set_to_chain in connected_whimsets:
                if set_to_chain is parent_whimset:
                    should_deactivate_parent_whimset = False
                if set_to_chain.chained_priority > highest_chained_priority:
                    highest_chained_priority = set_to_chain.chained_priority
                self._activate_whimset(set_to_chain,
                                       target=inherited_target_sim_info,
                                       chained=True)
        if should_deactivate_parent_whimset:
            self._deactivate_whimset(parent_whimset)
        op = distributor.ops.SetWhimComplete(whim_type.guid64)
        Distributor.instance().add_op(self._sim_info, op)
        score = self.get_score_for_whim(whim.score)
        if score > 0:
            self._sim_info.add_whim_bucks(score,
                                          SetWhimBucks.WHIM,
                                          source=whim.guid64)
        logger.info('Goal completed: {}, from Whim Set: {}', whim,
                    parent_whimset)
        thrashed = False
        if highest_chained_priority > 0:
            thrashed = self._try_and_thrash_whims(
                highest_chained_priority, extra_prohibited_whims={whim_type})
        if not thrashed:
            self._offer_whims(prohibited_whims={whim_type})

    def _create_whim(self, whimset, prohibited_whims):
        potential_target = self._get_target_for_whimset(whimset)
        if potential_target is None and whimset.force_target is not None:
            return
        if whimset.secondary_target is not None:
            secondary_target = whimset.secondary_target(self._sim_info)
            if secondary_target is None:
                return
        else:
            secondary_target = None
        sim = self._sim_info.get_sim_instance(
            allow_hidden_flags=ALL_HIDDEN_REASONS_EXCEPT_UNINITIALIZED)
        disallowed_whims = self._get_currently_active_whim_types(
        ) | prohibited_whims
        weighted_whims = [(possible_whim.weight, possible_whim.goal)
                          for possible_whim in whimset.whims
                          if possible_whim.goal not in disallowed_whims]
        while weighted_whims:
            selected_whim = sims4.random.pop_weighted(weighted_whims)
            old_whim_instance_and_whimset = self._completed_goals.get(
                selected_whim)
            if old_whim_instance_and_whimset is not None and old_whim_instance_and_whimset[
                    0].is_on_cooldown():
                continue
            pretest = selected_whim.can_be_given_as_goal(
                sim, None, inherited_target_sim_info=potential_target)
            if pretest:
                whim = selected_whim(
                    sim_info=self._sim_info,
                    goal_id=self._goal_id_generator(),
                    inherited_target_sim_info=potential_target,
                    secondary_sim_info=secondary_target)
                return whim

    def _create_anti_thrashing_cooldown(self, whim_data):
        def end_cooldown(_):
            whim_data.anti_thrashing_alarm_handle = None

        whim_data.anti_thrashing_alarm_handle = alarms.add_alarm(
            self,
            create_time_span(minutes=WhimsTracker.WHIM_ANTI_THRASHING_TIME),
            end_cooldown)

    def _offer_whims(self,
                     prohibited_whimsets=EMPTY_SET,
                     prohibited_whims=EMPTY_SET):
        if self._whims_needed == 0:
            return
        if self._sim_info.is_npc:
            return
        if not self._sim_info.is_instanced(
                allow_hidden_flags=ALL_HIDDEN_REASONS_EXCEPT_UNINITIALIZED):
            return
        if services.current_zone().is_zone_shutting_down:
            return
        whimsets_on_cooldown = self._get_whimsets_on_cooldown()
        for (index, whim_data) in enumerate(self._active_whims):
            if whim_data.whim is not None:
                continue
            if index == self.emotional_whim_index:
                emotional_whimset = self.get_emotional_whimset()
                if emotional_whimset is None:
                    logger.info('No emotional whimset found for mood {}.',
                                self._sim_mood)
                else:
                    possible_whimsets = {emotional_whimset}
                    possible_whimsets -= self._get_currently_used_whimsets()
                    possible_whimsets -= prohibited_whimsets
                    possible_whimsets -= whimsets_on_cooldown
                    prioritized_whimsets = [(self.get_priority(whimset),
                                             whimset)
                                            for whimset in possible_whimsets]
                    while prioritized_whimsets:
                        whimset = sims4.random.pop_weighted(
                            prioritized_whimsets)
                        if whimset is None:
                            break
                        goal = self._create_whim(whimset, prohibited_whims)
                        if goal is None:
                            continue
                        goal.setup()
                        goal.register_for_on_goal_completed_callback(
                            self._on_goal_completed)
                        goal.show_goal_awarded_notification()
                        whim_data.whim = goal
                        whim_data.whimset = whimset
                        self._create_anti_thrashing_cooldown(whim_data)
                        with telemetry_helper.begin_hook(
                                writer,
                                TELEMETRY_HOOK_WHIM_EVENT,
                                sim_info=self._sim_info) as hook:
                            hook.write_int(TELEMETRY_WHIM_EVENT_TYPE,
                                           TelemetryWhimEvents.ADDED)
                            hook.write_guid(TELEMETRY_WHIM_GUID, goal.guid64)
                        self._goals_dirty = True
                        break
            else:
                possible_whimsets = self.get_active_whimsets()
            possible_whimsets -= self._get_currently_used_whimsets()
            possible_whimsets -= prohibited_whimsets
            possible_whimsets -= whimsets_on_cooldown
            prioritized_whimsets = [(self.get_priority(whimset), whimset)
                                    for whimset in possible_whimsets]
            while prioritized_whimsets:
                whimset = sims4.random.pop_weighted(prioritized_whimsets)
                if whimset is None:
                    break
                goal = self._create_whim(whimset, prohibited_whims)
                if goal is None:
                    continue
                goal.setup()
                goal.register_for_on_goal_completed_callback(
                    self._on_goal_completed)
                goal.show_goal_awarded_notification()
                whim_data.whim = goal
                whim_data.whimset = whimset
                self._create_anti_thrashing_cooldown(whim_data)
                with telemetry_helper.begin_hook(
                        writer, TELEMETRY_HOOK_WHIM_EVENT,
                        sim_info=self._sim_info) as hook:
                    hook.write_int(TELEMETRY_WHIM_EVENT_TYPE,
                                   TelemetryWhimEvents.ADDED)
                    hook.write_guid(TELEMETRY_WHIM_GUID, goal.guid64)
                self._goals_dirty = True
                break
        self._send_goals_update()

    def _try_and_thrash_whims(self,
                              priority,
                              extra_prohibited_whims=EMPTY_SET):
        whims_thrashed = set()
        for (index, whim_data) in enumerate(self._active_whims):
            if index == self.emotional_whim_index:
                continue
            if whim_data.whim is None:
                continue
            if not whim_data.anti_thrashing_alarm_handle is not None:
                if whim_data.whim.locked:
                    continue
                if self.get_priority(whim_data.whimset) >= priority:
                    continue
                if not sims4.random.random_chance(
                        WhimsTracker.WHIM_THRASHING_CHANCE * 100):
                    continue
                whims_thrashed.add(type(whim_data.whim))
                self._remove_whim(whim_data.whim, TelemetryWhimEvents.CANCELED)
        if not whims_thrashed:
            return False
        prohibited_whims = whims_thrashed | extra_prohibited_whims
        self._offer_whims(prohibited_whims=prohibited_whims)
        return True

    def _send_goals_update(self):
        if not self._goals_dirty:
            return
        logger.debug('Sending whims update for {}.  Current active whims: {}',
                     self._sim_info,
                     self._active_whims,
                     owner='jjacobson')
        current_whims = []
        for (index, whim_data) in enumerate(self._active_whims):
            whim = whim_data.whim
            if whim is None or self._hidden:
                whim_goal = DistributorOps_pb2.WhimGoal()
                current_whims.append(whim_goal)
            else:
                goal_target_id = 0
                goal_whimset = whim_data.whimset
                goal_target = whim.get_required_target_sim_info()
                goal_target_id = goal_target.id if goal_target is not None else 0
                whim_goal = DistributorOps_pb2.WhimGoal()
                whim_goal.whim_guid64 = whim.guid64
                whim_name = whim.get_display_name()
                if whim_name is not None:
                    whim_goal.whim_name = whim_name
                whim_goal.whim_score = self.get_score_for_whim(whim.score)
                whim_goal.whim_noncancel = whim.noncancelable
                whim_display_icon = whim.display_icon
                if whim_display_icon is not None:
                    whim_goal.whim_icon_key.type = whim_display_icon.type
                    whim_goal.whim_icon_key.group = whim_display_icon.group
                    whim_goal.whim_icon_key.instance = whim_display_icon.instance
                whim_goal.whim_goal_count = whim.max_iterations
                whim_goal.whim_current_count = whim.completed_iterations
                whim_goal.whim_target_sim = goal_target_id
                whim_tooltip = whim.get_display_tooltip()
                if whim_tooltip is not None:
                    whim_goal.whim_tooltip = whim_tooltip
                if index == self.emotional_whim_index:
                    whim_goal.whim_mood_guid64 = self._sim_mood().guid64
                else:
                    whim_goal.whim_mood_guid64 = 0
                whim_goal.whim_tooltip_reason = goal_whimset.whim_reason(
                    *whim.get_localization_tokens())
                whim_goal.whim_locked = whim.locked
                current_whims.append(whim_goal)
        if self._goals_dirty:
            self._sim_info.current_whims = current_whims
            self._goals_dirty = False

    @classproperty
    def _tracker_lod_threshold(cls):
        return SimInfoLODLevel.FULL

    def on_lod_update(self, old_lod, new_lod):
        if new_lod < self._tracker_lod_threshold:
            self.clean_up()
        elif old_lod < self._tracker_lod_threshold:
            sim_msg = services.get_persistence_service().get_sim_proto_buff(
                self._sim_info.id)
            if sim_msg is not None:
                self._sim_info.set_whim_bucks(sim_msg.gameplay_data.whim_bucks,
                                              SetWhimBucks.LOAD)
                self.cache_whim_goal_proto(sim_msg.gameplay_data.whim_tracker)
class GlobalGenderPreferenceTuning:
    __qualname__ = 'GlobalGenderPreferenceTuning'
    GENDER_PREFERENCE = TunableMapping(
        key_type=TunableEnumEntry(
            sims.sim_info_types.Gender,
            sims.sim_info_types.Gender.MALE,
            description='The gender to index the gender preference to.'),
        value_type=TunableReference(
            services.get_instance_manager(sims4.resources.Types.STATISTIC),
            description=
            'The statistic that represents the matching gender preference'),
        description=
        'A mapping between gender and the gender preference statistic for easy lookup.'
    )
    GENDER_PREFERENCE_WEIGHTS = TunableList(
        description=
        'A weightings list for the weighted random choice of sexual preference.',
        tunable=TunableTuple(
            gender_preference=TunableEnumEntry(
                GenderPreference,
                GenderPreference.LIKES_NEITHER,
                description='The gender to index the gender preference to.'),
            weight=Tunable(int, 0, description='The minimum possible skill.'),
            description=
            'A mapping between gender and the gender preference statistic for easy lookup.'
        ))
    GENDER_PREFERENCE_MAPPING = TunableMapping(
        key_type=TunableEnumEntry(
            GenderPreference,
            GenderPreference.LIKES_NEITHER,
            description='The gender to index the gender preference to.'),
        value_type=TunableMapping(
            key_type=TunableEnumEntry(
                sims.sim_info_types.Gender,
                sims.sim_info_types.Gender.MALE,
                description='The gender to index the gender preference to.'),
            value_type=TunableSet(
                TunableReference(
                    services.get_instance_manager(
                        sims4.resources.Types.STATISTIC),
                    description=
                    'The statistic that represents the matching gender preference'
                )),
            description=
            'A mapping between gender and the gender preference statistic for easy lookup.'
        ),
        description=
        'A mapping between gender and the gender preference statistic for easy lookup.'
    )
    enable_autogeneration_same_sex_preference = False
    ENABLE_AUTOGENERATION_SAME_SEX_PREFERENCE_THRESHOLD = Tunable(
        description=
        "\n        A value that, once crossed, indicates the player's allowance of same-\n        sex relationships with townie auto-generation.\n        ",
        tunable_type=float,
        default=1.0)
    ENABLED_AUTOGENERATION_SAME_SEX_PREFERENCE_WEIGHTS = TunableList(
        description=
        '\n        An alternative weightings list for the weighted random choice of sexual\n        preference after a romantic same-sex relationship has been kindled.\n        ',
        tunable=TunableTuple(
            gender_preference=TunableEnumEntry(
                GenderPreference,
                GenderPreference.LIKES_NEITHER,
                description='The gender to index the gender preference to.'),
            weight=Tunable(int, 0, description='The minimum possible skill.'),
            description=
            'A mapping between gender and the gender preference statistic for easy lookup.'
        ))
示例#24
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)
class VetClinicZoneDirector(BusinessZoneDirectorMixin,
                            VisitorSituationOnArrivalZoneDirectorMixin,
                            SchedulingZoneDirector):
    INSTANCE_TUNABLES = {
        'customer_situation_type_curve':
        SituationCurve.TunableFactory(
            description=
            "\n            When customer situations are being generated, they'll be pulled\n            based on the tuning in this.\n            \n            The desired count in this tuning is not used.\n            \n            Otherwise it situation count is pulled from business multipliers.\n            ",
            tuning_group=GroupNames.BUSINESS,
            get_create_params={'user_facing': False}),
        'employee_situation':
        TunableReference(
            description=
            '\n            Employee situation to put employees in. \n            ',
            manager=services.get_instance_manager(Types.SITUATION),
            tuning_group=GroupNames.BUSINESS),
        'exam_table_test':
        TunableObjectMatchesDefinitionOrTagTest(
            description=
            '\n            Tests used to count number of exam tables that are in this zone.  \n            The number of these found will limit the number of customers \n            situations that are generated.\n            ',
            tuning_group=GroupNames.BUSINESS),
        'podium_call_to_action':
        TunableReference(
            description=
            '\n            Call to action to use to highlight the vet podium when visiting the vet.\n            ',
            manager=services.get_instance_manager(
                sims4.resources.Types.CALL_TO_ACTION)),
        'waiting_customer_cap':
        _ObjectBasedWaitingCustomerCap.TunableFactory()
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._num_exam_tables = 0
        self._default_uniform = {}
        self._custom_uniform = {}
        self._vet_to_customer_assignments = {}
        self._waiting_situations = OrderedDict()
        self._reservations = {}
        self._has_cta_been_seen = False
        self._cta_disabled = False

    def _save_custom_zone_director(self, zone_director_proto, writer):
        writer.write_uint64s(TRACKED_VET_ASSIGNMENTS_VETS,
                             list(self._vet_to_customer_assignments.keys()))
        for (vet_id, customer_assignments
             ) in self._vet_to_customer_assignments.items():
            writer.write_uint64s(
                TRACKED_VET_ASSIGNMENTS_CUSTOMERS.format(vet_id),
                list(customer_assignments))
        writer.write_uint64s(TRACKED_WAITING_SITUATION_IDS,
                             list(self._waiting_situations.keys()))
        for (situation_id,
             waiting_situations) in self._waiting_situations.items():
            writer.write_uint64s(
                TRACKED_WAITING_SITUATION_CUSTOMERS.format(situation_id),
                list(waiting_situations))
        writer.write_bool(CTA_DISABLED, self._cta_disabled)
        super()._save_custom_zone_director(zone_director_proto, writer)

    def _load_custom_zone_director(self, zone_director_proto, reader):
        if reader is not None:
            vets_with_assigned_customers = reader.read_uint64s(
                TRACKED_VET_ASSIGNMENTS_VETS, [])
            for vet_id in vets_with_assigned_customers:
                assigned_customers = reader.read_uint64s(
                    TRACKED_VET_ASSIGNMENTS_CUSTOMERS.format(vet_id), [])
                if assigned_customers:
                    self._vet_to_customer_assignments[vet_id] = list(
                        assigned_customers)
            waiting_situation_ids = reader.read_uint64s(
                TRACKED_WAITING_SITUATION_IDS, [])
            for situation_id in waiting_situation_ids:
                situation_customers = reader.read_uint64s(
                    TRACKED_WAITING_SITUATION_CUSTOMERS.format(situation_id),
                    [])
                if situation_customers:
                    self._waiting_situations[situation_id] = list(
                        situation_customers)
            self._cta_disabled = reader.read_bool(CTA_DISABLED, False)
        super()._load_custom_zone_director(zone_director_proto, reader)

    def on_startup(self):
        super().on_startup()
        self._load_default_uniforms()
        self.refresh_configuration()

    def clear_state(self):
        self._vet_to_customer_assignments.clear()
        self._waiting_situations.clear()
        self._reservations.clear()

    def on_loading_screen_animation_finished(self):
        if any(sim_info.is_pet for sim_info in self._traveled_sim_infos):
            self._trigger_podium_call_to_action()
        super().on_loading_screen_animation_finished()

    def handle_sim_summon_request(self, sim_info, purpose):
        super().handle_sim_summon_request(sim_info, purpose)
        if sim_info.is_pet:
            self._trigger_podium_call_to_action()

    def _trigger_podium_call_to_action(self):
        if services.current_zone(
        ).active_household_changed_between_save_and_load(
        ) or services.current_zone().time_has_passed_in_world_since_zone_save(
        ):
            self._cta_disabled = False
        if self._cta_disabled:
            return
        if self._has_cta_been_seen or self._business_manager.is_active_household_and_zone(
        ):
            return
        services.call_to_action_service().begin(self.podium_call_to_action,
                                                self)
        self._has_cta_been_seen = True

    def on_cta_ended(self, value):
        self._cta_disabled = True

    def on_shutdown(self):
        if self._business_manager is not None:
            self._business_manager.prepare_for_off_lot_simulation()
        super().on_shutdown()

    def on_exit_buildbuy(self):
        super().on_exit_buildbuy()
        self.refresh_configuration()

    def create_situations_during_zone_spin_up(self):
        if self.business_manager is not None and self.business_manager.is_open:
            if services.current_zone(
            ).time_has_passed_in_world_since_zone_save(
            ) or services.current_zone(
            ).active_household_changed_between_save_and_load():
                self.clear_state()
            self._business_manager.start_already_opened_business()
            self._on_customer_situation_request()
        super().create_situations_during_zone_spin_up()

    def _process_traveled_sim(self, sim_info):
        current_zone = services.current_zone()
        if current_zone.is_first_visit_to_zone or not not (
                current_zone.time_has_passed_in_world_since_zone_save()
                or not (current_zone.
                        active_household_changed_between_save_and_load()
                        or not (sim_info.startup_sim_location is not None
                                and services.active_lot().is_position_on_lot(
                                    sim_info.startup_sim_location.transform.
                                    translation)))):
            super()._process_traveled_sim(sim_info)
        else:
            self._request_spawning_of_sim_at_location(
                sim_info, sims.sim_spawner_service.SimSpawnReason.TRAVELING)

    def _process_zone_saved_sim(self, sim_info):
        if services.current_zone(
        ).time_has_passed_in_world_since_zone_save() or services.current_zone(
        ).active_household_changed_between_save_and_load():
            business_manager = services.business_service(
            ).get_business_manager_for_zone()
            if business_manager is not None and business_manager.is_employee(
                    sim_info):
                self._on_reinitiate_zone_saved_sim(sim_info)
            else:
                self._on_clear_zone_saved_sim(sim_info)
        else:
            super()._process_zone_saved_sim(sim_info)

    def _should_create_npc_business_manager(self):
        return True

    def _get_new_npc_business_manager(self):
        npc_business_manager = VetClinicManager()
        npc_business_manager.set_zone_id(services.current_zone_id())
        npc_business_manager.set_owner_household_id(None)
        return npc_business_manager

    def _get_employee_situation_for_employee_type(self, employee_type):
        return self.employee_situation

    def _get_npc_employee_situation_for_employee_type(self, employee_type):
        return self.employee_situation

    def _get_desired_employee_count(self, employee_type):
        return self._num_exam_tables

    def _on_customer_situation_request(self):
        self.remove_stale_customer_situations()
        desired_situation_count = self._get_num_desired_customer_situations()
        current_customer_count = len(self._customer_situation_ids)
        if current_customer_count >= desired_situation_count:
            waiting_customers = sum(1 for _ in self.customer_situations_gen(
                lambda s: not s.customer_has_been_seen))
            waiting_customer_cap = self.waiting_customer_cap.get_cap_amount()
            if waiting_customer_cap <= waiting_customers:
                return
        (new_customer_situation, params
         ) = self.customer_situation_type_curve.get_situation_and_params()
        if new_customer_situation is None:
            return
        else:
            situation_id = self.start_customer_situation(
                new_customer_situation, create_params=params)
            if situation_id is None:
                logger.info(
                    'Trying to create a new customer situation for vet clinic but failed.'
                )
                return

    def apply_zone_outfit(self, sim_info, situation):
        (outfit_data, outfit_key) = self.get_zone_outfit(sim_info)
        if outfit_data is not None:
            sim_info.generate_merged_outfit(outfit_data,
                                            (OutfitCategory.CAREER, 0),
                                            sim_info.get_current_outfit(),
                                            outfit_key)
            sim_info.set_current_outfit((OutfitCategory.CAREER, 0))
            sim_info.resend_current_outfit()

    def get_zone_outfit(self, sim_info):
        gender = sim_info.clothing_preference_gender
        (outfit_index,
         outfit_data) = self._custom_uniform.get(gender, (0, None))
        if outfit_data is None:
            outfit_data = self._default_uniform.get(gender, None)
        return (outfit_data, (OutfitCategory.CAREER, outfit_index))

    def _load_default_uniforms(self):
        self._default_uniform[Gender.MALE] = self._load_uniform_from_resource(
            VetClinicTuning.UNIFORM_EMPLOYEE_MALE)
        self._default_uniform[
            Gender.FEMALE] = self._load_uniform_from_resource(
                VetClinicTuning.UNIFORM_EMPLOYEE_FEMALE)

    def _load_uniform_from_resource(self, uniform_resource):
        sim_info_wrapper = SimInfoBaseWrapper()
        sim_info_wrapper.load_from_resource(uniform_resource)
        sim_info_wrapper.set_current_outfit((OutfitCategory.CAREER, 0))
        return sim_info_wrapper

    def refresh_configuration(self):
        self._update_from_venue_config()
        self._update_exam_table_count()

    def _update_from_venue_config(self):
        config_data = build_buy.get_current_venue_config(
            services.current_zone_id())
        if config_data is None:
            return
        vet_clinic_config = Venue_pb2.VetClinicConfiguration()
        vet_clinic_config.ParseFromString(config_data)
        self._custom_uniform.clear()
        for (i, outfit_data) in enumerate(vet_clinic_config.outfits):
            if i not in VetEmployeeOutfitType:
                break
            gender = Gender.MALE if i == VetEmployeeOutfitType.MALE_EMPLOYEE else Gender.FEMALE
            sim_info_wrapper = None
            mannequin_data = outfit_data.mannequin
            if mannequin_data.HasField('mannequin_id'):
                sim_info_wrapper = SimInfoBaseWrapper()
                sim_info_wrapper.load_sim_info(outfit_data.mannequin)
                sim_info_wrapper.set_current_outfit((OutfitCategory.CAREER, 0))
            self._custom_uniform[gender] = (outfit_data.outfit_index,
                                            sim_info_wrapper)

    def _update_exam_table_count(self):
        self._num_exam_tables = sum(
            1 for obj in services.object_manager().get_valid_objects_gen()
            if self.exam_table_test(objects=(obj, )))
        if self._business_manager is not None:
            self._business_manager.set_exam_table_count(self._num_exam_tables)

    @property
    def num_exam_tables(self):
        return self._num_exam_tables

    def _get_num_desired_customer_situations(self):
        business_manager = self._business_manager
        if business_manager is None or business_manager.is_owned_by_npc:
            return self._num_exam_tables
        situation_count = business_manager.get_ideal_customer_count()
        tracker = services.business_service(
        ).get_business_tracker_for_household(
            business_manager.owner_household_id,
            business_manager.business_type)
        situation_count += tracker.addtitional_customer_count
        return situation_count

    def on_customers_waiting(self,
                             situation_id,
                             customer_ids,
                             player_situation=False):
        self._waiting_situations[situation_id] = customer_ids
        if player_situation:
            self._waiting_situations.move_to_end(situation_id, last=False)

    def on_vet_assigned(self, situation_id, vet_id, customer_ids):
        if situation_id in self._reservations:
            del self._reservations[situation_id]
        if situation_id in self._waiting_situations:
            del self._waiting_situations[situation_id]
        self._vet_to_customer_assignments[vet_id] = customer_ids

    def on_customer_situation_being_destroyed(self, situation_id):
        if situation_id in self._waiting_situations:
            del self._waiting_situations[situation_id]
        if situation_id in self._reservations:
            del self._reservations[situation_id]

    def remove_from_vet(self, vet_id):
        if vet_id in self._vet_to_customer_assignments.keys():
            del self._vet_to_customer_assignments[vet_id]

    def is_assigned_to_vet(self, customer_id, vet_id=None):
        if vet_id is not None:
            customers = self._vet_to_customer_assignments.get(vet_id, tuple())
            return customer_id in customers
        for cust_id in itertools.chain(
                self._vet_to_customer_assignments.values()):
            if cust_id == customer_id:
                return True
        return False

    def is_waiting_for_services(self, customer_sim_id):
        for situation_id in self._waiting_situations:
            if customer_sim_id in self._waiting_situations[situation_id]:
                return True
        return False

    def is_vet_attending_any_customers(self, vet_id):
        if vet_id in self._vet_to_customer_assignments.keys():
            return len(self._vet_to_customer_assignments[vet_id]) > 0
        return False

    def customer_situations_gen(self, criteria_test=None):
        situation_manager = services.get_zone_situation_manager()
        for situation_id in self._customer_situation_ids:
            situation = situation_manager.get(situation_id)
            if situation is None:
                continue
            if criteria_test is None:
                yield situation
            elif criteria_test(situation):
                yield situation

    def waiting_sims_gen(self, potential_reserver_id):
        now = services.time_service().sim_now
        for situation_id in self._waiting_situations:
            if situation_id in self._reservations:
                reservation = self._reservations[situation_id]
                if not not now < reservation['expiration'] and reservation[
                        'reserver_id'] != potential_reserver_id:
                    continue
            else:
                for sim_id in self._waiting_situations[situation_id]:
                    yield services.object_manager().get(sim_id)

    def reserve_waiting_sim(self, reserved_sim_id, reserver_id):
        for situation_id in self._waiting_situations:
            if reserved_sim_id in self._waiting_situations[situation_id]:
                self._reservations[situation_id] = {
                    'expiration':
                    services.time_service().sim_now +
                    interval_in_sim_minutes(30),
                    'reserver_id':
                    reserver_id
                }

    def bill_owner_for_treatment(self, sim):
        if self._business_manager is not None:
            for customer_situation in self.customer_situations_gen():
                if not customer_situation.is_sim_in_situation(sim):
                    continue
                self._business_manager.bill_owner_for_treatment(
                    *customer_situation.get_payment_data())
                customer_situation.apply_value_of_service()
                break

    @property
    def supported_business_types(self):
        return SUPPORTED_BUSINESS_TYPES
示例#26
0
class LicenseSongSuperInteraction(SuperInteraction):
    INSTANCE_TUNABLES = {
        'music_styles':
        TunableList(
            TunableReference(
                description=
                '\n            Which music styles are available for this interaction.  This\n            should be only the Written Music Style for the particular\n            instrument.\n            ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.RECIPE),
                class_restrictions=(MusicStyle, ),
                reload_dependent=True))
    }

    @classmethod
    def _verify_tuning_callback(cls):
        for style in cls.music_styles:
            for track in style.music_tracks:
                if not track.check_for_unlock:
                    logger.error(
                        "MusicTrack {} does not have check_for_unlock set to False.  This is required for MusicTracks that can be 'Licensed'.",
                        track.__name__)

    def __init__(self,
                 aop,
                 context,
                 track=None,
                 unlockable_name=None,
                 **kwargs):
        super().__init__(aop,
                         context,
                         unlockable_name=unlockable_name,
                         **kwargs)
        self._track = track
        self._unlockable_name = unlockable_name

    @flexmethod
    def _get_name(cls,
                  inst,
                  target=DEFAULT,
                  context=DEFAULT,
                  track=None,
                  unlockable_name=None,
                  **kwargs):
        if unlockable_name is not None and track.music_track_name is not None:
            return track.music_track_name(unlockable_name)
        inst_or_cls = inst if inst is not None else cls
        return super(SuperInteraction, inst_or_cls)._get_name(target=target,
                                                              context=context,
                                                              **kwargs)

    @classmethod
    def potential_interactions(cls, target, context, **kwargs):
        if context.sim is None:
            return
        if context.sim.sim_info.unlock_tracker is None:
            return
        for style in cls.music_styles:
            for track in style.music_tracks:
                unlocks = context.sim.sim_info.unlock_tracker.get_unlocks(
                    track)
                if unlocks:
                    for unlock in unlocks:
                        yield AffordanceObjectPair(
                            cls,
                            target,
                            cls,
                            None,
                            track=unlock.tuning_class,
                            pie_menu_category=style.pie_menu_category,
                            unlockable_name=unlock.name,
                            **kwargs)
示例#27
0
class DoParameterizedAutonomyPingFromRole(HasTunableSingletonFactory,
                                          AutoFactoryInit):
    FACTORY_TUNABLES = {
        'description':
        '\n            Parameterized autonomy ping to run either during preroll or\n            as soon as the sim is assigned this role.\n            ',
        'commodities':
        TunableSet(
            description=
            '\n            Set of commodities to run parameterized autonomy against after\n            running this interaction.\n            ',
            tunable=TunableReference(
                description=
                '\n                The type of commodity to search for.\n                ',
                manager=services.statistic_manager())),
        'static_commodities':
        TunableSet(
            description=
            '\n            Set of static commodities to run parameterized autonomy against\n            after running this interaction.\n            ',
            tunable=TunableReference(
                description=
                '\n                The type of static commodity to search for.\n                ',
                manager=services.static_commodity_manager())),
        'source':
        TunableEnumEntry(
            description=
            '\n            Set this to *spoof* the source of this interaction. This will have\n            various gameplay effects and should be used after due consideration.\n            ',
            tunable_type=InteractionSource,
            default=InteractionSource.AUTONOMY),
        'priority':
        TunableEnumEntry(
            description=
            '\n            The priority level at which this autonomy will run.\n            ',
            tunable_type=Priority,
            default=Priority.Low),
        'run_priority':
        OptionalTunable(
            description=
            '\n            If enabled, specify the run priority at which the selected affordance\n            (if any is selected) will run.\n            ',
            tunable=TunableEnumEntry(tunable_type=Priority,
                                     default=Priority.Low)),
        'radius_to_consider':
        Tunable(
            description=
            '\n            The radius around the sim that targets must be in to be valid\n            for Parameterized Autonomy.  Anything outside this radius will\n            be ignored.  A radius of 0 is considered infinite.\n            ',
            tunable_type=float,
            default=0),
        'consider_scores_of_zero':
        Tunable(
            description=
            '\n            The autonomy request will consider scores of zero.  This allows sims to to choose things they \n            might not desire.\n            ',
            tunable_type=bool,
            default=False),
        'test_connectivity_to_target':
        Tunable(
            description=
            '\n            If checked, this test will ensure the Sim can pass a pt to\n            pt connectivity check to the advertising object.\n            ',
            tunable_type=bool,
            default=True),
        'off_lot_rule':
        OptionalTunable(tunable=TunableOffLotAutonomy()),
        'full_ping_on_fail':
        OptionalTunable(
            description=
            '\n            If enabled, if the parameterized ping fails to find an interaction\n            it will do a full autonomy ping after the specified delay.\n            ',
            tunable=TunableTimeSpan(default_minutes=1))
    }

    def __call__(self, role_state, role_affordance_target, situation=None):
        sim = role_state.sim
        context = InteractionContext(sim,
                                     self.source,
                                     self.priority,
                                     run_priority=self.run_priority,
                                     bucket=InteractionBucketType.DEFAULT)
        autonomy_request = autonomy.autonomy_request.AutonomyRequest(
            sim,
            FullAutonomy,
            commodity_list=self.commodities,
            static_commodity_list=self.static_commodities,
            apply_opportunity_cost=False,
            is_script_request=True,
            context=context,
            si_state_view=sim.si_state,
            limited_autonomy_allowed=True,
            radius_to_consider=self.radius_to_consider,
            consider_scores_of_zero=self.consider_scores_of_zero,
            autonomy_mode_label_override='ParameterizedAutonomy',
            off_lot_autonomy_rule_override=self.off_lot_rule,
            test_connectivity_to_target_object=self.
            test_connectivity_to_target,
            reping_delay_on_fail=self.full_ping_on_fail()
            if self.full_ping_on_fail is not None else None)
        sim.queue_autonomy_request(autonomy_request)
        sim.run_full_autonomy_next_ping()
示例#28
0
class DestroySpecifiedObjectsFromTargetInventory(XevtTriggeredElement,
                                                 HasTunableFactory,
                                                 AutoFactoryInit):
    ALL = 'ALL'
    FACTORY_TUNABLES = {
        'description':
        '\n            Destroy every object in the target inventory that passes the tuned\n            tests.\n            ',
        'inventory_owner':
        TunableEnumEntry(
            description=
            '\n            The participant of the interaction whose inventory will be checked\n            for objects to destroy.\n            ',
            tunable_type=ParticipantType,
            default=ParticipantType.Object),
        'object_tests':
        TunableTestSet(
            description=
            '\n            A list of tests to apply to all objects in the target inventory.\n            Every object that passes these tests will be destroyed.\n            '
        ),
        'count':
        TunableVariant(
            description=
            "\n            The max number of objects to destroy. For example: A Sim has 2\n            red guitars and 1 blue guitar, and we're destroying guitars with\n            count = 2. Possible destroyed objects are: 2 red guitars, or 1 red\n            guitar and 1 blue guitar.\n            ",
            number=TunableRange(tunable_type=int, default=1, minimum=0),
            locked_args={'all': ALL},
            default='all'),
        'loots_to_run_before_destroy':
        TunableList(
            description=
            '\n            A list of loots to be run before destroying the object. The loots\n            will have the tuned participant as the Actor and the object being\n            destroyed as the target.\n            ',
            tunable=TunableReference(
                description=
                '\n                A reference to a loot to run against the object being destroyed.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.ACTION)))
    }

    def _do_behavior(self):
        participant = self.interaction.get_participant(self.inventory_owner)
        inventory = participant.inventory_component
        if inventory is None:
            logger.error(
                'Participant {} does not have an inventory to check for objects to destroy.',
                participant,
                owner='tastle')
            return
        objects_to_destroy = set()
        for obj in inventory:
            single_object_resolver = event_testing.resolver.SingleObjectResolver(
                obj)
            if not self.object_tests.run_tests(single_object_resolver):
                continue
            objects_to_destroy.add(obj)
        num_destroyed = 0
        for obj in objects_to_destroy:
            if self.count == self.ALL:
                count = obj.stack_count()
            else:
                count = min(self.count - num_destroyed, obj.stack_count())
            resolver = SingleActorAndObjectResolver(
                participant.sim_info, obj,
                self) if participant.is_sim else DoubleObjectResolver(
                    obj, self)
            for loot in self.loots_to_run_before_destroy:
                loot.apply_to_resolver(resolver)
            if not inventory.try_destroy_object(
                    obj,
                    count=count,
                    source=inventory,
                    cause=
                    'Destroying specified objects from target inventory extra.'
            ):
                logger.error('Error trying to destroy object {}.',
                             obj,
                             owner='tastle')
            num_destroyed += count
            if self.count != self.ALL:
                if num_destroyed >= self.count:
                    break
        objects_to_destroy.clear()
示例#29
0
class PushAffordanceFromRole(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'description':
        '\n            Push the specific affordance onto the sim.\n            ',
        'affordance':
        TunableReference(manager=services.affordance_manager()),
        'super_affordance_for_mixer':
        OptionalTunable(
            description=
            '\n            If we want to push mixer directly in the affordance tuning for this\n            role state, we would need to provide a super affordance here to\n            handle the mixer.\n            ',
            tunable=TunableReference(manager=services.affordance_manager()),
            disabled_name='do_not_need_si',
            enabled_name='provide_si'),
        'source':
        TunableEnumEntry(tunable_type=InteractionSource,
                         default=InteractionSource.SCRIPT),
        'priority':
        TunableEnumEntry(
            description=
            '\n            Priority to push the interaction\n            ',
            tunable_type=Priority,
            default=Priority.High),
        'run_priority':
        TunableEnumEntry(
            description=
            '\n            Priority to run the interaction. None means use the (push) priority\n            ',
            tunable_type=Priority,
            default=None),
        'target':
        TunableEnumEntry(
            description=
            '\n            The target of the affordance. We will try to get\n            the target from the situation the role sim is\n            running.\n            ',
            tunable_type=SituationAffordanceTarget,
            default=SituationAffordanceTarget.NO_TARGET),
        'leave_situation_on_failure':
        Tunable(
            description=
            '\n            If set to True, when push affordance on the sim fails, sim will\n            leave the situation.\n            ',
            tunable_type=bool,
            default=False),
        'add_situation_liability':
        Tunable(
            description=
            '\n            If set to True, we will add a liability to the pushed interaction\n            such that we will cancel the situation owning this role state\n            if the interaction (and its continuations) are completed or \n            canceled.\n            ',
            tunable_type=bool,
            default=False)
    }

    def __call__(self,
                 role_state,
                 role_affordance_target,
                 situation=None,
                 **kwargs):
        sim = role_state.sim
        affordance = self.affordance
        source = self.source
        priority = self.priority
        run_priority = self.run_priority
        if run_priority is None:
            run_priority = priority
        interaction_context = InteractionContext(sim,
                                                 source,
                                                 priority,
                                                 run_priority=run_priority,
                                                 **kwargs)
        target = role_state._get_target_for_push_affordance(
            self.target,
            situation=situation,
            role_affordance_target=role_affordance_target)
        try:
            push_result = False
            if affordance.is_super:
                push_result = sim.push_super_affordance(
                    affordance, target, interaction_context)
            else:
                super_affordance = self.super_affordance_for_mixer
                if super_affordance is not None:
                    potential_parent_si = sim.si_state.get_si_by_affordance(
                        super_affordance)
                    if potential_parent_si is not None:
                        aop = AffordanceObjectPair(affordance, target,
                                                   super_affordance,
                                                   potential_parent_si)
                        push_result = aop.test_and_execute(interaction_context)
            if push_result:
                if self.add_situation_liability:
                    liability = SituationLiability(situation)
                    push_result.interaction.add_liability(
                        SITUATION_LIABILITY, liability)
            elif self.leave_situation_on_failure:
                situation_manager = services.get_zone_situation_manager()
                situation_manager.remove_sim_from_situation(sim, situation.id)
        except AttributeError:
            logger.error(
                'Attribute Error occurred pushing interaction {} on sim: {} for role_state:{}',
                affordance,
                sim,
                role_state,
                owner='msantander')
            raise
示例#30
0
 def __init__(self, **kwargs):
     super().__init__(skill_to_test=TunableReference(
         services.statistic_manager(),
         description='The skill used to earn the Simoleons, if applicable.'
     ),
                      **kwargs)