weak_map with GetSession Is broken and does not persist between rounds and crashes

GetSession() works with ANY type in a session so long as you don’t have a game with “rounds”

With rounds, GetSession() + weak_map will crash your game 99% of the time.

In the code below you will see my test cases for GetSession in a round-based game.

  1. Can’t persist maps (critical for save data in multiplayer games)
  2. Persisting data at all crashes most of the time

Again, in a non-round game, you can store any type of data in the weak_map and GetSession works perfectly.


using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

var Global_CustomPlayers : weak_map(session, [player]custom_player) = map{}
var Global_SaveData : weak_map(session, [player]save_data) = map{}
var Global_Flattened : weak_map(session, [player]int) = map{}
var Global_SimpleInt : weak_map(session, int) = map{}

custom_player := class():
    var Eliminations : int = 0

save_data := struct:
    Eliminations : int
    TimeInGame : float


game_manager := class(creative_device):

    # Runs when the device is started in a running game
    OnBegin<override>()<suspends>:void=

        Sleep(2.0) #Let the game load before running tests (just in case)

        # TestCase1Classes() #Does not persist data betweens rounds and crashes game
        # TestCase2Structs() #Does not persist data between rounds and crashes game
        # TestCase3FlattenedDataWithMaps() #Loading basic values with maps still crashes the game
        # TestCase4CastingMaps()
        TestCase5SimpleData() #Doesn't seem to work all the time and also crashes game

    TestCase5SimpleData():void=

        if (SimpleInt := Global_SimpleInt[GetSession()]):
            Print("Loaded Random Useless int: {SimpleInt}")
        else:
            Print("Saving random useless int")
            SimpleInt := 33
            if (set Global_SimpleInt[GetSession()]= SimpleInt) {}
            else: 
                Print("Failed to save int")


    TestCase4CastingMaps():void=

        AllPlayers := GetPlayspace().GetPlayers()
        if (Player := AllPlayers[0]):
            # Loading existing player
            Print("About to load existing player")
            
            if (Raw := Global_CustomPlayers[GetSession()]):
                Print("Got a session map at least")
                for (Actor : Raw):
                    Print("Got an actor at least")
                    if (CustomPlayer := custom_player[Actor]):
                        Print("Elims From Casting: {CustomPlayer.Eliminations}")
            else:
                # If it has never been initialized, initialize it now
                if (not Global_CustomPlayers[GetSession()]):
                    if (set Global_CustomPlayers[GetSession()] = map{}) {}

                # Save custom player with 3 eliminations
                Print("Saving player data for first time")
                CustomPlayer := custom_player{Eliminations := 3}
                if (set Global_CustomPlayers[GetSession()][Player] = CustomPlayer) {}

    TestCase3FlattenedDataWithMaps():void=
        #TEST CASE #3: Flattened data
        AllPlayers := GetPlayspace().GetPlayers()
        
        # If it has never been initialized, initialize it now
        if (not Global_Flattened[GetSession()]):
            if (set Global_Flattened[GetSession()] = map{}) {}

        if (Player := AllPlayers[0]):
            Print("Total players: {AllPlayers.Length}")
            # Loading existing player
            Print("attempting to even get the map alone")
            if (Elims := Global_Flattened[GetSession()]): #crashes here
                for (E : Elims):
                    Print("Object")

            Print("About to load existing player")
            #Got it to work one time, but still crashed the game
            if (PlayerElims := Global_Flattened[GetSession()][Player]):
                Print("Eliminations Should Equal 3--Actual: {PlayerElims}")
            else:
                # Save custom player with 3 eliminations
                Print("Saving player data for first time")
                Elims := 3
                if (set Global_Flattened[GetSession()][Player] = Elims) {}
                else: 
                    Print("Failed to save player data")
        
    TestCase2Structs():void=    
        #TEST CASE #2: Using Structs
        AllPlayers := GetPlayspace().GetPlayers()
        
        # If it has never been initialized, initialize it now
        if (not Global_SaveData[GetSession()]):
            if (set Global_SaveData[GetSession()] = map{}) {}

        if (Player := AllPlayers[0]):
            # Loading existing player
            Print("About to load existing player")
            #Does NOT work between rounds and sometimes crashes the game
            if (SaveData := Global_SaveData[GetSession()][Player]):
                Print("Eliminations Should Equal 3--Actual: {SaveData.Eliminations}")
            else:
                # Save custom player with 3 eliminations
                Print("Saving player data for first time")
                SaveData := save_data{Eliminations := 3, TimeInGame := GetSimulationElapsedTime()}
                if (set Global_SaveData[GetSession()][Player] = SaveData) {}        
        
    TestCase1Classes():void=
        #TEST CASE #1: Using Classes
        AllPlayers := GetPlayspace().GetPlayers()
        
        # If it has never been initialized, initialize it now
        if (not Global_CustomPlayers[GetSession()]):
            if (set Global_CustomPlayers[GetSession()] = map{}) {}


        #TEST CASE #1: Using Classes
        if (Player := AllPlayers[0]):
            # Loading existing player
            Print("About to load existing player")
            #Does NOT work between rounds and sometimes crashes the game
            if (CustomPlayer := Global_CustomPlayers[GetSession()][Player]):
                Print("Eliminations Should Equal 3--Actual: {CustomPlayer.Eliminations}")
            else:
                # Save custom player with 3 eliminations
                Print("Saving player data for first time")
                CustomPlayer := custom_player{Eliminations := 3}
                if (set Global_CustomPlayers[GetSession()][Player] = CustomPlayer) {}
5 Likes

I’ve brought up the weak_map issues and limitations with members of the Verse team and they have an active discussion going about it. I’ve also pointed the relevant people to this forum post.

There are also your related posts on Twitter/X in case someone posts interesting info there:

4 Likes

Use of a player reachable from the value of a weak_map(session, ...) across rounds is a known bug (a few other types forcefully destroyed between rounds exhibit similar behavior). We’re currently trying to come up with a solution that doesn’t limit usefulness. I’m surprised by the Global_SimpleInt test crashing. I’m currently attempting to reproduce this locally.

2 Likes

I’ve attached my project if you want it. Make sure it’s nothing on my end. Even on the simple Int it will crash most of the time on the next round the moment I try and “load” the data.

WeakMapProject.zip (71.8 KB)

2 Likes

At first I thought it was “Player” itself, but I soon learned that maps can’t be stored between rounds either, not even with a primitive type like int or string. I’ve tried casting too just in case with the same crash results.

I haven’t reproduced the crash, though there have been some changes in this area that likely prevent the crash. However, I do observe the state is not persisted between rounds, except unless you happened to use “Push Verse Changes”. I’ve started on a fix, though can’t make any claims about when it will be available.

3 Likes