revert(world): the edit mode world

This reverts commit f6914940. The client's level editor builds a zone's objects
itself from its own files, so a world server that constructs none of them has no use.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 18:37:23 -06:00
parent a4c4c24a5c
commit 3526ee2eb0
14 changed files with 14 additions and 219 deletions

View File

@@ -98,11 +98,6 @@ CppScripts::Script* const ScriptComponent::GetScript() {
void ScriptComponent::SetScript(const std::string& scriptName) {
// Scripts are managed by the CppScripts class and are effecitvely singletons
// and they may also be used by other script components so DON'T delete them.
// Edit mode (docs/EditMode.md): the zone control object stays, its zone script does not run
if (Game::zoneManager && Game::zoneManager->GetEditMode() && m_Parent->GetObjectID() == dZoneManager::ZONE_CONTROL_OBJECT_ID) {
m_Script = CppScripts::GetInvalidScript();
return;
}
m_Script = CppScripts::GetScript(m_Parent, scriptName);
}

View File

@@ -118,8 +118,7 @@ namespace GameMessages {
std::vector<Entity*> racingControllers = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL);
for (Entity* racingController : racingControllers) {
auto* racingComponent = racingController->GetComponent<RacingControlComponent>();
// Edit mode: a race track's zone control doesn't take the GM into a race (or send them out for having no car)
if (racingComponent != nullptr && !Game::zoneManager->GetEditMode()) {
if (racingComponent != nullptr) {
racingComponent->OnPlayerLoaded(entity);
}
}

View File

@@ -5,7 +5,6 @@ set(DGAME_DUTILITIES_SOURCES "BrickDatabase.cpp"
"CheatDetection.cpp"
"DashboardActions.cpp"
"EconomyLedger.cpp"
"EditMode.cpp"
"DashboardNotify.cpp"
"GUID.cpp"
"LiveEvents.cpp"

View File

@@ -1,13 +0,0 @@
#include "EditMode.h"
#include <algorithm>
#include "CharacterComponent.h"
#include "Game.h"
#include "GeneralUtils.h"
#include "dConfig.h"
eGameMasterLevel EditMode::RequiredLevel() {
const auto level = GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("editor_gm_level")).value_or(CharacterComponent::DEFAULT_EDITOR_GM_LEVEL);
return static_cast<eGameMasterLevel>(std::clamp<uint32_t>(level, 1, 9));
}

View File

@@ -1,18 +0,0 @@
#pragma once
#include "eGameMasterLevel.h"
/**
* Edit mode (docs/EditMode.md): a private instance of a zone whose world server constructs none of the zone's level
* objects, spawners or triggers, for editing the zone in the game client. Only the plumbing is here; nothing starts
* such an instance yet.
*/
namespace EditMode {
// Whether an account may be in an edit mode instance
inline bool MayEnter(const eGameMasterLevel accountLevel, const eGameMasterLevel requiredLevel) {
return accountLevel != eGameMasterLevel::CIVILIAN && accountLevel >= requiredLevel;
}
// The account GM level an edit mode instance lets in: editor_gm_level (9 unless set)
eGameMasterLevel RequiredLevel();
}

View File

@@ -86,7 +86,6 @@
#include "DestroyableComponent.h"
#include "Game.h"
#include "PropertyManagementComponent.h"
#include "EditMode.h"
#include "AssetManager.h"
#include "ZoneOverrideStore.h"
#include "LevelProgressionComponent.h"
@@ -245,7 +244,6 @@ int main(int argc, char** argv) {
uint32_t cloneID = 0;
uint32_t maxClients = 8;
uint32_t ourPort = 2007;
bool editMode = false;
//Check our arguments:
for (int32_t i = 0; (i + 1) < argc; i++) {
@@ -260,7 +258,6 @@ int main(int argc, char** argv) {
else if (argument == "-clone") cloneID = valOptional.value_or(0);
else if (argument == "-maxclients") maxClients = valOptional.value_or(8);
else if (argument == "-port") ourPort = valOptional.value_or(2007);
else if (argument == "-editmode") editMode = valOptional.value_or(0) != 0;
}
Game::config = new dConfig("worldconfig.ini");
@@ -429,8 +426,6 @@ int main(int argc, char** argv) {
Profiler::Scope navmesh("Navmesh and physics load", Profiler::Phase::PHYSICS);
dpWorld::Initialize(zoneID);
}
// A GM's edit mode instance (docs/EditMode.md): master says so when it starts the world
Game::zoneManager->SetEditMode(editMode);
Game::zoneManager->Initialize(LWOZONEID(zoneID, g_InstanceID, cloneID));
PacketCapture::SetClone(cloneID);
g_CloneID = cloneID;
@@ -586,8 +581,7 @@ int main(int argc, char** argv) {
DashboardNotify::Flush();
DashboardNotify::SendPlayerPositions(g_InstanceID);
MessageInspector::Update();
// (an edit mode instance has no server objects, so no live events either)
if (!Game::zoneManager->GetEditMode()) LiveEvents::Update();
LiveEvents::Update();
UgcManifest::Update();
Metrics::StartMeasurement(MetricVariable::UpdateSpawners);
@@ -898,17 +892,6 @@ namespace {
} else {
LOG("User %s authenticated with correct key.", username.GetAsString().c_str());
// An edit mode instance takes only accounts that may use /editmode, however they found its address
if (Game::zoneManager->GetEditMode()) {
const auto account = Database::Get()->GetAccountInfo(username.GetAsString());
if (!EditMode::MayEnter(account ? account->maxGmLevel : eGameMasterLevel::CIVILIAN, EditMode::RequiredLevel())) {
LOG("User %s may not enter an edit mode instance; disconnecting them", username.GetAsString().c_str());
Game::server->Disconnect(it->second.sysAddr, eServerDisconnectIdentifiers::KICK);
g_PendingUsers.erase(it);
return;
}
}
UserManager::Instance()->DeleteUser(masterAddr);
//Create our user and send them in:

View File

@@ -47,7 +47,7 @@ Level::Level(Zone* parentZone, const std::string& filepath, int32_t sceneID) {
static_cast<LevelFile::ChunkHeader&>(header) = chunkHeader;
m_ChunkHeaders.insert(std::make_pair(id, header));
}
LoadSceneObjects(levelFile.objects, m_SceneID);
LoadSceneObjects(levelFile.objects);
}
void Level::MakeSpawner(const SceneObject& obj, int32_t sceneID) {
@@ -120,18 +120,7 @@ void Level::MakeSpawner(const SceneObject& obj, int32_t sceneID) {
Game::zoneManager->MakeSpawner(spawnInfo);
}
void Level::LoadSceneObjects(const std::vector<SceneObject>& objects, const int32_t sceneID) {
// Edit mode: the GM's client has the level's objects from its own files, so none is made here. The oldest files
// keep the spawn point in an object, which is still read.
if (Game::zoneManager->GetEditMode()) {
for (const auto& obj : objects) {
if (obj.lot != LOT_MARKER_PLAYER_START) continue;
Game::zoneManager->GetZoneMut()->SetSpawnPos(obj.position);
Game::zoneManager->GetZoneMut()->SetSpawnRot(obj.rotation);
}
return;
}
void Level::LoadSceneObjects(const std::vector<SceneObject>& objects) {
CDFeatureGatingTable* featureGatingTable = CDClientManager::GetTable<CDFeatureGatingTable>();
CDFeatureGating gating;
@@ -192,7 +181,7 @@ void Level::LoadSceneObjects(const std::vector<SceneObject>& objects, const int3
}
if (obj.lot == 176) { //Spawner
MakeSpawner(obj, sceneID);
MakeSpawner(obj, m_SceneID);
} else { //Regular object
EntityInfo info;
info.spawnerID = 0;
@@ -202,7 +191,7 @@ void Level::LoadSceneObjects(const std::vector<SceneObject>& objects, const int3
info.rot = obj.rotation;
info.settings = obj.settings;
info.scale = obj.scale;
info.scene = sceneID;
info.scene = m_SceneID;
Game::entityManager->CreateEntity(info);
}
}

View File

@@ -20,12 +20,11 @@ public:
static void MakeSpawner(const SceneObject& obj, int32_t sceneID = -1);
// Makes the objects and spawners of a level file. In edit mode (dZoneManager::GetEditMode) none: only the player
// start marker's place is taken
static void LoadSceneObjects(const std::vector<SceneObject>& objects, int32_t sceneID = -1);
std::map<uint32_t, Header> m_ChunkHeaders;
private:
Zone* m_ParentZone;
int32_t m_SceneID = -1;
//private functions:
void LoadSceneObjects(const std::vector<SceneObject>& objects);
};

View File

@@ -138,8 +138,7 @@ void Zone::LoadZoneIntoMemory() {
}
}
// Edit mode: no spawner networks (the paths themselves are kept, they are only data)
if (m_FileFormatVersion >= Zone::FileFormatVersion::EarlyAlpha && !Game::zoneManager->GetEditMode()) {
if (m_FileFormatVersion >= Zone::FileFormatVersion::EarlyAlpha) {
for (const Path& path : m_Paths) {
if (path.pathType != PathType::Spawner) continue;
SpawnerInfo info{};
@@ -287,8 +286,7 @@ void Zone::LoadScene(ZoneScene&& zoneScene) {
LWOSCENEID lwoSceneID(scene.id, scene.sceneType);
std::string luTriggersPath = scene.filename.substr(0, scene.filename.size() - 4) + ".lutriggers";
// Edit mode: no triggers
if (!Game::zoneManager->GetEditMode() && Game::assetManager->HasFile((m_ZonePath + luTriggersPath).c_str())) LoadLUTriggers(luTriggersPath, scene);
if (Game::assetManager->HasFile((m_ZonePath + luTriggersPath).c_str())) LoadLUTriggers(luTriggersPath, scene);
m_Scenes[lwoSceneID] = std::move(scene);
}

View File

@@ -57,7 +57,8 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) {
EntityInfo info;
info.lot = zoneControlTemplate;
info.id = ZONE_CONTROL_OBJECT_ID;
/* Yep its hardcoded like this in the client too, this exact value. */
info.id = 0x3FFF'FFFFFFFELL;
Entity* zoneControl = Game::entityManager->CreateEntity(info, nullptr, nullptr, true);
m_ZoneControlObject = zoneControl;
@@ -72,9 +73,7 @@ void dZoneManager::Initialize(const LWOZONEID& zoneID) {
LOG("Zone prepared in: %llu ms", (endTime - startTime));
// The vanity objects are server objects the client's editor would not know
if (!m_EditMode) VanityUtilities::SpawnVanity();
else LOG("Zone %i is in edit mode: no level objects, spawners, triggers or vanity objects were loaded", zoneID.GetMapID());
VanityUtilities::SpawnVanity();
}
dZoneManager::~dZoneManager() {

View File

@@ -23,9 +23,6 @@ private:
void LoadWorldConfig();
public:
// The zone control object's ID, the same fixed value in the client (LevelManager::LoadZoneControlObject, 0x01082660)
static constexpr LWOOBJID ZONE_CONTROL_OBJECT_ID = 0x3FFF'FFFFFFFELL;
void Initialize(const LWOZONEID& zoneID);
~dZoneManager();
@@ -33,13 +30,6 @@ public:
Zone* GetZoneMut() const;
const Zone* GetZone() const { return GetZoneMut(); };
bool HasZone() const { return m_pZone != nullptr; };
/**
* A GM's edit mode (docs/EditMode.md): the zone's level objects, spawners, spawner networks and triggers are not
* loaded, so the GM's client is the only one that has them. Set before Initialize.
*/
bool GetEditMode() const { return m_EditMode; }
void SetEditMode(bool value) { m_EditMode = value; }
void LoadZone(const LWOZONEID& zoneID); //Discard the current zone (if any) and loads a new zone.
/* Adds a spawner to the zone with the specified ID. */
@@ -113,7 +103,6 @@ private:
bool m_PlayerLoseCoinsOnDeath = false;
bool m_DisableSaveLocation = false;
bool m_MountsAllowed = true;
bool m_EditMode = false;
bool m_PetsAllowed = true;
std::map<LWOOBJID, Spawner*> m_Spawners;
std::optional<WorldConfig> m_WorldConfig = std::nullopt;

View File

@@ -1,54 +0,0 @@
# Edit mode (plumbing)
A private instance of a zone for editing the zone in the game client. The client loads the zone's objects from its own
files; the server never reads the client's files, so in a normal instance its own copies of the same objects are there
too. In an edit mode instance the server constructs none of them.
**Not exposed yet:** no command or packet from a client starts an edit mode instance. This is what exists for one to use.
## The flag
| Where | What |
|---|---|
| World -> master | `MasterPackets::CreatePrivateZone::editMode`, one more byte written only when set (`ZoneInstanceManager::CreatePrivateZone(..., editMode)`) |
| Master | `Instance::GetIsEditMode`; `InstanceManager::CreatePrivateInstance(..., editMode)` |
| Master -> world | The launch argument `-editmode 1` (`StartWorldServer`) |
| World | `dZoneManager::GetEditMode`, set before the zone loads |
A reload (`/reloadworld`, a zone file change) or a live update replaces a private instance with one that has the same
password and edit mode. A request to join a private instance that is still starting is answered once it is ready.
## What an edit mode instance loads
| Loaded | Not loaded |
|---|---|
| The zone file: spawn point, scenes, scene transitions, boundaries, paths (as data) | Level objects (`.lvl`), carver-only blockers |
| The terrain's scene map, the navmesh and the physics world | Spawners (LOT 176) and spawner networks (spawner paths) |
| The level files' revisions (the map checksum sent to the client) | Triggers (`.lutriggers`) |
| The zone control object, with the zone's own template | The zone control object's server script |
| The player, their items, pets and mounts; GM commands | Vanity objects, live event objects |
The zone control object stays: the client makes its own from the `ZoneTable` template under the same fixed ID
(`LevelManager::LoadZoneControlObject`, 0x01082660), the server's construction must match it, and server code expects
the object. Its script is replaced by one that does nothing. Its other components (an activity, a race) stay but are
idle: a race track's zone control does not take the player into a race.
The client waits on nothing a level object sends: it reports the level loaded when its own resource queue is empty and
finishes the login on its character and the server's "done loading" (`LWONetworkInterface::Run`, 0x00b32360).
## The map checksum
The client compares the checksum the server sends in `LOAD_STATIC_ZONE` with the one of its own files
(`LWONetworkInterface::iSendMessage`, 0x00b35870), and logs out with "invalid map version" when they differ. The
checksum is made of the zone and scene files' revisions. Edit mode does not change that: the server's copy of the zone
must have the revisions the client's has.
## Who gets in
The world server of an edit mode instance disconnects accounts below `editor_gm_level` (worldconfig.ini, default 9).
Master never sends anyone to a private instance from a normal transfer or matchmaking.
## The save's `edit` attribute
A character's save has `edit="1"` when its account's GM level is at least `editor_gm_level`, `0` for everyone else.
The client takes anything but 0 as the editor being enabled.

View File

@@ -7,7 +7,6 @@ set(DGAMETEST_SOURCES
"MailPacketTests.cpp"
"MatchmakingTests.cpp"
"EconomyLedgerTests.cpp"
"EditModeTests.cpp"
"PlayerReportsLimitTests.cpp"
"SlashCommandPermissionTests.cpp"
"StaleSaveGuardTests.cpp"

View File

@@ -1,69 +0,0 @@
// Edit mode (docs/EditMode.md): who may enter, and a level file's objects not being made.
#include "GameDependencies.h"
#include "EditMode.h"
#include "EntityManager.h"
#include "Level.h"
#include "LevelFile.h"
#include "Zone.h"
#include "GeneralUtils.h"
#include "dConfig.h"
#include "eObjectBits.h"
#include "dZoneManager.h"
#include <gtest/gtest.h>
class EditModeTests : public GameDependenciesTest {
protected:
void SetUp() override { SetUpDependencies(); }
void TearDown() override { TearDownDependencies(); }
};
TEST_F(EditModeTests, OnlyEditorAccountsMayEnter) {
EXPECT_TRUE(EditMode::MayEnter(eGameMasterLevel::OPERATOR, eGameMasterLevel::OPERATOR));
EXPECT_FALSE(EditMode::MayEnter(eGameMasterLevel::DEVELOPER, eGameMasterLevel::OPERATOR));
EXPECT_FALSE(EditMode::MayEnter(eGameMasterLevel::CIVILIAN, eGameMasterLevel::OPERATOR));
EXPECT_FALSE(EditMode::MayEnter(eGameMasterLevel::CIVILIAN, eGameMasterLevel::CIVILIAN));
EXPECT_TRUE(EditMode::MayEnter(eGameMasterLevel::DEVELOPER, eGameMasterLevel::JUNIOR_DEVELOPER));
// editor_gm_level, 9 unless set
EXPECT_EQ(EditMode::RequiredLevel(), eGameMasterLevel::OPERATOR);
Game::config->SetDatabaseValues({ { "editor_gm_level", "8" } }, {});
EXPECT_EQ(EditMode::RequiredLevel(), eGameMasterLevel::DEVELOPER);
Game::config->SetDatabaseValues({ { "editor_gm_level", "0" } }, {});
EXPECT_EQ(EditMode::RequiredLevel(), eGameMasterLevel::FORUM_MODERATOR);
Game::config->SetDatabaseValues({}, {});
}
// In edit mode a level file's objects and spawners are not made; the player start marker still gives the spawn point
TEST_F(EditModeTests, LevelObjectsAreNotMade) {
Game::zoneManager->SetEditMode(true);
ASSERT_TRUE(Game::zoneManager->GetEditMode());
std::vector<SceneObject> objects;
SceneObject object;
object.id = 1001;
object.lot = 2365;
objects.push_back(object);
SceneObject spawner;
spawner.id = 1002;
spawner.lot = 176;
objects.push_back(spawner);
SceneObject start;
start.id = 1003;
start.lot = LOT_MARKER_PLAYER_START;
start.position = NiPoint3(10.0f, 20.0f, 30.0f);
objects.push_back(start);
Level::LoadSceneObjects(objects, 4);
// (a level object's ID gets the client bit when it is made)
for (const LWOOBJID id : { 1001, 1002, 1003 }) {
EXPECT_EQ(Game::entityManager->GetEntity(id), nullptr);
LWOOBJID made = id;
GeneralUtils::SetBit(made, eObjectBits::CLIENT);
EXPECT_EQ(Game::entityManager->GetEntity(made), nullptr);
}
EXPECT_TRUE(Game::zoneManager->GetSpawners().empty());
EXPECT_EQ(Game::zoneManager->GetZone()->GetSpawnPos(), NiPoint3(10.0f, 20.0f, 30.0f));
}