Пример #1
0
class RoutingComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=types.ROUTING_COMPONENT):
    _pathplan_context = None
    FACTORY_TUNABLES = {'plan_context_data': TunableTuple(description='\n            Data used to populate fields on the path plan context.\n            ', default_context=PathPlanContextWrapper.TunableFactory(description="\n                If no age override is specified, the default path plan data to\n                use for this agent's path planning.\n                "), context_age_species_overrides=TunableList(description='\n                List of age-species path plan context overrides for a specific\n                routing agent.\n                ', tunable=TunableTuple(description='\n                    Overrides to the path plan context of the agent defined by a\n                    combination of age and species.\n                    ', age=TunableEnumEntry(description='\n                        The age this override applies to.\n                        ', tunable_type=Age, default=Age.ADULT), species=TunableEnumEntry(description='\n                        The species this override applies to.\n                        ', tunable_type=SpeciesExtended, default=SpeciesExtended.HUMAN, invalid_enums=(SpeciesExtended.INVALID,)), context_override=PathPlanContextWrapper.TunableFactory()))), 'walkstyle_behavior': WalksStyleBehavior.TunableFactory(description='\n            Define the walkstyle behavior for owners of this component.\n            '), 'object_routing_component': OptionalTunable(description='\n            If enabled, this object will have an Object Routing component, which\n            controls an object routing behavior based on triggered states.\n            ', tunable=ObjectRoutingComponent.TunableFactory())}

    def __init__(self, owner, **kwargs):
        super().__init__(owner, **kwargs)
        self.owner = owner
        walkstyle_behavior = self.get_walkstyle_behavior()
        self._walkstyle_requests = [WalkStyleRequest(self.owner, walkstyle=walkstyle_behavior.default_walkstyle, priority=-1)]
        self._walk_style_handles = {}
        self.wading_buff_handle = None
        self.last_route_has_wading_nodes = False
        self._routing_stage_event_callbacks = defaultdict(CallableList)
        if owner.is_sim:
            owner.remove_component(objects.components.types.FOOTPRINT_COMPONENT)
        self._path_plan_context_map = {}
        self.on_slot = None
        self.stand_slot_reservation_removed_callbacks = CallableList()
        self._active_follow_path_weakref = None
        self.on_follow_path = CallableList()
        self.on_plan_path = CallableList()
        self.on_intended_location_changed = CallableList()
        self._current_path = None
        self._routing_slave_data = []
        self._routing_master_ref = None
        self._default_agent_radius = None
        self._route_event_context = RouteEventContext()
        self._route_interaction = None
        self._animation_context = None
        self._initial_carry_targets = None
        self._route_event_provider_requests = None

    def get_subcomponents_gen(self):
        yield from super().get_subcomponents_gen()
        if self.object_routing_component is not None:
            object_routing_component = self.object_routing_component(self.owner)
            yield from object_routing_component.get_subcomponents_gen()

    @property
    def current_path(self):
        return self._current_path

    @property
    def object_radius(self):
        return self._pathplan_context.agent_radius

    @property
    def route_event_context(self):
        return self._route_event_context

    @componentmethod
    def register_routing_stage_event(self, routing_stage_event, callback):
        self._routing_stage_event_callbacks[routing_stage_event].register(callback)

    def _on_routing_stage_event(self, routing_stage_event, **kwargs):
        callbacks = self._routing_stage_event_callbacks.get(routing_stage_event)
        if callbacks is not None:
            callbacks(self.owner, routing_stage_event, **kwargs)

    @contextmanager
    def temporary_walkstyle_request(self, walkstyle_request_factory):
        try:
            request = walkstyle_request_factory(self.owner)
            request.start()
            yield None
        finally:
            request.stop()

    @componentmethod
    def unregister_routing_stage_event(self, routing_stage_event, callback):
        self._routing_stage_event_callbacks[routing_stage_event].unregister(callback)

    @componentmethod
    def get_walkstyle_behavior(self):
        return self.walkstyle_behavior

    @componentmethod
    def get_cost_valid_walkstyle(self, cost_tuning):
        for walkstyle in self._walkstyle_requests:
            if walkstyle.priority != WalkStylePriority.COMBO:
                walkstyle_cost = cost_tuning.get(walkstyle.walkstyle, None)
                if walkstyle_cost is None:
                    return walkstyle.walkstyle
                current_value = self.owner.get_stat_value(walkstyle_cost.walkstyle_cost_statistic)
                if current_value - walkstyle_cost.cost > walkstyle_cost.walkstyle_cost_statistic.min_value:
                    return walkstyle.walkstyle
        walkstyle_behavior = self.get_walkstyle_behavior()
        return walkstyle_behavior.default_walkstyle

    @componentmethod
    def get_default_walkstyle(self):
        return self.walkstyle_behavior.get_default_walkstyle(self.owner)

    @componentmethod
    def get_walkstyle(self):
        for walkstyle in self._walkstyle_requests:
            if walkstyle.priority != WalkStylePriority.COMBO:
                return walkstyle.walkstyle
        walkstyle_behavior = self.get_walkstyle_behavior()
        return walkstyle_behavior.default_walkstyle

    @componentmethod
    def get_walkstyle_for_path(self, path):
        return self.walkstyle_behavior.get_walkstyle_for_path(self.owner, path)

    @componentmethod
    def get_walkstyle_list(self):
        return tuple(request.walkstyle for request in self._walkstyle_requests)

    @componentmethod
    def get_walkstyle_requests(self):
        return self._walkstyle_requests

    def _get_walkstyle_key(self):
        if self.owner.is_sim:
            return (self.owner.age, self.owner.gender, self.owner.extended_species)
        return (Age.ADULT, Gender.MALE, SpeciesExtended.HUMAN)

    @componentmethod
    def request_walkstyle(self, walkstyle_request, uid):
        if walkstyle_request.priority != WalkStylePriority.COMBO and not has_walkstyle_info(walkstyle_request.walkstyle, *self._get_walkstyle_key()):
            return
        self._walkstyle_requests.append(walkstyle_request)
        self._walkstyle_requests.sort(reverse=True, key=operator.attrgetter('priority'))
        self._walk_style_handles[uid] = RegistryHandle(lambda : self._unrequest_walkstyle(walkstyle_request))
        self._update_walkstyle()

    @componentmethod
    def remove_walkstyle(self, uid):
        if uid in self._walk_style_handles:
            self._walk_style_handles[uid].release()
            del self._walk_style_handles[uid]

    def _unrequest_walkstyle(self, walkstyle_request):
        self._walkstyle_requests.remove(walkstyle_request)
        self._update_walkstyle()

    def _update_walkstyle(self):
        for primitive in self.owner.primitives:
            try:
                primitive.request_walkstyle_update()
            except AttributeError:
                pass

    @componentmethod
    def get_additional_scoring_for_surface(self, surface_type):
        return self._pathplan_context.surface_preference_scoring.get(surface_type, 0)

    @componentmethod
    def add_location_to_quadtree(self, *args, **kwargs):
        path_plan_context = self._pathplan_context
        return path_plan_context.add_location_to_quadtree(*args, **kwargs)

    @componentmethod
    def remove_location_from_quadtree(self, *args, **kwargs):
        path_plan_context = self._pathplan_context
        return path_plan_context.remove_location_from_quadtree(*args, **kwargs)

    @property
    def _pathplan_context(self):
        age_key = getattr(self.owner, 'age', DEFAULT)
        species_key = getattr(self.owner, 'extended_species', DEFAULT)
        combined_override_key = (age_key, species_key)
        if combined_override_key in self._path_plan_context_map:
            return self._path_plan_context_map[combined_override_key]
        for override in self.plan_context_data.context_age_species_overrides:
            if override.age == age_key:
                if override.species == species_key:
                    self._path_plan_context_map[combined_override_key] = override.context_override(self.owner)
                    return self._path_plan_context_map[combined_override_key]
        self._path_plan_context_map.clear()
        self._path_plan_context_map[combined_override_key] = self.plan_context_data.default_context(self.owner)
        return self._path_plan_context_map[combined_override_key]

    @componentmethod
    def get_routing_context(self):
        return self.pathplan_context

    def on_sim_added(self):
        self._update_quadtree_location()

    def on_sim_removed(self):
        self.on_slot = None
        self._routing_master_ref = None
        self.clear_routing_slaves()

    def on_reset_internal_state(self, reset_reason):
        self.clear_routing_slaves()

    def add_callbacks(self):
        self.owner.register_on_location_changed(self._update_quadtree_location)
        self.owner.register_on_location_changed(self._check_violations)
        if self.get_walkstyle_behavior().supports_wading_walkstyle_buff(self.owner):
            self.owner.register_on_location_changed(self.get_walkstyle_behavior().check_for_wading)
        self.on_plan_path.append(self._on_update_goals)
        self.on_intended_location_changed.append(self.owner.refresh_los_constraint)
        self.on_intended_location_changed.append(self.owner._update_social_geometry_on_location_changed)
        self.on_intended_location_changed.append(lambda *_, **__: self.owner.two_person_social_transforms.clear())
        self.on_intended_location_changed.append(self.owner.update_intended_position_on_active_lot)

    def remove_callbacks(self):
        self.on_intended_location_changed.clear()
        if self._on_update_goals in self.on_plan_path:
            self.on_plan_path.remove(self._on_update_goals)
        if self.owner._on_location_changed_callbacks is not None and self._check_violations in self.owner._on_location_changed_callbacks:
            self.owner.unregister_on_location_changed(self._check_violations)
        if self.owner._on_location_changed_callbacks is not None and self._update_quadtree_location in self.owner._on_location_changed_callbacks:
            self.owner.unregister_on_location_changed(self._update_quadtree_location)

    @property
    def pathplan_context(self):
        return self._pathplan_context

    def get_or_create_routing_context(self):
        return self._pathplan_context

    @property
    def routing_context(self):
        return self._pathplan_context

    @property
    def connectivity_handles(self):
        return self._pathplan_context.connectivity_handles

    @property
    def is_moving(self):
        if self.owner.is_sim:
            return not self.owner.location.almost_equal(self.owner.intended_location)
        else:
            return self.current_path is not None

    @property
    def routing_master(self):
        if self._routing_master_ref is not None:
            return self._routing_master_ref()

    @routing_master.setter
    def routing_master(self, value):
        self._routing_master_ref = value.ref() if value is not None else None

    @property
    def route_interaction(self):
        return self._route_interaction

    @property
    def animation_context(self):
        if self._route_interaction is not None:
            return self._route_interaction.animation_context
        return self._animation_context

    SLAVE_RADIUS_MODIFIER = 0.5
    MAX_ALLOWED_AGENT_RADIUS = 0.25

    def _update_agent_radius(self):
        agent_radius_datas = [slave_data for slave_data in self._routing_slave_data if slave_data.should_increase_master_agent_radius]
        if not agent_radius_datas:
            return
        max_x = max(abs(slave_data.offset[0]) for slave_data in agent_radius_datas)
        max_x = max(max_x*self.SLAVE_RADIUS_MODIFIER, self._default_agent_radius)
        self._pathplan_context.agent_radius = min(self.MAX_ALLOWED_AGENT_RADIUS, max_x)

    def add_routing_slave(self, slave_data):
        if len(self._routing_slave_data) == 0:
            self._default_agent_radius = self._pathplan_context.agent_radius
        slave_data.slave.routing_master = self.owner
        self._routing_slave_data.append(slave_data)
        slave_data.on_add()
        self._update_agent_radius()

    def clear_routing_slaves(self):
        for slave_data in self._routing_slave_data:
            slave_data.slave.routing_master = None
            slave_data.on_release()
        self._routing_slave_data.clear()
        self._restore_agent_radius()
        self._update_agent_radius()

    @componentmethod
    def get_routing_slave_data(self):
        return self._routing_slave_data

    @componentmethod
    def get_formation_data_for_slave(self, obj):
        for slave_data in self._routing_slave_data:
            if slave_data.slave is obj:
                return slave_data

    @componentmethod
    def get_all_routing_slave_data_gen(self):
        yield from self._routing_slave_data

    @componentmethod
    def get_routing_slave_data_count(self, formation_type):
        return sum(1 for slave_data in self._routing_slave_data if slave_data.formation_type is formation_type)

    def clear_slave(self, slave):
        for slave_data in self._routing_slave_data:
            if slave_data.slave is slave:
                self._routing_slave_data.remove(slave_data)
                slave_data.on_release()
                break
        self._restore_agent_radius()
        slave.routing_master = None
        self._update_agent_radius()

    @componentmethod
    def write_slave_data_msg(self, route_msg, path=None):
        transitioning_sims = ()
        actor = self.owner
        if actor.is_sim:
            if actor.transition_controller is not None:
                transitioning_sims = actor.transition_controller.get_transitioning_sims()
        for slave_data in self.get_routing_slave_data():
            if slave_data.should_slave_for_path(path):
                if slave_data.slave in transitioning_sims:
                    continue
                (slave_actor, slave_msg) = slave_data.add_routing_slave_to_pb(route_msg, path=path)
                slave_actor.write_slave_data_msg(slave_msg, path=path)
        for slave_actor in actor.children:
            if not slave_actor.is_sim:
                continue
            carry_walkstyle_behavior = slave_actor.get_walkstyle_behavior().carry_walkstyle_behavior
            if carry_walkstyle_behavior is None:
                continue
            with ProtocolBufferRollback(route_msg.slaves) as slave_msg:
                slave_msg.id = slave_actor.id
                slave_msg.type = Routing_pb2.SlaveData.SLAVE_PAIRED_CHILD
                walkstyle_override_msg = slave_msg.walkstyle_overrides.add()
                walkstyle_override_msg.from_walkstyle = 0
                walkstyle_override_msg.to_walkstyle = carry_walkstyle_behavior.default_carry_walkstyle
                for (walkstyle, carry_walkstyle) in carry_walkstyle_behavior.carry_walkstyle_overrides.items():
                    walkstyle_override_msg = slave_msg.walkstyle_overrides.add()
                    walkstyle_override_msg.from_walkstyle = walkstyle
                    walkstyle_override_msg.to_walkstyle = carry_walkstyle
                slave_actor.write_slave_data_msg(slave_msg, path=path)

    def _restore_agent_radius(self):
        if self._default_agent_radius is not None:
            if len(self._routing_slave_data) == 0:
                self._pathplan_context.agent_radius = self._default_agent_radius
                self._default_agent_radius = None

    def contains_slave(self, slave):
        return any(slave.id == slave_data.slave.id for slave_data in self._routing_slave_data)

    def _on_update_goals(self, goal_list, starting):
        NUM_GOALS_TO_RESERVE = 2
        for (index, goal) in enumerate(goal_list, start=1):
            if index > NUM_GOALS_TO_RESERVE:
                break
            if starting:
                self.add_location_to_quadtree(placement.ItemType.SIM_INTENDED_POSITION, position=goal.position, orientation=goal.orientation, routing_surface=goal.routing_surface_id, index=index)
            else:
                self.remove_location_from_quadtree(placement.ItemType.SIM_INTENDED_POSITION, index=index)

    def set_portal_mask_flag(self, flag):
        self._pathplan_context.set_portal_key_mask(self._pathplan_context.get_portal_key_mask() | flag)

    def clear_portal_mask_flag(self, flag):
        self._pathplan_context.set_portal_key_mask(self._pathplan_context.get_portal_key_mask() & ~flag)

    def set_portal_discouragement_mask_flag(self, flag):
        self._pathplan_context.set_portal_discourage_key_mask(self._pathplan_context.get_portal_discourage_key_mask() | flag)

    def clear_portal_discouragement_mask_flag(self, flag):
        self._pathplan_context.set_portal_discourage_key_mask(self._pathplan_context.get_portal_discourage_key_mask() & ~flag)

    @componentmethod
    def update_portal_locks(self):
        sim = self.owner
        sim.manager.add_portal_lock(sim, sim._portal_added_callback)

    def _update_quadtree_location(self, *_, **__):
        self.add_location_to_quadtree(placement.ItemType.SIM_POSITION)

    def add_stand_slot_reservation(self, interaction, position, routing_surface, excluded_sims):
        interaction.add_liability(STAND_SLOT_LIABILITY, StandSlotReservationLiability(self.owner, interaction))
        excluded_sims.add(self.owner)
        self._stand_slot_reservation = position
        self.add_location_to_quadtree(placement.ItemType.ROUTE_GOAL_SUPPRESSOR, position=position, routing_surface=routing_surface)
        pathplan_context = self._pathplan_context
        reservation_radius = pathplan_context.agent_radius*2
        polygon = sims4.geometry.generate_circle_constraint(6, position, reservation_radius)
        self.on_slot = (position, polygon, routing_surface)
        UserFootprintHelper.force_move_sims_in_polygon(polygon, routing_surface, exclude=excluded_sims)

    def remove_stand_slot_reservation(self, interaction):
        self.remove_location_from_quadtree(placement.ItemType.ROUTE_GOAL_SUPPRESSOR)
        self.on_slot = None
        self.stand_slot_reservation_removed_callbacks(sim=self)

    def get_stand_slot_reservation_violators(self, excluded_sims=()):
        if not self.on_slot:
            return
        (_, polygon, routing_surface) = self.on_slot
        violators = []
        excluded_sims = {sim for sim in itertools.chain((self.owner,), excluded_sims)}
        for sim_nearby in placement.get_nearby_sims_gen(polygon.centroid(), routing_surface, radius=polygon.radius(), exclude=excluded_sims):
            if sims4.geometry.test_point_in_polygon(sim_nearby.position, polygon):
                if not sim_nearby.ignore_blocking_near_destination:
                    violators.append(sim_nearby)
        return violators

    def _check_violations(self, *_, **__):
        if services.privacy_service().check_for_late_violators(self.owner):
            return
        for reaction_trigger in self.owner.reaction_triggers.values():
            reaction_trigger.intersect_and_execute(self.owner)

    def create_route_interaction(self):
        if self.owner.is_sim:
            aop = AffordanceObjectPair(AnimationInteraction, None, AnimationInteraction, None, hide_unrelated_held_props=False)
            context = InteractionContext(self.owner, InteractionContext.SOURCE_SCRIPT, Priority.High)
            self._route_interaction = aop.interaction_factory(context).interaction
        else:
            self._animation_context = AnimationContext()
            self._animation_context.add_ref(self._current_path)
        for slave_data in self.get_routing_slave_data():
            slave_data.slave.routing_component.create_route_interaction()

    def cancel_route_interaction(self):
        if self._route_interaction is not None:
            self._route_interaction.cancel(FinishingType.AUTO_EXIT, 'Route Ended.')
            self._route_interaction.on_removed_from_queue()
            self._route_interaction = None
        if self._animation_context is not None:
            self._animation_context.release_ref(self._current_path)
            self._animation_context = None
        for slave_data in self.get_routing_slave_data():
            slave_data.slave.routing_component.cancel_route_interaction()

    def set_follow_path(self, follow_path):
        self._active_follow_path_weakref = weakref.ref(follow_path)

    def clear_follow_path(self):
        self._active_follow_path_weakref = None

    def _get_active_follow_path(self):
        if self._active_follow_path_weakref is not None:
            return self._active_follow_path_weakref()

    def get_approximate_cancel_location(self):
        follow_path = self._get_active_follow_path()
        if follow_path is not None:
            ret = follow_path.get_approximate_cancel_location()
            if ret is not None:
                return ret
        return (self.owner.intended_transform, self.owner.intended_routing_surface)

    @componentmethod
    def set_routing_path(self, path):
        if path is None:
            if gsi_handlers.route_event_handlers.archiver.enabled:
                gsi_handlers.route_event_handlers.archive_route_events(self._current_path, self.owner, gsi_handlers.route_event_handlers.PATH_TYPE_FINISHED, clear=True)
            self._on_routing_stage_event(RoutingStageEvent.ROUTE_END, path=self._current_path)
            self.cancel_route_interaction()
            self._current_path = None
            if self.owner.is_sim and self.owner.transition_controller is not None and self._initial_carry_targets != self.owner.posture_state.carry_targets:
                self.owner.transition_controller.derail(DerailReason.CONSTRAINTS_CHANGED, self.owner)
            self._initial_carry_targets = None
            return
        if self.owner.is_sim:
            self._initial_carry_targets = self.owner.posture_state.carry_targets
        self._current_path = path
        if self.pathplan_context.disable_fake_portals:
            self._current_path.remove_fake_portals()
        self.create_route_interaction()
        self._on_routing_stage_event(RoutingStageEvent.ROUTE_START, path=path)
        walkstyle = self.walkstyle_behavior.apply_walkstyle_to_path(self.owner, self._current_path)
        origin_q = (float(self.owner.orientation.x), float(self.owner.orientation.y), float(self.owner.orientation.z), float(self.owner.orientation.w))
        origin_t = (float(self.owner.position.x), float(self.owner.position.y), float(self.owner.position.z))
        (age, gender, species) = self._get_walkstyle_key()
        self._current_path.nodes.apply_initial_timing(origin_q, origin_t, walkstyle, age, gender, species, int(services.time_service().sim_now), services.current_zone_id())

    @componentmethod
    def update_routing_path(self, time_offset):
        if self._current_path is None:
            return
        walkstyle = self.walkstyle_behavior.apply_walkstyle_to_path(self.owner, self._current_path, time_offset=time_offset)
        (age, gender, species) = self._get_walkstyle_key()
        self._current_path.nodes.update_timing(walkstyle, age, gender, species, time_offset, services.current_zone_id())

    @componentmethod
    def update_slave_positions_for_path(self, path, transform, orientation, routing_surface, distribute=True, canceled=False):
        transitioning_sims = ()
        if self.owner.is_sim:
            if self.owner.transition_controller is not None:
                transitioning_sims = self.owner.transition_controller.get_transitioning_sims()
        for slave_data in self.get_routing_slave_data():
            if slave_data.slave in transitioning_sims:
                continue
            slave_data.update_slave_position(transform, orientation, routing_surface, distribute=distribute, path=path, canceled=canceled)

    def add_route_event_provider(self, request):
        if self._route_event_provider_requests is None:
            self._route_event_provider_requests = []
        self._route_event_provider_requests.append(request)

    def remove_route_event_provider(self, request):
        if self._route_event_provider_requests is not None and request in self._route_event_provider_requests:
            self._route_event_provider_requests.remove(request)
        if not self._route_event_provider_requests:
            self._route_event_provider_requests = None

    def route_event_executed(self, event_id):
        if self._route_event_context is None:
            return
        self._route_event_context.handle_route_event_executed(event_id, self.owner, path=self._current_path)

    def route_event_skipped(self, event_id):
        if self._route_event_context is None:
            return
        self._route_event_context.handle_route_event_skipped(event_id, self.owner, path=self._current_path)

    def remove_route_event_by_data(self, event_data):
        if self._route_event_context is None:
            return
        self._route_event_context.remove_route_event_by_data(event_data)

    @componentmethod
    def route_finished(self, path_id):
        for primitive in self.owner.primitives:
            if hasattr(primitive, 'route_finished'):
                primitive.route_finished(path_id)

    @componentmethod
    def route_time_update(self, path_id, current_time):
        for primitive in self.owner.primitives:
            if hasattr(primitive, 'route_time_update'):
                primitive.route_time_update(path_id, current_time)

    def _gather_route_events(self, path, **kwargs):
        owner = self.owner
        if owner.is_sim:
            interaction = owner.transition_controller.interaction if owner.transition_controller is not None else None
            if interaction is not None and interaction.is_super:
                interaction.provide_route_events(self._route_event_context, owner, path, **kwargs)
            owner.Buffs.provide_route_events_from_buffs(self._route_event_context, owner, path, **kwargs)
        broadcaster_service = services.current_zone().broadcaster_service
        broadcaster_service.provide_route_events(self._route_event_context, owner, path, **kwargs)
        if owner.weather_aware_component is not None:
            owner.weather_aware_component.provide_route_events(self._route_event_context, owner, path, **kwargs)
        if self._route_event_provider_requests is not None:
            for request in self._route_event_provider_requests:
                request.provide_route_events(self._route_event_context, owner, path, **kwargs)
        object_manager = services.object_manager(owner.zone_id)
        if object_manager is not None:
            for node in path.nodes:
                if node.portal_object_id != 0:
                    portal_object = object_manager.get(node.portal_object_id)
                    if portal_object is not None:
                        portal_object.provide_route_events(node.portal_id, self._route_event_context, owner, path, node=node, **kwargs)

    def clear_route_events(self, *args, **kwargs):
        if self._route_event_context is None:
            return
        self._route_event_context.clear_route_events()
        for slave_data in self.get_routing_slave_data():
            slave_data.slave.routing_component.clear_route_events()

    def schedule_and_process_route_events_for_new_path(self, path):
        if self._route_event_context is None:
            return
        if self.owner.is_sim and not self.owner.posture.mobile:
            return
        self.clear_route_events()
        start_time = RouteEventContext.ROUTE_TRIM_START
        end_time = min(start_time + ROUTE_EVENT_WINDOW_DURATION, path.duration())
        self._gather_route_events(path, start_time=start_time, end_time=end_time)
        self._route_event_context.schedule_route_events(self.owner, path)
        self._route_event_context.process_route_events(self.owner)
        for slave_data in self.get_routing_slave_data():
            slave_data.slave.routing_component.schedule_and_process_route_events_for_new_path(path)
        if gsi_handlers.route_event_handlers.archiver.enabled:
            gsi_handlers.route_event_handlers.archive_route_events(path, self.owner, gsi_handlers.route_event_handlers.PATH_TYPE_INITIAL)

    def append_route_events_to_route_msg(self, route_msg):
        if self._route_event_context is None:
            return
        self._route_event_context.append_route_events_to_route_msg(route_msg)
        for slave_data in self.get_routing_slave_data():
            slave_data.slave.routing_component.append_route_events_to_route_msg(route_msg)

    def update_route_events_for_current_path(self, path, current_time, time_offset):
        (failed_events, failed_types) = self._route_event_context.prune_stale_events_and_get_failed_types(self.owner, path, current_time)
        start_time = current_time
        window_duration = ROUTE_EVENT_WINDOW_DURATION
        if time_offset < 0:
            window_duration -= time_offset
        end_time = min(start_time + window_duration, path.duration())
        self._gather_route_events(path, failed_types=failed_types, start_time=start_time, end_time=end_time)
        self._route_event_context.schedule_route_events(self.owner, path, start_time=start_time)
        should_update = True if failed_events else False or self._route_event_context.has_pending_events_to_process()
        for slave_data in self.get_routing_slave_data():
            should_update |= slave_data.slave.routing_component.update_route_events_for_current_path(path, current_time, time_offset)
        if gsi_handlers.route_event_handlers.archiver.enabled and gsi_handlers.route_event_handlers.update_log_enabled:
            gsi_handlers.route_event_handlers.archive_route_events(path, self.owner, gsi_handlers.route_event_handlers.PATH_TYPE_UPDATE)
        return should_update

    def process_updated_route_events(self):
        self._route_event_context.process_route_events(self.owner)
        for slave_data in self.get_routing_slave_data():
            slave_data.slave.routing_component.process_updated_route_events()

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

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

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

    def setup(self):
        self._setup = True

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def get_actual_target_sim_info(self):
        pass

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

    def get_required_target_sim_info(self):
        pass

    def get_secondary_sim_info(self):
        return self._secondary_sim_info

    @property
    def created_time(self):
        pass

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

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

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

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

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

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

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

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

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

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

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

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

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

    def show_goal_awarded_notification(self):
        if self.goal_awarded_notification is None:
            return
        icon_override = IconInfoData(icon_resource=self.display_icon)
        secondary_icon_override = IconInfoData(obj_instance=self._sim_info)
        notification = self.goal_awarded_notification(self._sim_info)
        notification.show_dialog(
            additional_tokens=self.get_localization_tokens(),
            icon_override=icon_override,
            secondary_icon_override=secondary_icon_override)
Пример #3
0
class SituationGoal(metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.SITUATION_GOAL)):
    __qualname__ = 'SituationGoal'
    INSTANCE_SUBCLASSES_ONLY = True
    IS_TARGETED = False
    INSTANCE_TUNABLES = {'_display_name': TunableLocalizedStringFactory(description='\n                Display name for the Situation Goal. It takes one token, the\n                target (0) of this situation goal.\n                ', tuning_group=GroupNames.UI), '_icon': TunableResourceKey('PNG:missing_image', description='\n                Icon to be displayed for the Situation Goal.\n                ', needs_tuning=True, resource_types=sims4.resources.CompoundTypes.IMAGE), '_pre_tests': TunableSituationGoalPreTestSet(description='\n                A set of tests on the player sim and environment that all must\n                pass for the goal to be given to the player. e.g. Player Sim\n                has cooking skill level 7.\n                ', tuning_group=GroupNames.TESTS), '_post_tests': TunableSituationGoalPostTestSet(description='\n                A set of tests that must all pass when the player satisfies the\n                goal_test for the goal to be consider completed. e.g. Player\n                has Drunk Buff when Kissing another sim at Night.\n                ', tuning_group=GroupNames.TESTS), '_environment_pre_tests': TunableSituationGoalEnvironmentPreTestSet(description='\n                A set of sim independent pre tests.\n                e.g. There are five desks.\n                ', tuning_group=GroupNames.TESTS), 'role_tags': TunableSet(TunableEnumEntry(Tag, Tag.INVALID), description='\n                This goal will only be given to Sims in SituationJobs or Role\n                States marked with one of these tags.\n                '), '_cooldown': TunableSimMinute(description='\n                The cooldown of this situation goal.  Goals that have been\n                completed will not be chosen again for the amount of time that\n                is tuned.\n                ', default=600, minimum=0), '_iterations': Tunable(description='\n                     Number of times the player must perform the action to complete the goal\n                     ', tunable_type=int, default=1), '_score': Tunable(description='\n                    The number of points received for completing the goal.\n                    ', tunable_type=int, default=10), '_goal_loot_list': TunableList(description='\n            A list of pre-defined loot actions that will applied to every\n            sim in the situation when this situation goal is completed.\n             \n            Do not use this loot list in an attempt to undo changes made by\n            the RoleStates to the sim. For example, do not attempt\n            to remove buffs or commodities added by the RoleState.\n            ', tunable=SituationGoalLootActions.TunableReference()), '_tooltip': TunableLocalizedStringFactory(description='\n            Tooltip for this situation goal. It takes one token, the\n            actor (0) of this situation goal.'), 'noncancelable': Tunable(description='\n            Checking this box will prevent the player from canceling this goal in the whim system.', tunable_type=bool, needs_tuning=True, default=False), 'time_limit': Tunable(description='\n            Timeout (in Sim minutes) for Sim to complete this goal. The default state of 0 means\n            time is unlimited. If the goal is not completed in time, any tuned penalty loot is applied.', tunable_type=int, default=0), 'penalty_loot_list': TunableList(description='\n            A list of pre-defined loot actions that will applied to the Sim who fails\n            to complete this goal within the tuned time limit.\n            ', tunable=SituationGoalLootActions.TunableReference()), 'goal_completion_notification': OptionalTunable(tunable=UiDialogNotification.TunableFactory(description='\n                A TNS that will fire when this situation goal is completed.\n                ')), 'audio_sting_on_complete': TunableResourceKey(description='\n            The sound to play when this goal is completed.\n            ', default=None, resource_types=(sims4.resources.Types.PROPX,), tuning_group=GroupNames.AUDIO), 'goal_completion_modal_dialog': OptionalTunable(tunable=UiDialogOk.TunableFactory(description='\n                A modal dialog that will fire when this situation goal is\n                completed.\n                '))}

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

    def __init__(self, sim_info=None, situation=None, goal_id=0, count=0, **kwargs):
        self._sim_info = sim_info
        self._situation = situation
        self.id = goal_id
        self._on_goal_completed_callbacks = CallableList()
        self._completed_time = None
        self._count = count

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

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

    def create_seedling(self):
        actor_id = 0 if self._sim_info is None else self._sim_info.sim_id
        seedling = situations.situation_serialization.GoalSeedling(type(self), actor_id, self._count)
        return seedling

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

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

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

    def _on_goal_completed(self):
        self._completed_time = services.time_service().sim_now
        for loots in self._goal_loot_list:
            for loot in loots.goal_loot_actions:
                for sim in self._situation.all_sims_in_situation_gen():
                    loot.apply_to_resolver(sim.get_resolver())
        client = services.client_manager().get_first_client()
        if client is not None:
            active_sim = client.active_sim
            resolver = SingleSimResolver(active_sim)
            if self.goal_completion_notification is not None:
                notification = self.goal_completion_notification(active_sim, resolver=resolver)
                notification.show_dialog()
            if self.goal_completion_modal_dialog is not None:
                dialog = self.goal_completion_modal_dialog(active_sim, resolver=resolver)
                dialog.show_dialog()
        self._on_goal_completed_callbacks(self, True)

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

    def debug_force_complete(self, target_sim):
        self._count = self._iterations
        self._on_goal_completed()

    def handle_event(self, sim_info, event, resolver):
        if self._sim_info is not None and self._sim_info is not sim_info:
            return
        if self._run_goal_completion_tests(sim_info, event, resolver):
            if self._count >= self._iterations:
                self._on_goal_completed()
            else:
                self._on_iteration_completed()

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

    def _get_actual_target_sim_info(self):
        pass

    def get_required_target_sim_info(self):
        pass

    @property
    def created_time(self):
        pass

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

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

    def get_localization_tokens(self):
        if self._sim_info is None:
            return (self._numerical_token,)
        required_tgt_sim = self.get_required_target_sim_info()
        if required_tgt_sim is None:
            return (self._numerical_token, self._sim_info)
        return (self._numerical_token, self._sim_info, required_tgt_sim)

    @property
    def display_name(self):
        return self._display_name(*self.get_localization_tokens())

    @property
    def tooltip(self):
        return self._tooltip(*self.get_localization_tokens())

    @property
    def score(self):
        return self._score

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

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

    @property
    def _numerical_token(self):
        return self.max_iterations
Пример #4
0
class CraftingComponent(Component,
                        component_name=types.CRAFTING_COMPONENT,
                        persistence_key=protocols.PersistenceMaster.
                        PersistableData.CraftingComponent,
                        allow_dynamic=True):
    def __init__(self, owner):
        super().__init__(owner)
        self._crafting_process = None
        self._use_base_recipe = False
        self.object_mutated_listeners = CallableList()
        self._servings_statistic_tracker_handle = None
        self._quality_change_callback_added = False
        self._spoil_listener_handle = None
        self._is_final_product = False
        self._last_spoiled_time = None
        self._spoil_timer_state_value = CraftingTuning.SPOILED_STATE_VALUE

    @property
    def is_final_product(self):
        return self._is_final_product

    def set_final_product(self, is_final_product):
        self._is_final_product = is_final_product

    @componentmethod_with_fallback(lambda: (DEFAULT, DEFAULT))
    def get_template_content_overrides(self):
        is_final_product = self._crafting_process.phase is None or self._crafting_process.phase.object_info_is_final_product
        if is_final_product or self.owner.name_component is None:
            return (DEFAULT, DEFAULT)
        name_component = self._crafting_process.recipe.final_product_definition.cls.tuned_components.name
        if name_component is not None and name_component.templates:
            selected_template = random.choice(name_component.templates)
            return (selected_template.template_name,
                    selected_template.template_description)
        return (DEFAULT, DEFAULT)

    def on_add(self, *_, **__):
        tracker = self.owner.get_tracker(CraftingTuning.SERVINGS_STATISTIC)
        self._servings_statistic_tracker_handle = tracker.add_watcher(
            self._on_servings_change)
        if tracker.has_statistic(CraftingTuning.SERVINGS_STATISTIC):
            current_value = tracker.get_value(
                CraftingTuning.SERVINGS_STATISTIC)
            if current_value is not None:
                self.owner.update_tooltip_field(TooltipFieldsComplete.servings,
                                                max(int(current_value), 0),
                                                should_update=True)
        if self.owner.state_component is not None:
            self.owner.add_state_changed_callback(self._on_object_state_change)
            self._quality_change_callback_added = True
            consumable_state_value = self.owner.get_state_value_from_stat_type(
                CraftingTuning.CONSUME_STATISTIC)
            if consumable_state_value is not None:
                self._on_object_state_change(self.owner,
                                             consumable_state_value.state,
                                             consumable_state_value,
                                             consumable_state_value)
            quality_state_value = self.owner.get_state_value_from_stat_type(
                CraftingTuning.QUALITY_STATISTIC)
            if quality_state_value is not None:
                self._on_object_state_change(self.owner,
                                             quality_state_value.state,
                                             quality_state_value,
                                             quality_state_value)
            freshness_state = CraftingTuning.LOCK_FRESHNESS_STATE_VALUE.state
            if self.owner.has_state(freshness_state):
                freshness_locked_state_value = self.owner.get_state(
                    freshness_state)
                if freshness_locked_state_value is CraftingTuning.LOCK_FRESHNESS_STATE_VALUE:
                    self._on_object_state_change(
                        self.owner, freshness_locked_state_value.state,
                        freshness_locked_state_value,
                        freshness_locked_state_value)

    def on_remove(self, *_, **__):
        if self._servings_statistic_tracker_handle is not None:
            tracker = self.owner.get_tracker(CraftingTuning.SERVINGS_STATISTIC)
            if tracker.has_watcher(self._servings_statistic_tracker_handle):
                tracker.remove_watcher(self._servings_statistic_tracker_handle)
            self._servings_statistic_tracker_handle = None
        if self._quality_change_callback_added:
            self.owner.remove_state_changed_callback(
                self._on_object_state_change)
            self._quality_change_callback_added = False
        if self._spoil_listener_handle is not None:
            spoil_tracker = self.owner.get_tracker(
                self._spoil_timer_state_value.state.linked_stat)
            spoil_tracker.remove_listener(self._spoil_listener_handle)
        self._remove_hovertip()

    def on_mutated(self):
        self.object_mutated_listeners()
        self.object_mutated_listeners.clear()
        self.owner.remove_component(types.CRAFTING_COMPONENT)
        self.on_remove()

    @property
    def crafter_sim_id(self):
        if self._crafting_process.crafter_sim_id:
            return self._crafting_process.crafter_sim_id
        return 0

    def _on_servings_change(self, stat_type, old_value, new_value):
        if stat_type is CraftingTuning.SERVINGS_STATISTIC:
            owner = self.owner
            self.owner.update_tooltip_field(TooltipFieldsComplete.servings,
                                            max(int(new_value), 0),
                                            should_update=True)
            current_inventory = owner.get_inventory()
            if current_inventory is not None:
                current_inventory.push_inventory_item_update_msg(owner)
            if new_value <= 0 and old_value != new_value:
                self.on_mutated()

    def _on_object_state_change(self, owner, state, old_value, new_value):
        state_value = None
        if state is CraftingTuning.QUALITY_STATE:
            state_value = new_value
        elif state is CraftingTuning.FRESHNESS_STATE:
            if new_value in CraftingTuning.QUALITY_STATE_VALUE_MAP:
                state_value = new_value
            else:
                self.owner.update_tooltip_field(
                    TooltipFieldsComplete.quality_description, None)
                if self.owner.has_state(CraftingTuning.QUALITY_STATE):
                    state_value = self.owner.get_state(
                        CraftingTuning.QUALITY_STATE)
        if state_value is not None:
            value_quality = CraftingTuning.QUALITY_STATE_VALUE_MAP.get(
                state_value)
            if value_quality is not None:
                self.owner.update_tooltip_field(
                    TooltipFieldsComplete.quality,
                    value_quality.state_star_number)
                self.owner.update_tooltip_field(
                    TooltipFieldsComplete.quality_description, None)
        if owner.has_state(
                CraftingTuning.SPOILED_STATE_VALUE.state) and owner.get_state(
                    CraftingTuning.SPOILED_STATE_VALUE.state
                ) is CraftingTuning.SPOILED_STATE_VALUE:
            if self._crafting_process is not None:
                recipe = self._get_recipe()
                if recipe.show_spoiled_quality_description:
                    self.owner.update_tooltip_field(
                        TooltipFieldsComplete.quality_description,
                        CraftingTuning.SPOILED_STRING)
                self.owner.update_tooltip_field(
                    TooltipFieldsComplete.spoiled_time, 0)
        elif owner.has_state(
                CraftingTuning.MASTERWORK_STATE
        ) and owner.get_state(
                CraftingTuning.MASTERWORK_STATE
        ) is CraftingTuning.MASTERWORK_STATE_VALUE and self._crafting_process is not None:
            recipe = self._get_recipe()
            if recipe is not None and recipe.masterwork_name is not None:
                self.owner.update_tooltip_field(
                    TooltipFieldsComplete.quality_description,
                    recipe.masterwork_name)
        if state is CraftingTuning.CONSUMABLE_STATE:
            value_consumable = CraftingTuning.CONSUMABLE_STATE_VALUE_MAP.get(
                new_value)
            if value_consumable is not None:
                self.owner.update_tooltip_field(
                    TooltipFieldsComplete.percentage_left, value_consumable)
        if new_value is CraftingTuning.CONSUMABLE_EMPTY_STATE_VALUE and old_value is not CraftingTuning.CONSUMABLE_EMPTY_STATE_VALUE:
            self._remove_hovertip()
        if new_value is CraftingTuning.LOCK_FRESHNESS_STATE_VALUE:
            self.owner.update_tooltip_field(TooltipFieldsComplete.spoiled_time,
                                            0)
            if self._spoil_listener_handle is not None:
                spoil_tracker = self.owner.get_tracker(
                    self._spoil_timer_state_value.state.linked_stat)
                spoil_tracker.remove_listener(self._spoil_listener_handle)
                self._spoil_listener_handle = None
        self.owner.update_object_tooltip()

    def _add_hovertip(self):
        if self._is_final_product and self._is_finished():
            self._add_consumable_hovertip()

    def _is_finished(self):
        crafting_process = self._crafting_process
        tracker = None
        stat = CraftingTuning.PROGRESS_STATISTIC
        if crafting_process.current_ico is not None:
            tracker = crafting_process.current_ico.get_tracker(stat)
        if tracker is None:
            tracker = self.owner.get_tracker(stat)
        if not (tracker is None or not tracker.has_statistic(stat)):
            tracker = crafting_process.get_tracker(stat)
        if tracker.has_statistic(
                stat) and tracker.get_value(stat) != stat.max_value:
            return crafting_process.is_complete
        return True

    def _get_recipe(self):
        recipe = self._crafting_process.recipe
        if self._use_base_recipe:
            recipe = recipe.get_base_recipe()
        return recipe

    def _add_consumable_hovertip(self):
        owner = self.owner
        owner.hover_tip = ui_protocols.UiObjectMetadata.HOVER_TIP_CONSUMABLE_CRAFTABLE
        crafting_process = self._crafting_process
        recipe = self._get_recipe()
        if recipe is None:
            return
        self.owner.update_tooltip_field(
            TooltipFieldsComplete.recipe_name,
            recipe.get_recipe_name(crafting_process.crafter))
        crafted_by_text = crafting_process.get_crafted_by_text(
            is_from_gallery=self.owner.is_from_gallery)
        crafted_with_text = crafting_process.get_crafted_with_text()
        if crafted_by_text is not None:
            if crafted_with_text is not None:
                crafted_by_text = LocalizationHelperTuning.NEW_LINE_LIST_STRUCTURE(
                    crafted_by_text, crafted_with_text)
            crafter_sim_id = crafting_process.crafter_sim_id
            if crafter_sim_id is not None:
                self.owner.update_tooltip_field(
                    TooltipFieldsComplete.crafter_sim_id, crafter_sim_id)
        elif crafted_with_text is not None:
            crafted_by_text = crafted_with_text
        self.owner.update_tooltip_field(TooltipFieldsComplete.crafted_by_text,
                                        crafted_by_text)
        if owner.has_state(CraftingTuning.QUALITY_STATE):
            value_quality = CraftingTuning.QUALITY_STATE_VALUE_MAP.get(
                owner.get_state(CraftingTuning.QUALITY_STATE))
            if value_quality is not None:
                self.owner.update_tooltip_field(
                    TooltipFieldsComplete.quality,
                    value_quality.state_star_number)
        if owner.has_state(
                CraftingTuning.MASTERWORK_STATE
        ) and owner.get_state(
                CraftingTuning.MASTERWORK_STATE
        ) is CraftingTuning.MASTERWORK_STATE_VALUE and recipe.masterwork_name is not None:
            self.owner.update_tooltip_field(
                TooltipFieldsComplete.quality_description,
                recipe.masterwork_name)
        inscription = crafting_process.inscription
        if inscription is not None:
            self.owner.update_tooltip_field(TooltipFieldsComplete.inscription,
                                            inscription)
        self._add_spoil_listener(
            state_override=recipe.spoil_time_commodity_override)
        if recipe.time_until_spoiled_string_override is not None:
            self.owner.update_tooltip_field(
                TooltipFieldsComplete.spoiled_time_text,
                recipe.time_until_spoiled_string_override)
        subtext = self.owner.get_state_strings()
        if subtext is not None:
            self.owner.update_tooltip_field(TooltipFieldsComplete.subtext,
                                            subtext)
        recipe.update_hovertip(self.owner, crafter=crafting_process.crafter)
        current_inventory = owner.get_inventory()
        if current_inventory is not None:
            current_inventory.push_inventory_item_update_msg(owner)
        self.owner.on_hovertip_requested()
        self.owner.update_object_tooltip()

    def update_simoleon_tooltip(self):
        self._crafting_process.apply_simoleon_value(self.owner)
        recipe = self._get_recipe()
        recipe.update_hovertip(self.owner,
                               crafter=self._crafting_process.crafter)
        self.owner.update_object_tooltip()

    def update_quality_tooltip(self):
        owner = self.owner
        recipe = self._get_recipe()
        if owner.has_state(CraftingTuning.QUALITY_STATE):
            value_quality = CraftingTuning.QUALITY_STATE_VALUE_MAP.get(
                owner.get_state(CraftingTuning.QUALITY_STATE))
            if value_quality is not None:
                self.owner.update_tooltip_field(
                    TooltipFieldsComplete.quality,
                    value_quality.state_star_number)
        if owner.has_state(CraftingTuning.MASTERWORK_STATE):
            quality_description = recipe.masterwork_name if owner.get_state(
                CraftingTuning.MASTERWORK_STATE
            ) is CraftingTuning.MASTERWORK_STATE_VALUE else None
            if quality_description is not None:
                self.owner.update_tooltip_field(
                    TooltipFieldsComplete.quality_description,
                    quality_description)

    def _remove_hovertip(self):
        owner = self.owner
        owner.hover_tip = ui_protocols.UiObjectMetadata.HOVER_TIP_DISABLED
        self.owner.update_tooltip_field(TooltipFieldsComplete.recipe_name,
                                        None)
        self.owner.update_tooltip_field(
            TooltipFieldsComplete.recipe_description, None)
        self.owner.update_tooltip_field(TooltipFieldsComplete.crafter_sim_id,
                                        0)
        self.owner.update_tooltip_field(TooltipFieldsComplete.crafted_by_text,
                                        None)
        self.owner.update_tooltip_field(TooltipFieldsComplete.quality, 0)
        self.owner.update_tooltip_field(TooltipFieldsComplete.servings, 0)
        self.owner.update_tooltip_field(TooltipFieldsComplete.spoiled_time, 0)
        self.owner.update_tooltip_field(TooltipFieldsComplete.percentage_left,
                                        None)
        self.owner.update_tooltip_field(TooltipFieldsComplete.style_name, None)
        if self.owner.get_tooltip_field(
                TooltipFieldsComplete.simoleon_value) is not None:
            self.owner.update_tooltip_field(
                TooltipFieldsComplete.simoleon_value, owner.current_value)
        self.owner.update_tooltip_field(TooltipFieldsComplete.main_icon, None)
        self.owner.update_tooltip_field(TooltipFieldsComplete.sub_icons, None)
        self.owner.update_tooltip_field(
            TooltipFieldsComplete.quality_description, None)
        self.owner.update_tooltip_field(TooltipFieldsComplete.subtext, None)
        self.owner.update_object_tooltip()

    def _on_spoil_time_changed(self, _, spoiled_time):
        if self._last_spoiled_time is None or self._last_spoiled_time != spoiled_time:
            self._last_spoiled_time = spoiled_time
        if RetailComponent.SOLD_STATE is not None and self.owner.has_state(
                RetailComponent.SOLD_STATE.state) and self.owner.get_state(
                    RetailComponent.SOLD_STATE.state
                ) is RetailComponent.SOLD_STATE:
            self.owner.update_tooltip_field(TooltipFieldsComplete.spoiled_time,
                                            0,
                                            should_update=True)
            return
        if spoiled_time is not None:
            time_in_ticks = spoiled_time.absolute_ticks()
            self.owner.update_tooltip_field(TooltipFieldsComplete.spoiled_time,
                                            time_in_ticks,
                                            should_update=True)
            logger.debug('{} will get spoiled at {}', self.owner, spoiled_time)
        else:
            self.owner.update_tooltip_field(TooltipFieldsComplete.spoiled_time,
                                            0,
                                            should_update=True)

    def _on_spoiled(self, _):
        self._last_spoiled_time = None

    def _add_spoil_listener(self, state_override=None):
        check_operator = operator.lt
        if state_override is not None:
            self._spoil_timer_state_value = state_override.state_to_track
            check_operator = state_override.commodity_check_operator
        if self._spoil_listener_handle is None:
            if self.owner.has_state(self._spoil_timer_state_value.state):
                linked_stat = self._spoil_timer_state_value.state.linked_stat
                tracker = self.owner.get_tracker(linked_stat)
                if tracker is None:
                    return
                threshold = sims4.math.Threshold()
                threshold.value = self._spoil_timer_state_value.range.upper_bound
                threshold.comparison = check_operator
                self._spoil_listener_handle = tracker.create_and_add_listener(
                    linked_stat,
                    threshold,
                    self._on_spoiled,
                    on_callback_alarm_reset=self._on_spoil_time_changed)

    def _on_crafting_process_updated(self):
        if self._crafting_process.recipe is not None:
            self._add_hovertip()
            self.owner.update_component_commodity_flags()

    @componentmethod
    def set_crafting_process(self,
                             crafting_process,
                             use_base_recipe=False,
                             is_final_product=False,
                             from_load=False):
        if is_final_product and (not from_load
                                 and self._crafting_process is not None
                                 ) and crafting_process.multiple_order_process:
            new_process = crafting_process.copy_for_serve_interaction(
                crafting_process.get_order_or_recipe())
            self._crafting_process.linked_process = new_process
            self._crafting_process = new_process
        else:
            self._crafting_process = crafting_process
        self._use_base_recipe = use_base_recipe
        self._is_final_product = is_final_product
        self._on_crafting_process_updated()

    @componentmethod
    def get_crafting_process(self):
        return self._crafting_process

    @componentmethod
    def on_crafting_process_finished(self):
        self._add_hovertip()
        self.owner.update_component_commodity_flags()
        crafting_process = self._crafting_process
        if crafting_process is None:
            return
        crafting_process.clear_refundables()
        if crafting_process.current_ico is None:
            crafting_process.current_ico = self.owner
        recipe = crafting_process.recipe
        if self._use_base_recipe:
            recipe = recipe.get_base_recipe()
        skill_test = recipe.skill_test
        if crafting_process.crafter_sim_id is None:
            return
        sim_info = services.sim_info_manager().get(
            crafting_process.crafter_sim_id)
        created_object_quality = self.owner.get_state(
            CraftingTuning.QUALITY_STATE) if self.owner.has_state(
                CraftingTuning.QUALITY_STATE) else None
        created_object_masterwork = self.owner.get_state(
            CraftingTuning.MASTERWORK_STATE) if self.owner.has_state(
                CraftingTuning.MASTERWORK_STATE) else None
        services.get_event_manager().process_event(
            test_events.TestEvent.ItemCrafted,
            sim_info=sim_info,
            crafted_object=self.owner,
            skill=skill_test.skill if skill_test is not None else None,
            quality=created_object_quality,
            masterwork=created_object_masterwork)

    @componentmethod
    def get_recipe(self):
        if self._crafting_process.recipe is None:
            return
        if self.owner.definition is not self._crafting_process.recipe.final_product.definition:
            for linked_recipe in self._crafting_process.recipe.linked_recipes_map.values(
            ):
                if self.owner.definition is linked_recipe.final_product.definition:
                    return linked_recipe
        return self._crafting_process.recipe

    @componentmethod
    def get_photo_definition(self):
        recipe = self.get_recipe()
        if recipe is None:
            return
        return recipe.photo_definition

    @componentmethod_with_fallback(lambda *_, **__: None)
    def get_craftable_property(self, property_type):
        recipe = self._get_recipe()
        crafting_process = self._crafting_process
        if crafting_process is None:
            return
        if property_type == GameObjectProperty.RECIPE_NAME:
            return recipe.get_recipe_name(crafting_process.crafter)
        if property_type == GameObjectProperty.RECIPE_DESCRIPTION:
            return recipe.recipe_description(crafting_process.crafter)
        logger.error(
            'Requested crafting property_type {} not found on game_object'.
            format(property_type),
            owner='camilogarcia')

    @componentmethod
    def get_consume_affordance(self, context=None):
        if not self._is_final_product:
            logger.warn(
                "Attempting to get consume affordances for something that isn't final product."
            )
            return
        error_dict = {}
        consumable_component = self.owner.consumable_component
        if consumable_component is not None:
            if context is not None:
                for consume_affordance in consumable_component.consume_affordances:
                    result = consume_affordance.test(target=self.owner,
                                                     context=context)
                    if result:
                        return consume_affordance
                    error_dict[consume_affordance] = result
            else:
                consume_affordance = next(
                    iter(consumable_component.consume_affordances), None)
                if consume_affordance is not None:
                    return consume_affordance
        for affordance in self.owner.super_affordances():
            from crafting.crafting_interactions import GrabServingSuperInteraction
            if issubclass(affordance, GrabServingSuperInteraction):
                if not affordance.consume_affordances_override:
                    return affordance
        if error_dict:
            logger.error(
                'Failed to find valid consume affordance. Consumable component affordances tested as follows:\n\n{}',
                error_dict)

    @componentmethod_with_fallback(lambda: None)
    def get_notebook_information(self, notebook_entry, notebook_sub_entries):
        recipe = self.get_recipe()
        if not recipe.use_ingredients:
            return
        sub_entries = (SubEntryData(recipe.guid64, False), )
        return (notebook_entry(None, sub_entries=sub_entries), )

    def _icon_override_gen(self):
        recipe = self.get_recipe()
        if recipe.icon_override is not None:
            yield recipe.icon_override

    @componentmethod
    def set_ready_to_serve(self):
        self._crafting_process.ready_to_serve = True

    def component_super_affordances_gen(self, **kwargs):
        recipe = self.get_recipe()
        recipe = recipe.get_base_recipe()
        if not self._use_base_recipe or recipe is None:
            return
        for sa in recipe.final_product.super_affordances:
            yield sa
        for linked_recipe in recipe.linked_recipes_map.values():
            for sa in linked_recipe.final_product.super_affordances:
                yield sa
        if not (self._crafting_process.is_complete or self._crafting_process.
                ready_to_serve) or recipe.resume_affordance:
            yield recipe.resume_affordance
        else:
            yield CraftingTuning.DEFAULT_RESUME_AFFORDANCE

    def component_interactable_gen(self):
        yield self

    def on_client_connect(self, client):
        if self._crafting_process.recipe is not None:
            self._add_hovertip()

    @componentmethod_with_fallback(lambda: False)
    def has_servings_statistic(self):
        tracker = self.owner.get_tracker(CraftingTuning.SERVINGS_STATISTIC)
        if tracker is None or not tracker.has_statistic(
                CraftingTuning.SERVINGS_STATISTIC):
            return False
        return True

    def _on_households_loaded_update(self):
        self._add_hovertip()

    @componentmethod_with_fallback(lambda: None)
    def post_tooltip_save_data_stored(self):
        if self._last_spoiled_time is not None:
            self._on_spoil_time_changed(None, self._last_spoiled_time)

    def get_recipe_effect_overrides(self, effect_name):
        if self._crafting_process.recipe is not None and self._crafting_process.recipe:
            effect_override = self._crafting_process.recipe.vfx_overrides.get(
                effect_name)
            if effect_override is not None:
                return effect_override
        return effect_name

    def component_anim_overrides_gen(self):
        if self._crafting_process.recipe is not None and self._crafting_process.recipe.anim_overrides is not None:
            yield self._crafting_process.recipe.anim_overrides

    def save(self, persistence_master_message):
        logger.info(
            '[PERSISTENCE]: ----Start saving crafting component of {0}.',
            self.owner)
        self.owner.update_tooltip_field(TooltipFieldsComplete.spoiled_time,
                                        0,
                                        should_update=True)
        persistable_data = protocols.PersistenceMaster.PersistableData()
        persistable_data.type = protocols.PersistenceMaster.PersistableData.CraftingComponent
        crafting_save = persistable_data.Extensions[
            protocols.PersistableCraftingComponent.persistable_data]
        self._crafting_process.save(crafting_save.process)
        crafting_save.use_base_recipe = self._use_base_recipe
        crafting_save.is_final_product = self._is_final_product
        persistence_master_message.data.extend([persistable_data])

    def load(self, crafting_save_message):
        logger.info(
            '[PERSISTENCE]: ----Start loading crafting component of {0}.',
            self.owner)
        crafting_component_data = crafting_save_message.Extensions[
            protocols.PersistableCraftingComponent.persistable_data]
        crafting_process = CraftingProcess()
        crafting_process.load(crafting_component_data.process)
        self.set_crafting_process(crafting_process,
                                  crafting_component_data.use_base_recipe,
                                  crafting_component_data.is_final_product,
                                  from_load=True)
        recipe = self.get_recipe()
        if recipe is not None:
            if crafting_process.crafted_value is not None:
                self.owner.base_value = crafting_process.crafted_value
            services.current_zone().register_callback(
                zone_types.ZoneState.HOUSEHOLDS_AND_SIM_INFOS_LOADED,
                self._on_households_loaded_update)
            self.owner.append_tags(recipe.apply_tags)
Пример #5
0
class SituationGoal(metaclass=HashedTunedInstanceMetaclass,
                    manager=services.get_instance_manager(
                        sims4.resources.Types.SITUATION_GOAL)):
    __qualname__ = 'SituationGoal'
    INSTANCE_SUBCLASSES_ONLY = True
    IS_TARGETED = False
    INSTANCE_TUNABLES = {
        '_display_name':
        TunableLocalizedStringFactory(
            description=
            '\n                Display name for the Situation Goal. It takes one token, the\n                target (0) of this situation goal.\n                ',
            tuning_group=GroupNames.UI),
        '_icon':
        TunableResourceKey(
            'PNG:missing_image',
            description=
            '\n                Icon to be displayed for the Situation Goal.\n                ',
            needs_tuning=True,
            resource_types=sims4.resources.CompoundTypes.IMAGE),
        '_pre_tests':
        TunableSituationGoalPreTestSet(
            description=
            '\n                A set of tests on the player sim and environment that all must\n                pass for the goal to be given to the player. e.g. Player Sim\n                has cooking skill level 7.\n                ',
            tuning_group=GroupNames.TESTS),
        '_post_tests':
        TunableSituationGoalPostTestSet(
            description=
            '\n                A set of tests that must all pass when the player satisfies the\n                goal_test for the goal to be consider completed. e.g. Player\n                has Drunk Buff when Kissing another sim at Night.\n                ',
            tuning_group=GroupNames.TESTS),
        '_environment_pre_tests':
        TunableSituationGoalEnvironmentPreTestSet(
            description=
            '\n                A set of sim independent pre tests.\n                e.g. There are five desks.\n                ',
            tuning_group=GroupNames.TESTS),
        'role_tags':
        TunableSet(
            TunableEnumEntry(Tag, Tag.INVALID),
            description=
            '\n                This goal will only be given to Sims in SituationJobs or Role\n                States marked with one of these tags.\n                '
        ),
        '_cooldown':
        TunableSimMinute(
            description=
            '\n                The cooldown of this situation goal.  Goals that have been\n                completed will not be chosen again for the amount of time that\n                is tuned.\n                ',
            default=600,
            minimum=0),
        '_iterations':
        Tunable(
            description=
            '\n                     Number of times the player must perform the action to complete the goal\n                     ',
            tunable_type=int,
            default=1),
        '_score':
        Tunable(
            description=
            '\n                    The number of points received for completing the goal.\n                    ',
            tunable_type=int,
            default=10),
        '_goal_loot_list':
        TunableList(
            description=
            '\n            A list of pre-defined loot actions that will applied to every\n            sim in the situation when this situation goal is completed.\n             \n            Do not use this loot list in an attempt to undo changes made by\n            the RoleStates to the sim. For example, do not attempt\n            to remove buffs or commodities added by the RoleState.\n            ',
            tunable=SituationGoalLootActions.TunableReference()),
        '_tooltip':
        TunableLocalizedStringFactory(
            description=
            '\n            Tooltip for this situation goal. It takes one token, the\n            actor (0) of this situation goal.'
        ),
        'noncancelable':
        Tunable(
            description=
            '\n            Checking this box will prevent the player from canceling this goal in the whim system.',
            tunable_type=bool,
            needs_tuning=True,
            default=False),
        'time_limit':
        Tunable(
            description=
            '\n            Timeout (in Sim minutes) for Sim to complete this goal. The default state of 0 means\n            time is unlimited. If the goal is not completed in time, any tuned penalty loot is applied.',
            tunable_type=int,
            default=0),
        'penalty_loot_list':
        TunableList(
            description=
            '\n            A list of pre-defined loot actions that will applied to the Sim who fails\n            to complete this goal within the tuned time limit.\n            ',
            tunable=SituationGoalLootActions.TunableReference()),
        'goal_completion_notification':
        OptionalTunable(tunable=UiDialogNotification.TunableFactory(
            description=
            '\n                A TNS that will fire when this situation goal is completed.\n                '
        )),
        'audio_sting_on_complete':
        TunableResourceKey(
            description=
            '\n            The sound to play when this goal is completed.\n            ',
            default=None,
            resource_types=(sims4.resources.Types.PROPX, ),
            tuning_group=GroupNames.AUDIO),
        'goal_completion_modal_dialog':
        OptionalTunable(tunable=UiDialogOk.TunableFactory(
            description=
            '\n                A modal dialog that will fire when this situation goal is\n                completed.\n                '
        ))
    }

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

    def __init__(self,
                 sim_info=None,
                 situation=None,
                 goal_id=0,
                 count=0,
                 **kwargs):
        self._sim_info = sim_info
        self._situation = situation
        self.id = goal_id
        self._on_goal_completed_callbacks = CallableList()
        self._completed_time = None
        self._count = count

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

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

    def create_seedling(self):
        actor_id = 0 if self._sim_info is None else self._sim_info.sim_id
        seedling = situations.situation_serialization.GoalSeedling(
            type(self), actor_id, self._count)
        return seedling

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

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

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

    def _on_goal_completed(self):
        self._completed_time = services.time_service().sim_now
        for loots in self._goal_loot_list:
            for loot in loots.goal_loot_actions:
                for sim in self._situation.all_sims_in_situation_gen():
                    loot.apply_to_resolver(sim.get_resolver())
        client = services.client_manager().get_first_client()
        if client is not None:
            active_sim = client.active_sim
            resolver = SingleSimResolver(active_sim)
            if self.goal_completion_notification is not None:
                notification = self.goal_completion_notification(
                    active_sim, resolver=resolver)
                notification.show_dialog()
            if self.goal_completion_modal_dialog is not None:
                dialog = self.goal_completion_modal_dialog(active_sim,
                                                           resolver=resolver)
                dialog.show_dialog()
        self._on_goal_completed_callbacks(self, True)

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

    def debug_force_complete(self, target_sim):
        self._count = self._iterations
        self._on_goal_completed()

    def handle_event(self, sim_info, event, resolver):
        if self._sim_info is not None and self._sim_info is not sim_info:
            return
        if self._run_goal_completion_tests(sim_info, event, resolver):
            if self._count >= self._iterations:
                self._on_goal_completed()
            else:
                self._on_iteration_completed()

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

    def _get_actual_target_sim_info(self):
        pass

    def get_required_target_sim_info(self):
        pass

    @property
    def created_time(self):
        pass

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

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

    def get_localization_tokens(self):
        if self._sim_info is None:
            return (self._numerical_token, )
        required_tgt_sim = self.get_required_target_sim_info()
        if required_tgt_sim is None:
            return (self._numerical_token, self._sim_info)
        return (self._numerical_token, self._sim_info, required_tgt_sim)

    @property
    def display_name(self):
        return self._display_name(*self.get_localization_tokens())

    @property
    def tooltip(self):
        return self._tooltip(*self.get_localization_tokens())

    @property
    def score(self):
        return self._score

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

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

    @property
    def _numerical_token(self):
        return self.max_iterations
Пример #6
0
class BuffComponent(objects.components.Component, component_name=objects.components.types.BUFF_COMPONENT):
    __qualname__ = 'BuffComponent'
    DEFAULT_MOOD = TunableReference(services.mood_manager(), description='The default initial mood.')
    UPDATE_INTENSITY_BUFFER = TunableRange(description="\n        A buffer that prevents a mood from becoming active unless its intensity\n        is greater than the current active mood's intensity plus this amount.\n        \n        For example, if this tunable is 1, and the Sim is in a Flirty mood with\n        intensity 2, then a different mood would become the active mood only if\n        its intensity is 3+.\n        \n        If the predominant mood has an intensity that is less than the active\n        mood's intensity, that mood will become the active mood.\n        ", tunable_type=int, default=1, minimum=0)
    EXCLUSIVE_SET = TunableList(description='\n        A list of buff groups to determine which buffs are exclusive from each\n        other within the same group.  A buff cannot exist in more than one exclusive group.\n        \n        The following rule of exclusivity for a group:\n        1. Higher weight will always be added and remove any lower weight buffs\n        2. Lower weight buff will not be added if a higher weight already exist in component\n        3. Same weight buff will always be added and remove any buff with same weight.\n        \n        Example: Group 1:\n                    Buff1 with weight of 5 \n                    Buff2 with weight of 1\n                    Buff3 with weight of 1\n                 Group 2:\n                    Buff4 with weight of 6\n        \n        If sim has Buff1, trying to add Buff2 or Buff3 will not be added.\n        If sim has Buff2, trying to add Buff3 will remove Buff2 and add Buff3\n        If sim has Buff2, trying to add Buff1 will remove Buff 2 and add Buff3\n        If sim has Buff4, trying to add Buff1, Buff2, or Buff3 will be added and Buff4 will stay \n                          on component \n        ', tunable=TunableList(tunable=TunableTuple(buff_type=TunableReference(description='\n                    Buff in exclusive group\n                    ', manager=services.get_instance_manager(sims4.resources.Types.BUFF)), weight=Tunable(description='\n                    weight to determine if this buff should be added and\n                    remove other buffs in the exclusive group or not added at all.\n                    \n                    Example: Buff1 with weight of 5 \n                             Buff2 with weight of 1\n                             Buff3 with weight of 1\n                    \n                    If sim has Buff1, trying to add Buff2 or Buff3 will not be added.\n                    If sim has Buff2, trying to add Buff3 will remove Buff2 and add Buff3\n                    if sim has Buff2, trying to add Buff1 will remove Buff 2 and add Buff3\n                    ', tunable_type=int, default=1))))

    def __init__(self, owner):
        super().__init__(owner)
        self._active_buffs = {}
        self._get_next_handle_id = UniqueIdGenerator()
        self._success_chance_modification = 0
        self._active_mood = self.DEFAULT_MOOD
        self._active_mood_intensity = 0
        self._active_mood_buff_handle = None
        self.on_mood_changed = CallableList()
        self.on_mood_changed.append(self._publish_mood_update)
        self.on_mood_changed.append(self._send_mood_changed_event)
        self.load_in_progress = False
        self.on_buff_added = CallableList()
        self.on_buff_removed = CallableList()
        self.buff_update_alarms = {}
        if self._active_mood is None:
            logger.error('No default mood tuned in buff_component.py')
        elif self._active_mood.buffs:
            initial_buff_ref = self._active_mood.buffs[0]
            if initial_buff_ref and initial_buff_ref.buff_type:
                self._active_mood_buff_handle = self.add_buff(initial_buff_ref.buff_type)

    def __iter__(self):
        return self._active_buffs.values().__iter__()

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

    def on_sim_ready_to_simulate(self):
        for buff in self:
            buff.on_sim_ready_to_simulate()
        self._publish_mood_update()

    def on_sim_removed(self, *args, **kwargs):
        for buff in self:
            buff.on_sim_removed(*args, **kwargs)

    def clean_up(self):
        for (buff_type, buff_entry) in tuple(self._active_buffs.items()):
            self.remove_auto_update(buff_type)
            buff_entry.clean_up()
        self._active_buffs.clear()
        self.on_mood_changed.clear()
        self.on_buff_added.clear()
        self.on_buff_removed.clear()

    @objects.components.componentmethod
    def add_buff_from_op(self, buff_type, buff_reason=None):
        (can_add, _) = self._can_add_buff_type(buff_type)
        if not can_add:
            return False
        buff_commodity = buff_type.commodity
        if buff_commodity is not None:
            if not buff_type.refresh_on_add and self.has_buff(buff_type):
                return False
            tracker = self.owner.get_tracker(buff_commodity)
            if buff_commodity.convergence_value == buff_commodity.max_value:
                tracker.set_min(buff_commodity)
            else:
                tracker.set_max(buff_commodity)
            self.set_buff_reason(buff_type, buff_reason, use_replacement=True)
        else:
            self.add_buff(buff_type, buff_reason=buff_reason)
        return True

    @objects.components.componentmethod
    def add_buff(self, buff_type, buff_reason=None, update_mood=True, commodity_guid=None, replacing_buff=None, timeout_string=None, transition_into_buff_id=0, change_rate=None, immediate=False):
        replacement_buff_type = self._get_replacement_buff_type(buff_type)
        if replacement_buff_type is not None:
            return self.owner.add_buff(replacement_buff_type, buff_reason=buff_reason, update_mood=update_mood, commodity_guid=commodity_guid, replacing_buff=buff_type, timeout_string=timeout_string, transition_into_buff_id=transition_into_buff_id, change_rate=change_rate, immediate=immediate)
        (can_add, conflicting_buff_type) = self._can_add_buff_type(buff_type)
        if not can_add:
            return
        buff = self._active_buffs.get(buff_type)
        if buff is None:
            buff = buff_type(self.owner, commodity_guid, replacing_buff, transition_into_buff_id)
            self._active_buffs[buff_type] = buff
            buff.on_add(self.load_in_progress)
            self._update_chance_modifier()
            if update_mood:
                self._update_current_mood()
            if self.owner.household is not None:
                services.get_event_manager().process_event(test_events.TestEvent.BuffBeganEvent, sim_info=self.owner, sim_id=self.owner.sim_id, buff=buff_type)
                self.register_auto_update(self.owner, buff_type)
            self.on_buff_added(buff_type)
        handle_id = self._get_next_handle_id()
        buff.add_handle(handle_id, buff_reason=buff_reason)
        self.send_buff_update_msg(buff, True, change_rate=change_rate, immediate=immediate)
        if conflicting_buff_type is not None:
            self.remove_buff_by_type(conflicting_buff_type)
        return handle_id

    def _get_replacement_buff_type(self, buff_type):
        if buff_type.trait_replacement_buffs is not None:
            trait_tracker = self.owner.trait_tracker
            for (trait, replacement_buff_type) in buff_type.trait_replacement_buffs.items():
                while trait_tracker.has_trait(trait):
                    return replacement_buff_type

    def register_auto_update(self, sim_info_in, buff_type_in):
        if buff_type_in in self.buff_update_alarms:
            self.remove_auto_update(buff_type_in)
        if sim_info_in.is_selectable and buff_type_in.visible:
            self.buff_update_alarms[buff_type_in] = alarms.add_alarm(self, create_time_span(minutes=15), lambda _, sim_info=sim_info_in, buff_type=buff_type_in: services.get_event_manager().process_event(test_events.TestEvent.BuffUpdateEvent, sim_info=sim_info, sim_id=sim_info.sim_id, buff=buff_type), True)

    def remove_auto_update(self, buff_type):
        if buff_type in self.buff_update_alarms:
            alarms.cancel_alarm(self.buff_update_alarms[buff_type])
            del self.buff_update_alarms[buff_type]

    @objects.components.componentmethod
    def remove_buff(self, handle_id, update_mood=True, immediate=False, on_destroy=False):
        for (buff_type, buff_entry) in self._active_buffs.items():
            while handle_id in buff_entry.handle_ids:
                should_remove = buff_entry.remove_handle(handle_id)
                if should_remove:
                    del self._active_buffs[buff_type]
                    buff_entry.on_remove(not self.load_in_progress and not on_destroy)
                    if not on_destroy:
                        if update_mood:
                            self._update_current_mood()
                        self._update_chance_modifier()
                        self.send_buff_update_msg(buff_entry, False, immediate=immediate)
                        services.get_event_manager().process_event(test_events.TestEvent.BuffEndedEvent, sim_info=self.owner, sim_id=self.owner.sim_id, buff=buff_type)
                    if buff_type in self.buff_update_alarms:
                        self.remove_auto_update(buff_type)
                    self.on_buff_removed(buff_type)
                break

    @objects.components.componentmethod
    def get_buff_type(self, handle_id):
        for (buff_type, buff_entry) in self._active_buffs.items():
            while handle_id in buff_entry.handle_ids:
                return buff_type

    @objects.components.componentmethod
    def has_buff(self, buff_type):
        return buff_type in self._active_buffs

    @objects.components.componentmethod
    def get_active_buff_types(self):
        return self._active_buffs.keys()

    @objects.components.componentmethod
    def get_buff_reason(self, handle_id):
        for buff_entry in self._active_buffs.values():
            while handle_id in buff_entry.handle_ids:
                return buff_entry.buff_reason

    @objects.components.componentmethod
    def debug_add_buff_by_type(self, buff_type):
        (can_add, conflicting_buff_type) = self._can_add_buff_type(buff_type)
        if not can_add:
            return False
        if buff_type.commodity is not None:
            tracker = self.owner.get_tracker(buff_type.commodity)
            state_index = buff_type.commodity.get_state_index_matches_buff_type(buff_type)
            if state_index is not None:
                index = state_index + 1
                if index < len(buff_type.commodity.commodity_states):
                    commodity_to_value = buff_type.commodity.commodity_states[index].value - 1
                else:
                    commodity_to_value = buff_type.commodity.max_value
                tracker.set_value(buff_type.commodity, commodity_to_value)
            else:
                logger.error('commodity ({}) has no states with buff ({}), Buff will not be added.', buff_type.commodity, buff_type)
                return False
        else:
            self.add_buff(buff_type)
        if conflicting_buff_type is not None:
            self.remove_buff_by_type(conflicting_buff_type)
        return True

    @objects.components.componentmethod
    def remove_buff_by_type(self, buff_type, on_destroy=False):
        buff_entry = self._active_buffs.get(buff_type)
        self.remove_buff_entry(buff_entry, on_destroy=on_destroy)

    @objects.components.componentmethod
    def remove_buff_entry(self, buff_entry, on_destroy=False):
        if buff_entry is not None:
            if buff_entry.commodity is not None:
                tracker = self.owner.get_tracker(buff_entry.commodity)
                commodity_inst = tracker.get_statistic(buff_entry.commodity)
                if commodity_inst is not None and commodity_inst.core:
                    if not on_destroy:
                        logger.callstack('Attempting to explicitly remove the buff {}, which is given by a core commodity.                                           This would result in the removal of a core commodity and will be ignored.', buff_entry, owner='tastle', level=sims4.log.LEVEL_ERROR)
                    return
                tracker.remove_statistic(buff_entry.commodity, on_destroy=on_destroy)
            elif buff_entry.buff_type in self._active_buffs:
                buff_entry.on_remove(on_destroy)
                del self._active_buffs[buff_entry.buff_type]
                if not on_destroy:
                    self._update_chance_modifier()
                    self._update_current_mood()
                    self.send_buff_update_msg(buff_entry, False)
                    services.get_event_manager().process_event(test_events.TestEvent.BuffEndedEvent, sim_info=self.owner, buff=type(buff_entry), sim_id=self.owner.id)

    @objects.components.componentmethod
    def set_buff_reason(self, buff_type, buff_reason, use_replacement=False):
        if use_replacement:
            replacement_buff_type = self._get_replacement_buff_type(buff_type)
            if replacement_buff_type is not None:
                buff_type = replacement_buff_type
        buff_entry = self._active_buffs.get(buff_type)
        if buff_entry is not None and buff_reason is not None:
            buff_entry.buff_reason = buff_reason
            self.send_buff_update_msg(buff_entry, True)

    @objects.components.componentmethod
    def buff_commodity_changed(self, handle_id, change_rate=None):
        for (_, buff_entry) in self._active_buffs.items():
            while handle_id in buff_entry.handle_ids:
                if buff_entry.show_timeout:
                    self.send_buff_update_msg(buff_entry, True, change_rate=change_rate)
                break

    @objects.components.componentmethod
    def get_success_chance_modifier(self):
        return self._success_chance_modification

    @objects.components.componentmethod
    def get_actor_scoring_modifier(self, affordance):
        total = 0
        for buff_entry in self._active_buffs.values():
            total += buff_entry.effect_modification.get_affordance_scoring_modifier(affordance)
        return total

    @objects.components.componentmethod
    def get_actor_success_modifier(self, affordance):
        total = 0
        for buff_entry in self._active_buffs.values():
            total += buff_entry.effect_modification.get_affordance_success_modifier(affordance)
        return total

    @objects.components.componentmethod
    def get_mood(self):
        return self._active_mood

    @objects.components.componentmethod
    def get_mood_animation_param_name(self):
        param_name = self._active_mood.asm_param_name
        if param_name is not None:
            return param_name
        (mood, _, _) = self._get_largest_mood(predicate=lambda mood: return True if mood.asm_param_name else False)
        return mood.asm_param_name

    @objects.components.componentmethod
    def get_mood_intensity(self):
        return self._active_mood_intensity

    @objects.components.componentmethod
    def get_effective_skill_level(self, skill):
        if skill.stat_type == skill:
            skill = self.owner.get_stat_instance(skill)
            if skill is None:
                return 0
        modifier = 0
        for buff_entry in self._active_buffs.values():
            modifier += buff_entry.effect_modification.get_effective_skill_modifier(skill)
        return skill.get_user_value() + modifier

    @objects.components.componentmethod
    def effective_skill_modified_buff_gen(self, skill):
        if skill.stat_type == skill:
            skill = self.owner.get_stat_instance(skill)
        for buff_entry in self._active_buffs.values():
            modifier = buff_entry.effect_modification.get_effective_skill_modifier(skill)
            while modifier != 0:
                yield (buff_entry, modifier)

    @objects.components.componentmethod
    def is_appropriate(self, tags):
        final_appropriateness = Appropriateness.DONT_CARE
        for buff in self._active_buffs:
            appropriateness = buff.get_appropriateness(tags)
            while appropriateness > final_appropriateness:
                final_appropriateness = appropriateness
        if final_appropriateness == Appropriateness.NOT_ALLOWED:
            return False
        return True

    def get_additional_create_ops_gen(self):
        yield GenericProtocolBufferOp(Operation.SIM_MOOD_UPDATE, self._create_mood_update_msg())
        for buff in self:
            while buff.visible:
                yield GenericProtocolBufferOp(Operation.SIM_BUFF_UPDATE, self._create_buff_update_msg(buff, True))

    def _publish_mood_update(self):
        if self.owner.valid_for_distribution and self.owner.visible_to_client == True:
            Distributor.instance().add_op(self.owner, GenericProtocolBufferOp(Operation.SIM_MOOD_UPDATE, self._create_mood_update_msg()))

    def _send_mood_changed_event(self):
        if not self.owner.is_npc:
            self.owner.whim_tracker.refresh_emotion_whim()
        services.get_event_manager().process_event(test_events.TestEvent.MoodChange, sim_info=self.owner)

    def _create_mood_update_msg(self):
        mood_msg = Commodities_pb2.MoodUpdate()
        mood_msg.sim_id = self.owner.id
        mood_msg.mood_key = self._active_mood.guid64
        mood_msg.mood_intensity = self._active_mood_intensity
        return mood_msg

    def _create_buff_update_msg(self, buff, equipped, change_rate=None):
        buff_msg = Sims_pb2.BuffUpdate()
        buff_msg.buff_id = buff.guid64
        buff_msg.sim_id = self.owner.id
        buff_msg.equipped = equipped
        if buff.buff_reason is not None:
            buff_msg.reason = buff.buff_reason
        if equipped and buff.show_timeout:
            (timeout, rate_multiplier) = buff.get_timeout_time()
            buff_msg.timeout = timeout
            buff_msg.rate_multiplier = rate_multiplier
            if change_rate is not None:
                if change_rate == 0:
                    progress_arrow = Sims_pb2.BUFF_PROGRESS_NONE
                elif change_rate > 0:
                    progress_arrow = Sims_pb2.BUFF_PROGRESS_UP if not buff.flip_arrow_for_progress_update else Sims_pb2.BUFF_PROGRESS_DOWN
                else:
                    progress_arrow = Sims_pb2.BUFF_PROGRESS_DOWN if not buff.flip_arrow_for_progress_update else Sims_pb2.BUFF_PROGRESS_UP
                buff_msg.buff_progress = progress_arrow
        buff_msg.is_mood_buff = buff.is_mood_buff
        buff_msg.commodity_guid = buff.commodity_guid or 0
        if buff.mood_override is not None:
            buff_msg.mood_type_override = buff.mood_override.guid64
        buff_msg.transition_into_buff_id = buff.transition_into_buff_id
        return buff_msg

    def send_buff_update_msg(self, buff, equipped, change_rate=None, immediate=False):
        if not buff.visible:
            return
        if self.owner.valid_for_distribution and self.owner.is_sim and self.owner.is_selectable:
            buff_msg = self._create_buff_update_msg(buff, equipped, change_rate=change_rate)
            if gsi_handlers.buff_handlers.sim_buff_log_archiver.enabled:
                gsi_handlers.buff_handlers.archive_buff_message(buff_msg, equipped, change_rate)
            Distributor.instance().add_op(self.owner, GenericProtocolBufferOp(Operation.SIM_BUFF_UPDATE, buff_msg))

    def _can_add_buff_type(self, buff_type):
        if not buff_type.can_add(self.owner):
            return (False, None)
        mood = buff_type.mood_type
        if mood is not None and mood.excluding_traits is not None and self.owner.trait_tracker.has_any_trait(mood.excluding_traits):
            return (False, None)
        if buff_type.exclusive_index is None:
            return (True, None)
        for conflicting_buff_type in self._active_buffs:
            while conflicting_buff_type.exclusive_index == buff_type.exclusive_index:
                if buff_type.exclusive_weight < conflicting_buff_type.exclusive_weight:
                    return (False, None)
                return (True, conflicting_buff_type)
        return (True, None)

    def _update_chance_modifier(self):
        positive_success_buff_delta = 0
        negative_success_buff_delta = 1
        for buff_entry in self._active_buffs.values():
            if buff_entry.success_modifier > 0:
                positive_success_buff_delta += buff_entry.get_success_modifier
            else:
                negative_success_buff_delta *= 1 + buff_entry.get_success_modifier
        self._success_chance_modification = positive_success_buff_delta - (1 - negative_success_buff_delta)

    def _get_largest_mood(self, predicate=None, buffs_to_ignore=()):
        weights = {}
        polarity_to_changeable_buffs = collections.defaultdict(list)
        polarity_to_largest_mood_and_weight = {}
        for buff_entry in self._active_buffs.values():
            current_mood = buff_entry.mood_type
            current_weight = buff_entry.mood_weight
            while not current_mood is None:
                if current_weight == 0:
                    pass
                if not (predicate is not None and predicate(current_mood)):
                    pass
                if buff_entry in buffs_to_ignore:
                    pass
                current_polarity = current_mood.buff_polarity
                if buff_entry.is_changeable:
                    polarity_to_changeable_buffs[current_polarity].append(buff_entry)
                total_current_weight = weights.get(current_mood, 0)
                total_current_weight += current_weight
                weights[current_mood] = total_current_weight
                (largest_mood, largest_weight) = polarity_to_largest_mood_and_weight.get(current_polarity, (None, None))
                if largest_mood is None:
                    polarity_to_largest_mood_and_weight[current_polarity] = (current_mood, total_current_weight)
                else:
                    while total_current_weight > largest_weight:
                        polarity_to_largest_mood_and_weight[current_polarity] = (current_mood, total_current_weight)
        all_changeable_buffs = []
        for (buff_polarity, changeable_buffs) in polarity_to_changeable_buffs.items():
            (largest_mood, largest_weight) = polarity_to_largest_mood_and_weight.get(buff_polarity, (None, None))
            if largest_mood is not None:
                for buff_entry in changeable_buffs:
                    if buff_entry.mood_override is not largest_mood:
                        all_changeable_buffs.append((buff_entry, largest_mood))
                    largest_weight += buff_entry.mood_weight
                polarity_to_largest_mood_and_weight[buff_polarity] = (largest_mood, largest_weight)
            else:
                weights = {}
                largest_weight = 0
                for buff_entry in changeable_buffs:
                    if buff_entry.mood_override is not None:
                        all_changeable_buffs.append((buff_entry, None))
                    current_mood = buff_entry.mood_type
                    current_weight = buff_entry.mood_weight
                    total_current_weight = weights.get(current_mood, 0)
                    total_current_weight += current_weight
                    weights[current_mood] = total_current_weight
                    while total_current_weight > largest_weight:
                        largest_weight = total_current_weight
                        largest_mood = current_mood
                while largest_mood is not None and largest_weight != 0:
                    polarity_to_largest_mood_and_weight[buff_polarity] = (largest_mood, largest_weight)
        largest_weight = 0
        largest_mood = self.DEFAULT_MOOD
        active_mood = self._active_mood
        if polarity_to_largest_mood_and_weight:
            (mood, weight) = max(polarity_to_largest_mood_and_weight.values(), key=operator.itemgetter(1))
            if weight > largest_weight or weight == largest_weight and mood is active_mood:
                largest_weight = weight
                largest_mood = mood
        return (largest_mood, largest_weight, all_changeable_buffs)

    def _update_current_mood(self):
        (largest_mood, largest_weight, changeable_buffs) = self._get_largest_mood()
        if largest_mood is not None:
            intensity = self._get_intensity_from_mood(largest_mood, largest_weight)
            if self._should_update_mood(largest_mood, intensity, changeable_buffs):
                if self._active_mood_buff_handle is not None:
                    active_mood_buff_handle = self._active_mood_buff_handle
                    self.remove_buff(active_mood_buff_handle, update_mood=False)
                    if active_mood_buff_handle == self._active_mood_buff_handle:
                        self._active_mood_buff_handle = None
                    else:
                        return
                self._active_mood = largest_mood
                self._active_mood_intensity = intensity
                if len(largest_mood.buffs) >= intensity:
                    tuned_buff = largest_mood.buffs[intensity]
                    if tuned_buff is not None and tuned_buff.buff_type is not None:
                        self._active_mood_buff_handle = self.add_buff(tuned_buff.buff_type, update_mood=False)
                if gsi_handlers.buff_handlers.sim_mood_log_archiver.enabled and self.owner.valid_for_distribution and self.owner.visible_to_client == True:
                    gsi_handlers.buff_handlers.archive_mood_message(self.owner.id, self._active_mood, self._active_mood_intensity, self._active_buffs, changeable_buffs)
                caches.clear_all_caches()
                self.on_mood_changed()
        for (changeable_buff, mood_override) in changeable_buffs:
            changeable_buff.mood_override = mood_override
            self.send_buff_update_msg(changeable_buff, True)

    def _get_intensity_from_mood(self, mood, weight):
        intensity = 0
        for threshold in mood.intensity_thresholds:
            if weight >= threshold:
                intensity += 1
            else:
                break
        return intensity

    def _should_update_mood(self, mood, intensity, changeable_buffs):
        active_mood = self._active_mood
        active_mood_intensity = self._active_mood_intensity
        if mood is active_mood:
            return intensity != active_mood_intensity
        total_weight = sum(buff_entry.mood_weight for buff_entry in self._active_buffs.values() if buff_entry.mood_type is active_mood)
        active_mood_intensity = self._get_intensity_from_mood(active_mood, total_weight)
        if changeable_buffs and not self._active_mood.is_changeable:
            buffs_to_ignore = [changeable_buff for (changeable_buff, _) in changeable_buffs]
            (largest_mood, largest_weight, _) = self._get_largest_mood(buffs_to_ignore=buffs_to_ignore)
            new_intensity = self._get_intensity_from_mood(largest_mood, largest_weight)
            if self._should_update_mood(largest_mood, new_intensity, None):
                active_mood = largest_mood
                active_mood_intensity = new_intensity
        if active_mood.is_changeable and mood.buff_polarity == active_mood.buff_polarity:
            return True
        if not intensity or intensity < active_mood_intensity:
            return True
        if intensity >= active_mood_intensity + self.UPDATE_INTENSITY_BUFFER:
            return True
        if mood is self.DEFAULT_MOOD or active_mood is self.DEFAULT_MOOD:
            return True
        return False
Пример #7
0
class PersistenceService(Service):

    def __init__(self):
        super().__init__()
        self._save_locks = []
        self._read_write_locked = False
        self._save_game_data_proto = serialization.SaveGameData()
        self.save_timeline = None
        self._unlocked_callbacks = CallableList()
        self.once_per_session_telemetry_sent = False
        self.save_error_code = persistence_error_types.ErrorCodes.NO_ERROR
        self._zone_data_pb_cache = {}
        self._world_id_to_region_id_cache = {}
        self._sim_data_pb_cache = None
        self._household_pb_cache = None
        self._world_ids = frozenset()

    def setup(self, **kwargs):
        self._time_of_last_save = None

    def build_caches(self):
        self._world_id_to_region_id_cache.clear()
        self._zone_data_pb_cache.clear()
        for zone in self._save_game_data_proto.zones:
            self._zone_data_pb_cache[zone.zone_id] = zone
            world_id = zone.world_id
            if world_id in self._world_id_to_region_id_cache:
                continue
            neighborhood_id = zone.neighborhood_id
            for neighborhood in self._save_game_data_proto.neighborhoods:
                if neighborhood.neighborhood_id == neighborhood_id:
                    self._world_id_to_region_id_cache[world_id] = neighborhood.region_id
        self._world_ids = frozenset(z.world_id for z in self._zone_data_pb_cache.values())
        self.dirty_sim_data_pb_cache()
        self._household_pb_cache = None

    def dirty_sim_data_pb_cache(self):
        self._sim_data_pb_cache = None

    def _get_sim_data_pb_cache(self):
        if self._sim_data_pb_cache is None:
            self._sim_data_pb_cache = {sim_pb.sim_id: index for (index, sim_pb) in enumerate(self._save_game_data_proto.sims)}
        return self._sim_data_pb_cache

    def _get_household_data_pb_cache(self):
        if self._household_pb_cache is not None:
            cache_len = len(self._household_pb_cache)
            actual_len = len(self._save_game_data_proto.households)
            if cache_len < actual_len:
                self._household_pb_cache = None
            elif cache_len > actual_len:
                logger.error('_household_pb_cache contains more than it should', owner='tingyul')
        if self._household_pb_cache is None:
            self._household_pb_cache = {household_pb.household_id: index for (index, household_pb) in enumerate(self._save_game_data_proto.households)}
        return self._household_pb_cache

    def is_save_locked(self):
        if self._read_write_locked:
            return True
        elif not self._save_locks:
            return False
        return True

    def is_save_locked_exclusively_by_holder(self, lock_holder):
        return self.is_save_locked() and (len(self._save_locks) == 1 and lock_holder in self._save_locks)

    def get_save_lock_tooltip(self):
        if self._read_write_locked:
            return PersistenceTuning.SAVE_FAILED_REASONS.generic
        elif self._save_locks:
            return self._save_locks[-1].get_lock_save_reason()

    def set_read_write_lock(self, is_locked, reference_id):
        changed_lock = self._read_write_locked != is_locked
        self._read_write_locked = is_locked
        if changed_lock:
            if is_locked:
                self._send_lock_save_message(lambda : PersistenceTuning.SAVE_FAILED_REASONS.generic)
            elif self.is_save_locked():
                self._send_lock_save_message(self.get_save_lock_tooltip)
            else:
                self._send_unlock_save_message()
        self._try_invoke_unlocked_callbacks()

    def get_save_game_data_proto(self):
        return self._save_game_data_proto

    def lock_save(self, lock_holder):
        self._save_locks.append(lock_holder)
        self._send_lock_save_message(lock_holder.get_lock_save_reason)

    def unlock_save(self, lock_holder, send_event=True):
        if lock_holder in self._save_locks:
            self._save_locks.remove(lock_holder)
        self._try_invoke_unlocked_callbacks()
        if send_event:
            if not self.is_save_locked():
                self._send_unlock_save_message()
            else:
                self._send_lock_save_message(self.get_save_lock_tooltip)

    def _send_lock_save_message(self, reason_provider):
        distributor = Distributor.instance()
        if distributor is not None and distributor.client is not None:
            msg = UI_pb2.GameSaveLockUnlock()
            msg.is_locked = True
            msg.lock_reason = reason_provider()
            distributor.add_event(MSG_GAME_SAVE_LOCK_UNLOCK, msg)

    def _send_unlock_save_message(self):
        distributor = Distributor.instance()
        if distributor is not None and distributor.client is not None:
            msg = UI_pb2.GameSaveLockUnlock()
            msg.is_locked = False
            distributor.add_event(MSG_GAME_SAVE_LOCK_UNLOCK, msg)

    def remove_save_locks(self):
        if self._save_locks:
            self._save_locks.clear()
            self._send_unlock_save_message()
        self._unlocked_callbacks.clear()

    def add_save_unlock_callback(self, callback):
        if self.is_save_locked():
            self._unlocked_callbacks.register(callback)
        else:
            callback()

    def _try_invoke_unlocked_callbacks(self):
        if self._unlocked_callbacks and not self.is_save_locked():
            self._unlocked_callbacks()
            self._unlocked_callbacks.clear()

    def _create_save_timeline(self):
        self._destroy_save_timeline(self.save_timeline)
        self.save_timeline = scheduling.Timeline(services.time_service().sim_now)

    def _destroy_save_timeline(self, timeline):
        if self.save_timeline is not timeline:
            raise RuntimeError('Attempting to destroy the wrong timeline!')
        if self.save_timeline is not None:
            self.save_timeline = None
            timeline.teardown()

    def save_using(self, save_generator, *args, **kwargs):

        def call_save_game_gen(timeline):
            result = yield from save_generator(timeline, *args, **kwargs)
            return result
            yield

        self._create_save_timeline()
        element = elements.GeneratorElement(call_save_game_gen)
        element = elements.WithFinallyElement(element, self._destroy_save_timeline)
        element_handle = self.save_timeline.schedule(element)
        return element_handle

    def save_to_scratch_slot_gen(self, timeline):
        save_game_data = SaveGameData(0, 'scratch', True, None)
        save_result_code = yield from self.save_game_gen(timeline, save_game_data, send_save_message=False, check_cooldown=False)
        return save_result_code
        yield

    def save_game_gen(self, timeline, save_game_data, send_save_message=True, check_cooldown=False, ignore_callback=False):
        (result_code, failure_reason) = yield from self._save_game_gen(timeline, save_game_data, check_cooldown=check_cooldown)
        if send_save_message:
            msg = UI_pb2.GameSaveComplete()
            msg.return_status = result_code
            msg.save_cooldown = self._get_cooldown()
            if failure_reason is not None:
                msg.failure_reason = failure_reason
            msg.slot_id = save_game_data.slot_id
            distributor = Distributor.instance()
            distributor.add_event(MSG_GAME_SAVE_COMPLETE, msg)
        return result_code
        yield

    def _save_game_gen(self, timeline, save_game_data, check_cooldown=True):
        save_lock_reason = self.get_save_lock_tooltip()
        if save_lock_reason is not None:
            return (SaveGameResult.FAILED_SAVE_LOCKED, save_lock_reason)
            yield
        current_time = services.server_clock_service().now()
        result_code = SaveGameResult.FAILED_ON_COOLDOWN
        if self._time_of_last_save is not None:
            cooldown = (current_time - self._time_of_last_save).in_real_world_seconds()
        else:
            cooldown = PersistenceTuning.SAVE_GAME_COOLDOWN + 1
        if not check_cooldown or cooldown > PersistenceTuning.SAVE_GAME_COOLDOWN:
            result_code = SaveGameResult.SUCCESS
            error_code_string = None
            try:
                yield from self._fill_and_send_save_game_protobufs_gen(timeline, save_game_data.slot_id, save_game_data.slot_name, auto_save_slot_id=save_game_data.auto_save_slot_id)
            except Exception as e:
                result_code = SaveGameResult.FAILED_EXCEPTION_OCCURRED
                error_code_string = persistence_error_types.generate_exception_code(self.save_error_code, e)
                logger.exception('Save failed due to Exception', exc=e)
                with telemetry_helper.begin_hook(save_telemetry_writer, TELEMETRY_HOOK_SAVE_FAIL) as hook:
                    hook.write_int(TELEMETRY_FIELD_ERROR_CODE, self.save_error_code)
                    hook.write_int(TELEMETRY_FIELD_STACK_HASH, sims4.hash_util.hash64(error_code_string))
            finally:
                self.save_error_code = persistence_error_types.ErrorCodes.NO_ERROR
        if check_cooldown:
            if result_code == SaveGameResult.SUCCESS:
                self._time_of_last_save = current_time
        failure_reason = self._get_failure_reason_for_result_code(result_code, error_code_string)
        return (result_code, failure_reason)
        yield

    def _get_failure_reason_for_result_code(self, result_code, exception_code_string):
        if result_code == SaveGameResult.SUCCESS:
            return
        if result_code == SaveGameResult.FAILED_ON_COOLDOWN:
            return PersistenceTuning.SAVE_FAILED_REASONS.on_cooldown
        if result_code == SaveGameResult.FAILED_EXCEPTION_OCCURRED:
            return PersistenceTuning.SAVE_FAILED_REASONS.exception_occurred(exception_code_string)
        return PersistenceTuning.SAVE_FAILED_REASONS.generic

    def _get_cooldown(self):
        if self._time_of_last_save is not None:
            current_time = services.server_clock_service().now()
            cooldown = PersistenceTuning.SAVE_GAME_COOLDOWN - (current_time - self._time_of_last_save).in_real_world_seconds()
            return cooldown
        return 0

    def _fill_and_send_save_game_protobufs_gen(self, timeline, slot_id, slot_name, auto_save_slot_id=None):
        self.save_error_code = persistence_error_types.ErrorCodes.SETTING_SAVE_SLOT_DATA_FAILED
        save_slot_data_msg = self.get_save_slot_proto_buff()
        save_slot_data_msg.slot_id = slot_id
        save_slot_data_msg.slot_name = slot_name
        if services.active_household_id() is not None:
            save_slot_data_msg.active_household_id = services.active_household_id()
        sims4.core_services.service_manager.save_all_services(self, save_slot_data=save_slot_data_msg)
        self.save_error_code = persistence_error_types.ErrorCodes.SAVE_CAMERA_DATA_FAILED
        camera.serialize(save_slot_data=save_slot_data_msg)

        def on_save_complete(slot_id, success):
            wakeable_element.trigger_soft_stop()

        self.save_error_code = persistence_error_types.ErrorCodes.SAVE_TO_SLOT_FAILED
        wakeable_element = element_utils.soft_sleep_forever()
        persistence_module.run_persistence_operation(persistence_module.PersistenceOpType.kPersistenceOpSave, self._save_game_data_proto, slot_id, on_save_complete)
        yield from element_utils.run_child(timeline, wakeable_element)
        if auto_save_slot_id is not None:
            self.save_error_code = persistence_error_types.ErrorCodes.AUTOSAVE_TO_SLOT_FAILED
            wakeable_element = element_utils.soft_sleep_forever()
            persistence_module.run_persistence_operation(persistence_module.PersistenceOpType.kPersistenceOpSave, self._save_game_data_proto, auto_save_slot_id, on_save_complete)
            yield from element_utils.run_child(timeline, wakeable_element)
        self.save_error_code = persistence_error_types.ErrorCodes.NO_ERROR

    def get_world_ids(self):
        return self._world_ids

    def get_lot_proto_buff(self, lot_id):
        zone_id = self.resolve_lot_id_into_zone_id(lot_id)
        if zone_id is not None:
            neighborhood_data = self.get_neighborhood_proto_buff(services.current_zone().neighborhood_id)
            if neighborhood_data is not None:
                for lot_owner_data in neighborhood_data.lots:
                    if zone_id == lot_owner_data.zone_instance_id:
                        return lot_owner_data

    def get_neighborhood_agnostic_lot_proto_buff(self, lot_id):
        (zone_id, neighborhood_id) = self.resolve_lot_id_into_zone_and_neighborhood_id(lot_id)
        if zone_id is None:
            return
        neighborhood_data = self.get_neighborhood_proto_buff(neighborhood_id)
        if neighborhood_data is None:
            return
        for lot_owner_data in neighborhood_data.lots:
            if zone_id == lot_owner_data.zone_instance_id:
                return lot_owner_data

    def get_zone_proto_buff(self, zone_id):
        if zone_id in self._zone_data_pb_cache:
            return self._zone_data_pb_cache[zone_id]

    def get_house_description_id(self, zone_id):
        zone_data = self.get_zone_proto_buff(zone_id)
        house_description_id = services.get_house_description_id(zone_data.lot_template_id, zone_data.lot_description_id, zone_data.active_plex)
        return house_description_id

    def get_lot_data_from_zone_data(self, zone_data):
        neighborhood_data = self.get_neighborhood_proto_buff(zone_data.neighborhood_id)
        if neighborhood_data is None:
            return
        for lot_data in neighborhood_data.lots:
            if zone_data.zone_id == lot_data.zone_instance_id:
                return lot_data

    def get_region_id_from_world_id(self, world_id):
        if world_id in self._world_id_to_region_id_cache:
            return self._world_id_to_region_id_cache[world_id]

    def get_world_id_from_zone(self, zone_id):
        zone_proto = self.get_zone_proto_buff(zone_id)
        if zone_proto is None:
            return 0
        return zone_proto.world_id

    def zone_proto_buffs_gen(self):
        if self._save_game_data_proto is not None:
            for zone in self._save_game_data_proto.zones:
                yield zone

    def get_open_street_proto_buff(self, world_id):
        if self._save_game_data_proto is not None:
            for open_street in self._save_game_data_proto.streets:
                if open_street.world_id == world_id:
                    return open_street

    def add_open_street_proto_buff(self, open_street_proto):
        if self._save_game_data_proto is not None:
            self._save_game_data_proto.streets.append(open_street_proto)

    def get_lot_id_from_zone_id(self, zone_id):
        for zone in self._save_game_data_proto.zones:
            if zone.zone_id == zone_id:
                return zone.lot_id
        logger.error('lot id does not exist for zone with id ({}).', zone_id)

    def get_household_id_from_lot_id(self, lot_id):
        lot_owner_info = self.get_lot_proto_buff(lot_id)
        if lot_owner_info is not None:
            for household in lot_owner_info.lot_owner:
                return household.household_id

    def get_household_id_from_zone_id(self, zone_id):
        zone_data = self.get_zone_proto_buff(zone_id)
        if zone_data is not None:
            return zone_data.household_id

    def resolve_lot_id_into_zone_id(self, lot_id, neighborhood_id=None, ignore_neighborhood_id=False):
        if neighborhood_id is None:
            neighborhood_id = services.current_zone().neighborhood_id
        if self._save_game_data_proto is not None:
            for zone in self._save_game_data_proto.zones:
                if zone.lot_id == lot_id:
                    if not ignore_neighborhood_id:
                        if zone.neighborhood_id == neighborhood_id:
                            return zone.zone_id
                    return zone.zone_id

    def resolve_lot_id_into_zone_and_neighborhood_id(self, lot_id):
        if self._save_game_data_proto is not None:
            for zone in self._save_game_data_proto.zones:
                if zone.lot_id == lot_id:
                    return (zone.zone_id, zone.neighborhood_id)
        return (None, None)

    def get_save_slot_proto_guid(self):
        if self._save_game_data_proto is not None:
            return self._save_game_data_proto.guid

    def get_save_slot_proto_buff(self):
        if self._save_game_data_proto is not None:
            return self._save_game_data_proto.save_slot

    def get_account_proto_buff(self):
        if self._save_game_data_proto is not None:
            return self._save_game_data_proto.account

    def add_sim_proto_buff(self, sim_id):
        sim_data_pb_cache = self._get_sim_data_pb_cache()
        sim_data_pb_cache[sim_id] = len(self._save_game_data_proto.sims)
        sim_pb = self._save_game_data_proto.sims.add()
        return sim_pb

    def del_sim_proto_buff(self, sim_id):
        if self._save_game_data_proto is None:
            return
        for (index, sim_msg) in enumerate(self._save_game_data_proto.sims):
            if sim_msg.sim_id == sim_id:
                del self._save_game_data_proto.sims[index]
                break
        else:
            logger.error('Attempting to delete Sim {} that is absent in the save file.', sim_id)
        self.dirty_sim_data_pb_cache()

    def get_sim_proto_buff(self, sim_id):
        sim_data_pb_cache = self._get_sim_data_pb_cache()
        index = sim_data_pb_cache.get(sim_id)
        if index is not None and index < len(self._save_game_data_proto.sims):
            return self._save_game_data_proto.sims[index]

    def add_household_proto_buff(self, household_id):
        household_pb_cache = self._get_household_data_pb_cache()
        household_pb_cache[household_id] = len(self._save_game_data_proto.households)
        household_pb = self._save_game_data_proto.households.add()
        return household_pb

    def del_household_proto_buff(self, household_id):
        if self._save_game_data_proto is None:
            return
        for (index, household_msg) in enumerate(self._save_game_data_proto.households):
            if household_msg.household_id == household_id:
                del self._save_game_data_proto.households[index]
                break
        else:
            logger.error('Attempting to delete Household {} that is absent in the save file.', household_id)
        self._household_pb_cache = None

    def get_household_proto_buff(self, household_id):
        household_pb_cache = self._get_household_data_pb_cache()
        index = household_pb_cache.get(household_id)
        if index is not None and index < len(self._save_game_data_proto.households):
            return self._save_game_data_proto.households[index]

    def all_household_protos(self):
        if self._save_game_data_proto is not None:
            return tuple(self._save_game_data_proto.households)
        return tuple()

    def get_neighborhood_proto_buff(self, neighborhood_id):
        if self._save_game_data_proto is not None:
            for neighborhood in self._save_game_data_proto.neighborhoods:
                if neighborhood.neighborhood_id == neighborhood_id:
                    return neighborhood

    def get_neighborhoods_proto_buf_gen(self):
        for neighborhood_proto_buf in self._save_game_data_proto.neighborhoods:
            yield neighborhood_proto_buf

    def get_neighborhood_proto_buf_from_zone_id(self, zone_id):
        zone_proto = self.get_zone_proto_buff(zone_id)
        if zone_proto is None:
            return
        neighborhood_proto = self.get_neighborhood_proto_buff(zone_proto.neighborhood_id)
        return neighborhood_proto

    def add_mannequin_proto_buff(self):
        return self._save_game_data_proto.mannequins.add()

    def get_mannequin_proto_buff(self, mannequin_id):
        if self._save_game_data_proto is not None:
            for mannequin_data in self._save_game_data_proto.mannequins:
                if mannequin_data.mannequin_id == mannequin_id:
                    return mannequin_data

    def del_mannequin_proto_buff(self, mannequin_id):
        if self._save_game_data_proto is not None:
            for (index, mannequin_data) in enumerate(self._save_game_data_proto.mannequins):
                if mannequin_data.mannequin_id == mannequin_id:
                    del self._save_game_data_proto.mannequins[index]
                    break

    def prepare_mannequin_for_cas(self, outfit_data):
        self.del_mannequin_proto_buff(outfit_data.sim_id)
        sim_info_data_proto = self.add_mannequin_proto_buff()
        sim_info_data_proto.mannequin_id = outfit_data.sim_id
        outfit_data.save_sim_info(sim_info_data_proto)
        current_zone_id = services.current_zone_id()
        sim_info_data_proto.zone_id = current_zone_id
        sim_info_data_proto.world_id = self.get_world_id_from_zone(current_zone_id)
        return sim_info_data_proto

    def all_travel_group_proto_gen(self):
        if self._save_game_data_proto is not None:
            for travel_group in self._save_game_data_proto.travel_groups:
                yield travel_group

    def get_travel_group_proto_buff(self, travel_group_id):
        if self._save_game_data_proto is not None:
            for travel_group in self._save_game_data_proto.travel_groups:
                if travel_group.travel_group_id == travel_group_id:
                    return travel_group

    def add_travel_group_proto_buff(self):
        return self._save_game_data_proto.travel_groups.add()

    def del_travel_group_proto_buff(self, travel_group_id):
        if self._save_game_data_proto is not None:
            for (index, travel_group) in enumerate(self._save_game_data_proto.travel_groups):
                if travel_group.travel_group_id == travel_group_id:
                    del self._save_game_data_proto.travel_groups[index]
                    break

    def try_send_once_per_session_telemetry(self):
        if not self.once_per_session_telemetry_sent:
            try:
                HouseholdRegionTelemetryData.send_household_region_telemetry()
            except Exception:
                logger.exception('Exception thrown inside try_send_once_per_session_telemetry()', owner='jwilkinson')
            finally:
                self.once_per_session_telemetry_sent = True