sbox-issues icon indicating copy to clipboard operation
sbox-issues copied to clipboard

Static field collections without upgraders reset to empty on hotload and when cloud version differs from local

Open andy013 opened this issue 2 months ago • 0 comments

Branch

staging

Describe the bug

When you upload your project to sbox.game and later test it locally with different static field values, collections can unexpectedly lose their data. This happens because the engine loads the cloud version first, then hotloads to your local version - but your local field initializers never run.

You'd expect your local code to work exactly as written, but the cloud version silently overwrites it. There's no warning or error - your collections just appear empty.

In this example project I am using CircularBuffer as that is what I was using when the bug first occurred, but it can happen with any collections that use the DefaultUpgrader (Queue, Stack etc.).

This bug confused the hell out of me because I had no idea that what I had uploaded to sbox.game could interfere with my project when testing multiplayer locally in the editor.

Example:

Cloud version uploaded to sbox.game: public static CircularBuffer<int> TestBuffer = new(5);

Local version you're testing: public static CircularBuffer<int> TestBuffer = new(5, new int[] {1,2,3,4,5});

When testing locally in multiplayer, TestBuffer.Count will be 0 instead of 5.

I think it's caused by DefaultUpgrader.OnTryCreateNewInstance reusing the old instance when the type hasn't changed therefore bypassing the field initializer.

It's this line: newInstance = newType != type ? RuntimeHelpers.GetUninitializedObject(newType) : oldInstance;

The bug also affects instance fields when the instance is statically referenced:

public static MyClass Instance = new();
  // MyClass.someQueue field will also reset

The bug also happens when you just hotload in the editor. If you save public static CircularBuffer<int> TestBuffer { get; private set; } = new(5); then change it to public static CircularBuffer<int> TestBuffer { get; private set; } = new(5, new int[] { 1, 2, 3, 4, 5 }); later when you check Count() it will still be 0. Only fully restarting the editor will fix it.

To Reproduce

  1. Download and open this repo: circularbufferbug.zip
  2. Publish it to sbox.game
  3. Open TestClass.cs
  4. Comment out the TestBuffer initialization and uncomment out the other one.
  5. Play the game.
  6. Join the game with another client.
  7. Observe console logs: TestBuffer.Count = 0 even though the local code initialized it with 5 ints.

Expected behavior

Local field initializers should run on hotload.

Media/Files

No response

Additional context

No response

andy013 avatar Nov 07 '25 01:11 andy013