class EffectiveSkillModifier(HasTunableSingletonFactory, BaseGameEffectModifier):
    FACTORY_TUNABLES = {'description': '\n        The modifier to change the effective skill or skill_tag tuned in the\n        modifier key The value of the modifier can be negative..\n        ', 'modifier_key': TunableVariant(description='\n            ', skill_type=Skill.TunableReference(description='\n                            What skill to apply the modifier on.', pack_safe=True), skill_tag=TunableEnumEntry(description='\n                            What skill tag to apply the modifier on.', tunable_type=tag.Tag, default=tag.Tag.INVALID)), 'modifier_value': Tunable(description='\n            The value to change the effective skill. Can be negative.', tunable_type=int, default=0)}

    def __init__(self, modifier_key, modifier_value, **kwargs):
        super().__init__(GameEffectType.EFFECTIVE_SKILL_MODIFIER)
        self.modifier_key = modifier_key
        self.modifier_value = modifier_value

    def can_modify(self, skill):
        if self.modifier_key is skill.skill_type:
            return True
        return self.modifier_key in skill.tags

    def get_modifier_value(self, skill):
        if self.can_modify(skill):
            return self.modifier_value
        return 0
class Venue(metaclass=HashedTunedInstanceMetaclass,
            manager=services.get_instance_manager(sims4.resources.Types.VENUE)
            ):
    __qualname__ = 'Venue'
    INSTANCE_TUNABLES = {
        'display_name':
        TunableLocalizedString(
            description=
            '\n            Name that will be displayed for the venue\n            ',
            export_modes=ExportModes.All),
        'display_name_incomplete':
        TunableLocalizedString(
            description=
            '\n            Name that will be displayed for the incomplete venue\n            ',
            export_modes=ExportModes.All),
        'venue_description':
        TunableLocalizedString(
            description='Description of Venue that will be displayed',
            export_modes=ExportModes.All),
        'venue_icon':
        TunableResourceKey(None,
                           resource_types=sims4.resources.CompoundTypes.IMAGE,
                           description='Venue Icon for UI',
                           export_modes=ExportModes.All),
        'venue_thumbnail':
        TunableResourceKey(None,
                           resource_types=sims4.resources.CompoundTypes.IMAGE,
                           description='Image of Venue that will be displayed',
                           export_modes=ExportModes.All),
        'allow_game_triggered_events':
        Tunable(
            description=
            '\n            Whether this venue can have game triggered events. ex for careers\n            ',
            tunable_type=bool,
            default=False),
        'background_event_schedule':
        TunableSituationWeeklyScheduleFactory(
            description=
            '\n            The Background Events that run on this venue. They run underneath\n            any user facing Situations and there can only be one at a time. The\n            schedule times and durations are windows in which background events\n            can start.\n            '
        ),
        'special_event_schedule':
        TunableSituationWeeklyScheduleFactory(
            description=
            '\n            The Special Events that run on this venue. These run on top of\n            Background Events. We run only one user facing event at a time, so\n            if the player started something then this may run in the\n            background, otherwise the player will be invited to join in on this\n            Venue Special Event.\n            '
        ),
        'required_objects':
        TunableList(
            description=
            '\n            A list of objects that are required to be on a lot before\n            that lot can be labeled as this venue.\n            ',
            tunable=TunableVenueObject(
                description=
                "\n                    Specify object tag(s) that must be on this venue.\n                    Allows you to group objects, i.e. weight bench,\n                    treadmill, and basketball goals are tagged as\n                    'exercise objects.'\n                    \n                    This is not the same as automatic objects tuning. \n                    Please read comments for both the fields.\n                    "
            ),
            export_modes=ExportModes.All),
        'npc_summoning_behavior':
        sims4.tuning.tunable.TunableMapping(
            description=
            '\n            Whenever an NPC is summoned to a lot by the player, determine\n            which action to take based on the summoning purpose. The purpose\n            is a dynamic enum: venues.venue_constants.NPCSummoningPurpose.\n            \n            The action will generally involve either adding a sim to an existing\n            situation or creating a situation then adding them to it.\n            \n            \\depot\\Sims4Projects\\Docs\\Design\\Open Streets\\Open Street Invite Matrix.xlsx\n            \n            residential: This is behavior pushed on the NPC if this venue was a residential lot.\n            create_situation: Place the NPC in the specified situation/job pair.\n            add_to_background_situation: Add the NPC the currently running background \n            situation in the venue.\n            ',
            key_type=sims4.tuning.tunable.TunableEnumEntry(
                venues.venue_constants.NPCSummoningPurpose,
                venues.venue_constants.NPCSummoningPurpose.DEFAULT),
            value_type=TunableVariant(
                locked_args={'disabled': None},
                residential=ResidentialLotArrivalBehavior.TunableFactory(),
                create_situation=CreateAndAddToSituation.TunableFactory(),
                add_to_background_situation=AddToBackgroundSituation.
                TunableFactory(),
                default='disabled'),
            tuning_group=GroupNames.TRIGGERS),
        'player_requires_visitation_rights':
        OptionalTunable(
            description=
            'If enabled, then lots of this venue type  \n            will require player Sims that are not on their home lot to go through \n            the process of being greeted before they are\n            given full rights to using the lot.\n            ',
            tunable=TunableTuple(
                ungreeted=Situation.TunableReference(
                    description=
                    '\n                    The situation to create for ungreeted player sims on this lot.',
                    display_name='Player Ungreeted Situation'),
                greeted=Situation
                .TunableReference(
                    description=
                    '\n                    The situation to create for greeted player sims on this lot.',
                    display_name='Player Greeted Situation'))),
        'zone_fixup':
        TunableVariant(
            description=
            '\n            Specify what to do with a non resident NPC\n            when the zone has to be fixed up on load. \n            This fix up will occur if sim time or the\n            active household has changed since the zone was last saved.\n            ',
            residential=ResidentialZoneFixupForNPC.TunableFactory(),
            create_situation=CreateAndAddToSituation.TunableFactory(),
            add_to_background_situation=AddToBackgroundSituation.
            TunableFactory(),
            default='residential',
            tuning_group=GroupNames.SPECIAL_CASES),
        'travel_interaction_name':
        TunableVariant(
            description=
            '\n            Specify what name a travel interaction gets when this Venue is an\n            adjacent lot.\n            ',
            visit_residential=ResidentialTravelDisplayName.TunableFactory(
                description=
                '\n                The interaction name for when the destination lot is a\n                residence.\n                '
            ),
            visit_venue=TunableLocalizedStringFactory(
                description=
                '\n                The interaction name for when the destination lot is a\n                commercial venue.\n                Tokens: 0:ActorSim\n                Example: "Visit The Bar"\n                '
            ),
            tuning_group=GroupNames.SPECIAL_CASES),
        'travel_with_interaction_name':
        TunableVariant(
            description=
            '\n            Specify what name a travel interaction gets when this Venue is an\n            adjacent lot.\n            ',
            visit_residential=ResidentialTravelDisplayName.TunableFactory(
                description=
                '\n                The interaction name for when the destination lot is a\n                residence and the actor Sim is traveling with someone.\n                '
            ),
            visit_venue=TunableLocalizedStringFactory(
                description=
                '\n                The interaction name for when the destination lot is a\n                commercial venue and the actor is traveling with someone.\n                Tokens: 0:ActorSim\n                Example: "Visit The Bar With..."\n                '
            ),
            tuning_group=GroupNames.SPECIAL_CASES),
        'venue_requires_front_door':
        Tunable(
            description=
            '\n            True if this venue should run the front door generation code. \n            If it runs, venue will have the ring doorbell interaction and \n            its additional behavior.\n            ',
            tunable_type=bool,
            default=False),
        'automatic_objects':
        TunableList(
            description=
            '\n            A list of objects that is required to exist on this venue (e.g. the\n            mailbox). If any of these objects are missing from this venue, they\n            will be auto-placed on zone load.',
            tunable=TunableTuple(
                description=
                "\n                An item that is required to be present on this venue. The object's tag \n                will be used to determine if any similar objects are present. If no \n                similar objects are present, then the object's actual definition is used to \n                create an object of this type.\n                \n                This is not the same as required objects tuning. Please read comments \n                for both the fields.\n                \n                E.g. To require a mailbox to be present on a lot, tune a hypothetical basicMailbox \n                here. The code will not trigger as long as a basicMailbox, fancyMailbox, or \n                cheapMailbox are present on the lot. If none of them are, then a basicMailbox \n                will be automatically created.\n                ",
                default_value=TunableReference(
                    manager=services.definition_manager(),
                    description=
                    'The default object to use if no suitably tagged object is present on the lot.'
                ),
                tag=TunableEnumEntry(description='The tag to search for',
                                     tunable_type=tag.Tag,
                                     default=tag.Tag.INVALID))),
        'hide_from_buildbuy_ui':
        Tunable(
            description=
            '\n            If True, this venue type will not be available in the venue picker\n            in build/buy.\n            ',
            tunable_type=bool,
            default=False,
            export_modes=ExportModes.All),
        'allows_fire':
        Tunable(
            description=
            '\n            If True a fire can happen on this venue, \n            otherwise fires will not spawn on this venue.\n            ',
            tunable_type=bool,
            default=False),
        'allow_rolestate_routing_on_navmesh':
        Tunable(
            description=
            '\n            Allow all RoleStates routing permission on lot navmeshes of this\n            venue type. This is particularly useful for outdoor venue types\n            (lots with no walls), where it is awkward to have to "invite a sim\n            in" before they may route on the lot, be called over, etc.\n            \n            This tunable overrides the "Allow Npc Routing On Active Lot"\n            tunable of individual RoleStates.\n            ',
            tunable_type=bool,
            default=False)
    }

    @classmethod
    def _verify_tuning_callback(cls):
        if cls.special_event_schedule is not None:
            for entry in cls.special_event_schedule.schedule_entries:
                while entry.situation.venue_situation_player_job is None:
                    logger.error(
                        'Venue Situation Player Job {} tuned in Situation: {}',
                        entry.situation.venue_situation_player_job,
                        entry.situation)

    def __init__(self, **kwargs):
        self._active_background_event_id = None
        self._active_special_event_id = None
        self._background_event_schedule = None
        self._special_event_schedule = None

    def set_active_event_ids(self,
                             background_event_id=None,
                             special_event_id=None):
        self._active_background_event_id = background_event_id
        self._active_special_event_id = special_event_id

    @property
    def active_background_event_id(self):
        return self._active_background_event_id

    @property
    def active_special_event_id(self):
        return self._active_special_event_id

    def schedule_background_events(self, schedule_immediate=True):
        self._background_event_schedule = self.background_event_schedule(
            start_callback=self._start_background_event,
            schedule_immediate=False)
        if schedule_immediate:
            (
                best_time_span, best_data_list
            ) = self._background_event_schedule.time_until_next_scheduled_event(
                services.time_service().sim_now, schedule_immediate=True)
            if best_time_span is not None and best_time_span == date_and_time.TimeSpan.ZERO:
                while True:
                    for best_data in best_data_list:
                        self._start_background_event(
                            self._background_event_schedule, best_data)

    def schedule_special_events(self, schedule_immediate=True):
        self._special_event_schedule = self.special_event_schedule(
            start_callback=self._try_start_special_event,
            schedule_immediate=schedule_immediate)

    def _start_background_event(self, scheduler, alarm_data, extra_data=None):
        entry = alarm_data.entry
        situation = entry.situation
        situation_manager = services.get_zone_situation_manager()
        if self._active_background_event_id is not None and self._active_background_event_id in situation_manager:
            situation_manager.destroy_situation_by_id(
                self._active_background_event_id)
        situation_id = services.get_zone_situation_manager().create_situation(
            situation, user_facing=False, spawn_sims_during_zone_spin_up=True)
        self._active_background_event_id = situation_id

    def _try_start_special_event(self, scheduler, alarm_data, extra_data):
        entry = alarm_data.entry
        situation = entry.situation
        situation_manager = services.get_zone_situation_manager()
        if self._active_special_event_id is None:
            client_manager = services.client_manager()
            client = next(iter(client_manager.values()))
            invited_sim = client.active_sim
            active_sim_available = situation.is_situation_available(
                invited_sim)

            def _start_special_event(dialog):
                guest_list = None
                if dialog.accepted:
                    start_user_facing = True
                    guest_list = SituationGuestList()
                    guest_info = SituationGuestInfo.construct_from_purpose(
                        invited_sim.id, situation.venue_situation_player_job,
                        SituationInvitationPurpose.INVITED)
                    guest_list.add_guest_info(guest_info)
                else:
                    start_user_facing = False
                situation_id = situation_manager.create_situation(
                    situation,
                    guest_list=guest_list,
                    user_facing=start_user_facing)
                self._active_special_event_id = situation_id

            if not situation_manager.is_user_facing_situation_running(
            ) and active_sim_available:
                dialog = situation.venue_invitation_message(
                    invited_sim, SingleSimResolver(invited_sim))
                dialog.show_dialog(
                    on_response=_start_special_event,
                    additional_tokens=(
                        situation.display_name,
                        situation.venue_situation_player_job.display_name))
            else:
                situation_id = situation_manager.create_situation(
                    situation, user_facing=False)
                self._active_special_event_id = situation_id

    def shut_down(self):
        if self._background_event_schedule is not None:
            self._background_event_schedule.destroy()
        if self._special_event_schedule is not None:
            self._special_event_schedule.destroy()
        situation_manager = services.get_zone_situation_manager()
        if self._active_background_event_id is not None:
            situation_manager.destroy_situation_by_id(
                self._active_background_event_id)
            self._active_background_event_id = None
        if self._active_special_event_id is not None:
            situation_manager.destroy_situation_by_id(
                self._active_special_event_id)
            self._active_special_event_id = None

    @classmethod
    def lot_has_required_venue_objects(cls, lot):
        failure_reasons = []
        for required_object_tuning in cls.required_objects:
            object_test = required_object_tuning.object
            object_list = object_test()
            num_objects = len(object_list)
            while num_objects < required_object_tuning.number:
                pass
        failure_message = None
        failure = len(failure_reasons) > 0
        if failure:
            failure_message = ''
            for message in failure_reasons:
                failure_message += message + '\n'
        return (not failure, failure_message)

    def summon_npcs(self, npc_infos, purpose, host_sim_info=None):
        if self.npc_summoning_behavior is None:
            return
        summon_behavior = self.npc_summoning_behavior.get(purpose)
        if summon_behavior is None:
            summon_behavior = self.npc_summoning_behavior.get(
                venues.venue_constants.NPCSummoningPurpose.DEFAULT)
            if summon_behavior is None:
                return
        summon_behavior(npc_infos, host_sim_info)

    @classproperty
    def requires_visitation_rights(cls):
        return cls.player_requires_visitation_rights is not None

    @classproperty
    def player_ungreeted_situation_type(cls):
        if cls.player_requires_visitation_rights is None:
            return
        return cls.player_requires_visitation_rights.ungreeted

    @classproperty
    def player_greeted_situation_type(cls):
        if cls.player_requires_visitation_rights is None:
            return
        return cls.player_requires_visitation_rights.greeted
Beispiel #3
0
class DistancePlacementMixin:
    INSTANCE_TUNABLES = {
        'facing_radius':
        TunableRange(
            description=
            '\n            Facing constraint radius that will be used for the Sim looking \n            at the target position where the object will be placed.\n            ',
            tunable_type=int,
            default=1,
            minimum=1,
            tuning_group=GroupNames.CONSTRAINTS),
        'facing_range':
        TunableAngle(
            description=
            '\n            The max angle offset (in radians), the Sim can face away from the\n            object.\n            ',
            default=sims4.math.PI / 8),
        'placement_distance':
        TunableInterval(
            description=
            '\n           Distance in meters where the object will be placed.\n           ',
            tunable_type=float,
            default_lower=0,
            default_upper=10,
            minimum=0,
            tuning_group=GroupNames.CONSTRAINTS),
        'raytest_radius':
        TunableAngle(
            description=
            '\n            Radius of the ray test check that will be used to validate if the\n            Sim can see the target position where the object will be placed.\n            ',
            default=0.1,
            minimum=0.1,
            tuning_group=GroupNames.CONSTRAINTS),
        'raytest_offset':
        TunableInterval(
            description=
            "\n           Offset in meters from the ground from where the raytest should start\n           and stop.\n           i.e. If you're testing if the Sim should see the target position \n           from its eye level, you may want a value around the 1.7 (for an \n           adult).\n           ",
            tunable_type=float,
            default_lower=1.5,
            default_upper=1.5,
            minimum=0,
            tuning_group=GroupNames.CONSTRAINTS),
        'thrown_object_actor_name':
        Tunable(
            description=
            '\n            Offset in meters from the ground from where the raytest should \n            end.\n            ',
            tunable_type=str,
            default='carryObject',
            tuning_group=GroupNames.ANIMATION),
        'bounce_jig':
        SocialJigExplicit.TunableFactory(
            description=
            "\n            The jig to use for the object's bounce, where actor b is the\n            object's final resting position, and actor a is where the initial\n            bounce occurs, relative to the final position.\n            \n            The actor offsets will setup the distance of the bounce. If the\n            offsets are both 0, then there will be no bounce. The offset for\n            actor a will determine where the first bounce occurs.\n            ",
            tuning_group=GroupNames.CONSTRAINTS),
        'minimum_requirement_jig':
        OptionalTunable(
            description=
            '\n            If enabled, we will use a jig to guarantee that we find a place to\n            throw the ball from. From there, we find the furthest throw.\n            ',
            tunable=SocialJigExplicit.TunableFactory(
                description=
                "\n                The jig to use so we can find a good place to start our throw\n                from. Doesn't really matter which actor A and B are, as long as\n                there is enough distance between them for a throw. After we\n                place this jig, we FGL from that location to find the longest\n                throw possible.\n                "
            ),
            tuning_group=GroupNames.CONSTRAINTS)
    }
    CONSTRAINT_RADIUS_BUFFER = 0.1

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._starting_location = None
        self._distance_placement_transform = None
        self._bounce_transform = None
        self._routing_surface = None

    @flexmethod
    def _get_distance_placement_constraint(cls,
                                           inst,
                                           sim,
                                           target,
                                           participant_type=ParticipantType.
                                           Actor):
        inst_or_cls = inst if inst is not None else cls
        if inst is None or participant_type != ParticipantType.Actor:
            return ANYWHERE
        constraint = ANYWHERE
        if inst._distance_placement_transform is None or inst._bounce_transform is None:
            return Nowhere(
                "Distance Placement couldn't find a good location for the carry target."
            )
        if inst_or_cls._starting_location is not None:
            constraint = constraint.intersect(
                interactions.constraints.Position(
                    inst_or_cls._starting_location.transform.translation,
                    routing_surface=inst_or_cls._starting_location.
                    routing_surface))
        else:
            constraint = constraint.intersect(
                interactions.constraints.Circle(
                    inst_or_cls._distance_placement_transform.translation,
                    inst_or_cls.placement_distance.upper_bound +
                    DistancePlacementMixin.CONSTRAINT_RADIUS_BUFFER,
                    inst_or_cls.sim.routing_surface,
                    los_reference_point=DEFAULT))
        constraint = constraint.intersect(
            interactions.constraints.Facing(
                facing_range=inst_or_cls.facing_range,
                target_position=inst_or_cls._bounce_transform.translation))
        if constraint.valid:
            return constraint
        return Nowhere(
            "Distance Placement couldn't find a good location for the carry target."
        )

    def _update_water_depth_requirements(self, sim_a, sim_b, interval,
                                         **kwargs):
        min_water_depth = kwargs[
            'min_water_depth'] if 'min_water_depth' in kwargs else None
        max_water_depth = kwargs[
            'max_water_depth'] if 'max_water_depth' in kwargs else None
        constraint_a = WaterDepthIntervalConstraint.create_water_depth_interval_constraint(
            sim_a, interval)
        constraint_b = WaterDepthIntervalConstraint.create_water_depth_interval_constraint(
            sim_b, interval)

        def safe_min(*args):
            ret = None
            for x in args:
                if ret is None:
                    ret = x
                elif x is not None:
                    ret = min(ret, x)
            return ret

        def safe_max(*args):
            ret = None
            for x in args:
                if ret is None:
                    ret = x
                elif x is not None:
                    ret = min(ret, x)
            return ret

        if interval == WaterDepthIntervals.SWIM:
            min_water_depth = safe_min(min_water_depth,
                                       constraint_a.get_min_water_depth(),
                                       constraint_b.get_min_water_depth())
            max_water_depth = safe_max(max_water_depth,
                                       constraint_a.get_max_water_depth(),
                                       constraint_b.get_max_water_depth())
        else:
            min_water_depth = safe_max(min_water_depth,
                                       constraint_a.get_min_water_depth(),
                                       constraint_b.get_min_water_depth())
            max_water_depth = safe_min(max_water_depth,
                                       constraint_a.get_max_water_depth(),
                                       constraint_b.get_max_water_depth())
        kwargs['min_water_depth'] = min_water_depth
        kwargs['max_water_depth'] = max_water_depth
        return kwargs

    def find_starting_location(self):
        if self.carry_target is not None and self.minimum_requirement_jig is not None:
            fgl_flags = FGLSearchFlag.STAY_IN_CONNECTED_CONNECTIVITY_GROUP | FGLSearchFlag.SHOULD_TEST_ROUTING | FGLSearchFlag.CALCULATE_RESULT_TERRAIN_HEIGHTS | FGLSearchFlag.DONE_ON_MAX_RESULTS
            check_on_lot = self.sim.is_on_active_lot()
            if check_on_lot:
                fgl_flags = fgl_flags | FGLSearchFlag.STAY_IN_LOT
            fgl_kwargs = {
                'ignored_object_ids': {sim.id
                                       for sim in self.required_sims()},
                'positioning_type': JigPositioning.RelativeToSimA
            }
            loc_a = self.sim.location.duplicate()
            fgl_kwargs.update({'search_flags': fgl_flags})
            loc_b = loc_a.duplicate()
            lot = services.current_zone().lot

            def find_a_starting_location():
                for (transform_a, transform_b, routing_surface,
                     _) in self.minimum_requirement_jig.get_transforms_gen(
                         self.sim,
                         self.carry_target,
                         actor_loc=loc_a,
                         target_loc=loc_b,
                         fgl_kwargs=fgl_kwargs):
                    if not check_on_lot or lot.is_position_on_lot(
                            transform_a.translation):
                        if not lot.is_position_on_lot(transform_b.translation):
                            continue
                        return sims4.math.Location(transform_a,
                                                   routing_surface)
                    else:
                        return
                else:
                    return

            use_pool_surface = loc_a.routing_surface.type == SurfaceType.SURFACETYPE_POOL or 0 < get_water_depth_at_location(
                loc_a)
            if use_pool_surface:
                interval = WaterDepthIntervals.SWIM
                if loc_a.routing_surface.type != SurfaceType.SURFACETYPE_POOL:
                    self._routing_surface = SurfaceIdentifier(
                        loc_a.routing_surface.primary_id,
                        loc_a.routing_surface.secondary_id,
                        SurfaceType.SURFACETYPE_POOL)
                    loc_a = loc_a.clone(routing_surface=self._routing_surface)
            else:
                interval = WaterDepthIntervals.WALK
            fgl_kwargs = self._update_water_depth_requirements(
                self.sim, self.target, interval, **fgl_kwargs)
            start_loc = find_a_starting_location()
            if start_loc is not None:
                return start_loc
        return self.sim.location.duplicate()

    def setup_final_transforms(self, start_location, **fgl_kwargs):
        loc_b = start_location.clone(
            routing_surface=DEFAULT if self._routing_surface is None else self.
            _routing_surface)
        for (transform_a, transform_b, _,
             _) in self.bounce_jig.get_transforms_gen(self.sim,
                                                      self.carry_target,
                                                      actor_loc=start_location,
                                                      target_loc=loc_b,
                                                      fgl_kwargs=fgl_kwargs):
            self._distance_placement_transform = transform_a
            self._bounce_transform = transform_b
            break

    def _entered_pipeline(self):
        if self.carry_target is not None:
            fgl_flags = FGLSearchFlag.STAY_IN_CURRENT_BLOCK | FGLSearchFlag.STAY_IN_SAME_CONNECTIVITY_GROUP | FGLSearchFlag.SHOULD_TEST_ROUTING | FGLSearchFlag.CALCULATE_RESULT_TERRAIN_HEIGHTS | FGLSearchFlag.DONE_ON_MAX_RESULTS | FGLSearchFlag.SHOULD_RAYTEST
            fgl_kwargs = {
                'raytest_radius': self.raytest_radius,
                'raytest_start_offset': self.raytest_offset.lower_bound,
                'raytest_end_offset': self.raytest_offset.upper_bound,
                'ignored_object_ids': {sim.id
                                       for sim in self.required_sims()},
                'positioning_type': JigPositioning.RelativeToSimA
            }
            pick = self.context.pick
            if pick is not None and pick.routing_surface.type == SurfaceType.SURFACETYPE_POOL:
                swim_constraint = WaterDepthIntervalConstraint.create_water_depth_interval_constraint(
                    self.target, WaterDepthIntervals.SWIM)
                pick_location = sims4.math.Location(
                    sims4.math.Transform(pick.location), pick.routing_surface)
                if swim_constraint.is_location_water_depth_valid(
                        pick_location):
                    loc_a = pick_location
                    self._routing_surface = pick.routing_surface
                else:
                    self._routing_surface = SurfaceIdentifier(
                        pick.routing_surface.primary_id,
                        pick.routing_surface.secondary_id,
                        SurfaceType.SURFACETYPE_WORLD)
                    loc_a = pick_location.clone(
                        routing_surface=self._routing_surface)
                fgl_kwargs.update({
                    'search_flags':
                    fgl_flags,
                    'restrictions':
                    (sims4.geometry.RelativeFacingRange(self.sim.position,
                                                        0), )
                })
                self.setup_final_transforms(loc_a, **fgl_kwargs)
            else:
                self._starting_location = self.find_starting_location()
                if self._starting_location is not None:
                    check_on_lot = self.sim.is_on_active_lot()
                    if check_on_lot:
                        fgl_flags = fgl_flags | FGLSearchFlag.STAY_IN_LOT
                    loc_a = self._starting_location
                    fgl_flags |= FGLSearchFlag.SPIRAL_INWARDS
                    fgl_kwargs.update({
                        'max_distance':
                        self.placement_distance.upper_bound,
                        'min_distance':
                        self.placement_distance.lower_bound,
                        'restrictions': (sims4.geometry.RelativeFacingRange(
                            loc_a.transform.translation, 0), ),
                        'search_flags':
                        fgl_flags
                    })
                    self.setup_final_transforms(loc_a, **fgl_kwargs)
        return super()._entered_pipeline()

    def setup_asm_default(self, asm, *args, **kwargs):
        if self._distance_placement_transform is None:
            return False
        if self.carry_target is None:
            return False
        result = super().setup_asm_default(asm, *args, **kwargs)
        if not result:
            return result
        if not asm.set_actor_parameter(
                self.thrown_object_actor_name, self.carry_target,
                animation_constants.ASM_TARGET_TRANSLATION,
                self._distance_placement_transform.translation):
            return False
        if not asm.set_actor_parameter(
                self.thrown_object_actor_name, self.carry_target,
                animation_constants.ASM_TARGET_ORIENTATION,
                self._distance_placement_transform.orientation):
            return False
        throw_distance = (self._distance_placement_transform.translation -
                          self.sim.position).magnitude_2d()
        bounce_distance = (self._distance_placement_transform.translation -
                           self._bounce_transform.translation).magnitude_2d()
        asm.set_actor_parameter(self.thrown_object_actor_name,
                                self.carry_target, 'ThrowDistance',
                                throw_distance)
        asm.set_actor_parameter(self.thrown_object_actor_name,
                                self.carry_target, 'BounceDistance',
                                bounce_distance)
        if self._routing_surface is not None and self._routing_surface.type == routing.SurfaceType.SURFACETYPE_POOL:
            asm.set_actor_parameter(self.thrown_object_actor_name,
                                    self.carry_target, 'LandingSurface',
                                    POOL_LANDING_SURFACE)
        return True

    def _exited_pipeline(self, *args, **kwargs):
        if self._routing_surface is not None:
            self.carry_target.location = self.carry_target.location.clone(
                routing_surface=self._routing_surface)
        return super()._exited_pipeline(*args, **kwargs)
Beispiel #4
0
class AggregateSuperInteraction(SuperInteraction):
    INSTANCE_TUNABLES = {
        'aggregated_affordances':
        TunableList(
            description=
            '\n                A list of affordances composing this aggregate.  Distance\n                estimation will be used to break ties if there are multiple\n                valid interactions at the same priority level.\n                ',
            tunable=TunableTuple(
                description=
                '\n                    An affordance and priority entry.\n                    ',
                priority=Tunable(
                    description=
                    '\n                        The relative priority of this affordance compared to\n                        other affordances in this aggregate.\n                        ',
                    tunable_type=int,
                    default=0),
                affordance=SuperInteraction.TunableReference(
                    description=
                    '\n                        The aggregated affordance.\n                        ',
                    pack_safe=True)),
            tuning_group=GroupNames.GENERAL),
        'sim_to_push_affordance_on':
        TunableEnumEntry(
            description=
            '\n                The Sim to push the affordance on.  If this is Actor, the\n                affordance will be pushed as a continuation of this.\n                ',
            tunable_type=ParticipantType,
            default=ParticipantType.Actor,
            tuning_group=GroupNames.TRIGGERS),
        'use_aggregated_affordance_constraints':
        Tunable(
            description=
            "\n            If enabled, this interaction will pull it's constraints from the\n            interaction constraints of the aggregated affordances. The benefit\n            is that we are compatible with interactions we intend to run, even\n            if they have constraints different from one another. This prevents\n            us from having to add a bunch of tests to those affordances and a\n            generic constraint here.\n            ",
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.CONSTRAINTS)
    }
    _allow_user_directed = True

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

    @classproperty
    def affordances(cls):
        return (a.affordance.get_interaction_type()
                for a in cls.aggregated_affordances)

    @classmethod
    def _aops_sorted_gen(cls, target, **interaction_parameters):
        affordances = []
        for aggregated_affordance in cls.aggregated_affordances:
            aop = AffordanceObjectPair(aggregated_affordance.affordance,
                                       target,
                                       aggregated_affordance.affordance, None,
                                       **interaction_parameters)
            affordances.append((aggregated_affordance.priority, aop))
        return sorted(affordances, key=operator.itemgetter(0), reverse=True)

    @flexmethod
    def _get_tested_aops(cls, inst, target, context, **interaction_parameters):
        inst_or_cls = inst if inst is not None else cls
        if inst is not None and inst._valid_aops is not None:
            return inst._valid_aops
        aops_valid = []
        cls._allow_user_directed = False
        for (priority,
             aop) in inst_or_cls._aops_sorted_gen(target,
                                                  **interaction_parameters):
            test_result = aop.test(context)
            if test_result:
                if aop.affordance.allow_user_directed:
                    cls._allow_user_directed = True
                aops_valid.append((aop, priority))
        if inst is not None:
            inst._valid_aops = aops_valid
        return aops_valid

    @flexmethod
    def test(cls,
             inst,
             target=DEFAULT,
             context=DEFAULT,
             super_interaction=None,
             skip_safe_tests=False,
             **interaction_parameters):
        inst_or_cls = inst if inst is not None else cls
        result = super(__class__,
                       inst_or_cls).test(target=target,
                                         context=context,
                                         super_interaction=super_interaction,
                                         skip_safe_tests=skip_safe_tests,
                                         **interaction_parameters)
        if result:
            target = target if target is not DEFAULT else inst.target
            context = context if context is not DEFAULT else inst.context
            context = context.clone_for_sim(
                cls.get_participant(
                    participant_type=cls.sim_to_push_affordance_on,
                    sim=context.sim,
                    target=target))
            valid_aops = inst_or_cls._get_tested_aops(target, context,
                                                      **interaction_parameters)
            result = TestResult.TRUE if valid_aops else TestResult(
                False, 'No sub-affordances passed their tests.')
        return result

    @classmethod
    def consumes_object(cls):
        for affordance_tuple in cls.aggregated_affordances:
            if affordance_tuple.affordance.consumes_object():
                return True
        return False

    @classproperty
    def allow_user_directed(cls):
        return cls._allow_user_directed

    @flexmethod
    def _constraint_gen(cls,
                        inst,
                        sim,
                        target,
                        participant_type=ParticipantType.Actor,
                        **kwargs):
        inst_or_cls = cls if inst is None else inst
        yield from super(SuperInteraction, inst_or_cls)._constraint_gen(
            sim, target, participant_type=participant_type, **kwargs)
        if inst_or_cls.use_aggregated_affordance_constraints:
            aggregated_constraints = []
            affordances = []
            affordances = [
                aop.super_affordance for (aop, _) in inst._valid_aops
            ]
            affordances = affordances if not inst is not None or not inst._valid_aops is not None or affordances else [
                affordance_tuple.affordance
                for affordance_tuple in inst_or_cls.aggregated_affordances
            ]
            if not affordances:
                yield Nowhere
            for aggregated_affordance in affordances:
                intersection = ANYWHERE
                constraint_gen = aggregated_affordance.constraint_gen
                constraint_gen = super(SuperInteraction,
                                       aggregated_affordance)._constraint_gen
                for constraint in constraint_gen(
                        sim,
                        inst_or_cls.get_constraint_target(target),
                        participant_type=participant_type,
                        **kwargs):
                    intersection = constraint.intersect(intersection)
                    if not intersection.valid:
                        continue
                aggregated_constraints.append(intersection)
            if aggregated_constraints:
                yield create_constraint_set(
                    aggregated_constraints,
                    debug_name='AggregatedConstraintSet')

    def _do_perform_gen(self, timeline):
        sim = self.get_participant(self.sim_to_push_affordance_on)
        if sim == self.context.sim:
            context = self.context.clone_for_continuation(self)
        else:
            context = context.clone_for_sim(sim)
        max_priority = None
        aops_valid = []
        self._valid_aops = None
        valid_aops = self._get_tested_aops(self.target, context,
                                           **self.interaction_parameters)
        for (aop, priority) in valid_aops:
            if max_priority is not None:
                if priority < max_priority:
                    break
            aops_valid.append(aop)
            max_priority = priority
        if not aops_valid:
            logger.warn(
                'Failed to find valid super affordance in AggregateSuperInteraction: {}, did we not run its test immediately before executing it?',
                self)
            return ExecuteResult.NONE
            yield
        compatible_interactions = []
        for aop in aops_valid:
            interaction_result = aop.interaction_factory(context)
            if not interaction_result:
                raise RuntimeError(
                    'Failed to generate interaction from aop {}. {} [rmccord]'.
                    format(aop, interaction_result))
            interaction = interaction_result.interaction
            if self.use_aggregated_affordance_constraints:
                if interactions.si_state.SIState.test_compatibility(
                        interaction, force_concrete=True):
                    compatible_interactions.append(interaction)
            compatible_interactions.append(interaction)
        if not compatible_interactions:
            return ExecuteResult.NONE
            yield
        interactions_by_distance = []
        for interaction in compatible_interactions:
            if len(compatible_interactions) == 1:
                distance = 0
            else:
                (distance, _, _) = interaction.estimate_distance()
            if distance is not None:
                interactions_by_distance.append((distance, interaction))
            else:
                interactions_by_distance.append(
                    (sims4.math.MAX_INT32, interaction))
        (_, interaction) = min(interactions_by_distance,
                               key=operator.itemgetter(0))
        return AffordanceObjectPair.execute_interaction(interaction)
        yield
Beispiel #5
0
 def __init__(
         self,
         description='Holds information about carrying and putting down an object.',
         **kwargs):
     super().__init__(
         put_down_tuning=TunableVariant(reference=TunableReference(
             description=
             '\n                    Tuning for how to score where a Sim might want to set an\n                    object down.\n                    ',
             manager=services.get_instance_manager(
                 sims4.resources.Types.STRATEGY)),
                                        literal=TunablePutDownStrategy().
                                        TunableFactory(),
                                        default='literal'),
         state_based_put_down_tuning=TunableMapping(
             description=
             '\n                A mapping from a state value to a putdownstrategy. If the\n                owning object is in any of the states tuned here, it will use\n                that state\'s associated putdownstrategy in place of the one\n                putdownstrategy tuned in the "put_down_tuning" field. If the\n                object is in multiple states listed in this mapping, the\n                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=TunableReference(
                 description=
                 '\n                        Tuning for how to score where a Sim might want to set\n                        an object down.\n                        ',
                 manager=services.get_instance_manager(
                     sims4.resources.Types.STRATEGY)),
                                       literal=TunablePutDownStrategy().
                                       TunableFactory()),
             key_name='State',
             value_name='PutDownStrategy'),
         carry_affordances=OptionalTunable(TunableList(
             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=TunableList(
             description=
             '\n                A list of affordances that are generated when a Sim holding\n                this object selects another Sim to interact with. The generated\n                interactions will target the selected Sim but will have this\n                object set as their carry target.\n                ',
             tunable=TunableReference(
                 manager=services.affordance_manager())),
         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=interactions.constraints.TunableConstraintVariant(
                     description=
                     '\n                        A constraint that must be fulfilled in order to\n                        interact with this object.\n                        '
                 ))),
         allowed_hands=TunableVariant(locked_args={
             'both': (Hand.LEFT, Hand.RIGHT),
             'left_only': (Hand.LEFT, ),
             'right_only': (Hand.RIGHT, )
         },
                                      default='both'),
         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_on_long_route_only=Tunable(
             description=
             '\n                If True, then the Sim will not unholster this object (assuming\n                it was previously holstered) unless a transition involving a\n                long route is about to happen.\n                \n                If False, then the standard holstering rules apply.\n                ',
             tunable_type=bool,
             default=False),
         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),
         description=description,
         **kwargs)
class DramaNodeTest(HasTunableSingletonFactory, AutoFactoryInit, event_testing.test_base.BaseTest):
    FACTORY_TUNABLES = {'drama_nodes': TunableList(description='\n            The types of drama nodes that we want to check.\n            ', tunable=TunableReference(description='\n                A Drama node type we want to check.\n                ', manager=services.get_instance_manager(sims4.resources.Types.DRAMA_NODE), pack_safe=True)), 'check_scheduled_nodes': Tunable(description='\n            Check against nodes that are scheduled, but not actively running.\n            ', tunable_type=bool, default=True), 'check_active_nodes': Tunable(description='\n            Check against nodes that are actively running.\n            ', tunable_type=bool, default=True), 'exists': Tunable(description='\n            If checked then this drama node will pass if a node meeting the requirements exists.\n            Otherwise it will pass if there is not a node meeting the requirements.\n            ', tunable_type=bool, default=True), 'receiver_sim': OptionalTunable(description='\n            If enabled we will check that the receiver Sim is the tuned Sim.\n            ', tunable=TunableEnumEntry(description='\n                The Sim that we will make sure is the receiver Sim.\n                ', tunable_type=ParticipantTypeSingleSim, default=ParticipantTypeSingleSim.TargetSim)), 'time_to_run': OptionalTunable(description='\n            If enabled then we will check against the remaining time until the the drama node is scheduled to run.\n            ', tunable=TunableTuple(threshold=TunableThreshold(description='\n                    A threshold to compare the amount of time left for this drma node to be run.\n                    ', value=TunableTimeSpanSingleton(description='\n                        The amount of time to compare against.\n                        '), default=sims4.math.Threshold(TimeSpan.ZERO, sims4.math.Operator.GREATER_OR_EQUAL.function)), additional_threshold=OptionalTunable(description='\n                    If enabled then we will have a second threshold to compare against.\n                    ', tunable=TunableThreshold(description='\n                        A threshold to compare the amount of time left for this drma node to be run.\n                        ', value=TunableTimeSpanSingleton(description='\n                            The amount of time to compare against.\n                            '), default=sims4.math.Threshold(TimeSpan.ZERO, sims4.math.Operator.GREATER_OR_EQUAL.function)))))}

    def get_expected_args(self):
        if self.receiver_sim is None:
            return {}
        else:
            return {'receiver_sim': self.receiver_sim}

    @cached_test
    def __call__(self, receiver_sim=None):
        if not self.drama_nodes:
            if self.exists:
                return TestResult(False, 'No drama node exists meeting the requirements.', tooltip=self.tooltip)
            return TestResult.TRUE
        drama_scheduler = services.drama_scheduler_service()
        if self.check_scheduled_nodes and self.check_active_nodes:
            drama_node_gen = drama_scheduler.all_nodes_gen
        elif self.check_scheduled_nodes:
            drama_node_gen = drama_scheduler.scheduled_nodes_gen
        elif self.check_active_nodes:
            drama_node_gen = drama_scheduler.active_nodes_gen
        else:
            if self.exists:
                return TestResult(False, 'No drama node exists meeting the requirements.', tooltip=self.tooltip)
            return TestResult.TRUE
        if receiver_sim is not None:
            receiver_sim = next(iter(receiver_sim))
        now = services.time_service().sim_now
        for drama_node in drama_node_gen():
            if type(drama_node) not in self.drama_nodes:
                continue
            if receiver_sim is not None and drama_node.get_receiver_sim_info() is not receiver_sim:
                continue
            if self.time_to_run is not None:
                time_to_node = drama_node.selected_time - now
                if not self.time_to_run.threshold.compare(time_to_node):
                    continue
                if self.time_to_run.additional_threshold is not None and not self.time_to_run.additional_threshold.compare(time_to_node):
                    continue
            else:
                if self.exists:
                    return TestResult.TRUE
                return TestResult(False, 'Drama node meeting the requirements exists when we are asking for non-existence.', tooltip=self.tooltip)
        if self.exists:
            return TestResult(False, 'No drama node exists meeting the requirements.', tooltip=self.tooltip)
        return TestResult.TRUE
class NextFestivalTest(HasTunableSingletonFactory, AutoFactoryInit, event_testing.test_base.BaseTest):
    FACTORY_TUNABLES = {'drama_node': OptionalTunable(description='\n            If enabled then we will check a specific type of festival drama\n            node otherwise we will look at all of the festival drama nodes.\n            ', tunable=TunableReference(description='\n                Reference to the festival drama node that we want to be the\n                next one.\n                ', manager=services.get_instance_manager(sims4.resources.Types.DRAMA_NODE), class_restrictions=('FestivalDramaNode',)), enabled_by_default=True), 'negate': Tunable(description='\n            If enabled this test will pass if the next festival is not one of\n            the tuned nodes.\n            ', tunable_type=bool, default=False)}

    def get_expected_args(self):
        return {}

    @cached_test
    def __call__(self):
        drama_scheduler = services.drama_scheduler_service()
        best_time = None
        best_nodes = [type(node) for node in drama_scheduler.active_nodes_gen() if node.drama_node_type == DramaNodeType.FESTIVAL]
        if not best_nodes:
            for node in drama_scheduler.scheduled_nodes_gen():
                if node.drama_node_type != DramaNodeType.FESTIVAL:
                    continue
                new_time = node._selected_time - services.time_service().sim_now
                if best_time is None or new_time < best_time:
                    best_nodes = [type(node)]
                    best_time = new_time
                elif new_time == best_time:
                    best_nodes.append(type(node))
        if not best_nodes:
            if self.negate:
                return TestResult.TRUE
            return TestResult(False, 'No scheduled Festivals.')
        if self.drama_node is None or self.drama_node in best_nodes:
            if self.negate:
                return TestResult(False, 'Next scheduled Festival matches requested.')
            return TestResult.TRUE
        if self.negate:
            return TestResult.TRUE
        return TestResult(False, "Next scheduled Festival doesn't match requested.")
Beispiel #8
0
 def __init__(self, **kwargs):
     super().__init__(threshold=TunableThreshold(value=Tunable(int, -100, description='The value of the threshold that the commodity is compared against'), description='Threshold for which the sim experiences motive failure'), failure_interactions=TunableList(description="\n                             A list of interactions to be pushed when the Sim's\n                             commodity fails. Only the first one whose test\n                             passes will run.\n                             ", tunable=TunableReference(services.get_instance_manager(sims4.resources.Types.INTERACTION), description='The interaction to be pushed on the sim when the commodity fails.')), description='The behaviors for the commodity failing.', **kwargs)
Beispiel #9
0
 def __init__(self, **kwargs):
     super().__init__(positive_single_arrow=Tunable(float, 1, description='If the change rate for commodity is between this value and less than second arrow value, a single arrow will show up during commodity change.'), positive_double_arrow=Tunable(float, 20, description='If the change rate for commodity is between this value and less than triple arrow value, a double arrow will show up during commodity change.'), positive_triple_arrow=Tunable(float, 30, description='If the change rate for commodity is above this value then triple arrows will show up during commodity change.'), negative_single_arrow=Tunable(float, -1, description='If the change rate for commodity is between this value and less than second arrow value, a single arrow will show up during commodity change.'), negative_double_arrow=Tunable(float, -20, description='If the change rate for commodity is between this value and less than triple arrow value, a double arrow will show up during commodity change.'), negative_triple_arrow=Tunable(float, -30, description='If the change rate for commodity is above this value then triple arrows will show up during commodity change.'), **kwargs)
Beispiel #10
0
class DoCommand(XevtTriggeredElement, HasTunableFactory):
    ARG_TYPE_PARTICIPANT = 0
    ARG_TYPE_LITERAL = 1
    ARG_TYPE_TAG = 3

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

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

    def _do_behavior(self):
        full_command = self.command
        for arg in self.arguments:
            if arg.arg_type == self.ARG_TYPE_PARTICIPANT:
                for participant in self.interaction.get_participants(
                        arg.argument):
                    if hasattr(participant, 'id'):
                        full_command += ' {}'.format(participant.id)
                    else:
                        full_command += ' {}'.format(participant)
                else:
                    if arg.arg_type == self.ARG_TYPE_LITERAL:
                        full_command += ' {}'.format(arg.argument)
                    elif arg.arg_type == self.ARG_TYPE_TAG:
                        full_command += ' {}'.format(int(arg.argument))
                    else:
                        logger.error(
                            'Trying to run the Do Command element with an invalid arg type, {}.',
                            arg.arg_type,
                            owner='trevor')
                        return False
            elif arg.arg_type == self.ARG_TYPE_LITERAL:
                full_command += ' {}'.format(arg.argument)
            elif arg.arg_type == self.ARG_TYPE_TAG:
                full_command += ' {}'.format(int(arg.argument))
            else:
                logger.error(
                    'Trying to run the Do Command element with an invalid arg type, {}.',
                    arg.arg_type,
                    owner='trevor')
                return False
        client_id = services.client_manager().get_first_client_id()
        commands.execute(full_command, client_id)
        return True
Beispiel #11
0
 def __init__(self, **kwargs):
     super().__init__(threshold_value=Tunable(int, -80, description='Threshold for which below the sim is in commodity distress'), buff=TunableBuffReference(description='Buff that gets added to the sim when they are in the commodity distress state.'), distress_interaction=TunableReference(services.get_instance_manager(sims4.resources.Types.INTERACTION), description='The interaction to be pushed on the sim when the commodity reaches distress.'), incompatible_interactions=TunableAffordanceFilterSnippet(), replacement_affordance=TunableReference(services.get_instance_manager(sims4.resources.Types.INTERACTION), description='The affordance that will be pushed when the commodity '), priority=Tunable(int, 0, description='The relative priority of the override interaction being played over others.'), description='The behaviors that show that the commodity is in distress.', **kwargs)
Beispiel #12
0
 def __init__(self,
              target_default=ParticipantType.Object,
              locked_args={},
              carry_target_default=ParticipantType.Object,
              class_restrictions=(),
              **kwargs):
     super().__init__(tunable=TunableTuple(
         description=
         '\n                A continuation entry.\n                ',
         affordance=TunableReference(
             description=
             '\n                    The affordance to push as a continuation on the specified\n                    actor Sim.\n                    ',
             manager=services.affordance_manager(),
             class_restrictions=class_restrictions,
             pack_safe=True),
         si_affordance_override=TunableReference(
             description=
             "\n                When the tuned affordance is a mixer for a different SI, use\n                this to specify the mixer's appropriate SI. This is useful for\n                pushing socials.\n                ",
             manager=services.affordance_manager(),
             allow_none=True),
         actor=TunableEnumEntry(
             description=
             '\n                The Sim on which the affordance is pushed.\n                ',
             tunable_type=ParticipantType,
             default=ParticipantType.Actor),
         target=TunableEnumEntry(
             description=
             '\n                The participant the affordance will target.\n                ',
             tunable_type=ParticipantType,
             default=target_default),
         carry_target=OptionalTunable(
             description=
             '\n                If enabled, specify a carry target for this continuation.\n                ',
             tunable=TunableEnumEntry(
                 description=
                 '\n                    The participant the affordance will set as a carry target.\n                    ',
                 tunable_type=ParticipantType,
                 default=carry_target_default)),
         inventory_carry_target=TunableVariant(
             description=
             '\n                Item in inventory (of continuations actor) to use as carry\n                target for continuation if carry target is None\n                ',
             object_with_tag=CraftTaggedItemFactory(
                 locked_args={
                     'check_type': TunableContinuation.TAGGED_ITEM
                 }),
             object_with_definition=TunableTuple(definition=TunableReference(
                 description=
                 '\n                        The exact object definition to look for inside\n                        inventory.\n                        ',
                 manager=services.definition_manager()),
                                                 locked_args={
                                                     'check_type':
                                                     TunableContinuation.
                                                     ITEM_DEFINITION
                                                 }),
             object_with_base_definition=
             TunableTuple(definition=TunableReference(
                 description=
                 '\n                        The base definition to look for inside inventory.\n                        Objects that redirect (like counters) will match if base\n                        definition is the same.\n                        ',
                 manager=services.definition_manager()),
                          locked_args={
                              'check_type':
                              TunableContinuation.ITEM_TUNING_ID
                          }),
             locked_args={'None': None},
             default='None'),
         preserve_preferred_object=Tunable(
             description=
             "\n                If checked, the pushed interaction's preferred objects are\n                determined by the current preferred objects.\n                \n                If unchecked, the transition sequence would not award bonuses to\n                any specific part.\n                ",
             tunable_type=bool,
             default=True),
         locked_args=locked_args),
                      **kwargs)
Beispiel #13
0
class JigGroup(SideGroup):
    __qualname__ = 'JigGroup'
    INSTANCE_TUNABLES = {
        'jig':
        TunableReference(
            description=
            '\n            The jig to use for finding a place to do the social.',
            manager=services.definition_manager()),
        'participant_slot_map':
        TunableMapping(
            description=
            '\n            The slot index mapping on the jig keyed by participant type',
            key_type=TunableEnumEntry(ParticipantType, ParticipantType.Actor),
            value_type=Tunable(
                description='The slot index for the participant type',
                tunable_type=int,
                default=0)),
        'cancel_delay':
        TunableSimMinute(
            description=
            '\n        Amount of time a jig group must be inactive before it will shut down.\n        ',
            default=15)
    }
    DEFAULT_SLOT_INDEX_ACTOR = 1
    DEFAULT_SLOT_INDEX_TARGET = 0

    @classmethod
    def _get_jig_transforms(cls,
                            initiating_sim,
                            target_sim,
                            picked_object=None,
                            participant_slot_overrides=None):
        slot_map = cls.participant_slot_map if participant_slot_overrides is None else participant_slot_overrides
        actor_slot_index = slot_map.get(ParticipantType.Actor,
                                        cls.DEFAULT_SLOT_INDEX_ACTOR)
        target_slot_index = slot_map.get(ParticipantType.TargetSim,
                                         cls.DEFAULT_SLOT_INDEX_TARGET)
        if picked_object is not None and picked_object.carryable_component is None:
            try:
                return get_two_person_transforms_for_jig(
                    picked_object.definition, picked_object.transform,
                    picked_object.routing_surface, actor_slot_index,
                    target_slot_index)
            except RuntimeError:
                pass
        return fgl_and_get_two_person_transforms_for_jig(
            cls.jig, initiating_sim, actor_slot_index, target_sim,
            target_slot_index)

    def __init__(self,
                 *args,
                 si=None,
                 target_sim=None,
                 participant_slot_overrides=None,
                 **kwargs):
        super().__init__(si=si, target_sim=target_sim, *args, **kwargs)
        self._sim_transform_map = {}
        self.geometry = None
        initiating_sim = si.sim
        if initiating_sim is None or target_sim is None:
            logger.error(
                'JigGroup {} cannot init with initial sim {()} or target sim {()}',
                self.__name__, initiating_sim, target_sim)
            return
        picked_object = si.picked_object
        self.participant_slot_overrides = participant_slot_overrides
        (sim_transform, target_transform,
         routing_surface) = self._get_jig_transforms(
             initiating_sim,
             target_sim,
             picked_object=picked_object,
             participant_slot_overrides=self.participant_slot_overrides)
        self._jig_transform = target_transform
        if target_transform is not None:
            self._jig_polygon = placement.get_placement_footprint_polygon(
                target_transform.translation, target_transform.orientation,
                routing_surface, self.jig.get_footprint(0))
        else:
            self._jig_polygon = None
        self._sim_transform_map[initiating_sim] = sim_transform
        self._sim_transform_map[target_sim] = target_transform
        if target_transform is None:
            self._constraint = Nowhere()
            return
        target_forward = target_transform.transform_vector(
            sims4.math.FORWARD_AXIS)
        self._set_focus(target_transform.translation, target_forward,
                        routing_surface)
        self._initialize_constraint(notify=True)

    @classmethod
    def _verify_tuning_callback(cls):
        if cls.jig is None:
            logger.error('JigGroup {} must have a jig tuned.', cls.__name__)

    @classmethod
    def make_constraint_default(cls,
                                actor,
                                target_sim,
                                position,
                                routing_surface,
                                participant_type=ParticipantType.Actor,
                                picked_object=None,
                                participant_slot_overrides=None):
        (actor_transform, target_transform,
         routing_surface) = cls._get_jig_transforms(
             actor,
             target_sim,
             picked_object=picked_object,
             participant_slot_overrides=participant_slot_overrides)
        if actor_transform is None or target_transform is None:
            return Nowhere()
        if participant_type == ParticipantType.Actor:
            constraint_transform = actor_transform
        elif participant_type == ParticipantType.TargetSim:
            constraint_transform = target_transform
        else:
            return Anywhere()
        return interactions.constraints.Transform(
            constraint_transform,
            routing_surface=routing_surface,
            debug_name='JigGroupConstraint')

    def _relocate_group_around_focus(self, *args, **kwargs):
        return False

    @property
    def group_radius(self):
        if self._jig_polygon is not None:
            return self._jig_polygon.radius()
        return 0

    @property
    def jig_polygon(self):
        return self._jig_polygon

    @property
    def jig_transform(self):
        return self._jig_transform

    def get_constraint(self, sim):
        transform = self._sim_transform_map.get(sim, None)
        if transform is not None:
            return interactions.constraints.Transform(
                transform, routing_surface=self.routing_surface)
        if sim in self._sim_transform_map:
            return Nowhere()
        return Anywhere()

    def _make_constraint(self, *args, **kwargs):
        if self._constraint is None:
            constraints = [
                interactions.constraints.Transform(
                    t, routing_surface=self.routing_surface)
                for t in self._sim_transform_map.values()
            ]
            self._constraint = create_constraint_set(
                constraints) if constraints else Anywhere()
        return self._constraint

    _create_adjustment_alarm = socials.group.SocialGroup._create_adjustment_alarm

    def _consider_adjusting_sim(self, sim=None, initial=False):
        if not initial:
            for sim in self:
                for _ in self.queued_mixers_gen(sim):
                    pass
            if self.time_since_interaction().in_minutes() < self.cancel_delay:
                return
            self.shutdown(FinishingType.NATURAL)
Beispiel #14
0
class PlayEffect(distributor.ops.ElementDistributionOpMixin, HasTunableFactory,
                 AutoFactoryInit):
    JOINT_NAME_CURRENT_POSITION = 1899928870
    FACTORY_TUNABLES = {
        'effect_name':
        Tunable(description=
                '\n            The name of the effect to play.\n            ',
                tunable_type=str,
                default=''),
        'joint_name':
        OptionalTunable(
            description=
            '\n            Specify if the visual effect is attached to a slot and, if so, which\n            slot.\n            ',
            tunable=TunableStringHash32(
                description=
                '\n                The name of the slot this effect is attached to.\n                ',
                default='_FX_'),
            enabled_by_default=True,
            enabled_name='Slot',
            disabled_name='Current_Position',
            disabled_value=JOINT_NAME_CURRENT_POSITION),
        'play_immediate':
        Tunable(
            description=
            '\n            If checked, this effect will be triggered immediately, nothing\n            will block.\n\n            ex. VFX will be played immediately while \n            the Sim is routing or animating.\n            ',
            tunable_type=bool,
            default=False)
    }

    def __init__(self,
                 target,
                 effect_name='',
                 joint_name=0,
                 target_actor_id=0,
                 target_joint_name_hash=0,
                 mirror_effect=False,
                 auto_on_effect=False,
                 target_joint_offset=None,
                 play_immediate=False,
                 callback_event_id=None,
                 store_target_position=False,
                 transform_override=None,
                 **kwargs):
        super().__init__(effect_name=effect_name,
                         joint_name=joint_name,
                         play_immediate=play_immediate,
                         immediate=play_immediate,
                         **kwargs)
        self.target = target
        if target is not None:
            if target.inventoryitem_component is not None:
                forward_to_owner_list = target.inventoryitem_component.forward_client_state_change_to_inventory_owner
                if forward_to_owner_list:
                    if StateChange.VFX in forward_to_owner_list:
                        inventory_owner = target.inventoryitem_component.inventory_owner
                        if inventory_owner is not None:
                            self.target = inventory_owner
            if target.crafting_component is not None:
                effect_name = target.crafting_component.get_recipe_effect_overrides(
                    effect_name)
        self.target_transform = target.transform if target is not None else transform_override
        self.effect_name = effect_name
        self.auto_on_effect = auto_on_effect
        self.target_actor_id = target_actor_id
        self.target_joint_name_hash = target_joint_name_hash
        self.mirror_effect = mirror_effect
        self._stop_type = SOFT_TRANSITION
        self.target_joint_offset = target_joint_offset
        self.immediate = play_immediate
        self.callback_event_id = callback_event_id
        self.store_target_position = store_target_position

    def __repr__(self):
        return standard_angle_repr(self, self.effect_name)

    @property
    def _is_relative_to_transform(self):
        return self.joint_name == self.JOINT_NAME_CURRENT_POSITION

    def _on_target_location_changed(self, *_, **__):
        self.stop(immediate=True)
        self.start()

    def start(self, *_, **__):
        if self.target is None:
            logger.error(
                'Attempting to attach VFX without a target. Perhaps you mean to use start_one_shot()',
                owner='rmccord')
        if self._is_relative_to_transform:
            self.target.register_on_location_changed(
                self._on_target_location_changed)
        if not self._is_valid_target():
            return
        if not self.is_attached:
            self.attach(self.target)
            logger.info('VFX {} on {} START'.format(self.effect_name,
                                                    self.target))

    def start_one_shot(self):
        if self.target is not None and not self.target.is_terrain:
            distributor.ops.record(self.target, self)
        else:
            Distributor.instance().add_op_with_no_owner(self)

    def stop(self, *_, immediate=False, **kwargs):
        if self.target is None or not self.target.valid_for_distribution:
            return
        if self._is_relative_to_transform:
            self.target.unregister_on_location_changed(
                self._on_target_location_changed)
        if self.is_attached:
            if immediate:
                self._stop_type = HARD_TRANSITION
            else:
                self._stop_type = SOFT_TRANSITION
            self.detach()

    def _is_valid_target(self):
        if not self.target.valid_for_distribution:
            zone = services.current_zone()
            if zone is not None:
                zone_spin_up_service = zone.zone_spin_up_service
                if zone_spin_up_service is None:
                    logger.callstack(
                        'zone_spin_up_service was None in PlayEffect._is_valid_target(), for effect/target: {}/{}',
                        self,
                        self.target,
                        owner='johnwilkinson',
                        level=sims4.log.LEVEL_ERROR)
                    return False
                elif not zone_spin_up_service.is_finished:
                    return False
        return True

    def detach(self, *objects):
        super().detach(*objects)
        if services.current_zone().is_zone_shutting_down:
            return
        op = StopVFX(self.target.id,
                     self.actor_id,
                     stop_type=self._stop_type,
                     immediate=self.immediate)
        distributor.ops.record(self.target, op)
        logger.info('VFX {} on {} STOP'.format(self.effect_name, self.target))

    def write(self, msg):
        start_msg = VFXStart()
        if self.target is not None:
            start_msg.object_id = self.target.id
        start_msg.effect_name = self.effect_name
        start_msg.actor_id = self.actor_id
        start_msg.joint_name_hash = self.joint_name
        start_msg.target_actor_id = self.target_actor_id
        start_msg.target_joint_name_hash = self.target_joint_name_hash
        start_msg.mirror_effect = self.mirror_effect
        start_msg.auto_on_effect = self.auto_on_effect
        if self.target_joint_offset is not None:
            start_msg.target_joint_offset.x = self.target_joint_offset.x
            start_msg.target_joint_offset.y = self.target_joint_offset.y
            start_msg.target_joint_offset.z = self.target_joint_offset.z
        if self.callback_event_id is not None:
            start_msg.callback_event_id = self.callback_event_id
        if self._is_relative_to_transform:
            if self.store_target_position or self.target is None:
                transform = self.target_transform
            else:
                transform = self.target.transform
            start_msg.transform.translation.x = transform.translation.x
            start_msg.transform.translation.y = transform.translation.y
            start_msg.transform.translation.z = transform.translation.z
            start_msg.transform.orientation.x = transform.orientation.x
            start_msg.transform.orientation.y = transform.orientation.y
            start_msg.transform.orientation.z = transform.orientation.z
            start_msg.transform.orientation.w = transform.orientation.w
        self.serialize_op(msg, start_msg, protocols.Operation.VFX_START)
class DisplaySnippetPickerSuperInteraction(PickerSuperInteraction):
    INSTANCE_TUNABLES = {
        'picker_dialog':
        TunablePickerDialogVariant(
            description='\n            The item picker dialog.\n            ',
            available_picker_flags=ObjectPickerTuningFlags.ITEM,
            tuning_group=GroupNames.PICKERTUNING),
        'subject':
        TunableEnumFlags(
            description=
            "\n            To whom 'loot on selected' should be applied.\n            ",
            enum_type=ParticipantTypeSim,
            default=ParticipantTypeSim.Actor,
            tuning_group=GroupNames.PICKERTUNING),
        'display_snippets':
        TunableList(
            description=
            '\n            The list of display snippets available to select and paired loot actions\n            that will run if selected.\n            ',
            tunable=_PickerDisplaySnippet.TunableFactory(
                description=
                '\n                Display snippet available to select.\n                '
            ),
            tuning_group=GroupNames.PICKERTUNING),
        'display_snippet_text_tokens':
        LocalizationTokens.TunableFactory(
            description=
            '\n            Localization tokens passed into the display snippet text fields.\n            \n            When acting on the individual items within the snippet list, the \n            following text tokens will be appended to this list of tokens (in \n            order):\n            0: snippet instance display name\n            1: snippet instance display description\n            2: snippet instance display tooltip\n            3: tokens tuned alongside individual snippets within the snippet list\n            ',
            tuning_group=GroupNames.PICKERTUNING),
        'display_snippet_text_overrides':
        OptionalTunable(
            description=
            '\n            If enabled, display snippet text overrides for all snippets \n            to be displayed in the picker. \n            \n            Can be used together with the display snippet text tokens to \n            act as text wrappers around the existing snippet display data.\n            ',
            tunable=_DisplaySnippetTextOverrides.TunableFactory(
                description=
                '\n                Display snippet text overrides for all snippets to be displayed\n                in the picker. \n            \n                Can be used together with the display snippet text tokens to \n                act as text wrappers around the existing snippet display data.\n                '
            ),
            tuning_group=GroupNames.PICKERTUNING),
        'continuations':
        TunableList(
            description=
            '\n            List of continuations to push when a snippet is selected.\n            \n            ID of the snippet will be the PickedItemID participant in the \n            continuation.\n            ',
            tunable=TunableContinuation(),
            tuning_group=GroupNames.PICKERTUNING),
        'run_continuations_on_no_selection':
        Tunable(
            description=
            '\n            Checked, runs continuations regardless if anything is selected.\n            Unchecked, continuations are only run if something is selected.\n            ',
            tunable_type=bool,
            default=True,
            tuning_group=GroupNames.PICKERTUNING)
    }

    @classmethod
    def has_valid_choice(cls, target, context, **kwargs):
        snippet_count = 0
        for _ in cls.picker_rows_gen(target, context, **kwargs):
            snippet_count += 1
            if snippet_count >= cls.picker_dialog.min_selectable:
                return True
        return False

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

    @flexmethod
    def picker_rows_gen(cls, inst, target, context, **kwargs):
        inst_or_cls = inst if inst is not None else cls
        target = target if target is not DEFAULT else inst.target
        context = context if context is not DEFAULT else inst.context
        resolver = InteractionResolver(cls,
                                       inst,
                                       target=target,
                                       context=context)
        general_tokens = inst_or_cls.display_snippet_text_tokens.get_tokens(
            resolver)
        overrides = inst_or_cls.display_snippet_text_overrides
        index = 0
        for display_snippet_data in inst_or_cls.display_snippets:
            display_snippet = display_snippet_data.display_snippet
            resolver = InteractionResolver(
                cls,
                inst,
                target=target,
                context=context,
                picked_item_ids={display_snippet.guid64})
            test_result = display_snippet_data.test(resolver)
            is_enable = test_result.result
            if is_enable or test_result.tooltip is not None:
                snippet_default_tokens = (
                    display_snippet.display_name(*general_tokens)
                    if display_snippet.display_name is not None else None,
                    display_snippet.display_description(*general_tokens) if
                    display_snippet.display_description is not None else None,
                    display_snippet.display_tooltip(*general_tokens)
                    if display_snippet.display_tooltip is not None else None)
                snippet_additional_tokens = display_snippet_data.display_snippet_text_tokens.get_tokens(
                    resolver)
                tokens = general_tokens + snippet_default_tokens + snippet_additional_tokens
                display_snippet = overrides(
                    display_snippet_data.display_snippet)
                tooltip = None if not overrides is not None or test_result.tooltip is None else lambda *_, tooltip=test_result.tooltip: tooltip(
                    *tokens)
                tooltip = None if display_snippet.display_tooltip is None else lambda *_, tooltip=display_snippet.display_tooltip: tooltip(
                    *tokens)
                row = BasePickerRow(
                    is_enable=is_enable,
                    name=display_snippet.display_name(*tokens),
                    icon=display_snippet.display_icon,
                    tag=index,
                    row_description=display_snippet.display_description(
                        *tokens),
                    row_tooltip=tooltip)
                yield row
            index += 1

    def _on_display_snippet_selected(self, picked_choice, **kwargs):
        resolver = self.get_resolver(**kwargs)
        for loot_on_selected in self.display_snippets[
                picked_choice].loot_on_selected:
            loot_on_selected.apply_to_resolver(resolver)

    def on_choice_selected(self, picked_choice, **kwargs):
        if picked_choice is None:
            if self.run_continuations_on_no_selection:
                for continuation in self.continuations:
                    self.push_tunable_continuation(continuation)
            return
        display_snippet = self.display_snippets[picked_choice].display_snippet
        picked_item_set = {display_snippet.guid64}
        self._on_display_snippet_selected(picked_choice,
                                          picked_item_ids=picked_item_set)
        for continuation in self.continuations:
            self.push_tunable_continuation(continuation,
                                           picked_item_ids=picked_item_set)
Beispiel #16
0
class Commodity(HasTunableReference, TunedContinuousStatistic, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)):
    __qualname__ = 'Commodity'
    REMOVE_INSTANCE_TUNABLES = ('initial_value',)
    INSTANCE_TUNABLES = {'stat_name': TunableLocalizedString(description='\n                Localized name of this commodity.\n                ', export_modes=ExportModes.All), 'min_value_tuning': Tunable(description='\n                The minimum value for this stat.\n                ', tunable_type=float, default=-100, export_modes=ExportModes.All), 'max_value_tuning': Tunable(description='\n                The maximum value for this stat.', tunable_type=float, default=100, export_modes=ExportModes.All), 'ui_sort_order': TunableRange(description='\n                Order in which the commodity will appear in the motive panel.\n                Commodities sort from lowest to highest.\n                ', tunable_type=int, default=0, minimum=0, export_modes=ExportModes.All), 'ui_visible_distress_threshold': Tunable(description='\n                When current value of commodity goes below this value, commodity\n                will appear in the motive panel tab.\n                ', tunable_type=float, default=0, 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), export_modes=ExportModes.All)), 'auto_satisfy_curve_tuning': TunableList(description='\n                A list of Vector2 points that define the auto-satisfy curve for\n                this commodity.\n                ', tunable=TunableVector2(description='\n                    Point on a Curve\n                    ', default=sims4.math.Vector2(0, 0))), 'auto_satisfy_curve_random_time_offset': TunableSimMinute(description='\n                An amount of time that when auto satisfy curves are being used\n                will modify the time current time being used to plus or minus\n                a random number between this value.\n                ', default=120), 'maximum_auto_satisfy_time': TunableSimMinute(description='\n                The maximum amount of time that the auto satisfy curves will\n                interpolate the values based on the current one before just\n                setting to the maximum value.\n                ', default=1440), 'initial_tuning': TunableTuple(description=' \n                The Initial value for this commodity. Can either be a single\n                value, range, or use auto satisfy curve to determine initial\n                value.  Use auto satisfy curve will take precedence over range\n                value and range value will take precedence over single value\n                range.\n                ', _use_auto_satisfy_curve_as_initial_value=Tunable(description="\n                    If checked, when we first add this commodity to a sim (sims only),\n                    the initial value of the commodity will be set according to\n                    the auto-satisfy curves defined by this commodity's tuning as\n                    opposed to the tuned initial value.    \n                    ", tunable_type=bool, needs_tuning=True, default=False), _value_range=OptionalTunable(description='\n                    If enabled then when we first add this commodity to a Sim the\n                    initial value of the commodity will be set to a random value\n                    within this interval.\n                    ', tunable=TunableInterval(description='\n                        An interval that will be used for the initial value of this\n                        commodity.\n                        ', tunable_type=int, default_lower=0, default_upper=100)), _value=Tunable(description='\n                    The initial value for this stat.', tunable_type=float, default=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), 'states': TunableList(description='\n                Commodity states based on thresholds.  This should be ordered\n                from worst state to best state.\n                ', tunable=TunableCommodityState()), 'commodity_distress': OptionalTunable(TunableCommodityDistress()), 'commodity_failure': OptionalTunable(TunableCommodityFailure()), 'remove_on_convergence': Tunable(description='\n                Commodity will be removed when convergence is met only if not\n                a core commodity.\n                ', tunable_type=bool, default=True), 'visible': Tunable(description='\n                Whether or not commodity should be sent to client.\n                ', tunable_type=bool, default=False, export_modes=ExportModes.All), '_add_if_not_in_tracker': Tunable(description="\n                If True, when we try to add or set the commodity, we will add\n                the commodity to the tracker if the tracker doesn't already have\n                it.\n                \n                e.g If a sim uses the toilet and we update bladder when that sim\n                doesn't have the bladder commodity in his/her tracker, we will\n                add the bladder commodity to that sim. \n                \n                Set this to false for the case of NPC behavior commodities like\n                Being a Maid or Being a Burglar.\n                ", tunable_type=bool, default=True), 'initial_as_default': Tunable(description='\n                Setting this to true will cause the default value returned during testing to be the \n                initial value tuned. This happens when a test is run on this commodity on a Sim that\n                does not have the commodity. Leaving this as false will instead return the convergence\n                value.\n                ', tunable_type=bool, default=False), 'arrow_data': TunableArrowData(description='\n                Used to determine when positive or negative arrows should show\n                up depending on the delta rate of the commodity.\n                ', export_modes=(ExportModes.ClientBinary,)), '_categories': TunableSet(description='\n                List of categories that this statistic is part of.\n                ', tunable=StatisticCategory), '_off_lot_simulation': OptionalTunable(TunableTuple(threshold=TunableThreshold(description='\n                    The threshold that will activate the increase in value\n                    when the commodity hits it.\n                    ', value=Tunable(description='\n                        The value that this threshold will trigger on.\n                        ', tunable_type=int, default=-50)), value=Tunable(description='\n                    The value that this commodity will increase by once it hits\n                    the tuned threshold while the sim is offlot.\n                    ', tunable_type=int, default=100), description='\n                Offlot simulation for this commodity.  The commodity will be\n                allowed to decay at a normal rate until it hits the tuned\n                threshold.  Once there it will then have its value added by the\n                tuned value.\n                ')), '_max_simulate_time_on_load': OptionalTunable(description="\n                If enabled, this commodity will only simulate for a max amount\n                of time when the player loads back into the lot with a new world\n                game time.\n                \n                By default, this is disabled. When disabled, the commodity will\n                simulate for however long between the lot's previous saved time\n                and the current world time. (Note: this is capped by PersistenceTuning.MAX_LOT_SIMULATE_ELAPSED_TIME)\n                ", tunable=TunableSimMinute(description="\n                    If set to > 0, on load, this object commodity will update its value to\n                    world time. And the commodity will simulate for the max amount of time\n                    specified in the tunable.\n                    EX: If tuned for the water commodity on plants to 6 hours --\n                    if the player leaves the lot for 4 hours and then comes back,\n                    the water commodity will update to what it should be 4 hours later.\n                    If the player leaves the lot for 8 hours and comes back, the water\n                    commodity will only update to what it should be 6 hours later.\n                    \n                    If set to 0, no matter how much time has elapsed since the\n                    player last visited the lot, this commodity's value will load\n                    to its last saved value.\n                    ", default=1440, minimum=0)), '_time_passage_fixup_type': TunableEnumEntry(description="\n            This is for commodities on SIMS only.\n            This option what we do with the commodity when the sim\n            gets instanced after time has elapsed since the last time the sim\n            was spawned.\n            \n            do not fixup: Means the commodity will stay the same value as it was\n                when the sim was last instantiated\n                \n            fixup using autosatisfy curve: The commodity's value will be set\n                based on its autosatisfy curve and the time between when the sim was\n                last saved. Note, this fixup will not occur for active household sims\n                if offlot simulation is enabled for this commodity.\n                \n            fixup using time elapsed: The commodity will decay linearly based on\n                when the sim was last saved. Use this for things like commodities\n                that control buff timers to make sure that the time remaining on\n                a buff remains consistent.\n            ", tunable_type=CommodityTimePassageFixupType, default=CommodityTimePassageFixupType.DO_NOT_FIXUP), 'use_stat_value_on_init': Tunable(description='\n            When set the initial value for the commodity will be set from the\n            commodity tuning.\n            If unchecked, the initial stat value will not be set on \n            initialization, but instead will use other systems (like the state)\n            to set its initial value.\n            ', tunable_type=bool, default=True), 'stat_asm_param': TunableStatAsmParam.TunableFactory(locked_args={'use_effective_skill_level': True})}
    initial_value = 0
    _auto_satisfy_curve = None
    use_autosatisfy_curve = True
    commodity_states = None

    @classmethod
    def _tuning_loaded_callback(cls):
        super()._tuning_loaded_callback()
        cls.initial_value = cls.initial_tuning._value
        cls._build_utility_curve_from_tuning_data(cls.ad_data)
        if cls.auto_satisfy_curve_tuning:
            point_list = [(point.x, point.y) for point in cls.auto_satisfy_curve_tuning]
            cls._auto_satisfy_curve = sims4.math.CircularUtilityCurve(point_list, 0, date_and_time.HOURS_PER_DAY)
        if cls.states:
            state_zero = cls.states[0]
            if state_zero.value < cls.min_value:
                logger.error('Worst state should not be lower than min value of commodity.  Please update tuning')
                cls.commodity_states = cls.states
            elif state_zero.value > cls.min_value:
                state = CommodityState(value=cls.min_value, buff=BuffReference())
                cls.commodity_states = (state,) + cls.states
            else:
                cls.commodity_states = cls.states
            previous_value = cls.max_value
            index = len(cls.commodity_states)
            for state in reversed(cls.commodity_states):
                index -= 1
                if state.value >= previous_value:
                    logger.error('{0} has a lower bound value of state at index:{1} that is higher than the previous state.  Please update tuning', cls, index)
                if state.buff_add_threshold is not None:
                    threshold_value = state.buff_add_threshold.value
                    if threshold_value < state.value or threshold_value > previous_value:
                        logger.error('{0} add buff threshold is out of range for state at index:{1}.  Please update tuning', cls, index)
                previous_value = state.value
                while state.buff is not None and state.buff.buff_type is not None:
                    state.buff.buff_type.add_owning_commodity(cls)

    @classmethod
    def _verify_tuning_callback(cls):
        if cls.visible and (cls.ui_visible_distress_threshold < cls.min_value or cls.ui_visible_distress_threshold > cls.max_value):
            logger.error('{} visible distress value {} is outside the min{} / max {} range.  Please update tuning', cls, cls.ui_visible_distress_threshold, cls.min_value, cls.max_value)

    def __init__(self, tracker, core=False):
        self._allow_convergence_callback_to_activate = False
        self._buff_handle = None
        super().__init__(tracker, self.get_initial_value())
        self._core = core
        self._buff_handle = None
        self._buff_threshold_callback = None
        self._current_state_index = None
        self._current_state_ge_callback_data = None
        self._current_state_lt_callback_data = None
        self._off_lot_callback_data = None
        self._distress_buff_handle = None
        self._exit_distress_callback_data = None
        self._distress_callback_data = None
        self._failure_callback_data = None
        self._convergence_callback_data = None
        self._suppress_client_updates = False
        self.force_apply_buff_on_start_up = False
        self.force_buff_reason = None
        if getattr(self.tracker.owner, 'is_simulating', True):
            activate_convergence_callback = self.default_value != self.get_value()
            self.on_initial_startup(from_init=True, activate_convergence_callback=activate_convergence_callback)

    @classproperty
    def initial_value_range(cls):
        return cls.initial_tuning._value_range

    @classproperty
    def use_auto_satisfy_curve_as_initial_value(cls):
        return cls.initial_tuning._use_auto_satisfy_curve_as_initial_value

    @classmethod
    def get_initial_value(cls):
        if cls.initial_value_range is None:
            return cls.initial_value
        return random.uniform(cls.initial_value_range.lower_bound, cls.initial_value_range.upper_bound)

    @classproperty
    def use_stat_value_on_initialization(cls):
        return cls.use_stat_value_on_init

    @property
    def core(self):
        return self._core

    @core.setter
    def core(self, value):
        self._core = value

    @property
    def is_visible(self):
        return self.visible

    def _setup_commodity_distress(self):
        if self.commodity_distress is not None:
            self._distress_callback_data = self.create_callback(Threshold(self.commodity_distress.threshold_value, operator.le), self._enter_distress)

    def _setup_commodity_failure(self):
        if self.commodity_failure is not None:
            self._failure_callback_data = self.create_callback(self.commodity_failure.threshold, self._commodity_fail)

    def on_initial_startup(self, from_init=False, activate_convergence_callback=True):
        self._setup_commodity_distress()
        if self._distress_callback_data is not None:
            self.add_callback_data(self._distress_callback_data)
        self._setup_commodity_failure()
        if self._failure_callback_data is not None:
            self.add_callback_data(self._failure_callback_data)
        if self.commodity_states:
            self._remove_state_callback()
            current_value = self.get_value()
            new_state_index = self._find_state_index(current_value)
            if self._current_state_index != new_state_index:
                self._set_state(new_state_index, current_value, from_init=from_init, send_client_update=False)
            self._add_state_callback()
        self.decay_enabled = not self.tracker.owner.is_locked(self)
        self.force_apply_buff_on_start_up = False
        if self.force_buff_reason is not None and self._buff_handle is not None:
            current_state = self.commodity_states[self._current_state_index]
            self.tracker.owner.set_buff_reason(current_state.buff.buff_type, self.force_buff_reason, use_replacement=True)
            self.force_buff_reason = None
        if self.remove_on_convergence and self._convergence_callback_data is None:
            self._convergence_callback_data = self.create_callback(Threshold(self.convergence_value, operator.eq), self._remove_self_from_tracker)
            if activate_convergence_callback:
                self.add_callback_data(self._convergence_callback_data)
                self._allow_convergence_callback_to_activate = False
            else:
                self._allow_convergence_callback_to_activate = True

    @contextlib.contextmanager
    def _suppress_client_updates_context_manager(self, from_load=False, is_rate_change=True):
        if self._suppress_client_updates:
            yield None
        else:
            self._suppress_client_updates = True
            try:
                yield None
            finally:
                self._suppress_client_updates = False
                if not from_load:
                    self.send_commodity_progress_msg(is_rate_change=is_rate_change)

    def _commodity_telemetry(self, hook, desired_state_index):
        if not self.tracker.owner.is_sim:
            return
        with telemetry_helper.begin_hook(writer, hook, sim=self.tracker.get_sim()) as hook:
            guid = getattr(self, 'guid64', None)
            if guid is not None:
                hook.write_guid('stat', self.guid64)
            else:
                logger.info('{} does not have a guid64', self)
            hook.write_int('oldd', self._current_state_index)
            hook.write_int('news', desired_state_index)

    def _update_state_up(self, stat_instance):
        with self._suppress_client_updates_context_manager():
            current_value = self.get_value()
            desired_state_index = self._find_state_index(current_value)
            if desired_state_index == self._current_state_index:
                desired_state_index = self._find_state_index(current_value + EPSILON)
            if desired_state_index != self._current_state_index:
                self._remove_state_callback()
                self._commodity_telemetry(TELEMETRY_HOOK_STATE_UP, desired_state_index)
                while self._current_state_index < desired_state_index:
                    next_index = self._current_state_index + 1
                    self._set_state(next_index, current_value, send_client_update=next_index == desired_state_index)
                self._update_state_callback(desired_state_index)
            else:
                logger.warn('{} update state up was called, but state did not change. current state_index:{}', self, self._current_state_index, owner='msantander')

    def _update_state_down(self, stat_instance):
        with self._suppress_client_updates_context_manager():
            current_value = self.get_value()
            desired_state_index = self._find_state_index(self.get_value())
            if desired_state_index == self._current_state_index:
                desired_state_index = self._find_state_index(current_value - EPSILON)
            if desired_state_index != self._current_state_index:
                self._remove_state_callback()
                self._commodity_telemetry(TELEMETRY_HOOK_STATE_DOWN, desired_state_index)
                while self._current_state_index > desired_state_index:
                    prev_index = self._current_state_index - 1
                    self._set_state(prev_index, current_value, send_client_update=prev_index == desired_state_index)
                self._update_state_callback(desired_state_index)
            else:
                logger.warn('{} update state down was called, but state did not change. current state_index:{}', self, self._current_state_index, owner='msantander')

    def _update_state_callback(self, desired_state_index):
        new_state_index = self._find_state_index(self.get_value())
        if new_state_index > desired_state_index:
            self._update_state_up(self)
        elif new_state_index < desired_state_index:
            self._update_state_down(self)
        else:
            self._add_state_callback()

    def _state_reset_callback(self, stat_instance, time):
        self._update_buff(self._get_change_rate_without_decay())

    def _remove_self_from_tracker(self, _):
        tracker = self._tracker
        if tracker is not None:
            tracker.remove_statistic(self.stat_type)

    def _off_lot_simulation_callback(self, _):
        self.add_value(self._off_lot_simulation.value)

    def start_low_level_simulation(self):
        if self._off_lot_simulation is None:
            self.decay_enabled = False
            return
        self._off_lot_callback_data = self.add_callback(self._off_lot_simulation.threshold, self._off_lot_simulation_callback)
        self.decay_enabled = True

    def stop_low_level_simulation(self):
        self.decay_enabled = False
        if self._off_lot_callback_data is not None:
            self.remove_callback(self._off_lot_callback_data)

    def stop_regular_simulation(self):
        self._remove_state_callback()
        self.decay_enabled = False
        if self._convergence_callback_data is not None:
            self.remove_callback(self._convergence_callback_data)
            self._convergence_callback_data = None
        if self._distress_callback_data is not None:
            self.remove_callback(self._distress_callback_data)
            self._distress_callback_data = None
        if self.commodity_distress is not None:
            self._exit_distress(self, True)
        if self._failure_callback_data is not None:
            self.remove_callback(self._failure_callback_data)
            self._failure_callback_data = None

    def _find_state_index(self, current_value):
        index = len(self.commodity_states) - 1
        while index >= 0:
            state = self.commodity_states[index]
            if current_value >= state.value:
                return index
            index -= 1
        return 0

    def _add_state_callback(self):
        next_state_index = self._current_state_index + 1
        if next_state_index < len(self.commodity_states):
            self._current_state_ge_callback_data = self.add_callback(Threshold(self.commodity_states[next_state_index].value, operator.ge), self._update_state_up, on_callback_alarm_reset=self._state_reset_callback)
        if self.commodity_states[self._current_state_index].value > self.min_value:
            self._current_state_lt_callback_data = self.add_callback(Threshold(self.commodity_states[self._current_state_index].value, operator.lt), self._update_state_down, on_callback_alarm_reset=self._state_reset_callback)

    def _remove_state_callback(self):
        if self._current_state_ge_callback_data is not None:
            self.remove_callback(self._current_state_ge_callback_data)
            self._current_state_ge_callback_data = None
        if self._current_state_lt_callback_data is not None:
            self.remove_callback(self._current_state_lt_callback_data)
            self._current_state_lt_callback_data = None
        if self._buff_threshold_callback is not None:
            self.remove_callback(self._buff_threshold_callback)
            self._buff_threshold_callback = None

    def _get_next_buff_commodity_decaying_to(self):
        transition_into_buff_id = 0
        if self._current_state_index is not None and self._current_state_index > 0:
            current_value = self.get_value()
            buff_tunable_ref = None
            if self.convergence_value <= current_value:
                buff_tunable_ref = self.commodity_states[self._current_state_index - 1].buff
            else:
                next_state_index = self._current_state_index + 1
                if next_state_index < len(self.commodity_states):
                    buff_tunable_ref = self.commodity_states[next_state_index].buff
            if buff_tunable_ref is not None:
                buff_type = buff_tunable_ref.buff_type
                if buff_type is not None and buff_type.visible:
                    transition_into_buff_id = buff_type.guid64
        return transition_into_buff_id

    def _add_buff_from_state(self, commodity_state):
        owner = self.tracker.owner
        if owner.is_sim:
            buff_tuning = commodity_state.buff
            transition_into_buff_id = self._get_next_buff_commodity_decaying_to() if buff_tuning.buff_type.visible else 0
            self._buff_handle = owner.add_buff(buff_tuning.buff_type, buff_reason=buff_tuning.buff_reason, commodity_guid=self.guid64, change_rate=self._get_change_rate_without_decay(), transition_into_buff_id=transition_into_buff_id)

    def _add_buff_callback(self, _):
        current_state = self.commodity_states[self._current_state_index]
        self.remove_callback(self._buff_threshold_callback)
        self._buff_threshold_callback = None
        self._add_buff_from_state(current_state)

    def _set_state(self, new_state_index, current_value, from_init=False, send_client_update=True):
        new_state = self.commodity_states[new_state_index]
        old_state_index = self._current_state_index
        self._current_state_index = new_state_index
        if self._buff_threshold_callback is not None:
            self.remove_callback(self._buff_threshold_callback)
            self._buff_threshold_callback = None
        if self._buff_handle is not None:
            self.tracker.owner.remove_buff(self._buff_handle)
            self._buff_handle = None
        if new_state.buff.buff_type:
            if new_state.buff_add_threshold is not None and not self.force_apply_buff_on_start_up and not new_state.buff_add_threshold.compare(current_value):
                self._buff_threshold_callback = self.add_callback(new_state.buff_add_threshold, self._add_buff_callback)
            else:
                self._add_buff_from_state(new_state)
        if (old_state_index is not None or from_init) and new_state.loot_list_on_enter is not None and self.tracker.owner.is_sim:
            resolver = event_testing.resolver.SingleSimResolver(self.tracker.owner)
            while True:
                for loot_action in new_state.loot_list_on_enter:
                    loot_action.apply_to_resolver(resolver)
        if send_client_update:
            self.send_commodity_progress_msg()

    def _enter_distress(self, stat_instance):
        if self.tracker.owner.get_sim_instance() is None:
            return
        if self.commodity_distress.buff.buff_type is not None:
            if self._distress_buff_handle is None:
                self._distress_buff_handle = self.tracker.owner.add_buff(self.commodity_distress.buff.buff_type, self.commodity_distress.buff.buff_reason, commodity_guid=self.guid64)
            else:
                logger.error('Distress Buff Handle is not none when entering Commodity Distress for {}.', self, owner='jjacobson')
        if self._exit_distress_callback_data is None:
            self._exit_distress_callback_data = self.add_callback(Threshold(self.commodity_distress.threshold_value, operator.gt), self._exit_distress)
        else:
            logger.error('Exit Distress Callback Data is not none when entering Commodity Distress for {}.', self, owner='jjacobson')
        self.tracker.owner.enter_distress(self)
        sim = self.tracker.owner.get_sim_instance()
        for si in itertools.chain(sim.si_state, sim.queue):
            while self.stat_type in si.commodity_flags:
                return
        context = interactions.context.InteractionContext(self.tracker.owner.get_sim_instance(), interactions.context.InteractionContext.SOURCE_AUTONOMY, interactions.priority.Priority.High, insert_strategy=QueueInsertStrategy.NEXT, bucket=interactions.context.InteractionBucketType.DEFAULT)
        self.tracker.owner.get_sim_instance().push_super_affordance(self.commodity_distress.distress_interaction, None, context)

    def _exit_distress(self, stat_instance, on_removal=False):
        if self._distress_buff_handle is not None:
            self.tracker.owner.remove_buff(self._distress_buff_handle)
            self._distress_buff_handle = None
        elif self.commodity_distress.buff.buff_type is not None and not on_removal:
            logger.error('Distress Buff Handle is none when exiting Commodity Distress for {}.', self, owner='jjacobson')
        if self._exit_distress_callback_data is not None:
            self.remove_callback(self._exit_distress_callback_data)
            self._exit_distress_callback_data = None
        elif not on_removal:
            logger.error('Exit distress called before exit distress callback has been setup for {}.', self, owner='jjacobson')
        self.tracker.owner.exit_distress(self)

    def _commodity_fail_object(self, stat_instance):
        context = interactions.context.InteractionContext(None, interactions.context.InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.Critical, bucket=interactions.context.InteractionBucketType.DEFAULT)
        owner = self.tracker.owner
        for failure_interaction in self.commodity_failure.failure_interactions:
            if not failure_interaction.immediate or not failure_interaction.simless:
                logger.error('Trying to use a non-immediate and/or non-simless\n                interaction as a commodity failure on an object. Object\n                commodity failures can only push immediate, simless\n                interactions. - trevor')
                break
            aop = interactions.aop.AffordanceObjectPair(failure_interaction, owner, failure_interaction, None)
            while aop.test_and_execute(context):
                break

    def _commodity_fail(self, stat_instance):
        owner = self.tracker.owner
        if not owner.is_sim:
            return self._commodity_fail_object(stat_instance)
        sim = owner.get_sim_instance()
        if sim is None:
            return
        context = interactions.context.InteractionContext(sim, interactions.context.InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.Critical, bucket=interactions.context.InteractionBucketType.DEFAULT)
        for failure_interaction in self.commodity_failure.failure_interactions:
            while sim.push_super_affordance(failure_interaction, None, context):
                break

    def fixup_on_sim_instantiated(self):
        sim = self.tracker.owner
        if self.time_passage_fixup_type() == CommodityTimePassageFixupType.FIXUP_USING_TIME_ELAPSED:
            time_sim_was_saved = sim.time_sim_was_saved
            if time_sim_was_saved is not None:
                if not sim.is_locked(self):
                    self.decay_enabled = True
                    self._last_update = time_sim_was_saved
                    self._update_value()
                    self.decay_enabled = False
        elif self.time_passage_fixup_type() == CommodityTimePassageFixupType.FIXUP_USING_AUTOSATISFY_CURVE and (sim.is_npc or self._off_lot_simulation is None):
            self.set_to_auto_satisfy_value()

    def set_to_auto_satisfy_value(self):
        if self.use_autosatisfy_curve and self._auto_satisfy_curve:
            now = services.time_service().sim_now
            time_sim_was_saved = self.tracker.owner.time_sim_was_saved
            if time_sim_was_saved is None and not self.use_auto_satisfy_curve_as_initial_value or time_sim_was_saved == now:
                return False
            random_time_offset = random.uniform(-1*self.auto_satisfy_curve_random_time_offset, self.auto_satisfy_curve_random_time_offset)
            now += interval_in_sim_minutes(random_time_offset)
            current_hour = now.hour() + now.minute()/date_and_time.MINUTES_PER_HOUR
            auto_satisfy_value = self._auto_satisfy_curve.get(current_hour)
            maximum_auto_satisfy_time = interval_in_sim_minutes(self.maximum_auto_satisfy_time)
            if time_sim_was_saved is None or time_sim_was_saved + maximum_auto_satisfy_time <= now:
                self._last_update = services.time_service().sim_now
                self.set_user_value(auto_satisfy_value)
                return True
            if time_sim_was_saved >= now:
                return False
            interpolation_time = (now - time_sim_was_saved).in_ticks()/maximum_auto_satisfy_time.in_ticks()
            current_value = self.get_user_value()
            new_value = (auto_satisfy_value - current_value)*interpolation_time + current_value
            self._last_update = services.time_service().sim_now
            self.set_user_value(new_value)
            return True
        return False

    def on_remove(self, on_destroy=False):
        super().on_remove(on_destroy=on_destroy)
        self.stop_regular_simulation()
        self.stop_low_level_simulation()
        if self._buff_handle is not None:
            self.tracker.owner.remove_buff(self._buff_handle, on_destroy=on_destroy)
            self._buff_handle = None
        if self._distress_buff_handle is not None:
            self.tracker.owner.remove_buff(self._distress_buff_handle, on_destroy=on_destroy)
            self._distress_buff_handle = None

    def _activate_convergence_callback(self):
        if self._allow_convergence_callback_to_activate:
            if self._convergence_callback_data is not None:
                self.add_callback_data(self._convergence_callback_data)
            self._allow_convergence_callback_to_activate = False

    def set_value(self, value, from_load=False, **kwargs):
        with self._suppress_client_updates_context_manager(from_load=from_load, is_rate_change=False):
            if not from_load:
                change = value - self.get_value()
                self._update_buff(change)
            super().set_value(value, from_load=from_load, **kwargs)
            if not from_load and self.visible:
                self.send_commodity_progress_msg(is_rate_change=False)
            self._update_buff(0)
            self._activate_convergence_callback()

    def _on_statistic_modifier_changed(self, notify_watcher=True):
        super()._on_statistic_modifier_changed(notify_watcher=notify_watcher)
        self.send_commodity_progress_msg()
        self._update_buff(self._get_change_rate_without_decay())
        self._update_callbacks()
        self._activate_convergence_callback()

    def _recalculate_modified_decay_rate(self):
        super()._recalculate_modified_decay_rate()
        if self._decay_rate_modifier > 1:
            self._update_buff(-self._decay_rate_modifier)
        else:
            self._update_buff(0)

    @property
    def buff_handle(self):
        return self._buff_handle

    def _update_buff(self, change_rate):
        if self._buff_handle is not None:
            self.tracker.owner.buff_commodity_changed(self._buff_handle, change_rate=change_rate)

    @property
    def state_index(self):
        return self._current_state_index

    @classmethod
    def get_state_index_matches_buff_type(cls, buff_type):
        if cls.commodity_states:
            for index in range(len(cls.commodity_states)):
                state = cls.commodity_states[index]
                if state.buff is None:
                    pass
                while state.buff.buff_type is buff_type:
                    return index

    @classproperty
    def max_value(cls):
        return cls.max_value_tuning

    @classproperty
    def min_value(cls):
        return cls.min_value_tuning

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

    @classproperty
    def default_value(cls):
        if not cls.initial_as_default:
            return cls._default_convergence_value
        return cls.initial_value

    @classproperty
    def is_skill(cls):
        return False

    @classproperty
    def add_if_not_in_tracker(cls):
        return cls._add_if_not_in_tracker

    @classproperty
    def max_simulate_time_on_load(cls):
        return cls._max_simulate_time_on_load

    def time_passage_fixup_type(self):
        return self._time_passage_fixup_type

    @classmethod
    def get_categories(cls):
        return cls._categories

    def send_commodity_progress_msg(self, is_rate_change=True):
        commodity_msg = self.create_commmodity_update_msg(is_rate_change=is_rate_change)
        if commodity_msg is None:
            return
        send_sim_commodity_progress_update_message(self.tracker.owner, commodity_msg)

    def create_commmodity_update_msg(self, is_rate_change=True):
        if self.tracker is None or not self.tracker.owner.is_sim:
            return
        if not self.visible:
            return
        if not self.commodity_states:
            return
        if self.state_index is None:
            return
        if self._suppress_client_updates:
            return
        commodity_msg = Commodities_pb2.CommodityProgressUpdate()
        commodity_msg.commodity_id = self.guid64
        commodity_msg.current_value = self.get_value()
        commodity_msg.rate_of_change = self.get_change_rate()
        commodity_msg.commodity_state_index = self.state_index
        commodity_msg.is_rate_change = is_rate_change
        return commodity_msg
class SocialMixerInteraction(SocialInteractionMixin, MixerInteraction):
    __qualname__ = 'SocialMixerInteraction'
    REMOVE_INSTANCE_TUNABLES = ('basic_reserve_object', 'basic_focus')
    basic_reserve_object = None
    GENDER_PREF_CONTENT_SCORE_PENALTY = Tunable(
        description=
        '\n        Penalty applied to content score when the social fails the gender preference test.\n        ',
        tunable_type=int,
        default=-1500)
    INSTANCE_TUNABLES = {
        'base_score':
        Tunable(
            description=
            ' \n            Base score when determining the content set value of this mixer\n            based on other mixers of the super affordance. This is the base\n            value used before any modification to content score.\n    \n            Modification to the content score for this affordance can come from\n            topics and moods\n    \n            USAGE: If you would like this mixer to more likely show up no matter\n            the topic and mood ons the sims tune this value higher.\n                                    \n            Formula being used to determine the autonomy score is Score =\n            Avg(Uc, Ucs) * W * SW, Where Uc is the commodity score, Ucs is the\n            content set score, W is the weight tuned the on mixer, and SW is the\n            weight tuned on the super interaction.\n            ',
            tuning_group=GroupNames.AUTONOMY,
            tunable_type=int,
            default=0),
        'social_context_preference':
        TunableMapping(
            description=
            '\n            A mapping of social contexts that will adjust the content score for\n            this mixer interaction. This is used conjunction with base_score.\n            ',
            tuning_group=GroupNames.AUTONOMY,
            key_type=SocialContextBit.TunableReference(),
            value_type=Tunable(tunable_type=float, default=0)),
        'relationship_bit_preference':
        TunableMapping(
            description=
            '\n            A mapping of relationship bits that will adjust the content score for\n            this mixer interaction. This is used conjunction with base_score.\n            ',
            tuning_group=GroupNames.AUTONOMY,
            key_type=RelationshipBit.TunableReference(),
            value_type=Tunable(tunable_type=float, default=0)),
        'trait_preference':
        TunableMapping(
            description=
            '\n            A mapping of traits that will adjust the content score for\n            this mixer interaction. This is used conjunction with base_score.\n            ',
            tuning_group=GroupNames.AUTONOMY,
            key_type=Trait.TunableReference(),
            value_type=Tunable(tunable_type=float, default=0)),
        'buff_preference':
        TunableMapping(
            description=
            '\n            A mapping of buffs that will adjust the content score for\n            this mixer interaction. This is used conjunction with base_score.\n            ',
            tuning_group=GroupNames.AUTONOMY,
            key_type=Buff.TunableReference(),
            value_type=Tunable(tunable_type=float, default=0)),
        'test_gender_preference':
        Tunable(
            description=
            '\n            If this is set, a gender preference test will be run between\n            the actor and target sims. If it fails, the social score will be\n            modified by a large negative penalty tuned with the tunable:\n            GENDER_PREF_CONTENT_SCORE_PENALTY\n            ',
            tuning_group=GroupNames.AUTONOMY,
            tunable_type=bool,
            default=False),
        'outcome':
        TunableOutcome(allow_multi_si_cancel=True)
    }

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

    @classmethod
    def _add_auto_constraint(cls, participant_type, auto_constraint):
        raise RuntimeError(
            '[bhill] This function is believed to be dead code and is scheduled for pruning. If this exception has been raised, the code is not dead and this exception should be removed.'
        )

    @classproperty
    def is_social(cls):
        return True

    @property
    def social_group(self):
        if self.super_interaction is not None:
            return self.super_interaction.social_group

    @staticmethod
    def _tunable_tests_enabled():
        return tunable_tests_enabled

    @classmethod
    def get_base_content_set_score(cls):
        return cls.base_score

    @classmethod
    def _test(cls, target, context, *args, **kwargs):
        if context.sim is target:
            return TestResult(False,
                              'Social Mixer Interactions cannot target self!')
        pick_target = context.pick.target if context.source == context.SOURCE_PIE_MENU else None
        if target is None and context.sim is pick_target:
            return TestResult(False,
                              'Social Mixer Interactions cannot target self!')
        return MixerInteraction._test(target, context, *args, **kwargs)

    @classmethod
    def get_score_modifier(cls, sim, target):
        if cls.test_gender_preference:
            gender_pref_test = GenderPreferenceTest(ParticipantType.Actor,
                                                    ParticipantType.TargetSim,
                                                    ignore_reciprocal=True)
            resolver = DoubleSimResolver(sim.sim_info, target.sim_info)
            result = resolver(gender_pref_test)
            if not result:
                return cls.GENDER_PREF_CONTENT_SCORE_PENALTY
        social_context_preference = 0
        relationship_bit_preference = 0
        trait_preference = 0
        buff_preference = 0
        if target is not None:
            sims = set(
                itertools.chain.from_iterable(
                    group for group in sim.get_groups_for_sim_gen()
                    if target in group))
            if sims:
                social_context = SocialContextTest.get_overall_short_term_context_bit(
                    *sims)
            else:
                relationship_track = sim.relationship_tracker.get_relationship_prevailing_short_term_context_track(
                    target.id)
                if relationship_track is not None:
                    social_context = relationship_track.get_active_bit()
                else:
                    social_context = None
            social_context_preference = cls.social_context_preference.get(
                social_context, 0)
            if cls.relationship_bit_preference:
                relationship_bit_preference = sum(
                    cls.relationship_bit_preference.get(rel_bit, 0)
                    for rel_bit in sim.relationship_tracker.get_all_bits(
                        target_sim_id=target.id))
            if cls.trait_preference:
                trait_preference = sum(
                    cls.trait_preference.get(trait, 0)
                    for trait in sim.trait_tracker.equipped_traits)
            if cls.buff_preference:
                buff_preference = sum(
                    score for (buff, score) in cls.buff_preference.items()
                    if sim.has_buff(buff))
        score_modifier = super().get_score_modifier(
            sim, target
        ) + social_context_preference + relationship_bit_preference + trait_preference + buff_preference
        return score_modifier

    def should_insert_in_queue_on_append(self):
        if super().should_insert_in_queue_on_append():
            return True
        if self.super_affordance is None:
            logger.error(
                '{} being added to queue without a super interaction or super affordance',
                self)
            return False
        ui_group_tag = self.super_affordance.visual_type_override_data.group_tag
        if ui_group_tag == tag.Tag.INVALID:
            return False
        for si in self.sim.si_state:
            while si.visual_type_override_data.group_tag == ui_group_tag:
                return True
        return False

    def get_asm(self, *args, **kwargs):
        return Interaction.get_asm(self, *args, **kwargs)

    def perform_gen(self, timeline):
        if self.social_group is None:
            raise AssertionError(
                'Social mixer interaction {} has no social group. [bhill]'.
                format(self))
        result = yield super().perform_gen(timeline)
        return result

    def build_basic_elements(self, sequence=()):
        sequence = super().build_basic_elements(sequence=sequence)
        if self.super_interaction.social_group is not None:
            listen_animation_factory = self.super_interaction.listen_animation
        else:
            listen_animation_factory = None
            for group in self.sim.get_groups_for_sim_gen():
                si = group.get_si_registered_for_sim(self.sim)
                while si is not None:
                    listen_animation_factory = si.listen_animation
                    break
        if listen_animation_factory is not None:
            for sim in self.required_sims():
                if sim is self.sim:
                    pass
                sequence = listen_animation_factory(sim.animation_interaction,
                                                    sequence=sequence)
                sequence = with_skippable_animation_time((sim, ),
                                                         sequence=sequence)

        def defer_cancel_around_sequence_gen(s, timeline):
            deferred_sis = []
            for sim in self.required_sims():
                while not (sim is self.sim or self.social_group is None):
                    if sim not in self.social_group:
                        pass
                    sis = self.social_group.get_sis_registered_for_sim(sim)
                    while sis:
                        deferred_sis.extend(sis)
            with self.super_interaction.cancel_deferred(deferred_sis):
                result = yield element_utils.run_child(timeline, s)
                return result

        sequence = functools.partial(defer_cancel_around_sequence_gen,
                                     sequence)
        if self.target_type & TargetType.ACTOR:
            return element_utils.build_element(sequence)
        if self.target_type & TargetType.TARGET and self.target is not None:
            sequence = self.social_group.with_target_focus(
                self.sim, self.sim, self.target, sequence)
        elif self.social_group is not None:
            sequence = self.social_group.with_social_focus(
                self.sim, self.sim, self.required_sims(), sequence)
        else:
            for social_group in self.sim.get_groups_for_sim_gen():
                sequence = social_group.with_social_focus(
                    self.sim, self.sim, self.required_sims(), sequence)
        communicable_buffs = collections.defaultdict(list)
        for sim in self.required_sims():
            for buff in sim.Buffs:
                while buff.communicable:
                    communicable_buffs_sim = communicable_buffs[sim]
                    communicable_buffs_sim.append(buff)
        for (sim, communicable_buffs_sim) in communicable_buffs.items():
            for other_sim in self.required_sims():
                if other_sim is sim:
                    pass
                resolver = DoubleSimResolver(sim.sim_info, other_sim.sim_info)
                for buff in communicable_buffs_sim:
                    buff.communicable.apply_to_resolver(resolver)
        return element_utils.build_element(sequence)

    def cancel_parent_si_for_participant(self, participant_type,
                                         finishing_type, cancel_reason_msg,
                                         **kwargs):
        social_group = self.social_group
        if social_group is None:
            return
        participants = self.get_participants(participant_type)
        for sim in participants:
            while sim is not None:
                social_group.remove(sim)
        group_tag = self.super_interaction.visual_type_override_data.group_tag
        if group_tag != Tag.INVALID:
            for si in self.sim.si_state:
                while si is not self.super_interaction and si.visual_type_override_data.group_tag == group_tag:
                    social_group = si.social_group
                    if social_group is not None:
                        while True:
                            for sim in participants:
                                while sim in social_group:
                                    social_group.remove(sim)

    @flexmethod
    def get_participants(cls,
                         inst,
                         participant_type,
                         sim=DEFAULT,
                         **kwargs) -> set:
        inst_or_cls = inst if inst is not None else cls
        result = super(MixerInteraction,
                       inst_or_cls).get_participants(participant_type,
                                                     sim=sim,
                                                     **kwargs)
        result = set(result)
        sim = inst.sim if sim is DEFAULT else sim
        if inst is not None and inst.social_group is None and (
                participant_type & ParticipantType.AllSims
                or participant_type & ParticipantType.Listeners):
            if inst is not None and inst.target_type & TargetType.GROUP:
                while True:
                    for other_sim in itertools.chain(
                            *list(sim.get_groups_for_sim_gen())):
                        if other_sim is sim:
                            pass
                        if other_sim.ignore_group_socials(
                                excluded_group=inst.social_group):
                            pass
                        result.add(other_sim)
        return tuple(result)

    def _trigger_interaction_start_event(self):
        super()._trigger_interaction_start_event()
        target_sim = self.get_participant(ParticipantType.TargetSim)
        if target_sim is not None:
            services.get_event_manager().process_event(
                test_events.TestEvent.InteractionStart,
                sim_info=target_sim.sim_info,
                interaction=self,
                custom_keys=self.get_keys_to_process_events())
            self._register_target_event_auto_update()

    def required_resources(self):
        resources = super().required_resources()
        resources.add(self.social_group)
        return resources
Beispiel #18
0
 def __init__(self, **kwargs):
     super().__init__(value=Tunable(description='\n                                lower bound value of the commodity state\n                                ', tunable_type=int, default=0, export_modes=ExportModes.All), buff=TunableBuffReference(description='\n                         Buff that will get added to sim when commodity is at\n                         this current state.\n                         ', reload_dependent=True), buff_add_threshold=OptionalTunable(TunableThreshold(description='\n                         When enabled, buff will not be added unless threshold\n                         has been met. Value for threshold must be within this\n                         commodity state.\n                         ')), icon=TunableResourceKey(description='\n                         Icon that is displayed for the current state of this\n                         commodity.\n                         ', default='PNG:missing_image', resource_types=sims4.resources.CompoundTypes.IMAGE, export_modes=ExportModes.All), fill_level=TunableEnumEntry(description='\n                         If set, this will determine how to color the motive bar.\n                         ', tunable_type=MotiveFillColorLevel, default=MotiveFillColorLevel.NO_FILL, export_modes=ExportModes.All), data_description=TunableLocalizedString(description='\n                         Localized description of the current commodity state.\n                         ', export_modes=ExportModes.All), fill_color=TunableColor.TunableColorRGBA(description='\n                         Fill color for motive bar\n                         ', export_modes=(ExportModes.ClientBinary,)), background_color=TunableColor.TunableColorRGBA(description='\n                         Background color for motive bar\n                         ', export_modes=(ExportModes.ClientBinary,)), tooltip_icon_list=TunableList(description='\n                         A list of icons to show in the tooltip of this\n                         commodity state.\n                         ', tunable=TunableResourceKey(description='\n                             Icon that is displayed what types of objects help\n                             solve this motive.\n                             ', default='PNG:missing_image', resource_types=sims4.resources.CompoundTypes.IMAGE), export_modes=(ExportModes.ClientBinary,)), loot_list_on_enter=TunableList(description='\n                          List of loots that will be applied when commodity\n                          value enters this state if owner of the commodity is a sim.\n                          ', tunable=TunableReference(services.get_instance_manager(sims4.resources.Types.ACTION))), **kwargs)
class FestivalRunningTest(HasTunableSingletonFactory, AutoFactoryInit, event_testing.test_base.BaseTest):
    FACTORY_TUNABLES = {'drama_node': OptionalTunable(description='\n            If enabled then we will check a specific type of festival drama\n            node otherwise we will look at all of the festival drama nodes.\n            ', tunable=TunableReference(description='\n                Reference to the festival drama node that we want to be running.\n                ', manager=services.get_instance_manager(sims4.resources.Types.DRAMA_NODE), class_restrictions=('FestivalDramaNode',)), enabled_by_default=True), 'check_if_on_festival_street': OptionalTunable(description="\n            If enabled, test against if the player is on the festival's street.\n            ", tunable=Tunable(description="\n                If checked, this test will pass only if the player is on the\n                festival's street. If unchecked, the test will pass only if the\n                player is not on the festival street.\n                ", tunable_type=bool, default=True)), 'valid_time_blocks': TunableTuple(description='\n            Festival drama nodes have a tunable pre-festival duration that\n            delay festival start to some point after the drama node has\n            started. For example, if the festival drama node has a pre-festival\n            duration of 2 hours and the drama node runs at 8am, the festival\n            will not start until 10am.\n\n            By default, this test passes if the festival drama node is running,\n            regardless if the festival is in its pre-festival duration. This\n            tuning changes that behavior.\n            ', pre_festival=Tunable(description='\n                If the festival is currently in its pre-festival duration,\n                test can pass if this is checked and fails if unchecked.\n                ', tunable_type=bool, default=True), running=Tunable(description='\n                If the festival is running (it is past its pre-festival\n                duration), test can pass if this is checked and fails if\n                unchecked.\n                ', tunable_type=bool, default=True)), 'negate': Tunable(description='\n            If enabled this test will pass if no festivals of the tuned\n            requirements are running.\n            ', tunable_type=bool, default=False)}
    test_events = (TestEvent.FestivalStarted,)

    def get_expected_args(self):
        return {}

    @cached_test
    def __call__(self):
        drama_scheduler = services.drama_scheduler_service()
        for node in drama_scheduler.active_nodes_gen():
            if self.drama_node is None:
                if node.drama_node_type != DramaNodeType.FESTIVAL:
                    continue
            elif type(node) is not self.drama_node:
                continue
            if self.check_if_on_festival_street is not None and self.check_if_on_festival_street != node.is_on_festival_street():
                continue
            if node.is_during_pre_festival():
                if not self.valid_time_blocks.pre_festival:
                    continue
            elif not self.valid_time_blocks.running:
                continue
            if self.negate:
                return TestResult(False, 'Drama nodes match the required conditions.')
            return TestResult.TRUE
        if self.negate:
            return TestResult.TRUE
        return TestResult(False, 'No drama nodes match the required conditions.')
class ObjectRewardsOperation(BaseLootOperation):
    __qualname__ = 'ObjectRewardsOperation'
    FACTORY_TUNABLES = {
        'object_rewards':
        TunableVariant(
            description=
            '\n            Object rewards when running the loot.  Rewards objects will be created\n            and sent to the tuned inventory.\n            Spawnerdata reference will load the reward data from the interaction \n            spawner tuning inside the spawner component of the participant selected\n            Rewardsdata tuning will allow you to tune the object rewards directly  \n            ',
            spawnerdata_reference=SpawnerInteractionTuning.TunableFactory(),
            rewardsdata_tuning=ObjectRewardsTuning.TunableFactory()),
        'notification':
        OptionalTunable(
            description=
            '\n            If enabled, a notification will be displayed when this object reward\n            is granted to a Sim.\n            ',
            tunable=TunableUiDialogNotificationSnippet(
                description=
                '\n                The notification to display when this object reward is granted\n                to the Sim. There is one additional token provided: a string\n                representing a bulleted list of all individual rewards granted.\n                '
            )),
        'force_family_inventory':
        Tunable(
            description=
            '\n            If Enabled, the rewards object(s) will be put in the family \n            inventory no matter what.  If not enabled, the object will try to\n            be added to the sim inventory, if that is not possible it will be\n            added to the family inventory as an automatic fallback.',
            tunable_type=bool,
            default=False),
        'transfer_stored_sim_info_to_reward':
        OptionalTunable(
            description=
            "\n            If enabled, the tuned participant will transfer its stored sim info\n            into the rewards created. This is mostly used for the cow plant\n            life essence, which will store the sim info of the sim from which\n            the life essence was drained.\n            \n            Ex: For cow plant's milk life essence, we want to transfer the dead\n            sim's sim info from the cow plant to the created essence drink.\n            ",
            tunable=TunableEnumEntry(
                description=
                '\n                The participant of this interaction which has a \n                StoredSimInfoComponent. The stored sim info will be transferred\n                to the created rewards and will then be removed from the source.\n                ',
                tunable_type=ParticipantType,
                default=ParticipantType.Object))
    }

    def __init__(self,
                 object_rewards,
                 notification,
                 force_family_inventory,
                 transfer_stored_sim_info_to_reward,
                 subject=None,
                 **kwargs):
        super().__init__(**kwargs)
        self._object_rewards = object_rewards
        self._notification = notification
        self._force_family_inventory = force_family_inventory
        self._transfer_stored_sim_info_to_reward = transfer_stored_sim_info_to_reward

    def _create_object_rewards(self, obj_weight_pair, obj_counter, resolver):
        obj_result = weighted_random_item(obj_weight_pair)
        for obj_reward in obj_result:
            created_obj = create_object(
                obj_reward,
                init=None,
                post_add=lambda *args: self._place_object(resolver=resolver,
                                                          *args))
            while created_obj is not None:
                obj_counter[obj_reward] += 1

    def _apply_to_subject_and_target(self, subject, target, resolver):
        if subject.is_npc:
            return
        obj_counter = Counter()
        if self._object_rewards.spawn_type == ObjectRewardsTuning.SPAWNER_REWARD:
            participant = resolver.get_participant(
                self._object_rewards.spawner_participant)
            if participant is not None:
                weighted_data = participant.interaction_spawner_data()
                if weighted_data is not None:
                    self._create_object_rewards(weighted_data, obj_counter,
                                                resolver)
        elif self._object_rewards.spawn_type == ObjectRewardsTuning.TUNABLE_REWARD:
            for _ in range(self._object_rewards.quantity):
                weight_pairs = [(data.weight, data.reward)
                                for data in self._object_rewards.reward_objects
                                ]
                self._create_object_rewards(weight_pairs, obj_counter,
                                            resolver)
        if obj_counter and self._notification is not None:
            obj_names = [
                LocalizationHelperTuning.get_object_count(count, obj)
                for (obj, count) in obj_counter.items()
            ]
            dialog = self._notification(subject, resolver=resolver)
            dialog.show_dialog(
                additional_tokens=(LocalizationHelperTuning.get_bulleted_list(
                    None, *obj_names), ))
        return True

    def _place_object(self, created_object, resolver=None):
        actor = resolver.get_participant(
            ParticipantType.Actor).get_sim_instance(
                allow_hidden_flags=ALL_HIDDEN_REASONS)
        created_object.update_ownership(actor, make_sim_owner=False)
        if self._transfer_stored_sim_info_to_reward is not None:
            stored_sim_source = resolver.get_participant(
                self._transfer_stored_sim_info_to_reward)
            sim_id = stored_sim_source.get_stored_sim_id()
            if sim_id is not None:
                created_object.add_dynamic_component(
                    types.STORED_SIM_INFO_COMPONENT.instance_attr,
                    sim_id=sim_id)
                stored_sim_source.remove_component(
                    types.STORED_SIM_INFO_COMPONENT.instance_attr)
                created_object.update_object_tooltip()
        if self._force_family_inventory or actor.inventory_component.can_add(
                created_object):
            if actor.inventory_component.player_try_add_object(created_object):
                return
        build_buy.move_object_to_household_inventory(created_object)
class TimeUntilFestivalTest(HasTunableSingletonFactory, AutoFactoryInit, event_testing.test_base.BaseTest):
    FACTORY_TUNABLES = {'drama_node': OptionalTunable(description='\n            If enabled then we will check a specific type of festival drama\n            node otherwise we will look at any of the festival drama nodes.\n            ', tunable=TunableReference(description='\n                Reference to the festival drama node that we want to test.\n                ', manager=services.get_instance_manager(sims4.resources.Types.DRAMA_NODE), class_restrictions=('FestivalDramaNode',)), enabled_by_default=True), 'max_time': Tunable(description='\n            Maximum time in hours between when the test occurs to the start of\n            the festival in order for the test to return true.\n            ', tunable_type=float, default=18.0), 'negate': Tunable(description='\n            If enabled this test will pass if the requested festival will not\n            start within the specified time.\n            ', tunable_type=bool, default=False)}

    def get_expected_args(self):
        return {}

    @cached_test
    def __call__(self):
        drama_scheduler = services.drama_scheduler_service()
        best_time = None
        for node in drama_scheduler.scheduled_nodes_gen():
            if node.drama_node_type != DramaNodeType.FESTIVAL:
                continue
            if not self.drama_node is None:
                if self.drama_node is type(node):
                    new_time = node.get_time_remaining()
                    if not best_time is None:
                        if new_time < best_time:
                            best_time = new_time
                    best_time = new_time
            new_time = node.get_time_remaining()
            if not best_time is None:
                if new_time < best_time:
                    best_time = new_time
            best_time = new_time
        if best_time is None:
            if not self.negate:
                return TestResult(False, 'No scheduled Festivals of type {}.', self.drama_node, tooltip=self.tooltip)
        elif best_time.in_hours() < self.max_time:
            if self.negate:
                return TestResult(False, 'Next scheduled Festival is within specified time', tooltip=self.tooltip)
        elif not self.negate:
            return TestResult(False, "Next scheduled Festival isn't within specified time", tooltip=self.tooltip)
        return TestResult.TRUE
Beispiel #22
0
class LotteryDramaNode(BaseDramaNode):
    INSTANCE_TUNABLES = {
        'payout':
        Tunable(
            description=
            "\n            The payout of the lottery to the winning Sim's household.\n            ",
            tunable_type=int,
            default=1000000),
        'lottery_event':
        TunableEnumEntry(
            description=
            '\n            The event that triggers the active household being added to the\n            lottery.\n            ',
            tunable_type=TestEvent,
            default=TestEvent.Invalid,
            invalid_enums=(TestEvent.Invalid, )),
        'minimum_sims':
        TunableRange(
            description=
            '\n            The minimum number of sims that we want to trigger a lottery\n            for.  If not enough households have signed up for the lottery we\n            will select random non-played sims to fill up the lottery\n            pool.\n            ',
            tunable_type=int,
            default=100,
            minimum=1),
        'end_time':
        TunableTimeOfWeek(
            description=
            '\n            The time that this Drama Node is going to end.\n            '
        ),
        'winning_sim_loot':
        LootActions.TunableReference(
            description=
            '\n            Loot action applied to the Winning Sim if they are in the active\n            household when the lottery completes.\n            '
        ),
        'losing_sim_loot':
        LootActions.TunableReference(
            description=
            '\n            Loot action applied to losing Sims if they are in the active\n            household when the lottery completes.\n            '
        ),
        'notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            The notification that we will display to explain the winner of the\n            lottery.\n            '
        )
    }

    @classproperty
    def simless(cls):
        return True

    @classproperty
    def persist_when_active(cls):
        return True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._end_alarm_handle = None
        self._lottery_sims = set()

    def cleanup(self, from_service_stop=False):
        super().cleanup(from_service_stop=from_service_stop)
        if self._end_alarm_handle is not None:
            alarms.cancel_alarm(self._end_alarm_handle)
        self._lottery_sims.clear()
        services.get_event_manager().unregister_single_event(
            self, self.lottery_event)

    def handle_event(self, sim_info, event, resolver):
        if sim_info is None:
            return
        self._lottery_sims.add(sim_info.sim_id)

    def _check_lottery_sim_criteria(self, sim_info):
        if sim_info.lod == SimInfoLODLevel.MINIMUM:
            return False
        if sim_info.is_teen_or_younger:
            return False
        if sim_info.household.hidden:
            return False
        elif sim_info.household.is_player_household:
            return False
        return True

    def _end_lottery(self, _):
        try:
            if not self._lottery_sims:
                return
            lottery_candidates = []
            active_household_candidates = []
            sim_info_manager = services.sim_info_manager()
            for sim_id in self._lottery_sims:
                sim_info = sim_info_manager.get(sim_id)
                if sim_info is None:
                    continue
                lottery_candidates.append(sim_info)
                if sim_info.is_selectable:
                    active_household_candidates.append(sim_info)
            if len(lottery_candidates) < self.minimum_sims:
                sims_to_get = self.minimum_sims - len(lottery_candidates)
                additional_candidates = [
                    sim_info for sim_info in sim_info_manager.values()
                    if self._check_lottery_sim_criteria(sim_info)
                ]
                if len(additional_candidates) < sims_to_get:
                    lottery_candidates.extend(additional_candidates)
                else:
                    lottery_candidates.extend(
                        random.sample(additional_candidates, sims_to_get))
            winning_sim_info = random.choice(lottery_candidates)
            winning_sim_info.household.funds.add(
                self.payout, Consts_pb2.FUNDS_HOLIDAY_LOTTERY)
            notification = self.notification(services.active_sim_info())
            notification.show_dialog(additional_tokens=(winning_sim_info, ))
            if winning_sim_info.is_selectable:
                resolver = SingleSimResolver(winning_sim_info)
                self.winning_sim_loot.apply_to_resolver(resolver)
            else:
                for sim_info in active_household_candidates:
                    resolver = SingleSimResolver(sim_info)
                    self.losing_sim_loot.apply_to_resolver(resolver)
        finally:
            services.drama_scheduler_service().complete_node(self.uid)

    def _setup_lottery(self):
        time = create_date_and_time(days=self.end_time.day,
                                    hours=self.end_time.hour,
                                    minutes=self.end_time.minute)
        time_until_end = services.time_service().sim_now.time_to_week_time(
            time)
        self._end_alarm_handle = alarms.add_alarm(self,
                                                  time_until_end,
                                                  self._end_lottery,
                                                  cross_zone=True)
        services.get_event_manager().register_single_event(
            self, self.lottery_event)

    def _run(self):
        self._setup_lottery()
        return DramaNodeRunOutcome.SUCCESS_NODE_INCOMPLETE

    def resume(self):
        self._setup_lottery()

    def _save_custom_data(self, writer):
        writer.write_uint64s(LOTTERY_CANIDATES_TOKEN, self._lottery_sims)

    def _load_custom_data(self, reader):
        self._lottery_sims = set(
            reader.read_uint64s(LOTTERY_CANIDATES_TOKEN, ()))
        return True
Beispiel #23
0
class AggregateMixerInteraction(MixerInteraction):
    INSTANCE_TUNABLES = {
        'aggregated_affordances':
        TunableList(
            description=
            '\n                A list of affordances composing this aggregate. A random one\n                will be chosen from sub-action weights if multiple interactions\n                pass at the same priority.\n                ',
            tunable=TunableTuple(
                description=
                '\n                    An affordance and priority entry.\n                    ',
                priority=Tunable(
                    description=
                    '\n                        The relative priority of this affordance compared to\n                        other affordances in this aggregate.\n                        ',
                    tunable_type=int,
                    default=0),
                affordance=MixerInteraction.TunableReference(
                    description=
                    '\n                        The aggregated affordance.\n                        ',
                    pack_safe=True)),
            tuning_group=GroupNames.GENERAL)
    }
    _allow_user_directed = True

    @classmethod
    def _aops_sorted_gen(cls,
                         target,
                         context,
                         super_interaction=DEFAULT,
                         **interaction_parameters):
        affordances = []
        source_interaction = context.sim.posture.source_interaction if super_interaction == DEFAULT else super_interaction
        for aggregated_affordance in cls.aggregated_affordances:
            aop = AffordanceObjectPair(aggregated_affordance.affordance,
                                       target, source_interaction.affordance,
                                       source_interaction,
                                       **interaction_parameters)
            affordances.append((aggregated_affordance.priority, aop))
        return sorted(affordances, key=operator.itemgetter(0), reverse=True)

    @classmethod
    def _test(cls, target, context, **interaction_parameters):
        result = super()._test(target, context, **interaction_parameters)
        if not result:
            return result
        cls._allow_user_directed = False
        context = context.clone_for_sim(sim=context.sim)
        for (_, aop) in cls._aops_sorted_gen(target, context,
                                             **interaction_parameters):
            result = aop.test(context)
            if result:
                if aop.affordance.allow_user_directed:
                    cls._allow_user_directed = True
                return result
        return TestResult(False, 'No sub-affordances passed their tests.')

    @classmethod
    def consumes_object(cls):
        for aggregated_affordance in cls.aggregated_affordances:
            if aggregated_affordance.affordance.consumes_object():
                return True
        return False

    @classproperty
    def allow_user_directed(cls):
        return cls._allow_user_directed

    def _do_perform_gen(self, timeline):
        context = self.context.clone_for_continuation(self)
        max_priority = None
        aops_valid = []
        invalid_aops_with_result = []
        for (priority, aop) in self._aops_sorted_gen(
                self.target,
                context,
                super_interaction=self.super_interaction,
                **self.interaction_parameters):
            if max_priority is not None:
                if priority < max_priority:
                    break
            test_result = aop.test(context)
            if test_result:
                aops_valid.append(aop)
                max_priority = priority
            else:
                invalid_aops_with_result.append((aop, test_result))
        if not aops_valid:
            logger.error(
                'Failed to find valid mixer affordance in AggregateMixerInteraction: {}, did we not run its test immediately before executing it?\n{}',
                self,
                invalid_aops_with_result,
                owner='rmccord')
            return ExecuteResult.NONE
            yield
        interactions_by_weight = []
        for aop in aops_valid:
            interaction_result = aop.interaction_factory(context)
            if not interaction_result:
                raise RuntimeError(
                    'Failed to generate interaction from aop {}. {} [rmccord]'.
                    format(aop, interaction_result))
            interaction = interaction_result.interaction
            if len(aops_valid) == 1:
                weight = 0
            else:
                weight = interaction.affordance.calculate_autonomy_weight(
                    context.sim)
            interactions_by_weight.append((weight, interaction))
        if not interactions_by_weight:
            return ExecuteResult.NONE
            yield
        (_, interaction) = max(interactions_by_weight,
                               key=operator.itemgetter(0))
        return AffordanceObjectPair.execute_interaction(interaction)
        yield

(
    TunableWeatherForecastListReference, TunableWeatherForecastListSnippet
) = define_snippet(
    'weather_forcast_list',
    TunableList(tunable=TunableTuple(
        description=
        '\n            A tuple of forecast and weight.\n            ',
        forecast=WeatherForecast.TunableReference(
            description=
            '\n                The weather forecast.\n                ',
            pack_safe=True),
        weight=Tunable(
            description=
            '\n                Weight of this forecast being selected.\n                ',
            tunable_type=int,
            default=1))))
(
    TunableWeatherSeasonalForecastsReference,
    TunableWeatherSeasonalForecastsSnippet
) = define_snippet(
    'weather_seasonal_forecasts',
    TunableMapping(
        key_type=TunableEnumEntry(
            description='\n            The part of the season.\n            ',
            tunable_type=SeasonSegment,
            default=SeasonSegment.MID),
        value_type=TunableWeatherForecastListReference(
            description=
            '\n            Potential forecasts for this part of the season.\n            ',
class RingDoorbellSuperInteraction(SuperInteraction):
    INSTANCE_TUNABLES = {
        '_nobody_home_failure_notification':
        UiDialogNotification.TunableFactory(
            description=
            '\n                Notification that displays if no one was home when they tried\n                to ring the doorbell.\n                '
        ),
        '_bad_relationship_failure_notification':
        UiDialogNotification.TunableFactory(
            description=
            "\n                Notification that displays if there wasn't high enough\n                relationship with any of the household members when they\n                tried to ring the doorbell.\n                "
        ),
        '_success_notification':
        UiDialogNotification.TunableFactory(
            description=
            '\n                Notification that displays if the user succeeded in becoming\n                greeted when they rang the doorbell.\n                '
        ),
        '_relationship_test':
        TunableRelationshipTest(
            description=
            '\n                The Relationship test ran between the sim running the\n                interaction and all of the npc family members to see if they\n                are allowed in.\n                ',
            locked_args={
                'subject': ParticipantType.Actor,
                'target_sim': ParticipantType.TargetSim
            }),
        '_always_allow_greeting':
        Tunable(
            description=
            '\n                If set to true, we will always allow the sim to be greeted.\n                ',
            tunable_type=bool,
            default=False)
    }

    def _get_owner_sim_infos(self):
        owner_household_or_travel_group = services.household_manager().get(
            services.current_zone().lot.owner_household_id)
        if owner_household_or_travel_group is None:
            owner_household_or_travel_group = services.travel_group_manager(
            ).get_travel_group_by_zone_id(services.current_zone_id())
        if owner_household_or_travel_group is None:
            return
        owner_sim_infos = tuple(owner_household_or_travel_group)
        return owner_sim_infos

    def _make_greeted(self):
        resolver = self.get_resolver()
        dialog = self._success_notification(self.sim, resolver)
        dialog.show_dialog()
        services.get_zone_situation_manager().make_waiting_player_greeted(
            self.sim)
        self._try_make_always_welcomed(self.sim)

    def _try_make_always_welcomed(self, sim):
        if any(
                sim.sim_info.has_trait(trait)
                for trait in VisitingTuning.ALWAYS_WELCOME_TRAITS):
            current_household = services.owning_household_of_active_lot()
            if current_household is None:
                logger.error(
                    'Current household is None when trying to run the ring doorbell interaction for visiting sim {}',
                    sim)
                return
            current_household.add_always_welcome_sim(sim.sim_info.id)

    def _show_nobody_home_dialog(self):
        resolver = self.get_resolver()
        dialog = self._nobody_home_failure_notification(self.sim, resolver)
        dialog.show_dialog()

    def _try_to_be_invited_in(self):
        if self._always_allow_greeting:
            self._make_greeted()
            return
        owner_sim_infos = self._get_owner_sim_infos()
        if owner_sim_infos is None:
            self._show_nobody_home_dialog()
            return
        occupants = tuple(sim_info for sim_info in owner_sim_infos
                          if sim_info.is_at_home)
        num_occupants = len(occupants)
        for sim_info in occupants:
            if sim_info.is_pet:
                num_occupants -= 1
            else:
                sim = sim_info.get_sim_instance(
                    allow_hidden_flags=ALL_HIDDEN_REASONS)
                if not sim is None:
                    if sim.has_running_and_queued_interactions_with_liability(
                            interactions.rabbit_hole.RABBIT_HOLE_LIABILTIY):
                        num_occupants -= 1
                num_occupants -= 1
        if num_occupants == 0:
            self._show_nobody_home_dialog()
            return
        for occupant in occupants:
            relationship_resolver = DoubleSimResolver(self.sim.sim_info,
                                                      occupant)
            if relationship_resolver(self._relationship_test):
                self._make_greeted()
                return
        dialog = self._bad_relationship_failure_notification(
            self.sim, resolver)
        dialog.show_dialog()

    def _post_perform(self):
        super()._post_perform()
        self.add_exit_function(self._try_to_be_invited_in)
class WeatherForecast(HasTunableReference,
                      metaclass=HashedTunedInstanceMetaclass,
                      manager=services.get_instance_manager(
                          Types.WEATHER_FORECAST)):
    INSTANCE_TUNABLES = {
        'calendar_icon':
        TunableIcon(
            description=
            '\n            The small icon for this forecast.\n            ',
            export_modes=ExportModes.All),
        'calendar_icon_large':
        TunableIcon(
            description=
            '\n            The large icon for this forecast.\n            ',
            export_modes=ExportModes.All),
        'calendar_icon_mascot':
        TunableIcon(
            description=
            '\n            Optional icon to use as the forecast mascot in the calendar.\n            ',
            allow_none=True,
            export_modes=ExportModes.All),
        'forecast_description':
        TunableLocalizedString(
            description=
            '\n            The description for this forecast.\n            ',
            export_modes=ExportModes.All),
        'forecast_name':
        TunableLocalizedString(
            description=
            '\n            The name for this forecast.\n            ',
            export_modes=ExportModes.All),
        'prescribed_weather_type':
        OptionalTunable(
            description=
            '\n            The types of prescribed weather this forecast counts as\n            ',
            tunable=TunableTuple(
                rain=Tunable(
                    description=
                    '\n                    If checked this forecast will be unavailable if rain is disabled\n                    ',
                    tunable_type=bool,
                    default=False),
                storm=Tunable(
                    description=
                    '\n                    If checked this forecast will be unavailable if storm is disabled\n                    ',
                    tunable_type=bool,
                    default=False),
                snow=Tunable(
                    description=
                    '\n                    If checked this forecast will be unavailable if snow is disabled\n                    ',
                    tunable_type=bool,
                    default=False),
                blizzard=Tunable(
                    description=
                    '\n                    If checked this forecast will be unavailable if blizzard is disabled\n                    ',
                    tunable_type=bool,
                    default=False))),
        'weather_event_time_blocks':
        TunableMapping(
            description=
            '\n            The weather events that make up this forecast.  Key is hour of day\n            that event would start, value is a list of potential events\n            ',
            key_type=Tunable(tunable_type=int, default=0),
            value_type=TunableList(
                description=
                '\n                List of the weather events that can occur in this time block\n                ',
                tunable=TunableTuple(
                    description=
                    '\n                    A tuple of information for the weather event.\n                    ',
                    weather_event=WeatherEvent.TunableReference(
                        description=
                        '\n                        The weather event.\n                        ',
                        pack_safe=True),
                    duration=TunableInterval(
                        description=
                        '\n                        Minimum and maximum time, in sim hours, this event can last.\n                        ',
                        tunable_type=float,
                        default_lower=1,
                        default_upper=4),
                    weight=Tunable(
                        description=
                        '\n                        Weight of this event being selected.\n                        ',
                        tunable_type=int,
                        default=1)))),
        'weather_ui_override':
        TunableMapping(
            description=
            '\n            If set, this overrides the weather type that is shown for the\n            specified group.\n            ',
            key_type=TunableEnumEntry(
                tunable_type=WeatherTypeGroup,
                default=WeatherTypeGroup.UNGROUPED,
                invalid_enums=(WeatherTypeGroup.UNGROUPED, )),
            value_type=TunableEnumEntry(
                tunable_type=WeatherType,
                default=WeatherType.UNDEFINED,
                invalid_enums=(WeatherType.UNDEFINED, )),
            tuning_group=GroupNames.SPECIAL_CASES)
    }

    @classmethod
    def get_weather_event(cls):
        weather_schedule = []
        for (beginning_hour,
             event_list) in cls.weather_event_time_blocks.items():
            weather_schedule.append((beginning_hour, event_list))
        weather_schedule.sort(key=operator.itemgetter(0))
        time_of_day = services.time_service().sim_now
        hour_of_day = time_of_day.hour()
        entry = weather_schedule[-1]
        weather_events = entry[1]
        for entry in weather_schedule:
            if entry[0] <= hour_of_day:
                weather_events = entry[1]
            else:
                break
        weighted_events = [(weather_event.weight, weather_event)
                           for weather_event in weather_events]
        chosen_weather_event = random.weighted_random_item(weighted_events)
        return (chosen_weather_event.weather_event,
                chosen_weather_event.duration.random_float())

    @classmethod
    def is_snowy(cls):
        if cls.prescribed_weather_type is None:
            return False
        return cls.prescribed_weather_type.snow or cls.prescribed_weather_type.blizzard

    @classmethod
    def is_rainy(cls):
        if cls.prescribed_weather_type is None:
            return False
        return cls.prescribed_weather_type.rain or cls.prescribed_weather_type.storm

    @classmethod
    def is_forecast_supported(cls, options, snow_safe, rain_safe):
        prescribed_weather_type = cls.prescribed_weather_type
        if prescribed_weather_type is None:
            return True
        if prescribed_weather_type.rain and (
                not rain_safe or options[PrecipitationType.RAIN]
                == WeatherOption.WEATHER_DISABLED):
            return False
        if prescribed_weather_type.snow and (
                not snow_safe or options[PrecipitationType.SNOW]
                == WeatherOption.WEATHER_DISABLED):
            return False
        if prescribed_weather_type.storm and (not rain_safe or
                                              options[PrecipitationType.RAIN]
                                              == WeatherOption.DISABLE_STORMS):
            return False
        elif prescribed_weather_type.blizzard and (
                not snow_safe or options[PrecipitationType.SNOW]
                == WeatherOption.DISABLE_STORMS):
            return False
        return True
Beispiel #27
0
class InventoryStorage:
    UI_SORT_TYPES = TunableList(
        description=
        "\n        A list of gameplay-based sort types used in the sim's inventory in the UI.\n        ",
        tunable=TunableTuple(
            description=
            '\n            Data that defines this sort for the inventory UI.\n            ',
            sort_name=TunableLocalizedString(
                description=
                '\n                The name displayed in the UI for this sort type.\n                '
            ),
            object_data=TunableVariant(
                description=
                '\n                The object data that determines the sort order of\n                this sort type.\n                ',
                states=TunableList(
                    description=
                    '\n                    States whose values are used to sort on for this sort type. \n                    ',
                    tunable=TunableReference(
                        description=
                        '\n                        A State to sort on.\n                        ',
                        manager=services.get_instance_manager(
                            sims4.resources.Types.OBJECT_STATE),
                        class_restrictions='ObjectState')),
                default='states'),
            is_ascending=Tunable(
                description=
                '\n                Whether a higher value from object_data will sort first.\n                If a high value means that the object should sort lower \n                (E.G. brokenness), this should be false.\n                ',
                tunable_type=bool,
                default=True),
            debug_name=Tunable(
                description=
                '\n                A unique name used to select this inventory sort type through \n                the console command ui.inventory.set_sort_filter when the inventory\n                ui is open.\n                ',
                tunable_type=str,
                default='NONE'),
            export_class_name='InventoryUISortTypeTuple',
            export_modes=ExportModes.ClientBinary))
    UI_FILTER_TYPES = TunableList(
        description=
        "\n        A list of filter categories containing filter types used to filter the sim's\n        inventory in the UI. The inventory can also be sorted by filter type; \n        filters lower on this list will sort lower when sorted by filter type.\n        ",
        tunable=TunableTuple(
            description=
            '\n            A category of filters in the UI. Contains a name and a list of filters.\n            ',
            filters=TunableList(
                description=
                '\n                The filters used in this category. \n                ',
                tunable=TunableTuple(
                    description=
                    '\n                    Data that defines a filter type in the inventory UI.\n                    ',
                    tags=TunableTags(
                        description=
                        '\n                        Tags that should be considered part of this filter.\n                        ',
                        binary_type=EnumBinaryExportType.EnumUint32),
                    filter_name=TunableLocalizedString(
                        description=
                        '\n                        The name displayed in the UI for this filter type.            \n                        '
                    ),
                    debug_name=Tunable(
                        description=
                        '\n                        A unique name used to select this inventory filter type through \n                        the console command ui.inventory.set_sort_filter when the inventory\n                        ui is open.\n                        ',
                        tunable_type=str,
                        default='NONE'),
                    export_class_name='InventoryUIFilterTypeTuple')),
            category_name=TunableLocalizedString(
                description=
                '\n                The name displayed in the UI for this filter category.\n                '
            ),
            export_class_name='InventoryUIFilterCategoryTuple',
            export_modes=ExportModes.ClientBinary))

    def __init__(self,
                 inventory_type,
                 item_location,
                 max_size=None,
                 allow_compaction=True,
                 allow_ui=True,
                 hidden_storage=False):
        self._objects = {}
        self._owners = WeakSet()
        self._inventory_type = inventory_type
        self._item_location = item_location
        self._max_size = max_size
        self._allow_compaction = allow_compaction
        self._allow_ui = allow_ui
        self._hidden_storage = hidden_storage
        self._stacks_with_options_counter = None

    def __len__(self):
        return len(self._objects)

    def __iter__(self):
        yield from iter(self._objects.values())

    def __contains__(self, obj_id):
        return obj_id in self._objects

    def __getitem__(self, obj_id):
        if obj_id in self._objects:
            return self._objects[obj_id]

    def __repr__(self):
        return 'InventoryStorage<{},{}>'.format(self._inventory_type,
                                                self._get_inventory_id())

    def register(self, owner):
        self._owners.add(owner)

    def unregister(self, owner):
        self._owners.discard(owner)

    def has_owners(self):
        if self._owners:
            return True
        return False

    def get_owners(self):
        return tuple(self._owners)

    @property
    def allow_ui(self):
        return self._allow_ui

    @allow_ui.setter
    def allow_ui(self, value):
        self._allow_ui = value

    def discard_object_id(self, obj_id):
        if obj_id in self._objects:
            del self._objects[obj_id]

    def discard_all_objects(self):
        for obj in self._objects.values():
            self._distribute_inventory_update_message(
                UI_pb2.InventoryItemUpdate.TYPE_REMOVE, obj)
            obj.inventoryitem_component.set_inventory_type(None, None)
        self._objects.clear()

    def can_insert(self, obj):
        if not obj.can_go_in_inventory_type(self._inventory_type):
            return False
        elif self._max_size is not None and sum(
                inventory_obj.stack_count()
                for inventory_obj in self) >= self._max_size:
            return False
        return True

    def insert(self, obj, inventory_object=None, compact=True):
        if not self.can_insert(obj):
            return False
        try:
            obj.on_before_added_to_inventory()
        except:
            logger.exception(
                'Exception invoking on_before_added_to_inventory. obj: {}',
                obj)
        self._insert(obj, inventory_object)
        try:
            obj.on_added_to_inventory()
        except:
            logger.exception(
                'Exception invoking on_added_to_inventory. obj: {}', obj)
        compacted_obj_id = None
        compacted_count = None
        if compact:
            (compacted_obj_id, compacted_count) = self._try_compact(obj)
        if compacted_obj_id is None:
            for owner in self._owners:
                try:
                    owner.on_object_inserted(obj)
                except:
                    logger.exception(
                        'Exception invoking on_object_inserted. obj: {}, owner: {}',
                        obj, owner)
            self._distribute_inventory_update_message(
                UI_pb2.InventoryItemUpdate.TYPE_ADD, obj)
            sent_stack_update = False
            if obj.inventoryitem_component.has_stack_option:
                if self._stacks_with_options_counter is None:
                    self._stacks_with_options_counter = defaultdict(int)
                stack_id = obj.inventoryitem_component.get_stack_id()
                stack_objects = self._stacks_with_options_counter[stack_id]
                if stack_objects == 0:
                    self._distribute_inventory_update_message(
                        UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj)
                    sent_stack_update = True
                self._stacks_with_options_counter[stack_id] += 1
            if not sent_stack_update:
                obj_owner = obj.inventoryitem_component.get_inventory().owner
                if obj_owner.is_sim and obj_owner.sim_info.favorites_tracker is not None and obj_owner.sim_info.favorites_tracker.is_favorite_stack(
                        obj):
                    self._distribute_inventory_update_message(
                        UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj)
        else:
            for owner in self._owners:
                try:
                    owner.on_object_id_changed(obj, compacted_obj_id,
                                               compacted_count)
                except:
                    logger.exception(
                        'Exception invoking on_object_id_changed. obj: {}, owner: {}',
                        obj, owner)
            self._distribute_inventory_update_message(
                UI_pb2.InventoryItemUpdate.TYPE_UPDATE,
                obj,
                obj_id=compacted_obj_id)
        return True

    def update_object_stack_by_id(self, obj_id, new_stack_id):
        if obj_id not in self._objects:
            return
        obj = self._objects[obj_id]
        self._distribute_inventory_update_message(
            UI_pb2.InventoryItemUpdate.TYPE_REMOVE, obj)
        obj.set_stack_id(new_stack_id)
        self._distribute_inventory_update_message(
            UI_pb2.InventoryItemUpdate.TYPE_ADD, obj)

    def remove(self, obj, count=1, move_to_object_manager=True):
        if obj.id not in self._objects:
            return False
        old_stack_count = obj.stack_count()
        split_obj = self._try_split(obj, count)
        try:
            obj.on_before_removed_from_inventory()
        except:
            logger.exception(
                'Exception invoking on_before_removed_from_inventory. obj: {}',
                obj)
        self._remove(obj, move_to_object_manager=move_to_object_manager)
        try:
            obj.on_removed_from_inventory()
        except:
            logger.exception(
                'Exception invoking on_removed_from_inventory. obj: {}', obj)
        if split_obj is None:
            for owner in self._owners:
                try:
                    owner.on_object_removed(obj)
                except:
                    logger.exception(
                        'Exception invoking on_object_removed. obj: {}, owner: {}',
                        obj, owner)
            self._distribute_inventory_update_message(
                UI_pb2.InventoryItemUpdate.TYPE_REMOVE, obj)
            if obj.inventoryitem_component.has_stack_option and self._stacks_with_options_counter is not None:
                stack_id = obj.inventoryitem_component.get_stack_id()
                self._stacks_with_options_counter[stack_id] -= 1
                if stack_id in self._stacks_with_options_counter <= 0:
                    if self._stacks_with_options_counter[stack_id] < 0:
                        logger.error(
                            'Counter went negative for stack_id {} with scheme {}',
                            stack_id,
                            obj.inventoryitem_component.stack_scheme,
                            owner='jdimailig')
                    del self._stacks_with_options_counter[stack_id]
        else:
            for owner in self._owners:
                try:
                    owner.on_object_id_changed(split_obj, obj.id,
                                               old_stack_count)
                except:
                    logger.exception(
                        'Exception invoking on_object_id_changed. obj: {}, owner: {}',
                        obj, owner)
            self._distribute_inventory_update_message(
                UI_pb2.InventoryItemUpdate.TYPE_UPDATE,
                split_obj,
                obj_id=obj.id)
        return True

    def _insert(self, obj, inventory_object):
        self._objects[obj.id] = obj
        obj.inventoryitem_component.set_inventory_type(self._inventory_type,
                                                       inventory_object)
        obj.item_location = self._item_location
        if self._inventory_type == InventoryType.SIM:
            obj.inventoryitem_component.is_hidden = self._hidden_storage
        object_manager = services.object_manager()
        if obj.id in object_manager:
            object_manager.move_to_inventory(
                obj,
                services.current_zone().inventory_manager)
            obj.set_parent(None)
            posture_graph_service = services.current_zone(
            ).posture_graph_service
            if posture_graph_service.is_object_pending_deletion(obj):
                posture_graph_service.finalize_object_deletion(obj)

    def _remove(self, obj, move_to_object_manager=False):
        if move_to_object_manager:
            services.current_zone().inventory_manager.move_to_world(
                obj, services.object_manager())
        obj.item_location = ItemLocation.ON_LOT
        obj.inventoryitem_component.set_inventory_type(
            None, None, from_removal=not move_to_object_manager)
        del self._objects[obj.id]

    def _get_compact_data(self, obj):
        try:
            obj.inventoryitem_component.save_for_stack_compaction = True
            return obj.get_attribute_save_data()
        finally:
            obj.inventoryitem_component.save_for_stack_compaction = False
            obj.post_tooltip_save_data_stored()

    def _try_compact(self, obj):
        if not self._allow_compaction:
            return (None, None)
        if len(self._objects) < 2:
            return (None, None)
        if obj.has_component(
                components.types.OBJECT_CLAIM_COMPONENT
        ) and obj.object_claim_component.requires_claiming:
            return (None, None)
        similar = None
        def_id = obj.definition.id
        data = self._get_compact_data(obj)
        stack_id = obj.inventoryitem_component.get_stack_id()
        for other in self._objects.values():
            if def_id != other.definition.id:
                continue
            if other is obj:
                continue
            if stack_id != other.inventoryitem_component.get_stack_id():
                continue
            if not any(interaction.should_reset_based_on_pipeline_progress
                       for interaction in other.interaction_refs):
                other_data = self._get_compact_data(other)
                if data == other_data:
                    similar = other
                    break
        if similar is None:
            return (None, None)
        similar_id = similar.id
        similar_count = similar.stack_count()
        self._remove(similar)
        similar.destroy(source=self, cause='InventoryStorage compaction')
        obj.update_stack_count(similar_count)
        return (similar_id, similar_count)

    def _try_split(self, obj, count):
        if count >= obj.stack_count():
            return
        clone = obj.inventoryitem_component.get_clone_for_stack_split()
        self._insert(clone, obj.inventoryitem_component.last_inventory_owner)
        clone.update_stack_count(-count)
        obj.set_stack_count(count)
        clone.on_added_to_inventory()
        return clone

    def _get_inventory_id(self):
        if InventoryTypeTuning.is_shared_between_objects(self._inventory_type):
            return int(self._inventory_type)
        if self._owners:
            return next(iter(self._owners)).owner.id
        logger.error(
            "Non-shared storage that's missing an owner: InventoryStorage<{},{}>",
            self._inventory_type, 0)
        return 0

    def _get_inventory_ui_type(self):
        if InventoryTypeTuning.is_shared_between_objects(self._inventory_type):
            return UI_pb2.InventoryItemUpdate.TYPE_SHARED
        return UI_pb2.InventoryItemUpdate.TYPE_OBJECT

    def _get_inventory_update_message(self,
                                      update_type,
                                      obj,
                                      obj_id=None,
                                      allow_while_zone_not_running=False):
        if not self._allow_ui:
            return
        if not services.current_zone(
        ).is_zone_running and not allow_while_zone_not_running:
            return
        if services.current_zone().is_zone_shutting_down:
            return
        msg = UI_pb2.InventoryItemUpdate()
        msg.type = update_type
        msg.inventory_id = self._get_inventory_id()
        msg.inventory_type = self._get_inventory_ui_type()
        msg.stack_id = obj.inventoryitem_component.get_stack_id()
        if obj_id is None:
            msg.object_id = obj.id
        else:
            msg.object_id = obj_id
        if update_type == UI_pb2.InventoryItemUpdate.TYPE_ADD:
            add_data = UI_pb2.InventoryItemData()
            add_data.definition_id = obj.definition.id
            msg.add_data = add_data
        if update_type == UI_pb2.InventoryItemUpdate.TYPE_ADD or update_type == UI_pb2.InventoryItemUpdate.TYPE_UPDATE:
            dynamic_data = UI_pb2.DynamicInventoryItemData()
            dynamic_data.value = obj.current_value
            dynamic_data.count = obj.stack_count()
            dynamic_data.new_object_id = obj.id
            dynamic_data.is_new = obj.new_in_inventory
            dynamic_data.sort_order = obj.get_stack_sort_order()
            icon_info = obj.get_icon_info_data()
            build_icon_info_msg(icon_info, None, dynamic_data.icon_info)
            recipe_name = obj.get_tooltip_field(
                TooltipFieldsComplete.recipe_name
            ) or obj.get_craftable_property(GameObjectProperty.RECIPE_NAME)
            if recipe_name is not None:
                dynamic_data.recipe_name = recipe_name
            if obj.custom_name is not None:
                dynamic_data.custom_name = obj.custom_name
            if InventoryStorage.UI_SORT_TYPES:
                sort_type = 0
                for sort_type_data in InventoryStorage.UI_SORT_TYPES:
                    value = None
                    try:
                        abs_value = None
                        state_component = obj.state_component
                        if state_component is None:
                            continue
                        for state in sort_type_data.object_data:
                            if state_component.has_state(state):
                                test_value = float(
                                    state_component.get_state(state).value)
                                abs_test_value = abs(test_value)
                                if value is None:
                                    value = test_value
                                elif abs_value < abs_test_value:
                                    value = test_value
                                    abs_value = abs_test_value
                    except TypeError:
                        pass
                    if value is not None:
                        sort_data_item = UI_pb2.InventoryItemSortData()
                        sort_data_item.type = sort_type
                        sort_data_item.value = value
                        dynamic_data.sort_data.append(sort_data_item)
                    sort_type += 1
            if update_type == UI_pb2.InventoryItemUpdate.TYPE_ADD:
                msg.add_data.dynamic_data = dynamic_data
            else:
                msg.update_data = dynamic_data
        if update_type == UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION:
            dynamic_data = UI_pb2.DynamicInventoryItemData()
            if obj.inventoryitem_component.has_stack_option:
                obj.inventoryitem_component.populate_stack_icon_info_data(
                    dynamic_data.icon_info)
            obj_owner = obj.inventoryitem_component.get_inventory().owner
            if obj_owner.is_sim:
                favorites_tracker = obj_owner.sim_info.favorites_tracker
                if favorites_tracker is not None:
                    if favorites_tracker.is_favorite_stack(obj):
                        dynamic_data.is_favorite = True
            msg.update_data = dynamic_data
        return msg

    def _distribute_inventory_update_message(self,
                                             update_type,
                                             obj,
                                             obj_id=None):
        msg = self._get_inventory_update_message(update_type,
                                                 obj,
                                                 obj_id=obj_id)
        if msg is not None:
            op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg)
            Distributor.instance().add_op_with_no_owner(op)

    def distribute_inventory_update_message(self, obj):
        if obj.id not in self._objects:
            return False
        msg = self._get_inventory_update_message(
            UI_pb2.InventoryItemUpdate.TYPE_UPDATE, obj)
        if msg is not None:
            op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg)
            Distributor.instance().add_op_with_no_owner(op)

    def distribute_inventory_stack_update_message(self, obj):
        if obj.id not in self._objects:
            return
        msg = self._get_inventory_update_message(
            UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj)
        if msg is not None:
            op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg)
            Distributor.instance().add_op_with_no_owner(op)

    def distribute_owned_inventory_update_message(self, obj, owner):
        if obj.id not in self._objects:
            return False
        msg = self._get_inventory_update_message(
            UI_pb2.InventoryItemUpdate.TYPE_UPDATE, obj)
        if msg is not None:
            op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg)
            Distributor.instance().add_op(owner, op)

    def get_item_update_ops_gen(self):
        stack_options_set = set()
        for obj in self._objects.values():
            message = self._get_inventory_update_message(
                UI_pb2.InventoryItemUpdate.TYPE_ADD,
                obj,
                allow_while_zone_not_running=True)
            if message is None:
                continue
            yield (obj,
                   GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE,
                                           message))
            if not obj.inventoryitem_component.has_stack_option:
                obj_owner = obj.inventoryitem_component.get_inventory().owner
                if obj_owner.is_sim:
                    if obj_owner.sim_info.favorites_tracker is None:
                        continue
                    stack_id = obj.inventoryitem_component.get_stack_id()
                    if stack_id in stack_options_set:
                        continue
                    option_msg = self._get_inventory_update_message(
                        UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION,
                        obj,
                        allow_while_zone_not_running=True)
                    if option_msg is not None:
                        stack_options_set.add(stack_id)
                        yield (obj,
                               GenericProtocolBufferOp(
                                   Operation.INVENTORY_ITEM_UPDATE,
                                   option_msg))
            else:
                stack_id = obj.inventoryitem_component.get_stack_id()
                if stack_id in stack_options_set:
                    continue
                option_msg = self._get_inventory_update_message(
                    UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION,
                    obj,
                    allow_while_zone_not_running=True)
                if option_msg is not None:
                    stack_options_set.add(stack_id)
                    yield (obj,
                           GenericProtocolBufferOp(
                               Operation.INVENTORY_ITEM_UPDATE, option_msg))

    def open_ui_panel(self, obj):
        if not self._allow_ui:
            return False
        msg = UI_pb2.OpenInventory()
        msg.object_id = obj.id
        msg.inventory_id = self._get_inventory_id()
        msg.inventory_type = self._get_inventory_ui_type()
        op = GenericProtocolBufferOp(Operation.OPEN_INVENTORY, msg)
        Distributor.instance().add_op_with_no_owner(op)
        return True
Beispiel #28
0
class DisplayComponent(Component,
                       HasTunableFactory,
                       AutoFactoryInit,
                       component_name=types.DISPLAY_COMPONENT):
    DISPLAY_STATE = TunableStateValueReference(
        description=
        '\n        The state a display object will be set to when it is parented to a\n        Display Parent.\n        '
    )
    DEFAULT_STATE = TunableStateValueReference(
        description=
        '\n        The default state a display object will be set to when it is unparented\n        from a Display Parent.\n        '
    )
    FACTORY_TUNABLES = {
        'display_parent':
        CraftTaggedItemFactory(
            description=
            '\n            If an object matches the tag(s), it will be considered a Display\n            Parent for this display object. All display objects with a Display\n            Component MUST have a Display Parent tuned, otherwise there is no\n            need in the Display Component.\n            '
        ),
        'use_display_state':
        Tunable(
            description=
            "\n            If enabled, this object will change to the Display State when it is\n            parented to a Display Parent. The Display State is tuned in the\n            objects.components.display_component module tuning. NOTICE: If you\n            are only tuning this and not tuning any Inventory State Triggers,\n            it's recommended that you use the Slot Component in the Native\n            Components section of the parent object.\n            ",
            tunable_type=bool,
            default=True),
        'inventory_state_triggers':
        TunableList(
            description=
            '\n            Change states on the owning object based on tests applied to the\n            inventory of the Display Parent. Tests will be done in order and\n            will stop at the first success.\n            ',
            tunable=TunableTuple(inventory_test=InventoryTest.TunableFactory(),
                                 set_state=TunableStateValueReference()))
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.inventory_state_triggers:
            services.get_event_manager().register_tests(
                self, (self.inventory_state_triggers[0].inventory_test, ))

    @property
    def _is_on_display_parent(self):
        parent = self.owner.parent
        if parent is None:
            return False
        return self.display_parent(crafted_object=parent,
                                   skill=None) is not None

    def handle_event(self, sim_info, event, resolver):
        if sim_info is not None:
            return
        if not self._is_on_display_parent:
            return
        self._handle_inventory_changed()

    def _handle_inventory_changed(self):
        obj_resolver = SingleObjectResolver(self.owner)
        for trigger in self.inventory_state_triggers:
            if obj_resolver(trigger.inventory_test):
                if self.owner.has_state(trigger.set_state.state):
                    self.owner.set_state(trigger.set_state.state,
                                         trigger.set_state)
                break

    def slotted_to_object(self, parent):
        if self._should_change_display_state(parent) and self.owner.has_state(
                self.DISPLAY_STATE.state):
            self.owner.set_state(self.DISPLAY_STATE.state, self.DISPLAY_STATE)
        self._handle_inventory_changed()

    def unslotted_from_object(self, parent):
        if self._should_change_display_state(parent) and self.owner.has_state(
                self.DEFAULT_STATE.state):
            self.owner.set_state(self.DEFAULT_STATE.state, self.DEFAULT_STATE)

    def _should_change_display_state(self, parent):
        if not self.use_display_state:
            return False
        return self.display_parent(crafted_object=parent, skill=None)
Beispiel #29
0
class FetchObjectSocialSuperInteraction(DistancePlacementMixin,
                                        SocialSuperInteraction):
    MAX_FETCH_RADIUS = 3
    INSTANCE_TUNABLES = {
        'fetch_constraint':
        TunableCircle(
            description=
            '\n            The circle constraint for other Sims in the social group to route\n            near the placement location.\n            ',
            radius=MAX_FETCH_RADIUS,
            tuning_group=GroupNames.CONSTRAINTS),
        'throw_xevent_id':
        Tunable(
            description=
            '\n            An xevent id for when the carry target is thrown from an animation.\n            ',
            tunable_type=int,
            default=0,
            tuning_group=GroupNames.ANIMATION)
    }

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

    @flexmethod
    def _constraint_gen(cls,
                        inst,
                        sim,
                        target,
                        participant_type=ParticipantType.Actor,
                        **kwargs):
        inst_or_cls = inst if inst is not None else cls
        yield from super(SuperInteraction, inst_or_cls)._constraint_gen(
            sim, target, participant_type=participant_type, **kwargs)
        if inst is not None and participant_type == ParticipantType.TargetSim:
            starting_location = inst._starting_location if inst._starting_location is not None else sim.location
            constraint = interactions.constraints.Circle(
                starting_location.transform.translation,
                inst.social_group.max_radius,
                starting_location.routing_surface)
            constraint = constraint.intersect(
                interactions.constraints.Facing(
                    target_position=starting_location.transform.translation))
            yield constraint
        placement_constraint = inst_or_cls._get_distance_placement_constraint(
            sim, target, participant_type=participant_type)
        yield placement_constraint

    def distribute_fetch_route(self, *args, **kwargs):
        for plan_primitive in self._plan_primitives:
            if FollowPath.should_follow_path(plan_primitive.sim,
                                             plan_primitive.path):
                follow_path_element = FollowPath(plan_primitive.sim,
                                                 plan_primitive.path)
                self._follow_path_elements.append(follow_path_element)
                follow_path_element.distribute_path_asynchronously()

    def build_basic_content(self, *args, **kwargs):
        self.store_event_handler(self.distribute_fetch_route,
                                 handler_id=self.throw_xevent_id)
        sequence = super().build_basic_content(*args, **kwargs)

        def plan_fetch_paths(timeline):
            for target_sim in self.get_participants(
                    ParticipantType.TargetSim | ParticipantType.Listeners):
                fetch_constraint = self.fetch_constraint.create_constraint(
                    target_sim,
                    target_position=self._distance_placement_transform.
                    translation,
                    routing_surface=self._routing_surface
                    if self._routing_surface is not None else DEFAULT)
                facing = interactions.constraints.Facing(
                    target_position=self._distance_placement_transform.
                    translation,
                    inner_radius=self.facing_radius)
                fetch_constraint = fetch_constraint.intersect(facing)
                if not fetch_constraint.valid:
                    continue
                goals = []
                handles = fetch_constraint.get_connectivity_handles(target_sim)
                for handle in handles:
                    goals.extend(handle.get_goals())
                if not goals:
                    pass
                else:
                    route = routing.Route(
                        target_sim.routing_location,
                        goals,
                        routing_context=target_sim.routing_context)
                    plan_primitive = PlanRoute(route,
                                               target_sim,
                                               interaction=self)
                    result = yield from element_utils.run_child(
                        timeline, plan_primitive)
                    if result:
                        if plan_primitive.path.nodes:
                            if plan_primitive.path.nodes.plan_success:
                                if plan_primitive.path.portal_obj is not None:
                                    logger.error(
                                        'Need sub interaction to route {} due to portal on path'
                                        .format(target_sim))
                                else:
                                    self._plan_primitives.append(
                                        plan_primitive)
            yield from element_utils.run_child(timeline, sequence)

        return build_element(plan_fetch_paths)
Beispiel #30
0
class IngredientTuning:
    __qualname__ = 'IngredientTuning'
    INGREDIENT_QUALITY_MAPPING = TunableMapping(description='\n        Mapping of all possible ingredient quality states to what possible\n        states will the ingredients have.\n        e.g. High quality ingredients need to be mapped to gardening high \n        quality, fish high quality or any state that will indicate what \n        high quality means on a different system.\n        ', key_type=ObjectStateValue.TunableReference(description='\n            The states that will define the ingredient quality.\n            '), value_type=TunableTuple(description='\n            Definition of the ingredient quality state.  This will define\n            the quality boost on the recipe and the possible states an \n            ingredient can have to have this state.\n            ', quality_boost=Tunable(description='\n                Value that will be added to the quality commodity whenever\n                this state is added.\n                ', tunable_type=int, default=1), state_value_list=TunableList(description='\n                List of ingredient states that will give this level of \n                ingredient quality.\n                ', tunable=ObjectStateValue.TunableReference(description='\n                    The states that will define the ingredient quality.\n                    '))))
    INGREDIENT_TAG_DISPLAY_MAPPING = TunableMapping(description='\n        Mapping of all object tags to their localized string that will display\n        on the ingredient list.\n        This will be used for displaying on the recipe\'s when an ingredient is \n        tuned by tag instead of object definition.\n        Example: Display objects of rag FISH as string "Any Fish"\n        ', key_type=TunableEnumEntry(description='\n            Tag corresponding at an ingredient type that can be used in a\n            recipe.\n            ', tunable_type=tag.Tag, default=tag.Tag.INVALID), value_type=TunableLocalizedStringFactory())
    INGREDIENT_TAG = TunableEnumEntry(description='\n        Tag to look for when iterating through objects to know if they are \n        ingredients.\n        All ingredients should be tuned with this tag.\n        ', tunable_type=tag.Tag, default=tag.Tag.INVALID)

    @classmethod
    def get_quality_bonus(cls, ingredient):
        for quality_details in IngredientTuning.INGREDIENT_QUALITY_MAPPING.values():
            for state_value in quality_details.state_value_list:
                while ingredient.state_value_active(state_value):
                    return quality_details.quality_boost
        return 0

    @classmethod
    def get_ingredient_quality_state(cls, quality_bonus):
        state_to_add = None
        bonus_selected = None
        for (quality_state_value, quality_details) in IngredientTuning.INGREDIENT_QUALITY_MAPPING.items():
            while (bonus_selected is None or quality_details.quality_boost <= bonus_selected) and bonus_selected >= quality_bonus:
                bonus_selected = quality_details.quality_boost
                state_to_add = quality_state_value
        return state_to_add

    @classmethod
    def get_ingredient_string_for_tag(cls, tag):
        string_factory = IngredientTuning.INGREDIENT_TAG_DISPLAY_MAPPING.get(tag)
        if string_factory:
            return string_factory()
        return