Files
DarkflameServer/tests/dWebTests/WorldViewTests.cpp
Aaron Kimbrell e213a7aeff feat: web dashboard and playground work
The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch:
accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy
reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live
events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved
captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes
they need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:43 -05:00

145 lines
6.5 KiB
C++

#include <gtest/gtest.h>
#include <cstring>
#include "PositionHistory.h"
#include "WorldScene.h"
namespace {
// Same layout as LevelObjectsTests: a .lvl with a file info chunk and an object chunk (version 41)
struct LvlWriter {
std::string data;
template<typename T> LvlWriter& Put(T value) { data.append(reinterpret_cast<const char*>(&value), sizeof(T)); return *this; }
template<typename T> void At(size_t pos, T value) { std::memcpy(data.data() + pos, &value, sizeof(T)); }
size_t Chunk(uint32_t id) {
const auto start = data.size();
Put<uint32_t>(0x4B4E4843).Put<uint32_t>(id).Put<uint16_t>(1).Put<uint16_t>(0).Put<uint32_t>(0).Put<uint32_t>(0);
At<uint32_t>(start + 16, static_cast<uint32_t>(data.size()));
return start;
}
void End(size_t chunk) { At<uint32_t>(chunk + 12, static_cast<uint32_t>(data.size() - chunk)); }
void Object(uint64_t id, uint32_t lot, float x, float y, float z, const std::string& settings) {
Put<uint64_t>(id).Put<uint32_t>(lot).Put<uint32_t>(0).Put<uint32_t>(0);
Put(x).Put(y).Put(z).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f).Put(2.0f);
Put<uint32_t>(static_cast<uint32_t>(settings.size()));
for (char c : settings) Put<uint16_t>(static_cast<uint16_t>(c));
Put<uint32_t>(0);
}
};
std::string SampleLevel() {
LvlWriter w;
auto info = w.Chunk(1000);
w.Put<uint32_t>(41).Put<uint32_t>(1).Put<uint32_t>(0).Put<uint32_t>(0).Put<uint32_t>(0);
w.End(info);
auto objects = w.Chunk(2001);
w.Put<uint32_t>(3);
w.Object(10, 4945, 1, 2, 3, "respawnname=0:NS_Portal\r");
w.Object(11, 176, -4, 0, 8, "spawntemplate=1:6010\nspawner_name=0:Crates");
w.Object(12, 1234, 5, 5, 5, "loadOnClientOnly=7:1");
w.End(objects);
return w.data;
}
}
TEST(WorldSceneTests, ReadsEveryObjectWithSpawnersResolved) {
const auto objects = WorldScene::ReadObjects(SampleLevel());
ASSERT_EQ(objects.size(), 3u);
EXPECT_EQ(objects[0].lot, 4945u);
EXPECT_EQ(objects[0].templateLot, 4945u);
EXPECT_EQ(objects[0].name, "NS_Portal");
EXPECT_FALSE(objects[0].spawner);
EXPECT_FLOAT_EQ(objects[0].z, 3);
EXPECT_FLOAT_EQ(objects[0].scale, 2);
EXPECT_TRUE(objects[1].spawner);
EXPECT_EQ(objects[1].templateLot, 6010u);
EXPECT_EQ(objects[1].name, "Crates");
EXPECT_TRUE(objects[2].clientOnly);
EXPECT_TRUE(objects[2].name.empty());
}
TEST(WorldSceneTests, DamagedFilesKeepWhatWasRead) {
auto level = SampleLevel();
level.resize(level.size() - 20);
EXPECT_EQ(WorldScene::ReadObjects(level).size(), 2u);
EXPECT_TRUE(WorldScene::ReadObjects("").empty());
}
TEST(WorldSceneTests, ClassifiesByMostTellingComponent) {
using WorldScene::Classify;
const auto index = [](eReplicaComponentType type) {
for (size_t i = 0; i < WorldScene::KINDS.size(); i++) if (WorldScene::KINDS[i] == type) return i;
return WorldScene::KINDS.size();
};
const auto raw = [](eReplicaComponentType type) { return static_cast<uint32_t>(type); };
// Render only: other
EXPECT_EQ(Classify({ raw(eReplicaComponentType::RENDER) }), WorldScene::KINDS.size());
EXPECT_EQ(Classify({}), WorldScene::KINDS.size());
// The registry's 7 is the destroyable component
EXPECT_EQ(Classify({ 7 }), index(eReplicaComponentType::DESTROYABLE));
// An enemy that can be smashed and offers missions is an enemy
EXPECT_EQ(Classify({ 7, raw(eReplicaComponentType::MISSION_OFFER), raw(eReplicaComponentType::BASE_COMBAT_AI) }), index(eReplicaComponentType::BASE_COMBAT_AI));
EXPECT_EQ(Classify({ raw(eReplicaComponentType::MISSION_OFFER), raw(eReplicaComponentType::RENDER) }), index(eReplicaComponentType::MISSION_OFFER));
}
TEST(PositionHistoryTests, KeepsFirstMovingAndIdleSamples) {
PositionHistory::Throttle throttle;
const LWOOBJID a = 1;
// First report in a world is kept
EXPECT_TRUE(throttle.Keep(a, 1000, 1, 0, 0, 0, 100, 5, 30));
// Moving, but too soon
EXPECT_FALSE(throttle.Keep(a, 1000, 1, 3, 0, 0, 102, 5, 30));
// Moving after the interval
EXPECT_TRUE(throttle.Keep(a, 1000, 1, 6, 0, 0, 105, 5, 30));
// Standing still: not until the idle interval
EXPECT_FALSE(throttle.Keep(a, 1000, 1, 6.1f, 0, 0, 120, 5, 30));
EXPECT_TRUE(throttle.Keep(a, 1000, 1, 6.1f, 0, 0, 135, 5, 30));
// Changing instance or zone keeps it at once
EXPECT_TRUE(throttle.Keep(a, 1000, 2, 6.1f, 0, 0, 136, 5, 30));
EXPECT_TRUE(throttle.Keep(a, 1100, 2, 6.1f, 0, 0, 137, 5, 30));
// Players are independent
EXPECT_TRUE(throttle.Keep(2, 1000, 1, 0, 0, 0, 137, 5, 30));
EXPECT_EQ(throttle.Size(), 2u);
throttle.Forget(137);
EXPECT_EQ(throttle.Size(), 2u);
throttle.Forget(138);
EXPECT_EQ(throttle.Size(), 0u);
}
TEST(PositionHistoryTests, BucketsThinLongReplays) {
// An hour at 5 seconds is 720 samples: every one is kept
EXPECT_EQ(PositionHistory::BucketFor(3600, 5, 3000), 1);
// A week needs 202-second buckets to stay under 3000 per player
EXPECT_EQ(PositionHistory::BucketFor(7 * 86400, 5, 3000), 202);
EXPECT_GE(PositionHistory::BucketFor(1, 5, 0), 1);
}
// Which models the game draws: as ObjectLoader2::LoadRenderComponent, and never objects it doesn't load (carver_only)
TEST(WorldSceneTests, ClientDrawsWhatTheGameDraws) {
LvlWriter w;
auto info = w.Chunk(1000);
w.Put<uint32_t>(41).Put<uint32_t>(1).Put<uint32_t>(0).Put<uint32_t>(0).Put<uint32_t>(0);
w.End(info);
auto objects = w.Chunk(2001);
w.Put<uint32_t>(6);
w.Object(1, 4630, 0, 0, 0, "carver_only=7:1\ncreate_physics=7:1"); // an invisible trigger cube
w.Object(2, 5651, 0, 0, 0, "carver_only=7:0"); // loaded, so drawn
w.Object(3, 5651, 0, 0, 0, "renderDisabled=7:0"); // any value hides it
w.Object(4, 176, 0, 0, 0, "spawntemplate=1:6010"); // drawn as what it spawns
w.Object(5, 176, 0, 0, 0, "spawner_name=0:Nothing"); // spawns nothing
w.Object(6, 5937, 0, 0, 0, ""); // left out by number
w.End(objects);
const auto read = WorldScene::ReadObjects(w.data);
ASSERT_EQ(read.size(), 6u);
using WorldScene::eClientDraw;
EXPECT_TRUE(read[0].carverOnly);
EXPECT_EQ(WorldScene::ClientDraws(read[0], "Environmental"), eClientDraw::HIDDEN);
EXPECT_EQ(WorldScene::ClientDraws(read[1], "Environmental"), eClientDraw::DRAWN);
EXPECT_EQ(WorldScene::ClientDraws(read[1], "BlockingVolume"), eClientDraw::HIDDEN);
EXPECT_EQ(WorldScene::ClientDraws(read[1], "PrimitiveModels"), eClientDraw::NO_MODEL);
EXPECT_EQ(WorldScene::ClientDraws(read[2], "Environmental"), eClientDraw::HIDDEN);
EXPECT_EQ(WorldScene::ClientDraws(read[3], "Smashables"), eClientDraw::DRAWN);
EXPECT_EQ(WorldScene::ClientDraws(read[4], "Smashables"), eClientDraw::NO_MODEL);
EXPECT_EQ(WorldScene::ClientDraws(read[5], "Environmental"), eClientDraw::HIDDEN);
}