MoveObjectRecord
How a world file stores its objects: one 47-byte record per pickup, enemy, lift or crate, kept as sparse metadata type 1 on the object's start cell.
Location
| Header | include/GalaxyEggbert/MoveObjectRecord.hpp |
|---|---|
| Source | src/GalaxyEggbert/MoveObjectRecord.cpp |
| Namespace | GalaxyEggbert |
| Object types | GalaxyEggbert::Def::ObjectType (include/GalaxyEggbert/Def/ObjectType.hpp), the original game's numbering |
Purpose
Everything in a level that moves or can be taken (the original game's "MoveObjects": treasure
chests, eggs, keys, lifts, crates, enemies, vehicles, the exit...) is not a voxel. It is stored in
the .vwr file as a sparse extra-metadata record of type
kMoveObjectMetadataType = 1, anchored at the cell
floor(posStartX/Y/Z). The anchor only buckets the record into a chunk; the exact float
positions are inside the payload. MoveObjectRecord is the plain struct, and three free
functions encode, collect and remove it.
Coordinates are raw grid space (0-99), like every other World
coordinate. Y is the centre of the occupied cell: an object standing on a floor block at Y = 0
has Y = 1. WorldRuntime converts records to its live
MobileObjSpec list and applies the −50 render shift on X and Z; see
Object Lifecycle.
Public API
Verified in MoveObjectRecord.hpp as of 2026-11-10.
| Field / function | Description |
|---|---|
Def::ObjectType type | What the object is (default ObjectType0). Stored as one byte. |
std::uint16_t visualIcon | Optional object-m.png tile override for variants whose look the type alone cannot give (for example the secret wooden case camouflaged as terrain). 0 = derive the icon from type and animation phase. |
posStartX/Y/Z, posEndX/Y/Z | Patrol end points. posEnd == posStart means the object does not move. |
float speed = 1.5f | Kept for compatibility; patrols are timed by the tick fields below. |
stepAdvanceTicks = 60, stepRecedeTicks = 60, timeStopStartTicks = 40, timeStopEndTicks = 40 | The original's four-phase patrol: dwell at start, advance, dwell at end, recede, in 20 Hz ticks. The defaults are placeholders for hand-made worlds, not values from the original. |
constexpr std::uint16_t kMoveObjectMetadataType = 1 | The metadata type of every object record. |
void PlaceMoveObject(Worlds::World&, const MoveObjectRecord&) | Encodes and stores the record at its anchor cell. A second object on the same anchor cell replaces the first. Throws std::out_of_range if the anchor is outside the world. |
std::vector<MoveObjectRecord> CollectMoveObjects(const Worlds::World&) | Decodes every object in the world. A payload of any length other than 45 or 47 bytes throws std::runtime_error. |
bool RemoveMoveObject(Worlds::World&, x, y, z) | Removes the object anchored at that cell (the editor's delete). |
Payload layout
Little-endian, fixed size:
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | type |
| 1 | 12 | posStartX, posStartY, posStartZ (float32) |
| 13 | 12 | posEndX, posEndY, posEndZ (float32) |
| 25 | 4 | speed |
| 29 | 16 | stepAdvanceTicks, stepRecedeTicks, timeStopStartTicks, timeStopEndTicks |
| 45 | 2 | visualIcon (uint16) |
The older 45-byte form without visualIcon is still read, with the icon set to 0. Writers always produce 47 bytes.
Usage
From WorldRuntime::ResyncFromWorld() (src/GalaxyEggbert/Game/WorldRuntime.cpp), turning records into live objects:
mobileObjects_.clear();
for (const auto& record : CollectMoveObjects(*world_))
{
if (!IsSupportedObjectType(record.type) ||
GetObjectDefinition(record.type).placementKind == ObjectPlacementKind::Null)
continue;
MobileObjSpec spec;
spec.type = record.type;
spec.visualIcon = record.visualIcon;
spec.posStartX = record.posStartX - static_cast<float>(kWorldCenterX);
...
mobileObjects_.push_back(spec);
}
Writers: the in-game WorldEditor (object placement and removal),
tools/GenerateSampleWorld3D.cpp, and the Python world builder
tools/worldkit.py, which writes the same bytes. tools/vwr_inspect.py
lists them.
Sibling codecs
Two smaller record types follow the same pattern (struct + place/collect functions over a World):
| Codec | Type | Payload | Notes |
|---|---|---|---|
BigDecorRecordinclude/GalaxyEggbert/BigDecorRecord.hpp | 3 | 2 bytes: the icon (uint16, non-zero) | A large scenery picture from explo.png (palms, houses, machines...), Eggbert 2's separate BigDecor layer. Not a voxel. The anchor cell gives its position. PlaceBigDecor() throws std::invalid_argument for icon 0; CollectBigDecor(), RemoveBigDecor(). Some icons are solid in 3D (kSolidBigDecorIcons, BlockDefinitionRegistry). |
HintZoneRecordinclude/GalaxyEggbert/HintZoneRecord.hpp | 4 | 4 bytes: recordIndex, sizeX, sizeY, sizeZ | Where one of a training mission's hints applies: a box from the anchor (its minimum corner) spanning size cells on each axis. Contains(x, y, z); PlaceHintZone() rejects zero sizes and boxes leaving the world; CollectHintZones() returns them sorted by recordIndex. Added 2026-10-25. |
Tests
tests/GalaxyEggbert/MoveObjectRecordTests.cpp:PlaceAndCollectRoundTripsAllFields,RoundTripsThroughSaveAndLoadFile,ReadsLegacyPayloadWithoutVisualOverride, empty worlds, and removal (present and absent).tests/GalaxyEggbert/BigDecorRecordTests.cppandHintZoneRecordTests.cpp: place/collect/replace/remove, file round-trips, rejection of invalid records.VerifyShippedWorlds: all shipped objects survive load → save → load;VerifyGEEditorObjects,VerifyGEWorldEditorandVerifyGEEditorStorageplace, remove and save objects through the editor;VerifyInteractionSystembuilds test worlds withPlaceMoveObject.