Verse array literal has stale data after adding elements - instances see different lengths

Summary

When new elements are added to a class-level array literal initializer in a plain Verse class, most device instances that instantiate that class still see the old, shorter array at runtime. Only 1-2 instances see the correct updated length. The array data is genuinely truncated, not just the .Length value. Failable array access fails for indices beyond the old array size. This causes silent logic failures with no compile-time or runtime error.

Please select what you are reporting on:

Verse

What Type of Bug are you experiencing?

Verse

Steps to Reproduce

  1. Create a simple data class and a helper class with an array literal containing 32 elements:
my_item := class<concrete>()
{
    ID : int = 0
    Name : string = ""
}

my_helper := class()
{
    Items : []my_item = array{
        my_item{ ID := 1,  Name := "Item1"  },
        my_item{ ID := 2,  Name := "Item2"  },
        my_item{ ID := 3,  Name := "Item3"  },
        my_item{ ID := 4,  Name := "Item4"  },
        my_item{ ID := 5,  Name := "Item5"  },
        my_item{ ID := 6,  Name := "Item6"  },
        my_item{ ID := 7,  Name := "Item7"  },
        my_item{ ID := 8,  Name := "Item8"  },
        my_item{ ID := 9,  Name := "Item9"  },
        my_item{ ID := 10, Name := "Item10" },
        my_item{ ID := 11, Name := "Item11" },
        my_item{ ID := 12, Name := "Item12" },
        my_item{ ID := 13, Name := "Item13" },
        my_item{ ID := 14, Name := "Item14" },
        my_item{ ID := 15, Name := "Item15" },
        my_item{ ID := 16, Name := "Item16" },
        my_item{ ID := 17, Name := "Item17" },
        my_item{ ID := 18, Name := "Item18" },
        my_item{ ID := 19, Name := "Item19" },
        my_item{ ID := 20, Name := "Item20" },
        my_item{ ID := 21, Name := "Item21" },
        my_item{ ID := 22, Name := "Item22" },
        my_item{ ID := 23, Name := "Item23" },
        my_item{ ID := 24, Name := "Item24" },
        my_item{ ID := 25, Name := "Item25" },
        my_item{ ID := 26, Name := "Item26" },
        my_item{ ID := 27, Name := "Item27" },
        my_item{ ID := 28, Name := "Item28" },
        my_item{ ID := 29, Name := "Item29" },
        my_item{ ID := 30, Name := "Item30" },
        my_item{ ID := 31, Name := "Item31" },
        my_item{ ID := 32, Name := "Item32" }
    }

    GetItem(Index:int):?my_item=
    {
        if (Item := Items[Index])
        {
            return option{Item}
        }
        return false
    }
}
  1. Create a creative_device subclass that instantiates this helper and logs diagnostics:
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Verse.org/Simulation/Tags }

my_test_device_tag := class(tag){}

my_test_device := class(creative_device)
{
    @editable DeviceID : int = 1

    Helper : my_helper = my_helper{}

    OnBegin<override>()<suspends>:void=
    {
        Print("DEBUG - DeviceID={DeviceID}, ArrayLength={Helper.Items.Length}")

        # Try to access this device's own item
        ItemIndex := DeviceID - 1
        MaybeItem := Helper.GetItem(ItemIndex)
        if (Item := MaybeItem?)
        {
            Print("DEBUG - DeviceID={DeviceID}, Found: ID={Item.ID}, Name={Item.Name}")
        }
        else
        {
            Print("BUG - DeviceID={DeviceID}, FAILED to access index {ItemIndex}")
        }

        # Also try to access the last item to test full array availability
        LastIndex := Helper.Items.Length - 1
        MaybeLastItem := Helper.GetItem(LastIndex)
        if (LastItem := MaybeLastItem?)
        {
            Print("DEBUG - DeviceID={DeviceID}, Last item: ID={LastItem.ID}, Name={LastItem.Name}")
        }
        else
        {
            Print("BUG - DeviceID={DeviceID}, FAILED to access last index {LastIndex}")
        }
    }
}
  1. Place 32 instances of my_test_device in the world, setting DeviceID on each to values 1 through 32. Tag each with my_test_device_tag.

  2. Build Verse code, launch a session, and confirm all 32 devices report ArrayLength=32 and successfully access their items. This establishes the baseline.

  3. Now add 12 new elements to the array in my_helper (Items 33-44):

        # ... existing items 1-32 above ...
        my_item{ ID := 33, Name := "Item33" },
        my_item{ ID := 34, Name := "Item34" },
        my_item{ ID := 35, Name := "Item35" },
        my_item{ ID := 36, Name := "Item36" },
        my_item{ ID := 37, Name := "Item37" },
        my_item{ ID := 38, Name := "Item38" },
        my_item{ ID := 39, Name := "Item39" },
        my_item{ ID := 40, Name := "Item40" },
        my_item{ ID := 41, Name := "Item41" },
        my_item{ ID := 42, Name := "Item42" },
        my_item{ ID := 43, Name := "Item43" },
        my_item{ ID := 44, Name := "Item44" }
  1. Place 12 new my_test_device instances for DeviceIDs 33-44.

  2. Build Verse code and launch a new session.

  3. Observe log output.

Expected Result

All 44 device instances should report ArrayLength=44 and successfully access both their own item and the last item in the array:

LogVerse: : DEBUG - DeviceID=1, ArrayLength=44
LogVerse: : DEBUG - DeviceID=1, Found: ID=1, Name=Item1
LogVerse: : DEBUG - DeviceID=1, Last item: ID=44, Name=Item44
LogVerse: : DEBUG - DeviceID=2, ArrayLength=44
LogVerse: : DEBUG - DeviceID=2, Found: ID=2, Name=Item2
LogVerse: : DEBUG - DeviceID=2, Last item: ID=44, Name=Item44
...
LogVerse: : DEBUG - DeviceID=33, ArrayLength=44
LogVerse: : DEBUG - DeviceID=33, Found: ID=33, Name=Item33
LogVerse: : DEBUG - DeviceID=33, Last item: ID=44, Name=Item44
...
LogVerse: : DEBUG - DeviceID=44, ArrayLength=44
LogVerse: : DEBUG - DeviceID=44, Found: ID=44, Name=Item44
LogVerse: : DEBUG - DeviceID=44, Last item: ID=44, Name=Item44

Observed Result

After adding elements 33-44 and rebuilding, most device instances report the old stale array length of 32. Only 1-2 instances report the correct length of 44. This is not just a .Length reporting issue. The array data is genuinely truncated at runtime. Failable array access (if (Item := Items[Index])) fails for indices 32+ on affected instances, meaning the newly added elements do not exist in memory for those instances despite being present in the source code.

This causes downstream logic to silently fall through to default values (zeros, empty strings, false) with no compile-time or runtime error, leading to incorrect game behavior that is extremely difficult to diagnose.

LogVerse: : DEBUG - DeviceID=1, ArrayLength=44
LogVerse: : DEBUG - DeviceID=1, Found: ID=1, Name=Item1
LogVerse: : DEBUG - DeviceID=1, Last item: ID=44, Name=Item44
LogVerse: : DEBUG - DeviceID=2, ArrayLength=32
LogVerse: : DEBUG - DeviceID=2, Found: ID=2, Name=Item2
LogVerse: : DEBUG - DeviceID=2, Last item: ID=32, Name=Item32
LogVerse: : DEBUG - DeviceID=3, ArrayLength=32
LogVerse: : DEBUG - DeviceID=3, Found: ID=3, Name=Item3
LogVerse: : DEBUG - DeviceID=3, Last item: ID=32, Name=Item32
...
LogVerse: : DEBUG - DeviceID=32, ArrayLength=32
LogVerse: : DEBUG - DeviceID=32, Found: ID=32, Name=Item32
LogVerse: : DEBUG - DeviceID=32, Last item: ID=32, Name=Item32
LogVerse: : DEBUG - DeviceID=33, ArrayLength=32
LogVerse: : BUG - DeviceID=33, FAILED to access index 32
LogVerse: : DEBUG - DeviceID=33, Last item: ID=32, Name=Item32
LogVerse: : DEBUG - DeviceID=34, ArrayLength=32
LogVerse: : BUG - DeviceID=34, FAILED to access index 33
LogVerse: : DEBUG - DeviceID=34, Last item: ID=32, Name=Item32
...
LogVerse: : DEBUG - DeviceID=44, ArrayLength=32
LogVerse: : BUG - DeviceID=44, FAILED to access index 43
LogVerse: : DEBUG - DeviceID=44, Last item: ID=32, Name=Item32

Platform(s)

Windows (UEFN Editor)

Island Code

9881-8842-2718

Additional Notes

  • The class containing the array (my_helper) is a plain Verse class, not a creative_device. Each creative_device instance creates its own copy via default construction (my_helper{}).
  • The array was originally 32 elements. After adding 12 new elements to bring it to 44, most runtime instances still contain only the original 32 elements.
  • At least one instance does see the correct 44 elements, confirming the source file compiles correctly.
  • Full Verse rebuilds and UEFN restarts have not resolved the issue.
  • In our production project, 46 creative_device instances reference this helper class across multiple streaming levels. The silent failure causes every newly added area (13 areas) to unlock for free with no cost check, since the failable lookups returned false and all requirement variables stayed at their default zero values.
  • There is no compile-time warning, no runtime error, and no log output indicating data staleness. The only way to detect the problem is to explicitly log .Length and compare against the source code.

FORT-1055505 has been created and its status is ā€˜Unconfirmed’. This is now in a queue to be reproduced and confirmed.

The issue is worse than initially reported. Moving to a single shared instance of the helper class does not fix the problem.

I moved the ā€œmy_helperā€ instance to a single location so that all device instances reference the same shared object instead of each creating their own. The shared instance still contains stale array data.

Example from the log after this change: The device for DeviceID=1 reads the item at index 0 and gets a mix of correct and incorrect field values. Some fields contain the correct current data while others contain stale or default values from a previous build. This happens on the single shared instance, not across multiple instances.

This means the bug is not caused by multiple instances of the class getting different cached data. The Verse runtime is serving stale or partially corrupted data from the array literal itself, even for a single instance. Fields within the same array element can have a mix of current and stale values.

I am going to attempt a workaround by removing the array literal initializer entirely and building the array at runtime using concatenation in a function. I will report back on whether this avoids the issue.

This bug is a serious data integrity problem. There is no compile-time or runtime error. The data silently contains wrong values. The only way to detect it is to manually log and verify every field against the source code.

Follow-up: The workaround of building the array at runtime instead of using a literal array initializer resolves the issue. All 44 elements are now accessible with correct data across all device instances.

No other code was changed. The only difference is how the array is populated. The original code defined the array as a literal initializer on the class field. The workaround removes that literal and instead populates the array imperatively at runtime through a function called once during startup.

Original (broken):

my_helper := class()
{
    Items : []my_item = array{
        my_item{ ID := 1,  Name := "Item1"  },
        my_item{ ID := 2,  Name := "Item2"  },
        # ... 44 total elements
        my_item{ ID := 44, Name := "Item44" }
    }
}

Workaround (working):

my_helper := class()
{
    var Items : []my_item = array{}

    BuildItems():void=
    {
        set Items = array{}
        set Items += array{my_item{ ID := 1,  Name := "Item1" }}
        set Items += array{my_item{ ID := 2,  Name := "Item2" }}
        # ... 44 total elements
        set Items += array{my_item{ ID := 44, Name := "Item44" }}
    }
}

While this workaround is functional, it is not ideal for performance. Verse arrays are immutable value types, so each concatenation allocates an entirely new copy of the array with one additional element. For 44 elements, that means 44 increasingly large array copies during startup. A literal initializer should not require this. Developers should not have to take this performance penalty to get correct data.

You can use a different type of setup for the array. You set the array and add the last value in any of the code within the {} this is much more efficient.

I had something like this for props and again it took much more of an impact doing the += array{} this method is highly more efficient i found…

Example

    set CapsuleZoneProps = {

        for(PropInstance : CapsuleZoneProps, PropInstance.Prop.IsValid\[\]):

            PropInstance

        }

Its setting the array ā€œCapsuleZonePropsā€œ to the last value ie PropInstance for every iteration within the for() that passes the checks.

This might help you in the interim just to make your code run better.

This video explains it really well so maybe have a look if anything just to improve your code while they fix the issue…

1 Like

Cool, thanks. I’ll check it out! :slight_smile:

1 Like

Thanks for sharing the video. It’s pretty cool information, but unfortunately I don’t think it helps in this case.

Array comprehensions (for...do) seem great when you’re iterating over an existing collection, like finding all devices with a tag and subscribing to their events. But for my BuildItems workaround, there’s nothing to iterate over. Each item in the array is a unique, handcrafted struct with different values.

To use a comprehension, I’d still need to put all the structs into a source collection first, which means building a literal array{} with all the elements. And that’s what I’m trying to work around, because of the UEFN/Verse bug I reported where large literal arrays get corrupted at runtime.

The sequential Append (e.g. set Array += array{ Element }) pattern is the workaround. It forces each element through dynamic allocation rather than relying on a single literal. It’s verbose and slow, but I don’t know what else can be done.

It’s possible my project is in some weird state where this specific literal array is getting corrupted, but either way, it’s still a bug. I hope Epic can find what’s causing it and fix it. Appreciate the link to the video - Mikey has some good content. :slightly_smiling_face:

Did you try renaming the array/class might help i know of a time something similar happened to someone else and they changed the names and it worked i think.

1 Like

I was just thinking about that! Let’s see what happens!

1 Like

I changed the class name of the item stored in the array from my_item to my_item_ex.

Strangely, that caused the result to be the exact opposite of before. :thinking:

The final 12 elements now correctly report the array as being 44 elements in size and have the assigned literal values. But the original 32 elements are now showing the array as having only 32 elements, and all of their values are the default values from the class definition, not the assigned literal values.

So I changed the item stored in the array from a class to a struct, and removed <concrete> for good measure. I also changed the name of the helper class from my_helper to my_helper_ex.

And, (drumroll please) … now all of the elements have the correct values and the array reports the correct size. :open_mouth: Mmkay. Yay??

So I change the names back to the originals, but leave my_item as a struct() instead of a class<concrete>().

And … it’s back to the way it was. It doesn’t work again.

So I guess I have to keep the renamed versions. Sigh. :face_exhaling:

Does anyone know what is causing this? Are there temporary intermediate files that can be deleted somewhere?

I think you should save both the correct working version and none working version of the code and supply epic with it. This isn’t the first time I’ve known of this issue albeit never had it myself. Someone at Epic needs to look into this makes no sense. You get no errors ect either which is just confusing and worse imo.

1 Like

To clearly restate the issue with what is now known. If you do this:

my_item := struct()
{
    ID : int = 0
    Name : string = ""
}

my_helper := class()
{
    Items : []my_item = array{
        my_item{ ID := 1,  Name := "Item1"  }
        my_item{ ID := 2,  Name := "Item2"  }
        my_item{ ID := 3,  Name := "Item3"  }
        my_item{ ID := 4,  Name := "Item4"  }
        # ... items 5-28
        my_item{ ID := 29, Name := "Item29" }
        my_item{ ID := 30, Name := "Item30" }
        my_item{ ID := 31, Name := "Item31" }
        my_item{ ID := 32, Name := "Item32" }
    }

    GetItem(Index:int):?my_item=
    {
        if (Item := Items[Index])
        {
            return option{Item}
        }
        return false
    }
}

And compile and use that code. And then one day later in time, do this:

my_item := struct()
{
    ID : int = 0
    Name : string = ""
}

my_helper := class()
{
    Items : []my_item = array{
        # ... items 1-32 from above ...
        my_item{ ID := 33, Name := "Item33" } # NEW
        my_item{ ID := 34, Name := "Item34" } # NEW
        my_item{ ID := 35, Name := "Item35" } # NEW
        my_item{ ID := 36, Name := "Item36" } # NEW
        my_item{ ID := 37, Name := "Item37" } # NEW
        my_item{ ID := 38, Name := "Item38" } # NEW
        my_item{ ID := 39, Name := "Item39" } # NEW
        my_item{ ID := 40, Name := "Item40" } # NEW
        my_item{ ID := 41, Name := "Item41" } # NEW
        my_item{ ID := 42, Name := "Item42" } # NEW
        my_item{ ID := 43, Name := "Item43" } # NEW
        my_item{ ID := 44, Name := "Item44" } # NEW
    }

    GetItem(Index:int):?my_item=
    {
        if (Item := Items[Index])
        {
            return option{Item}
        }
        return false
    }
}

Then the entire array may end up corrupted, as reported above. There are no errors. There are no notifications. Nothing. Just random garbage in the array that shows up when you least expect it and will not go away. Doesn’t matter if it is recompiled or anything else.

I don’t know if it always happens when new elements are added like this. Or only sometimes happens. Or what. But I do know it happened in my code.

In an attempt to work around this, if you then rename the item in the array, like this:

# RENAMED my_item to my_item_ex
my_item_ex := struct()
{
    ID : int = 0
    Name : string = ""
}

my_helper := class()
{
    Items : []my_item_ex = array{
        # ... items 1-44 from above ...
    }

    GetItem(Index:int):?my_item_ex=
    {
        if (Item := Items[Index])
        {
            return option{Item}
        }
        return false
    }
}

Then, from what I observed, a different, almost inverse, type corruption of values occurs. In any case. The array is again corrupted with garbage.

But if instead you rename the class that contains the literal array, like this:

my_item := struct()
{
    ID : int = 0
    Name : string = ""
}

# RENAMED my_helper to my_helper_ex
my_helper_ex := class()
{
    Items : []my_item = array{
        # ... items 1-44 from above ...
    }

    GetItem(Index:int):?my_item=
    {
        if (Item := Items[Index])
        {
            return option{Item}
        }
        return false
    }
}

Then that will make the issue stop. However, whatever caused the original issue still exists, and renaming the class containing the array back will return it to a corrupted garbage state.

I have no idea why. I’m just reporting it. Took 2 days to track this down and work around it. And in the end, I have the exact same code I started with (albeit with one single class renamed).

In my actual code, the original structure containing the data was named area_def. The original helper class that contained the array was named areas_helper. The workaround was to rename that class to something else. In my case I renamed it to areas_helper_ex.

1 Like

Did that array was an editable before? Editables are ā€œfrozenā€ with the custom value, and that frozen state is kept even if you remove the @editable attribute… (In scene graph a blue dot shows when value is frozen overriden, but in old creative devices it does not show)