A capped string now skips the characters it cut, so what follows it is read from the
right place. A request to join a private instance that is still starting is answered
once its world server is ready, instead of at once with an address nothing listens
on yet.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This reverts commit 4cf0909d. Nothing starts an edit mode instance any more. Its two
fixes that had nothing to do with edit mode come back in the next commit.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
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>
/api/v1/zones/{zoneId}: send a zone's edited files (multipart), follow the reload,
list, restore and clear the zone's sets, and take, release and look at its lock.
Every request needs an API key that names the zones_publish permission, an owner at
zone_publish_gm_level, and HTTPS or the server's own machine; refusals are counted
per address and every use is in the audit log. A publish is checked at its headers
before any of its body is taken, the files are checked and written on a worker
thread, and the zone's instances are replaced through the world reload, held while
players are there unless told to move them.
The Worlds page lists each zone's editors and lock holder for an admin.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
For a file under maps/ master watches the file a world would read now: the override
when one is there, else the client's. An override that appears, changes or goes away
makes the instances that read another version stale, also for files from the packs.
The dashboard can hold a zone's automatic reloads (WORLD_RELOAD_HOLD, a new message
after the existing ones) until a reload of the zone is asked for. The status sent to
the dashboard counts what reloads did per zone.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Three pieces that know nothing of how a set of files arrives:
- ZonePublish reads every file of a set with the server's own readers and says, file
by file, what is wrong; a set is good only when every file is.
- ZoneOverrideStore keeps a good set in the override folder: built in a staging
folder, flushed, swapped in with a rename, the set before it kept as a backup.
Restoring a backup and clearing a zone's set are the same swap.
- ZoneEditLock has the rules for a zone's editors and its one lock holder, and for
holding a reload while players are in the zone.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Tables for the one account holding a zone's lock and for each zone's assigned
editors, on MySQL and SQLite, with the interface and the test database.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A string length, the path chunk's length and an object's render technique count are
compared with the bytes left before memory is set aside for them. The terrain reader
is built into dCommon, so every server that reads zone files has it.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
With -editmode 1 the world loads the zone file, terrain scene map, navmesh and
level revisions, but makes no level objects, carver blockers, spawners, spawner
networks, triggers, vanity objects or live event objects. The zone control
object stays with the zone's own template, as the client makes the same one
itself (LevelManager::LoadZoneControlObject, 0x01082660); its server script
does nothing and a race track's does not take the player into a race. Such a
world lets in only accounts at editor_gm_level.
Plumbing only: no command starts an edit mode instance yet (docs/EditMode.md).
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
CreatePrivateZone carries editMode as one more byte, written only when set, so
a plain private zone is sent as before. Master keeps it on the instance, starts
the world server with -editmode 1, and gives a private instance's replacement
(a reload or a live update) the same flag. Nothing sets it yet.
A capped string now skips the characters it cut, so what follows it is read
from the right place. A request to join a private instance that is still
starting is answered once its world server is ready, instead of at once with
an address nothing listens on yet.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The client takes a non-zero edit as the editor being enabled. A character whose
account GM level is at least editor_gm_level (worldconfig.ini, default 9) is
saved with edit="1"; everyone else keeps 0. A civilian never gets it, whatever
the setting. The setting is in the dashboard's settings catalog.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
L_FIRE_TRANS_ROCKS_MANAGER waits until the rocks of its RockGroup are all
there (NumberOfRocks, 3), sends them to waypoint 1 five seconds later,
and each time all of them have told it they are back down sends them up
again after a second. L_FIRE_TRANS_ROCKS holds a rock that arrived at
waypoint 1 for 3.5 s and sends it back to waypoint 0, where it tells the
manager (PlatformDown). Both act on ArrivedAtDesiredWaypoint and
GoToWaypoint, which the mover has.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The script of the platforms a spinner moves once: a simple mover (or a
mover on a two point path) in the spinner's group_deactivated_event
group goes to its waypoint 1 when the spinner fires the event named in
the platform's name_deactivated_event. The NJ spinner script already
fires those events at its groups.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A simple mover is a LWOPlatform like a mover: made stopped at its
waypoint (12) and, unless startPathingOnLoad is false, waiting there (9)
for the shortest wait before it leaves for its other end
(ResumeTravelling 0x00c86b90, SetWaitingTime 0x00bdc860, RunPlatform
0x00c86e10). Live showed it on the NJ counterweights, which wait for
their quick build first; one without a quick build was left waiting for
a script here.
The client only reads startPathingOnLoad (LWOMovingPlatformComponent::
LoadConfigData 0x00d0f690) and never starts a platform by itself, so the
rule is the server's, taken from what live did with movers (12 with the
setting false, else 9 and off) and from the scripts: L_NJ_SPINNER_SERVER
stops its platform when it starts up, simple movers included, and the
simple movers that wait for an event without that script all have
startPathingOnLoad 0 in the maps.
The wait now runs in the component for both, in place of the timer a
built counterweight got, and a simple mover with startPathingOnLoad
false is constructed stopped (12).
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The tests compared the login response and SET_SESSION_KEY of the old
hand written code with the structs byte for byte, with the same seed
for both. The key no longer comes from the seeded engine, so the tests
leave its bytes out, and the copy of the old code writes the key the
way main writes it now.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Live serialized a mover leaving a waypoint not where the waypoint is but
past it: the GF shooting gallery subs 2 units on (speed 15), the FV
dragons 5.33 (speed 40), each along the stretch they came by, and a
dragon met 2.2 s into its 5 s wait was 88 units past its waypoint (1.219
of the next stretch). At either end of a path they left from the
waypoint itself.
It is the client's LWOPlatform at work, which the server ran too:
- SetWaitingTime (0x00bdc860) takes the object's velocities away only
when the platform has stopped or is at an end of its path, so through a
wait in between the object goes on as it came;
- the percent is how far the object is from the waypoint
(CalculatePercentTravelledToWaypoint 0x00be9050);
- UpdateVelocities (0x00bd4330) on leaving turns it the next stretch's
way from where it is, and it arrives when it passes the next waypoint
(CloseToNextWaypoint 0x00bd4140), where ProcessStateChange (0x00cce380)
puts it at the waypoint;
- a platform that has not stopped looks whether it arrived while it waits
too (HandleStoppedLogic 0x00bd16a0): the dragons were at waypoint 5
without having left 4.
The server's mover now moves by its velocity in the same way, so the
state it sends on leaving carries the place and percent live sent.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
LWOPlatformRotator on a path does not move: while it travels it turns at
the angular velocity SetAngularVelocity (0x00bfa380) works out, never
waits (IncrementWaitingTime 0x004a0390) and never arrives
(HandleStoppedLogic 0x00bdc660). The velocity is the one that turns from
no rotation to NiMatrix3::FromEulerAnglesXYZ (0x0116c3a0) of rotX, rotY
and rotZ (half turns in the level config, LoadConfigData 0x00c33e10) in
half the rate, as hkpKeyFrameUtility's
setUsingPositionOrientationPair (0x012a4050) gives it: the shortest
way, the axis times the angle over the time, none for whole turns. In
reverse it is the other way round.
The server's object now turns with it, without a serialization of its
physics (each client turns its own once the rotator is sent travelling),
and is put at its waypoint as SetupPath does. A quick build's rotator
waits for the build like a mover.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The client's trade window also refuses an item that can not be
deleted and a brick built model whose moderation status is not
approved. An offer with such an item is now refused like the other
offers the window can not make.
Whether an item can be deleted is what the inventory already works
out for a delete: not the item of the mount the player rides, and not
an item its row of the DeletionRestrictions table restricts.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
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>
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>
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>
All 450 live saves looked at had <mnt a="0"/>. The server wrote the item
of the mount being ridden, a value live never showed and the client
would start with although nobody rides after a load.
<mnt a="0"/> is now always written, riding or not. A save that holds
another value, or one that is not a number, is still read without
failing and gets 0 at the next save.
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>
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>
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>
Live sent the reputation key as a 64-bit number (LDF type 8) in all 451
character data packets looked at; the server sent it as an object ID
(type 9). It now goes out as type 8 with the other keys live sent; the
packet without them keeps type 9, and both are read.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
LWORocketLaunchComponentCommon::LoadConfigData (0x00c55150) reads the
level's targetZone, targetScene, GMLevel, propertyDefaultTargetZone,
rocketLaunchPreCondition, rocketUseLaunchPrecondition and the alternative
landing keys, and the table row replaces them only while
rocketLaunchUseDBDefaults is true, its default (0x00c12ea0). The server
used the row for every pad.
31 pads in the 1.10.64 maps turn the key off. Their zones and scenes are
their rows'; what only the level has are the launch preconditions in use
on the Venture Explorer's four pads ("237;8;127|128;6|55;107;282") and
on Avant Gardens' pads to Nimbus Station, its two properties and the
Venture Explorer. Those are now checked before a launch, as the row's are.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Every live save had <mnt a="..."/>, which
LWOMountControlComponent::LoadFromSaveData (0x00bf99c0) reads as the
character's mount item (0 on the 450 saves looked at). The server wrote
no such element.
MountControlComponent now writes it: the item of the mount being ridden,
0 on foot. The saved value is read and kept; nothing is mounted from it,
as riding does not go on over a world change. A save from before gains
<mnt a="0"/> and nothing else in it changes.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The three components were made from the registry and wrote nothing, so
the client read the next component's bits as theirs.
- LWOChestComponent::Deserialize (0x00c22010), construction only: the
player (u64) and the loot offer count (u32).
- LWOChoiceBuildComponent::Deserialize (0x00bf26d0): a flag, then the
state (u32, 0 waiting, 1 building), the built object and the user
(u64 each). Always in a construction.
- LWORollerComponent::Deserialize (0x00c15920), construction: the level's
activity_camera_pos and activity_camera_lookat_pos (3 floats each) and
the projectile skill (u32, the client starts at 1348); then the flag of
the block about its user, which is never set.
LWOGameObject::Deserialize (0x00f99690) reads the components in the order
of the object's component list, which ObjectLoader2::DoObjectComponentLoad
(0x0105ebb0) fills in the order of FillComponentOrder (0x0101f8e0): the
roller after the shooting gallery and before the moving platform, the
choice build after the moving platform and before the LUP exhibit and the
model, the chest after the zone control and before render. They are
placed against the components their objects have.
The gate rush control (103) is left out: its two registry entries have no
object, and there is no component for it.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Live kept the total before the running session in <char time> and the
session's seconds in <char stime>; both changed at a login, together with
llog (time 262486 and stime 7344 became time 269830 at the next login).
The client reads both (LWOCharacterComponent::LoadFromSaveData 0x00ca4910)
and shows their sum (SendMessage 0x00d34330, GetTimePlayed). The server
wrote everything into time and no stime.
Now time + stime is read (a missing stime is 0, so a save from before
keeps its total) and time and stime are written as live had them. A
session starts when the character is picked on the character list:
charinfo.last_login is loaded with the character, a save whose llog is
another login has its stime moved into time, and llog is written as the
session's login. Without a known login the session goes on.
GetTotalTimePlayed is still every second played. The dashboard's save
check knows stime as a number.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
LWOPropertyVendorComponent::onCheckUseRequirements (0x00c2e210) lets a
player use the property guard only when the level's
property_required_mission (LoadConfigData 0x00c2e550) is finished. The
server opened the vendor and rented the property on any request.
The guard is now not opened without the mission, and a rent request
without it is answered with PropertyRentalResponse code 9, which
onPropertyRentalResponse (0x00c56d70) shows as "you failed to rent" with
UGG_PROPRENT_Dont_have_achievement. A mission that was finished and is
being repeated counts, as in the client's list of finished missions. A
guard without the key asks for nothing.
The six property maps name missions 768, 872, 873, 874, 1293 and 1322.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
For the one construction a client gets of its own player after a seamless instance move. The client applies a
construction's frame to its player at once (LWOBasePhysComponent::Deserialize, 0x00bf5a10), and world transition
state 0 makes it fade the screen in from white (LWOCharacterComponent::PostCharacterLoaded, 0x00c71660). State 3 is
read but nothing acts on it. Nothing sets either flag yet.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
LWORocketLaunchComponentCommon does not launch a player under the row's
gmLevel (msgFireEventClientSide 0x00cb78b0) or, with useLaunchPrecondition,
one that fails the row's launchPrecondition (msgCheckUseRequirements
0x00c7d0d0). The server launched on any request. Rows: the test launcher
asks for GM level 9, the pads to Gnarled Forest and Forbidden Valley for
mission 474, Starbase 3001's for mission 956.
The alternative landing precondition pointer also starts out null for a
pad without a row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOProximityMonitorComponent::LoadDataFromTemplate (0x00c2dfb0) reads the
row's Proximities as ids of ProximityTypes rows, and SendMessage
(0x00cbb930) makes, on AddedToWorld, a proximity under the type's name and
radius (30 when missing) for each type loaded on its side.
The ids were read as two radii: every launch pad ("7,8", the client's
RocketSign and WorldLauncher types) got a rocketSmall sphere of 7 and a
rocketLarge one of 8 that nothing used. Now the row's and the type's
LoadOnServer decide; of the 40 rows only a test object's has one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWORailActivatorComponent::msgCheckUseRequirements (0x00c2c5b0) refuses a
use, after the quick build check, when the row's preconditions fail or
rail_activator_active is off, so a client does not ask. The server started
the rail on any use request. The rows ask for an equipped item (44) or a
completed mission (283 to 286).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOCharacterComponent::LoadFromSaveData (0x00ca4910) reads them, and live
saves had every one (450 of 450 characters saved at least once):
- cm, the most coins a character can hold: 9223372036854775807 on every
live save, the client's own value without it. Written when missing, an
existing one is kept, and nothing caps the coins by it.
- edit (editor level), gid and gn (guild id and name).
- mldt (when the property moderation notice was last shown): 0 when
missing, as most live saves.
- v and vd: the votes left and the day they are for (GetVotesLeft /
AdjustVotesLeft). Read back; a new day fills them up to the 3 of
WorldConfig's character_votes_per_day.
- lrx.. lrrw: the last respawn point reached, once one was reached in the
world.
- lzcs: the checksum of the zone in lzid.
Saves without them load as before. stime is not written: live kept the
session's seconds in it and the time before the session in time, which
would change what time means in existing saves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jetpack mode is not saved, and the equip skill that turns it on was cast
only when the item was put on, so a player still wearing a jetpack lost
it in the next world. Live sent SetJetPackMode (use, bypass checks, effect
167, the values of the jetpack's equip skill) shortly after the player's
own construction in 12 of 28 world loads of a player with a jetpack, and
constructed the player in jetpack mode in the other 16.
The jetpack behaviors among the equip skills of the equipped items are
run again when the player has loaded. Nothing new is saved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOActivityComponent::Deserialize (0x00d002b0) reads a flag, the player
count and each player with its 10 values; a construction is the only way
a client coming into range learns who is in the activity. The block was
written only while it was waiting for the next update. Live finish line
constructions (LOT 4967) had the flag and the count in 40 of 40, one of
them with a player in it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOPhantomPhysicsComponent::Deserialize (0x00c34db0) reads the effect
amount as a 32-bit float, and trigger commands are run by the server only:
the client has no parser of its own for "Friction,0.1". The amount stays
a float. No live packet of one of the 18 volumes with 0.1 was found, so
what live sent for them is not known.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>