fix(level): change scene object config as the client does while loading it

ReadLvlObjectData (1.10.64, 0x0103ba20) changes an object's config as it
loads it:
- before version 47 a respawn time is in milliseconds: a float over 100
  or any u32 becomes that many seconds, as a float;
- a model spawner (LOT 176 with spawntemplate 14) gets its subkey as its
  blueprintid unless it has one, DisableModelBehaviors true unless set,
  and preventRenderWrapping true;
- before version 44 an object with sceneIDOverrideEnabled gets
  sceneLayerIDOverride 0.

LevelFile now makes the same changes. On live worlds 1906 spawners in
114 scene files of versions 45-46 have a u32 respawn time; the world
already divided those by 1000 but as an integer, so they now keep their
fraction (a 2500 ms respawn is 2.5 s, not 2 s). No live scene has a
model spawner; 62 objects of version 43 scenes get sceneLayerIDOverride.

Check in game: enemies and smashables respawn after the same time as
before on Forbidden Valley, Avant Gardens and Gnarled Forest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 06:36:03 -05:00
parent 458fb601c9
commit 695bcf2352
3 changed files with 68 additions and 0 deletions

View File

@@ -505,6 +505,36 @@ TEST(LevelFileTests, SkipsEditorSettingsBySize) {
EXPECT_TRUE(oldLevel.objects.empty());
}
// Config the client changes as it loads an object
TEST(LevelFileTests, FixesUpConfigAsTheClient) {
ZoneBytes w;
w.Put<uint16_t>(30).Put<uint16_t>(0);
w.data.append(48 + 12, '\0');
w.Put<uint32_t>(0).Put<uint32_t>(4);
w.Object(30, 176, 0, "respawn=3:5000\nsceneIDOverrideEnabled=7:1");
w.Object(30, 176, 0, "respawn=5:20000");
w.Object(30, 176, 0, "respawn=3:20\nspawntemplate=1:14\nsubkey=9:123");
w.Object(30, 176, 0, "spawntemplate=1:14\nblueprintid=9:5\nDisableModelBehaviors=7:0");
std::istringstream stream(w.Done());
LevelFile level;
level.Read(stream);
ASSERT_EQ(level.objects.size(), 4u);
const auto value = [&level](size_t object, const std::u16string& key) {
const auto it = level.objects[object].settings.find(key);
return it == level.objects[object].settings.end() ? std::string("none") : it->second->GetString();
};
EXPECT_EQ(value(0, u"respawn"), "respawn=3:5.000000");
EXPECT_EQ(value(0, u"sceneLayerIDOverride"), "sceneLayerIDOverride=5:0");
EXPECT_EQ(value(1, u"respawn"), "respawn=3:20.000000");
EXPECT_EQ(value(2, u"respawn"), "respawn=3:20.000000");
EXPECT_EQ(value(2, u"blueprintid"), "blueprintid=9:123");
EXPECT_EQ(value(2, u"DisableModelBehaviors"), "DisableModelBehaviors=7:1");
EXPECT_EQ(value(2, u"preventRenderWrapping"), "preventRenderWrapping=7:1");
EXPECT_EQ(value(3, u"blueprintid"), "blueprintid=9:5");
EXPECT_EQ(value(3, u"DisableModelBehaviors"), "DisableModelBehaviors=7:0");
}
TEST(LevelFileTests, DamagedFilesKeepWhatWasRead) {
ZoneBytes w;
w.Put<uint16_t>(30).Put<uint16_t>(0);