[MAJOR] Accessing a persistent sub-object from subsequent round within `component.OnSimulate` always crashes at runtime

Summary

I discovered a case where accessing existing player data in a subsequent round from component.OnSimulate will always cause a runtime crash.
The crash happens in v33.30 and is fully reproducible (starting with the second round - VARIANT A).

Please select what you are reporting on:

Verse

What Type of Bug are you experiencing?

Stability

Steps to Reproduce

The example is fully self contained.

  • place an entity with the component code from below
  • compile and run a round
  • end the round
  • start another round

Variant A (async) always crashes. If you try the non-async variant B, it will not crash and work as expected.

using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/SceneGraph }
using { /Fortnite.com/Game }
using { /Fortnite.com/Playspaces }

# Some simpler player data class.
vz_player_data_v1 := class<final><persistable> {
  Number: int = 42
}

# A player store containing player data object.
vz_player_store := class<final><persistable> {
  PlayerData_v1: ?vz_player_data_v1 = false

  GetPlayerData()<transacts><decides>: vz_player_data_v1 = {
    PlayerData_v1?
  }
}

# Player store weak map.
var PlayerStore_1: weak_map(player, vz_player_store) = map {}

vz_test_persistence_component := class<final_super>(component) {

  var Subscriptions<private>: []cancelable = array {}

  #--------------------------#
  # PROPERTIES FOR VARIANT A #
  #--------------------------#

  RoundEvent<private>: event() = event() {}
  var IsRoundActive<private>: logic = false

  #------------------#
  # LIFETIME METHODS #
  #------------------#

  OnBeginSimulation<override>(): void = {
    (super:)OnBeginSimulation()

    if (RoundManager := Entity.GetFortRoundManager[]) {
      set Subscriptions = array {
        RoundManager.SubscribeRoundEnded(RoundEnded)

        # VARIANT A - CRASHING
        RoundManager.SubscribeRoundStarted(RoundStarted_A)

        # VARIANT B - OKAY
        #RoundManager.SubscribeRoundStarted(RoundStarted_B)
      }
    }
  }

  OnSimulate<override>()<suspends>: void = {
    # CRASHING VARIANT (A)
    if (not IsRoundActive?) {
      Print("Will await round start")
      RoundEvent.Await()
    } 
    LookupPlayspaceParticipant()
  }

  #-----------#
  # VARIANT A #
  #-----------#

  RoundStarted_A()<suspends>: void = {
    Print("Round started")
    set IsRoundActive = true
    RoundEvent.Signal()
  }

  RoundEnded(): void = {
    Print("Round ended")
    set IsRoundActive = false
  }

  #-----------#
  # VARIANT B #
  #-----------#

  RoundStarted_B()<suspends>: void = {
    LookupPlayspaceParticipant()
  }

  #------------------------------------------------------------------------------#
  #------------------------------------------------------------------------------#

  LookupPlayspaceParticipant(): void = {
    if (Playspace := Entity.GetPlayspaceForEntity[]) {
      for (Participant: Playspace.GetParticipants()) {
        ParticipantAdded(Participant)
      }
    }
  }

  ParticipantAdded(Participant: agent): void = {
    # Get the player store object for the given player, persist it if it's new.
    if (Player := player[Participant]) {
      PlayerStore := if (ExistingPlayerStore := PlayerStore_1[Player]) {
        Print("Using existing player store")
        ExistingPlayerStore
      } else {
        Print("Creating new player store")
        NewPlayerStore := vz_player_store {
          PlayerData_v1 := option {
            vz_player_data_v1 {}
          }
        }
        if {
          set PlayerStore_1[Player] = NewPlayerStore
          Print("Stored new player store")
        }
        NewPlayerStore
      }

      Print("Try accessing the player data 1")
      if (PlayerData := PlayerStore.PlayerData_v1?) {
        Print(ToDiagnostic(PlayerData))
      }

      Print("Try accessing the player data 2")
      if (PlayerData := PlayerStore.GetPlayerData[]) {
        Print(ToDiagnostic(PlayerData))
      }
    }
  }
}

Reproducible crash stack trace:

[2025.02.12-11.00.36:895][686]LogVerse: : Will await round start
[2025.02.12-11.00.55:428][940]LogVerse: : Round started
[2025.02.12-11.00.55:428][940]LogVerse: : Using existing player store
[2025.02.12-11.00.55:429][940]LogVerse: : Try accessing the player data 1
[2025.02.12-11.00.55:430][940]LogVerse: Error: VerseRuntimeErrors: Verse unrecoverable error: ErrRuntime_Internal: An internal runtime error occurred. There is no other information available. (Internal error encountered: Attempted to access vz_player_store_2147482641 via property __verse_0x5F3EF02F_PlayerStore_1, but vz_player_store_2147482641 is not valid (pending kill or garbage).)
Truncated callstack follows:
    (.../vz_test_persistence_component:)ParticipantAdded(:agent)  (Source: /vz_test_persistence_component.verse(117,10, 118,11))
    (.../vz_test_persistence_component:)LookupPlayspaceParticipant    (Source: /vz_test_persistence_component.verse(91,24, 92,25))
    OnSimulate  (Source: /vz_test_persistence_component.verse(59,30, 60,31))

Expected Result

It should not crash at runtime.

Observed Result

It always crashes at the beginning of the second and any further rounds in a uefn session.

Platform(s)

PC

hi @Knight_Breaker ,
Tried your example code and it failed. In the code added runtime fail blocker to restore the operation when the action fails and stops the runtime error.

Verse Language Quick Reference - Function calls

Failable function call: A failable function call has the form FunctionName[]. A function is marked as failable when its definition has the decides specifier.

I’m sorry but I genuinely fail to understand your message. Regarding my report, it’s been already confirmed as a known bug by an Epic staff and will be addressed in a future update.

hi @Knight_Breaker ,
Yes was a bit jumbled, leaving reference to documentation
Thanks

The failure in GetPlayerData is expected verse construct and it should not fail in the given example at all because the object we fetch is definitely set. However something happens internally that makes the runtime thing that the object is invalid. That’s the issue here and Epic will fix it. :wink:

1 Like

FORT-859334 has been ‘Closed’ as a duplicate of an existing known issue.

hi @Knight_Breaker ,
There is a new function in Verse for 34.00

GetParticipants()

which also allows multiple-user testing.

If your code uses GetPlayers() instead of GetParticipants() , you will not be able to test functionality using test players, because GetPlayers() will only return a list of players.

Verse API reference page for the GetParticipants function | Unreal Editor for Fortnite Documentation | Epic Developer Community

See also
Example Granting Items to Test Players with Verse GetParticipants function

Unrelated to the bug report. Plus the mentioned function existed before v34.00.

1 Like