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.
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…
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.
You may now remove your tinfoil hats… … 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.