Major - Join In Progress Has Multiple Issues

TL;DR
    ValidatePlayer<private>(Agent:agent)<suspends>:void=
        TimeoutPeriod:float=180.0
        race:
            block:
                loop:
                    if(Player:=player[Agent],Player.IsActive[]):
                        HandleValidPlayer(Player)
                        break
                    else:
                        Sleep(0.33) 
            block:
                Sleep(TimeoutPeriod)
                Print("Warning: ValidatePlayer: Timeout")

I have a proposed solution to this issue as well as supportive reasoning and a really nice infographic for you all today.

Essentially what is happening is that Epic has been focused on getting players into the game fast and that means that asynchronous sub processes of the Player Joins process can complete as quickly as possible, but this sanic speed optimization comes at a price.
A player controller, agent, camera, eta are being prepared in parallel to a player initializing over the network, and it seems that can all complete and get passed along with a reference to the Player through the Player Joined Playspace function for handling BUT without the player object actually being valid. :scream_cat:

This implies inheritance from Agent which is the super of Player, which in laymens terms means its a package deal. What happens to Player must also happen to Agent. As such, you’ll notice that you’ll also get spawned triggers from player spawners passing the agent before its ready:

Where the error occurs is that the Player and Agent in these cases aren’t valid.

… Or
If you would please put on your tinfoil hat as a safety precaution for this next part…
image we may proceed.
rather, my presumption is that they are actually placeholder objects which allow Epic to pass fake dependencies into initialization processes and swap them out with the real deal to optimize load times.
image
You may now remove your tinfoil hats… :saluting_face:… thank you.

So onto the tangible results of what this all means:

The Problem:

Invalid Player and Agent references are passed through Epics hacky initialization process into devices in the game allowing invalid Player/Agent references to attempt to be used prior to them being ready for use.

The Solution:

A player validation loop that ensures that the Player/Agent that you're referencing and hope to pass along into subsequent functions is actually going to work before you pass it.
    ValidatePlayer<private>(Agent:agent)<suspends>:void=
        TimeoutPeriod:float=180.0 # <- Time (seconds) before we abandon
        Interval:float=0.33 # <- Time (s) between attempts
        race: # <- race means whichever of the next 2 blocks finishes
              # first reigns supreme and the other is cancelled
            block: # <- Block 1 in the race
                loop:
                    if:
                        Player:=player[Agent] # <- Cast to player
                        Player.IsActive[] # <- Confirm player is active
                    then: # <- On Successful IsActive check
                        HandleValidPlayer(Player) # <- Pass them onto init
                        break # <- Break the loop
                    else: # <- On Failed IsActive check
                        Sleep(Interval) # <- See top: Interval
            block: # <- Block 2 in the race
                Sleep(TimeoutPeriod) # <- See top: TimeoutPeriod
                Print("Warning! ValidatePlayer: Timeout") # <- Logging
Without Comments / Scoping / Const Declarations
    ValidatePlayer(Agent:agent)<suspends>:void=
        race:
            block:
                loop:
                    if(Player:=player[Agent],Player.IsActive[]):
                        HandleValidPlayer(Player)
                        break
                    else:
                        Sleep(0.33)
            block:
                Sleep(180.0)

In Summary:

By passing players from the server join event or player spawner event into a validation function prior to forwarding them through any other initialization we can compensate for Epic's OSHA violat- "safety circumvention" so our players can enjoy the load time optimizations without simultaneously breaking our game logic or player init flow.

What this might look like in practice:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Fortnite.com/Playspaces }

game_manager:=class(creative_device):
    @editable
    Spawners:[]player_spawner_device=array{}

    OnBegin<override>()<suspends>:void=
        for(Player:GetPlayspace().GetPlayers()):
            ValidatePlayer(Player)
            spawn. OnPlayerJoined()
        for(Spawner:Spawners):
            spawn. OnPlayerSpawned(Spawner)

    # Player Spawner Variation
    OnPlayerSpawned(Spawner:player_spawner_device)<suspends>:void=
        loop:
            Agent:=Spawner.SpawnedEvent.Await()
            spawn. ValidatePlayer(Agent)

    # Playspace Joined Variation
    OnPlayerJoined()<suspends>:void=
        loop:
            Player:=GetPlayspace().PlayerAddedEvent().Await()
            spawn. ValidatePlayer(Player)

    ValidatePlayer(Agent:agent)<suspends>:void=
        race:
            block:
                loop:
                    if(Player:=player[Agent],Player.IsActive[]):
                        HandleValidPlayer(Player)
                        break
                    else:
                        Sleep(0.33)
            block:
                Sleep(180.0)

    HandleValidPlayer(Player:player):void=
        block:
        # This is where your normal initialization would go
        # Aka make your custom player, map it, etc

PS:
Please note that any Epic internal processes related to players being added to ongoing games in between rounds or at other inappropriate times for the game are outside of our control for as long as we use their broken systems. Unfortunately there is no way to prevent these issues outside of not using their premade round system, and creating custom initialization and player handling wherever possible. Yes, this may mean creating your own custom round system.

Also note that if you set Join in Progress to Spectator epic spawns and immediately eliminates your fort_char which will trigger your listeners to begin initialization. Without additional validation checks such as Player.IsActive[] or FortCharacter.IsActive[] this can lead to failed or partially failed initialization.

6 Likes