コード例 #1
0
 def _run(self, timeline):
     global logged_missing_interaction_callstack
     if self.interaction is None:
         if not logged_missing_interaction_callstack:
             logger.callstack(
                 'Attempting to run an animation {} without a corresponding interaction.',
                 self,
                 level=sims4.log.LEVEL_ERROR)
             logged_missing_interaction_callstack = True
         return False
     if self.asm_key is None:
         return True
     asm = self.get_asm()
     if asm is None:
         return False
     self._set_alternative_prop_overrides(asm)
     if self.overrides.balloons:
         balloon_requests = TunableBalloon.get_balloon_requests(
             self.interaction, self.overrides)
     else:
         balloon_requests = None
     success = timeline.run_child(
         animate_states(asm,
                        self.begin_states,
                        self.end_states,
                        overrides=self.overrides,
                        balloon_requests=balloon_requests,
                        repeat_begin_states=self.repeat,
                        interaction=self.interaction,
                        **self.animate_kwargs))
     return success
コード例 #2
0
def _register_balloon_requests(asm,
                               interaction,
                               overrides,
                               balloon_requests,
                               repeat=False):
    if not balloon_requests:
        return
    remaining_balloons = list(balloon_requests)
    for balloon_request in balloon_requests:
        balloon_delay = balloon_request.delay or 0
        if balloon_request.delay_randomization > 0:
            balloon_delay += random.random(
            ) * balloon_request.delay_randomization
        if asm.context.register_custom_event_handler(
                _create_balloon_request_callback(
                    balloon_request=balloon_request),
                None,
                balloon_delay,
                allow_stub_creation=True):
            remaining_balloons.remove(balloon_request)
        if repeat:
            break
    if remaining_balloons and not repeat:
        logger.error('Failed to schedule all requested balloons for {}', asm)
    elif repeat:
        balloon_requests = TunableBalloon.get_balloon_requests(
            interaction, overrides)
    return balloon_requests
コード例 #3
0
 def append_to_arb(cls, inst, asm, arb):
     if inst is not None and inst.overrides:
         inst.overrides.override_asm(asm)
         balloon_requests = TunableBalloon.get_balloon_requests(
             inst.interaction, inst.overrides)
         _register_balloon_requests(asm, inst.interaction, inst.overrides,
                                    balloon_requests)
     for state_name in cls.begin_states:
         asm.request(state_name, arb)
コード例 #4
0
def route_failure(sim, interaction, failure_reason, failure_object_id):
    global ROUTE_FAILURE_OVERRIDE_MAP
    if not sim.should_route_fail:
        return
    overrides = None
    if failure_reason is not None:
        if ROUTE_FAILURE_OVERRIDE_MAP is None:
            ROUTE_FAILURE_OVERRIDE_MAP = {
                TransitionFailureReasons.BLOCKING_OBJECT:
                RouteFailureTunables.route_fail_overrides_object,
                TransitionFailureReasons.RESERVATION:
                RouteFailureTunables.route_fail_overrides_reservation,
                TransitionFailureReasons.BUILD_BUY:
                RouteFailureTunables.route_fail_overrides_build,
                TransitionFailureReasons.NO_DESTINATION_NODE:
                RouteFailureTunables.route_fail_overrides_no_dest_node,
                TransitionFailureReasons.NO_PATH_FOUND:
                RouteFailureTunables.route_fail_overrides_no_path_found,
                TransitionFailureReasons.NO_VALID_INTERSECTION:
                RouteFailureTunables.
                route_fail_overrides_no_valid_intersection,
                TransitionFailureReasons.NO_GOALS_GENERATED:
                RouteFailureTunables.route_fail_overrides_no_goals_generated,
                TransitionFailureReasons.NO_CONNECTIVITY_TO_GOALS:
                RouteFailureTunables.route_fail_overrides_no_connectivity,
                TransitionFailureReasons.PATH_PLAN_FAILED:
                RouteFailureTunables.route_fail_overrides_path_plan_fail,
                TransitionFailureReasons.GOAL_ON_SLOPE:
                RouteFailureTunables.route_fail_overrides_goal_on_slope
            }
        if failure_reason in ROUTE_FAILURE_OVERRIDE_MAP:
            overrides = ROUTE_FAILURE_OVERRIDE_MAP[failure_reason]()
            if failure_object_id is not None:
                fail_obj = services.object_manager().get(failure_object_id)
                if fail_obj is not None:
                    if fail_obj.blocking_balloon_overrides is not None:
                        overrides.balloons = fail_obj.blocking_balloon_overrides
                    else:
                        overrides.balloon_target_override = fail_obj
    route_fail_anim = RouteFailureTunables.route_fail_animation(
        sim.posture.source_interaction, overrides=overrides, sequence=())
    supported_postures = route_fail_anim.get_supported_postures()
    if supported_postures:
        return build_element((route_fail_anim, flush_all_animations))
    else:
        balloon_requests = TunableBalloon.get_balloon_requests(
            interaction, route_fail_anim.overrides)
        return balloon_requests
コード例 #5
0
class ContentSetWithOverrides(ContentSet):
    FACTORY_TUNABLES = {
        'balloon_overrides':
        OptionalTunable(
            TunableList(
                description=
                '\n            Balloon Overrides lets you override the mixer balloons.\n            EX: Each of the comedy routine performances have a set of balloons.\n            However, the animation/mixer content is the same. We want to play\n            the same mixer content, but just have the balloons be different.\n            ',
                tunable=TunableBalloon())),
        'additional_mixers_to_cache':
        TunableInterval(
            description=
            "\n            Additional number of mixers to cache during a subaction request. For\n            mixer autonomy, we cache mixer for performance reasons. The baseline\n            cache size is determined by the mixer_interaction_cache_size tunable\n            on the Sim's autonomy component.\n            \n            An example for reason to add more mixers to cache if there are\n            large number of mixers tuned in this content set such as socials,\n            you may need to increase this number.  \n            \n            Please talk to GPE if you are about to add additional mixers.\n            ",
            tunable_type=int,
            minimum=0,
            default_lower=0,
            default_upper=0)
    }

    def __init__(self, balloon_overrides, additional_mixers_to_cache, *args,
                 **kwargs):
        super().__init__(*args, **kwargs)
        self.balloon_overrides = balloon_overrides
        self.additional_mixers_to_cache = additional_mixers_to_cache
コード例 #6
0
 def create_passive_ballon_request(sim, balloon_data):
     if gsi_handlers.balloon_handlers.archiver.enabled:
         gsi_entries = []
     else:
         gsi_entries = None
     resolver = SingleSimResolver(sim.sim_info)
     balloon_icon = TunableBalloon.select_balloon_icon(
         balloon_data.balloon_choices,
         resolver,
         gsi_entries=gsi_entries,
         gsi_interaction=None,
         gsi_balloon_target_override=None)
     category_icon = None
     if balloon_icon is not None:
         icon_info = balloon_icon.icon(resolver,
                                       balloon_target_override=None)
         if balloon_icon.category_icon is not None:
             category_icon = balloon_icon.category_icon(
                 resolver, balloon_target_override=None)
     else:
         icon_info = None
     if gsi_handlers.balloon_handlers.archiver.enabled:
         gsi_handlers.balloon_handlers.archive_balloon_data(
             sim, None, balloon_icon, icon_info, gsi_entries)
     if balloon_icon is not None and (icon_info[0] is not None
                                      or icon_info[1] is not None):
         (balloon_type,
          priority) = BALLOON_TYPE_LOOKUP[balloon_icon.balloon_type]
         balloon_overlay = balloon_icon.overlay
         request = BalloonRequest(sim, icon_info[0], icon_info[1],
                                  balloon_overlay, balloon_type, priority,
                                  TunableBalloon.BALLOON_DURATION,
                                  balloon_data.balloon_delay,
                                  balloon_data.balloon_delay_random_offset,
                                  category_icon)
         return request
コード例 #7
0
class WaypointInteraction(SuperInteraction):
    INSTANCE_TUNABLES = {'waypoint_constraint': TunableWaypointGeneratorVariant(tuning_group=GroupNames.ROUTING), 'waypoint_count': TunableRange(description='\n            The number of waypoints to select, from spawn points in the zone, to\n            visit for a Jog prior to returning to the original location.\n            ', tunable_type=int, default=2, minimum=2, tuning_group=GroupNames.ROUTING), 'waypoint_walk_style': WalkStyleRequest.TunableFactory(description='\n            The walkstyle to use when routing between waypoints.\n            ', tuning_group=GroupNames.ROUTING), 'waypoint_stitching': WaypointStitchingVariant(tuning_group=GroupNames.ROUTING), 'waypoint_randomize_orientation': Tunable(description='\n            Make Waypoint orientation random.  Default is velocity aligned.\n            ', tunable_type=bool, default=False, tuning_group=GroupNames.ROUTING), 'waypoint_clear_locomotion_mask': Tunable(description='\n            If enabled, override the locomotion queue mask.  This mask controls\n            which Animation Requests and XEvents get blocked during locomotion.\n            By default, the mask blocks everything.  If cleared, it blocks\n            nothing.  It also lowers the animation track used by locomotion to \n            9,999 from the default of 10,000.  Use with care, ask your GPE.\n            ', tunable_type=bool, default=False, tuning_group=GroupNames.ROUTING), 'waypoint_override_agent_radius': OptionalTunable(description='\n            If enabled, use the specified value as the agent radius when\n            generating goals for the waypoints. The agent radius is restored\n            for the actual route.\n            ', tunable=TunableRange(description='\n                The value to use as the agent radius when generating goals. \n                ', tunable_type=float, minimum=0, maximum=1.0, default=0.123), tuning_group=GroupNames.ROUTING), 'waypoint_route_fail_balloon': OptionalTunable(description='\n            Tuning for balloon to show when failing to plan a aroute for this waypoint interaction. \n            ', tunable=TunableBalloon(locked_args={'balloon_delay': 0, 'balloon_delay_random_offset': 0, 'balloon_chance': 100}), tuning_group=GroupNames.ROUTING)}

    def __init__(self, aop, *args, **kwargs):
        super().__init__(aop, *args, **kwargs)
        waypoint_info = kwargs.get('waypoint_info')
        if waypoint_info is not None:
            self._waypoint_generator = _WaypointGeneratorRallyable(waypoint_info)
        else:
            if aop.target is None and self.target_type is TargetType.ACTOR:
                target = self.sim
            else:
                target = aop.target
            self._waypoint_generator = self.waypoint_constraint(self.context, target)
        self._routing_infos = None
        self._goal_size = 0.0
        self.register_on_finishing_callback(self._clean_up_waypoint_generator)

    @classmethod
    def _test(cls, target, context, **interaction_parameters):
        sim = context.sim
        routing_master = sim.routing_master
        if routing_master is not None and sim.parent is not routing_master:
            return TestResult(False, '{} cannot run Waypoint interactions because they are following {}', sim, routing_master)
        return super()._test(target, context, **interaction_parameters)

    def _get_starting_constraint(self, *args, **kwargs):
        constraint = ANYWHERE
        target = self.target
        if self._waypoint_generator.is_for_vehicle and (target is not None and target.vehicle_component is not None) and not target.is_in_inventory():
            constraint = Circle(target.position, target.vehicle_component.minimum_route_distance, routing_surface=target.routing_surface)
            constraint = constraint.intersect(self._waypoint_generator.get_water_constraint())
        else:
            constraint = self._waypoint_generator.get_start_constraint()
        posture_constraint = self._waypoint_generator.get_posture_constraint()
        if posture_constraint is not None:
            constraint = constraint.intersect(posture_constraint)
        return constraint

    @flexmethod
    def _constraint_gen(cls, inst, *args, **kwargs):
        inst_or_cls = inst if inst is not None else cls
        if inst is not None:
            constraint = inst._get_starting_constraint(*args, **kwargs)
            yield constraint
        yield from super(__class__, inst_or_cls)._constraint_gen(*args, **kwargs)

    def cancel(self, *args, **kwargs):
        for sim_primitive in list(self.sim.primitives):
            if isinstance(sim_primitive, FollowPath):
                sim_primitive.detach()
        return super().cancel(*args, **kwargs)

    def _clean_up_waypoint_generator(self, _):
        self._waypoint_generator.clean_up()

    def _get_goals_for_constraint(self, constraint, routing_agent):
        goals = []
        handles = constraint.get_connectivity_handles(routing_agent)
        for handle in handles:
            goals.extend(handle.get_goals(always_reject_invalid_goals=True))
        return goals

    def _show_route_fail_balloon(self):
        balloon_tuning = self.waypoint_route_fail_balloon
        if balloon_tuning is None:
            return
        if not self.is_user_directed:
            return
        balloon_requests = balloon_tuning(self)
        if balloon_requests:
            chosen_balloon = random.random.choice(balloon_requests)
            if chosen_balloon is not None:
                chosen_balloon.distribute()

    def _run_interaction_gen(self, timeline):
        all_sims = self.required_sims()
        if not all_sims:
            return
        self._routing_infos = []
        routing_agent = self.sim
        for sim in all_sims:
            routing_context = sim.routing_context
            routing_agent = sim
            vehicle = None if not sim.posture.is_vehicle else sim.parent
            if vehicle is not None:
                if vehicle.vehicle_component is not None:
                    routing_agent = vehicle
                    routing_context = vehicle.routing_component.pathplan_context
            self._routing_infos.append((routing_agent, routing_context))
        waypoints = []
        default_agent_radius = None
        if self.waypoint_override_agent_radius is not None:
            if routing_agent.routing_component is not None:
                default_agent_radius = routing_agent.routing_component._pathplan_context.agent_radius
                routing_agent.routing_component._pathplan_context.agent_radius = self.waypoint_override_agent_radius
        try:
            for constraint in self._waypoint_generator.get_waypoint_constraints_gen(routing_agent, self.waypoint_count):
                goals = self._get_goals_for_constraint(constraint, routing_agent)
                if not goals:
                    continue
                if self.waypoint_randomize_orientation:
                    for goal in goals:
                        goal.orientation = sims4.math.angle_to_yaw_quaternion(random.uniform(0.0, sims4.math.TWO_PI))
                waypoints.append(goals)
        finally:
            if default_agent_radius is not None:
                routing_agent.routing_component._pathplan_context.agent_radius = default_agent_radius
        if not waypoints:
            return False
            yield
        self._goal_size = max(info[0].routing_component.get_routing_context().agent_goal_radius for info in self._routing_infos)
        self._goal_size *= self._goal_size
        if self.staging:
            for route_waypoints in itertools.cycle(self.waypoint_stitching(waypoints, self._waypoint_generator.loops)):
                result = yield from self._do_route_to_constraint_gen(route_waypoints, timeline)
                if not result:
                    return result
                    yield
            else:
                return result
                yield
        else:
            for route_waypoints in self.waypoint_stitching(waypoints, self._waypoint_generator.loops):
                result = yield from self._do_route_to_constraint_gen(route_waypoints, timeline)
            return result
            yield
        return True
        yield

    def _do_route_to_constraint_gen(self, waypoints, timeline):
        if self.is_finishing:
            return False
            yield
        plan_primitives = []
        for (i, routing_info) in enumerate(self._routing_infos):
            routing_agent = routing_info[0]
            routing_context = routing_info[1]
            route = routing.Route(routing_agent.routing_location, waypoints[-1], waypoints=waypoints[:-1], routing_context=routing_context)
            plan_primitive = PlanRoute(route, routing_agent, interaction=self)
            result = yield from element_utils.run_child(timeline, plan_primitive)
            if not result:
                self._show_route_fail_balloon()
                return False
                yield
            if not (plan_primitive.path.nodes and plan_primitive.path.nodes.plan_success):
                self._show_route_fail_balloon()
                return False
                yield
            plan_primitive.path.blended_orientation = self.waypoint_randomize_orientation
            plan_primitives.append(plan_primitive)
            if i == len(self._routing_infos) - 1:
                continue
            for node in plan_primitive.path.nodes:
                position = Vector3(*node.position)
                for goal in itertools.chain.from_iterable(waypoints):
                    if goal.routing_surface_id != node.routing_surface_id:
                        continue
                    dist_sq = (Vector3(*goal.position) - position).magnitude_2d_squared()
                    if dist_sq < self._goal_size:
                        goal.cost = routing.get_default_obstacle_cost()
        route_primitives = []
        track_override = None
        mask_override = None
        if self.waypoint_clear_locomotion_mask:
            mask_override = 0
            track_override = 9999
        for plan_primitive in plan_primitives:
            sequence = get_route_element_for_path(plan_primitive.sim, plan_primitive.path, interaction=self, force_follow_path=True, track_override=track_override, mask_override=mask_override)
            walkstyle_request = self.waypoint_walk_style(plan_primitive.sim)
            sequence = walkstyle_request(sequence=sequence)
            route_primitives.append(sequence)
        result = yield from element_utils.run_child(timeline, do_all(*route_primitives))
        return result
        yield

    @classmethod
    def get_rallyable_aops_gen(cls, target, context, **kwargs):
        key = 'waypoint_info'
        if key not in kwargs:
            waypoint_generator = cls.waypoint_constraint(context, target)
            kwargs[key] = waypoint_generator
        yield from super().get_rallyable_aops_gen(target, context, rally_constraint=waypoint_generator.get_start_constraint(), **kwargs)
コード例 #8
0
ファイル: basic.py プロジェクト: NeonOcean/Environment
	def __init__ (self, **kwargs):
		super().__init__(add_to_ensemble = AddToEnsemble.TunableFactory(),
						 add_to_household = AddToHouseholdElement.TunableFactory(),
						 add_to_travel_group = TravelGroupAdd.TunableFactory(),
						 adventure = Adventure.TunableFactory(),
						 audio_modification = TunableAudioModificationElement(),
						 audio_sting = TunableAudioSting.TunableFactory(),
						 balloon = TunableBalloon(),
						 broadcaster = BroadcasterRequest.TunableFactory(),
						 buff = TunableBuffElement(),
						 buff_fire_and_forget = BuffFireAndForgetElement.TunableFactory(),
						 business_buy_lot = BusinessBuyLot.TunableFactory(),
						 business_employee_action = BusinessEmployeeAction.TunableFactory(),
						 call_to_action_turn_off = TurnOffCallToAction.TunableFactory(),
						 camera_focus = CameraFocusElement.TunableFactory(),
						 career_selection = careers.career_tuning.CareerSelectElement.TunableFactory(),
						 change_age = ChangeAgeElement.TunableFactory(),
						 change_outfit = ChangeOutfitElement.TunableFactory(),
						 conditional_layer = ManipulateConditionalLayer.TunableFactory(),
						 create_object = ObjectCreationElement.TunableFactory(),
						 create_photo_memory = CreatePhotoMemory.TunableFactory(),
						 create_sim = SimCreationElement.TunableFactory(),
						 create_situation = CreateSituationElement.TunableFactory(),
						 deliver_bill = DeliverBill.TunableFactory(),
						 destroy_ensemble = DestroyEnsemble.TunableFactory(),
						 destroy_object = ObjectDestructionElement.TunableFactory(),
						 destroy_situations_by_tags = DestroySituationsByTagsElement.TunableFactory(),
						 destroy_specified_objects_from_target_inventory = DestroySpecifiedObjectsFromTargetInventory.TunableFactory(),
						 display_notebook_ui = NotebookDisplayElement.TunableFactory(),
						 do_command = DoCommand.TunableFactory(),
						 dynamic_spawn_point = DynamicSpawnPointElement.TunableFactory(),
						 end_vacation = TravelGroupEnd.TunableFactory(),
						 exit_carry_while_holding = TunableExitCarryWhileHolding(),
						 extend_vacation = TravelGroupExtend.TunableFactory(),
						 enter_carry_while_holding = EnterCarryWhileHolding.TunableFactory(),

						 fade_children = FadeChildrenElement.TunableFactory(),
						 familiar_bind = BindFamiliarElement.TunableFactory(),
						 familiar_dismiss = DismissFamiliarElement.TunableFactory(),
						 focus = TunableFocusElement(),
						 inventory_transfer = InventoryTransfer.TunableFactory(),
						 invite = InviteSimElement.TunableFactory(),
						 join_situation = JoinSituationElement.TunableFactory(),
						 leave_situation = LeaveSituationElement.TunableFactory(),
						 loot = LootElement.TunableFactory(),
						 lot_decoration = LotDecorationElement.TunableFactory(),
						 life_event = TunableLifeEventElement(),
						 mark_object_as_stolen = MarkObjectAsStolen.TunableFactory(),
						 notification = NotificationElement.TunableFactory(),
						 npc_summon = TunableSummonNpc(),
						 object_relationship_social = ObjectRelationshipSocialTrigger.TunableFactory(),
						 painting_state_transfer = PaintingStateTransfer.TunableFactory(),
						 parent_object = ParentObjectElement.TunableFactory(),
						 payment = PaymentElement.TunableFactory(),

						 play_stored_audio_from_source = TunablePlayStoredAudioFromSource.TunableFactory(),
						 pregnancy = PregnancyElement.TunableFactory(),
						 put_near = PutNearElement.TunableFactory(),
						 put_object_in_mail = PutObjectInMail.TunableFactory(),
						 put_sim_in_rabbit_hole = RabbitHoleElement.TunableFactory(),
						 record_trends = RecordTrendsElement.TunableFactory(),
						 remove_from_ensemble = RemoveFromEnsemble.TunableFactory(),
						 remove_from_travel_group = TravelGroupRemove.TunableFactory(),
						 replace_object = ReplaceObject.TunableFactory(),
						 retail_customer_action = RetailCustomerAction.TunableFactory(),
						 return_stolen_object = ReturnStolenObject.TunableFactory(),
						 route_events = RouteEventProviderRequest.TunableFactory(),
						 routing_formation = RoutingFormationElement.TunableFactory(),
						 royalty_payment = TunableRoyaltyPayment.TunableFactory(),
						 save_participant = SaveParticipantElement.TunableFactory(),
						 send_to_inventory = SendToInventory.TunableFactory(),

						 service_npc_request = ServiceNpcRequest.TunableFactory(),
						 set_as_head = SetAsHeadElement.TunableFactory(),
						 set_game_speed = TunableSetClockSpeed.TunableFactory(),
						 set_goodbye_notification = SetGoodbyeNotificationElement.TunableFactory(),
						 set_photo_filter = SetPhotoFilter.TunableFactory(),
						 set_posture = SetPosture.TunableFactory(),
						 set_visibility_state = SetVisibilityStateElement.TunableFactory(),
						 slot_objects_from_inventory = SlotObjectsFromInventory.TunableFactory(),
						 slot_item_transfer = SlotItemTransfer.TunableFactory(),
						 stat_transfer_remove = TunableStatisticTransferRemove(),
						 state_change = TunableStateChange(),
						 store_sim = StoreSimElement.TunableFactory(),
						 submit_to_festival_contest = FestivalContestSubmitElement.TunableFactory(),
						 switch_occult = SwitchOccultElement.TunableFactory(),
						 take_photo = TakePhoto.TunableFactory(),
						 track_diagnostic_action = TrackDiagnosticAction.TunableFactory(),

						 transfer_carry_while_holding = TransferCarryWhileHolding.TunableFactory(),
						 transfer_name = NameTransfer.TunableFactory(),
						 transfer_stored_audio_component = TransferStoredAudioComponent.TunableFactory(),
						 transience_change = TunableTransienceChange(),
						 trigger_reaction = ReactionTriggerElement.TunableFactory(),
						 university_enrollment_ui = UniversityEnrollmentElement.TunableFactory(),
						 update_family_portrait = UpdateFamilyPortrait.TunableFactory(),
						 update_object_value = UpdateObjectValue.TunableFactory(),
						 update_physique = UpdatePhysique.TunableFactory(),
						 update_display_number = UpdateDisplayNumber.TunableFactory(),
						 walls_up_override = SetWallsUpOverrideElement.TunableFactory(),
						 vfx = PlayVisualEffectElement.TunableFactory(),
						 **kwargs)
コード例 #9
0
class PassiveBalloons:
    @staticmethod
    def _validate_tuning(instance_class, tunable_name, source, value):
        if PassiveBalloons.BALLOON_LOCKOUT + PassiveBalloons.BALLOON_RANDOM >= PassiveBalloons.BALLOON_LONG_LOCKOUT:
            logger.error(
                'PassiveBalloons tuning value error! BALLOON_LONG_LOCKOUT must be tuned to be greater than BALLOON_LOCKOUT + BALLOON_RANDOM'
            )

    BALLOON_LOCKOUT = Tunable(
        description=
        '\n        The duration, in minutes, for the lockout time between displaying passive balloons.\n        ',
        tunable_type=int,
        default=10)
    BALLOON_RANDOM = Tunable(
        description=
        '\n        The duration, in minutes, for a random amount to be added to the lockout\n        time between displaying passive balloons.\n        ',
        tunable_type=int,
        default=20)
    BALLOON_LONG_LOCKOUT = Tunable(
        description=
        '\n        The duration, in minutes, to indicate that a long enough time has passed\n        since the last balloon, to trigger a delay of the next balloon by the\n        random amount of time from BALLOON_RANDOM. The reason for this is so\n        that newly spawned walkby sims that begin routing do not display their\n        first routing balloon immediately. Make sure that this is always higher\n        than the tuned values in BALLOON_LOCKOUT + BALLOON_RANDOM, or it will\n        not work as intended.\n        ',
        tunable_type=int,
        default=120,
        callback=_validate_tuning)
    MAX_NUM_BALLOONS = Tunable(
        description=
        '\n        The maximum number of passive balloon tuning data entries to process per\n        balloon display attempt\n        ',
        tunable_type=int,
        default=25)
    ROUTING_BALLOONS = TunableList(
        description=
        '\n        A weighted list of passive routing balloons.\n        ',
        tunable=TunableTuple(balloon=TunableBalloon(
            locked_args={
                'balloon_delay': 0,
                'balloon_delay_random_offset': 0,
                'balloon_chance': 100,
                'balloon_target': None
            }),
                             weight=Tunable(tunable_type=int, default=1)))

    @staticmethod
    def request_routing_to_object_balloon(sim, interaction):
        balloon_tuning = interaction.route_start_balloon
        if balloon_tuning is None:
            return
        if interaction.is_user_directed and not balloon_tuning.also_show_user_directed:
            return
        balloon_requests = balloon_tuning.balloon(interaction)
        if balloon_requests:
            choosen_balloon = random.choice(balloon_requests)
            if choosen_balloon is not None:
                choosen_balloon.distribute()

    @staticmethod
    def create_passive_ballon_request(sim, balloon_data):
        if gsi_handlers.balloon_handlers.archiver.enabled:
            gsi_entries = []
        else:
            gsi_entries = None
        resolver = SingleSimResolver(sim.sim_info)
        balloon_icon = TunableBalloon.select_balloon_icon(
            balloon_data.balloon_choices,
            resolver,
            gsi_entries=gsi_entries,
            gsi_interaction=None,
            gsi_balloon_target_override=None)
        category_icon = None
        if balloon_icon is not None:
            icon_info = balloon_icon.icon(resolver,
                                          balloon_target_override=None)
            if balloon_icon.category_icon is not None:
                category_icon = balloon_icon.category_icon(
                    resolver, balloon_target_override=None)
        else:
            icon_info = None
        if gsi_handlers.balloon_handlers.archiver.enabled:
            gsi_handlers.balloon_handlers.archive_balloon_data(
                sim, None, balloon_icon, icon_info, gsi_entries)
        if balloon_icon is not None and (icon_info[0] is not None
                                         or icon_info[1] is not None):
            (balloon_type,
             priority) = BALLOON_TYPE_LOOKUP[balloon_icon.balloon_type]
            balloon_overlay = balloon_icon.overlay
            request = BalloonRequest(sim, icon_info[0], icon_info[1],
                                     balloon_overlay, balloon_type, priority,
                                     TunableBalloon.BALLOON_DURATION,
                                     balloon_data.balloon_delay,
                                     balloon_data.balloon_delay_random_offset,
                                     category_icon)
            return request

    @staticmethod
    def request_passive_balloon(sim, time_now):
        if time_now - sim.next_passive_balloon_unlock_time > create_time_span(
                minutes=PassiveBalloons.BALLOON_LONG_LOCKOUT):
            lockout_time = random.randint(0, PassiveBalloons.BALLOON_RANDOM)
            sim.next_passive_balloon_unlock_time = services.time_service(
            ).sim_now + create_time_span(minutes=lockout_time)
            return
        balloon_requests = []
        if len(PassiveBalloons.ROUTING_BALLOONS
               ) > PassiveBalloons.MAX_NUM_BALLOONS:
            sampled_balloon_tuning = random.sample(
                PassiveBalloons.ROUTING_BALLOONS,
                PassiveBalloons.MAX_NUM_BALLOONS)
        else:
            sampled_balloon_tuning = PassiveBalloons.ROUTING_BALLOONS
        for balloon_weight_pair in sampled_balloon_tuning:
            balloon_request = PassiveBalloons.create_passive_ballon_request(
                sim, balloon_weight_pair.balloon)
            if balloon_request is not None:
                balloon_requests.append(
                    (balloon_weight_pair.weight, balloon_request))
        if len(balloon_requests) > 0:
            choosen_balloon = sims4.random.weighted_random_item(
                balloon_requests)
            if choosen_balloon is not None:
                choosen_balloon.distribute()
        lockout_time = PassiveBalloons.BALLOON_LOCKOUT + random.randint(
            0, PassiveBalloons.BALLOON_RANDOM)
        sim.next_passive_balloon_unlock_time = time_now + create_time_span(
            minutes=lockout_time)