The master server dropped a transfer request it started no instance
for, so the world server never heard back and the player waited. It
now answers the request with an empty address and the reason in place
of the port: no room for another instance (max_instances), a zone that
is not in the ZoneTable, or another reason. A request for a private
zone that does not exist is answered the same way.
The world server passes this on as a TRANSFER_TO_WORLD without an
address, which the client shows as a failed map change, and leaves the
character where it is. The auth server fails the login when there is
no character select to send the player to.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A player could change their offer after the other player gave the final
accept, and the trade completed with the new offer on their own accept.
Both clients drop their accepts when an offer changes, the one that
changed it and the one that receives the update, so the trade now does
the same and both players have to accept the offers as they are.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The client's trade window has 8 slots for each player and takes neither
a bound item nor an equipped one. An offer with more than 8 items, a
bound item or an equipped item is now refused like the other offers the
window can not make, when it arrives and again when the trade
completes.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A transfer or private zone request started a world server for whatever
map ID it named, and for as many instances as were asked for. A new
instance now needs a row in the ZoneTable for its zone, and no more
than max_instances (masterconfig.ini, 256 by default, 0 for no limit)
run at once. The character select has no row and is always started.
A request that gets no instance is logged and dropped. Before, the
handlers went on to use the missing instance.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
When the data of a model could not be inflated, the two later position
migrations went on with an empty model and wrote the result back. All
three now log the model and leave its row alone. The first migration's
log line also gets the model ID it formats.
The client's own sd0 reader has no limit on the chunks or on the
inflated size, so the limits of the reader here stay well above any
model: a chunk inflates to at most 256 KiB, and 64 MiB is 256 full
chunks.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The limit was 1000. The client's buy dialog counts up to 999 for an
item from a vendor, whatever the item's stack size is, so that is the
most a purchase can ask for.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
SetSessionKey, SessionKeyResponse and NewSessionAlert carry the key as
a 33 character string where the number was, which the auth, master and
world servers now pass. The packet decoder blanks that string in a
kept packet, and the packet tests write the string.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The key was a 32 bit number, sent to the client as the MD5 hash of its
decimal text. The client only stores the key and sends it back, so it
is now 16 bytes from the system entropy source written as 32 lowercase
hex characters, which fills the same 33 character field of the login
response.
The auth, master and world servers pass the key itself between them as
a 33 character string in place of the number: SET_SESSION_KEY,
SESSION_KEY_RESPONSE and NEW_SESSION_ALERT keep their IDs and carry the
string where the number was. The master server keeps one key for each
username, and a world server compares the key a client sends with the
one from the master server in constant time. A key stays valid until
the next login of its account replaces it, as before.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A zone transfer from a launchpad took the zone from the message when
the message asked for that. The client never does, so the zone is now
always the launchpad's default zone or the zone the player picked on
it. The log line no longer uses the sender named in the message.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Several messages used the player, the character or the user they named
without checking that there is one. A match request and a trade request
are now only handled for the player that sent them, a linked mission
request and a build mode change only for the sending player, and the
remaining handlers return when the player, character or user is
missing.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The messages that set a property's privacy option and name, start and
finish building, place, move and remove models and change a model's
behaviors were handled for any player on the property, and outside of
a property they used a property that is not there. They are now only
handled for the owner of the property the player is on. Only an item
from a model inventory is placed as a model.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The die request, the smash request, the loaded notice, chat commands
and the racing pickup notice were handled for whichever object they
named. They are now only handled for the player that sent them, or for
the vehicle that player drives where the client sends them for it. A
racing pickup also has to be an object of a race track that is not a
player, a vehicle or the zone's own object.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A level up was applied every time the client reported one. The next
level now has to exist, the player needs the score that level asks
for, and the message has to come from the player it is for.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The reader the message structs share made room for the length it read,
up to the message limit, before it found that the stream held less.
The length now has to fit in the data the stream has left first.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Several game messages read a 32 bit length and then read that many
characters without checking it, so the loop ran for the whole length
even after the message had ended. The length now has to fit in the
data the message has left. This covers the activity state, cinematic
path, death type, removed item info and inventory group messages and
the shared wide string reader.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
An sd0 buffer could hold any number of chunks, and each chunk grew the
inflated data by a full chunk whatever it held, so the data of a chunk
that was not full was followed by a gap. A buffer now holds at most
4096 chunks and inflates to at most 64 MiB, and every chunk is inflated
right after the one before it.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The AMF3 reader read an array inside an array to any depth and kept
reading values after the data had ended. It now refuses arrays nested
more than 64 deep and throws when the data ends before a value does.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A purchase took any count, and the coin, alternate currency and
commendation prices were worked out as 32 bit numbers that wrap for a
large count. A purchase is now for 1 to 1000 items and the prices are
64 bit numbers.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Selling paid for the count in the message while the move stopped at
what the stack held. The count now has to be between 1 and the stack's
count, the item can not be one the vendor already holds for buy back,
and the coins and alternate currency are worked out from the number of
items that left the inventory.
Buying back needs an item in the buy back inventory and a count the
stack holds. Both prices are worked out as 64 bit numbers.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
MissionDialogueOK accepted or completed the mission for whichever
object the message named as the responder. The responder now has to be
the player that sent the message, the object the dialogue belongs to
has to give or take the mission (on its own or through a random
mission's pool), a mission is only accepted when the player can accept
it and only completed when it is ready to complete. The object's
script runs after these checks.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A trade used the LOT and the count from the offer as the player sent
it. The offer is now resolved when it arrives and again when the trade
completes: the LOT comes from the held item, the count stops at the
held count, and an offer that names an unknown item, the same item
twice or a count of 0 is refused. The other player only receives an
item that was taken from the offering player.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A world server passed every routed packet on to the chat server under
whatever message ID it carried. It now passes on the messages a client
sends, and SHOW_ALL only for the level the showall command needs. The
message ID is read as the full 32 bit number, the packet has to be long
enough to hold a chat packet and the sender needs a character.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A world server connects to a chat server that was started again while
it runs. That connect now uses the password the servers share too, as
the chat server listens with it.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Only world servers connect to the chat server. It now starts as an
internal server, the same way the master server does, and the world
servers connect to it with the password they already use for the
master server.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The key came from the shared game random engine, which is seeded from
the start time. It now comes from std::random_device through a small
helper. The key stays a 32 bit number, so the packets do not change.
The master and world servers no longer write the key or its hash to
the log.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
- Zone::LoadZoneIntoMemory takes the paths the zone file reader read (lost in the merge of the raw terrain loading)
- the terrain reader is main's unchanged: files from before version 32 keep their colour and texture maps as the file
has them, and the dashboard's terrain tests expect that (bounds by the span between the first and last point too)
- the model component test counts the model block only: the item block in front of it is the ItemComponent's
- scene file tests: a file from before chunks keeps only its header, editor settings are a count of colours, and the
tests that log have a logger
- the settings catalog lists export_terrain_to_obj
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The save is stored as the client sent it in the new ugc_unsplit table, and each model split from it points back to it (ugc.unsplit_id), so the whole build can be looked at or restored later. A save is kept while any model split from it exists: deleting models removes saves no model points to any more, and deleting a character removes its saves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a brick by brick save is split into models, each model was centred on
the middle of its bones' origins, on the lowest origin. A brick's origin is at
a corner of it, not its middle, so a single 2x2 came back offset by half a
brick, and the model's floor was taken from the origins rather than from the
bricks.
Now each model is placed as the live server placed it: at the bottom middle
of the box around its bricks. Each brick's own box comes from the client's
brickdb by its design ID and is turned and moved by its bone (all 8 corners),
and the model's box holds every brick's. Its x and z middle and its lowest y
are the new origin, still snapped to the 0.8 grid on x and z, and every bone
moves by that; rigids and rotations are untouched. A design the brickdb has no
box for counts its bones' origins, as before, so no brick is left out.
Lxfml::NormalizePosition and Split take the boxes by design ID, and the brick
by brick save passes the ones read from the client (read on the first save,
then kept).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BrickBounds reads the <Bounding><AABB/> of every Primitives/<designID>.xml in
the client's res/brickdb.zip (through the asset manager, so packed and
unpacked clients both work), by design ID. A design listed only as another
primitive's alias gets that primitive's box. Get() reads the file the first
time it is called and keeps the boxes for every later call.
The zip is read by a small reader in dCommon (Zip::ForEachEntry) that handles
stored and deflated entries, with a raw inflate added to ZCompression.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lxfml::NormalizePosition moved only the first bone of each part, so after a
save a flexible part's other bones kept their world positions while the rest
of the model was moved to its own origin. Now every Bone moves by the same
amount, and the box that places the model holds every bone. Split maps every
bone of a part too, so a rigid system holding only a later bone keeps its
brick.
Only bones move, as the live server did: the Rigid transformations are sent
back as the client wrote them, and rotations are not touched.
Also:
- a model with no readable bone is kept as it is at the origin instead of
getting a center 10000 up
- the math is done in doubles and numbers are written as the shortest text
that reads back as the same float (-0.4, not -0.400002; 12.1678, not
12.1677)
- bricks and rigid systems come out in the file's order, so the same model
always splits into the same bytes
- parts over 5 MB are normalized like any other (the input is capped at
10 MB), and the loop's safety limit no longer drops every later model
The pivot is still snapped to the 0.8 grid (the bricks don't move in the
world; the model's position stays on the grid), which can put it half a
stud from the middle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LwoNameValue::FromString (0x010fa220) splits a config at every comma
and line break and leaves out entries without a '='; the world split
only at line breaks. Objects and sounds are now split the client's way.
ReadLvlObjectData (0x0103ba20) also adjusts an object's config as it
loads it, and now so does the world:
- before version 47 a respawn time was in milliseconds: a float over
100, or any u32, becomes seconds (a float);
- a model spawner (LOT 176 spawning template 14) gets its blueprint
from its subkey unless it has one, behaviors off unless it says
otherwise, and no render wrapping;
- before version 44 an object that overrides its scene goes to layer 0
of the scene it names.
Check in game: every world spawns its objects as before; spawner
respawn times are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before version 46 the u16 after a particle's effect names is the names'
wide null terminator, 0 in every shipped scene file. It is skipped
instead of kept as an unknown value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Files from before chunks now read their sounds and particles after the
objects, and chunked files their particle chunk:
- sounds (SceneLoader::LoadSounds, version 5 on, only before chunks):
position and config (before 35 a 260 character name, kept as
soundName);
- particles (SceneLoader::LoadParticles, version 5 on): priority (43
on), position, rotation, effect names and config (46 on; before, an
unused u16); the client stops at one with no effect, and before 44 a
file may end before them.
Every scene file of every client on disk reads each part to the next
part's start (up to the client's 16 byte padding): 5408 referenced by
zone files and 5125 on disk, versions 31-43 before chunks and 44-49
chunked. One client file, a LUP world, ends before its sounds; its
objects are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The world read only a scene's objects and skipped the rest with
file.ignore. Level now reads a .lvl the way SceneLoader::LoadLvlFile
does (1.10.64) and keeps what it reads.
Chunked files: the file info chunk gives where the environment, object
and particle chunks start (the chunks were walked in order before); the
environment chunk gives where the lighting, skydome and editor settings
start, and the client reads all three only when there is lighting.
Files from before chunks: the no-editor-settings byte only from version
32, the revision from 37, a file newer than 48 is not read, a file
older than 3 has no objects; then lighting, skydome, editor settings
and objects in turn.
Kept, by version:
- lighting: blend time (45 on, default 10), ambient, specular, upper
hemisphere, position, min and max draw distances (31 on; before 39
fog near and far only), cull settings (40 on), fog colour (31 on),
directional colour (36 on), and before 42 the zone's spawn point
(before 33 the fixed one the client uses);
- the skydome and its five other files (34 on; they were skipped from
33);
- the editor's colours (37 on, the first 32);
- each object's node type (38 on), glom ID (32 on) and render
techniques (7 on: a 64 byte name, then per technique a 64 byte name,
u32, u8 and 16 floats); before, a count of techniques was read and
the techniques themselves were not, so an object with any misread
the rest of the file.
Object config is split at line breaks as before. A file that ends early
keeps the objects read before its end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReadLvlObjectData (1.10.64, 0x0103ba20) loads every scene object except
those of LOT 1 (the player). The world spawned them; now it skips them.
No 1.10.64 scene has one; one older client scene file has two.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zone held a std::function that queried the SceneTable, so tests could swap it out. CDSceneTableTable now loads the table with the others, and Zone looks scenes up in it; tests fill its entries like the zone table's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LuzFile::ReadLUZFile, LuzReader::ReadScenes and ZoneLoader::ReadZoneFile
(1.10.64):
- a version older than 20 is read as 20;
- before version 30 a scene is a bare u32 SceneTable ID: in ID order,
each one with a SceneTable row gets the next index (-1 past 255) as
its ID and the row's sceneName as its file; the others are left out;
- from 30 to 32 the file has no scene IDs: a scene's ID is its index in
the file (they all had ID 0, colliding);
- a later scene with the same ID and layer replaces an earlier one, a
scene ID with no layer 0 scene is left out, and before version 41
only the layer 0 scenes are loaded.
No 1.10.64 zone file has a duplicate scene, a scene without a layer 0
scene or another layer before version 41, so what live zones load is
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LuzFile::ReadLUZFile copies the path chunk by its size and ReadLUZPaths
reads it with LevelPath::FromBuffer (1.10.64). Reading as they do:
- the paths are read from their chunk, never past its end;
- a chunk version of 2 or more, 10000 paths or more, or a path the
client refuses (version 19 or more, 10000 waypoints or more, more
than 99 name/value pairs on a waypoint) leaves the zone with no
paths, logged; the rest of the zone file still reads;
- before version 3 a path's type is a name ("platform" or a movement
path) and every waypoint has a rotation, the moving platform values
and a name/value list, whatever its type;
- property headers are there from version 3, spawner and camera
headers from version 4;
- a camera waypoint's FOV before version 11 is divided by 1.25;
- a property path's reputation multiplier outside 0-1000 becomes 0.
Every path chunk of every client's zone files on disk (858) reads to
its end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ZoneBoundary's destinationZone and destinationSceneID and the scene's NiColor, so main's scene tools and tests read
the zone this branch's reader loads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zone::GetScene and SimplePhysicsComponent's Get/SetAngularVelocity handlers were declared and used but their
definitions were lost where the merge kept this branch's side of the file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dashboard's copy of the terrain reader in dCommon is gone; main's in dZoneManager reads for everyone, and keeps
what the copy had over it: colour and texture maps from before version 32 become RGBA, cut to the resolution the
client keeps. The scene map (ZoneScenes) and the dCommon tests compile against that reader.
A scene ID takes its layer as the zone file numbers it (0 General, 1 Audio) as well as the enum, so main's scene tools
and tests build; /getscene prints the layer as a number.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's model behaviors (every rotation, DoDamage, OnTimer, OnStartup, MoveBackToStart, moves along the model's
local axes), raw terrain reading with scene lookups and scene tools, property model and brick by brick fixes and the
chat server's player count fix, merged onto this branch's structures:
- game messages stay structs; the angular velocity messages join them and the handler sets a message's target
- strips own their speed again and the block names, moves and rotations stay in the shared action table
(PropertyBehaviorActions, which the dashboard's behavior player reads; it now lists rotations too)
- the zone keeps reading its file through ZoneFile and takes the boundaries from it, then loads the raw terrain as main
does; the old dNavigation terrain reader is gone
- the show all response stays a struct and logs a player count mismatch as main does
- the property management component tells models who sent a chat message
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: the server mock keeps a copy of the last bitstream sent
The bitstreams sent are usually on the sender's stack, so the pointer it kept was dangling once Send returned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: PlaceModelResponse writes the model's rotation
The client's PlaceModelResponse::Deserialize (0x00dc0170) reads the rotation as an optional w, x, y, z quaternion, but the server wrote the 4-byte response after the rotation flag, so with any rotation other than identity the client ran out of data.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: naming a model no longer renames the property
The model info window sends UpdatePropertyOrModelForFilterCheck with isProperty false (client 0x00cc42e0); only the property editor sends it true (0x00c2e710). A model's name and description aren't stored yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: keep a model item's blueprint across saves
A brick built model item's config has blueprintid (lowercase), which the saved item XML didn't keep (only blueprintID), so a picked up model lost its blueprint on the next load and could not be placed again. It is now saved as bp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: write model behaviors only for property and inventory models
The client makes the mutable model component, which reads the behaviors and editing info, only when the object's config has propertyObjectID or inInventory (ObjectLoader2::LoadModelBehaviorsComponent); every other model gets the plain one, which reads only the model block. Writing the behaviors for every model made the client read them as the next component's data.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: the model vault uses the models' inventory groups, as the client
LWOInventoryComponent_Common::msgUpdateInventoryGroup (0x00c955b0) and msgUpdateInventoryGroupContents (0x00ce1f50) look the inventory type up in m_VaultToPlayerInventory first, which maps VAULT_MODELS to MODELS. The server refused group updates for the model vault.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat: raw terrain parsing for scene data
Replace old dNavigation/dTerrain raw parser with new Raw module in
dZoneManager. Parse heightmaps, color maps, and scene maps from .raw
files to determine which scene a position belongs to. Build scene
adjacency graph from terrain data and scene transitions.
Adds NiColor type, SceneColor lookup table, eSceneType enum, terrain
mesh generation with OBJ export, and debug slash commands for scene
visualization.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: deduplicate terrain grid lookup logic, remove unnecessary cast
Add IsValidForSceneLookup(), GetSceneIDAtGrid(), and GridToWorldPos()
to Raw::Chunk. Use them in GenerateTerrainMesh, GetSceneIDFromPosition,
SpawnScenePoints, and SpawnAllScenePoints instead of duplicated inline
math. Remove unnecessary static_cast in LoadSceneTransitionInfo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add version < 32 support to raw terrain parser
The parser was only correct for v32 files. For v<32:
- colorMapResolution is width-1, not width
- Color map reads width*width*4 bytes (BGRA per-pixel), not colorMapRes^2*4
- No DDS lightmap
- No texture settings byte or blend DDS
- Scene map: v31 reads (colorMapRes+1)^2 cells with border skip,
v<31 skips 1 byte
- No mesh data
Verified against all 309 .raw files across multiple client versions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: log and reject legacy raw terrain format, clean up ReadRaw API
Log a clear message and throw if the zone uses a legacy raw format
(LUZ version < 30) since no known maps use it. Remove ReadLegacyRaw
and the mapVersion parameter from ReadRaw.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: revert v33 scene field renames to unknown1/unknown2
These fields are skipped by the client in every known build (ptr += 12
and ptr += 4). No client ever reads them into a struct. Keep the
original unknown1/unknown2 names from main.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: load the zone without scene lookups when its raw terrain is missing or unreadable
Log at debug when it parses; build the terrain mesh only for the OBJ export; read the zone name and description only for version 31 and later, as the client does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: look up the scene at a position as the client does, in constant time
Chunk bounds span (width - 1) * scale; the nearest scene map cell is used and the far edge is kept one cell inside. Chunks are indexed by grid cell when they form a grid. Skipping reads instead of seeking, so a cut-off file can't seek past the end. Terrain mesh indices follow the vertex order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: scene point commands take a step and use brick LOTs from the BrickIDTable
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: rename NiColor::ToXYZ to Luminance
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: raw terrain reading, scene lookup, scene graph and scene commands
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: close the OBJ file before removing it so the test passes on Windows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: keep the zone boundary lines and read the luz scene count as the client does
Each boundary line's destination is read as a map ID and an instance ID into a LWOZONEID. The scene count is a byte before version 37 and a u32 from it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: name the fourth flair colour byte padding
The client never sets or reads it, so it holds leftover memory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: keep the scene map of version 31 and older raws
These files have a (resolution + 1)^2 scene map, kept in sceneMapLegacy. Version 31 uses its first resolution rows and columns; older versions are all scene 0, as in the client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor: simplify the raw heightmap size check
The dimensions are capped at kMaxResolution, so the product can't overflow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: remove the scene point spawn commands and send /getadjacentscenes as one message
The spawn commands were only for testing. /getscene and /getadjacentscenes stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>