Game network messages
Most communication with the game server
(and, indirectly, with other clients)
happens using serialized plNetMessage objects,
which are wrapped in Cli2Game_PropagateBuffer/Game2Cli_PropagateBuffer when sent to/from the game server.
The different plNetMessage subclasses are identified by their plCreatable class index.
Unlike the lower-level message protocol,
plNetMessages aren’t strictly separated by communication direction.
Many message types are in fact sent in both directions between client and server,
but others are only supposed to go in one direction.
In all cases,
the class index uniquely identifies the message class with no further context.
Below is an overview of the plNetMessage class hierarchy in the open-sourced client code,
along with the corresponding class indices and the intended message direction.
Classes marked as “abstract” are only used as base classes —
a message should never be a direct instance of one of these classes,
only of one of their non-abstract subclasses.
Classes marked as “unused” are fully defined in the open-sourced client code,
but never actually used by the client
and not supported by MOSS or DIRTSAND
(it’s unknown if Cyan’s server software supports them).
plNetMessage= 0x025e = 606 (abstract)plNetMsgRoomsList= 0x0263 = 611 (abstract)plNetMsgPagingRoom= 0x0218 = 536 (client -> server)plNetMsgGameStateRequest= 0x0265 = 613 (client -> server)
plNetMsgObject= 0x0268 = 616 (abstract)plNetMsgStreamedObject= 0x027b = 635 (abstract)plNetMsgSharedState= 0x027c = 636 (abstract)plNetMsgTestAndSet= 0x027d = 637 (client -> server)
plNetMsgSDLState= 0x02cd = 717 (client <-> server)plNetMsgSDLStateBCast= 0x0329 = 809 (client <-> server)
plNetMsgGetSharedState= 0x027e = 638 (client -> server, unused)plNetMsgObjStateRequest= 0x0286 = 646 (client -> server, unused)
plNetMsgStream= 0x026c = 620 (abstract)plNetMsgGameMessage= 0x026b = 619 (client <-> server)plNetMsgGameMessageDirected= 0x032e = 814 (client <-> server)plNetMsgLoadClone= 0x03b3 = 947 (client <-> server)
plNetMsgVoice= 0x0279 = 633 (client <-> server)plNetMsgObjectUpdateFilter= 0x029d = 669 (client -> server, not handled by MOSS or DIRTSAND)plNetMsgMembersListReq= 0x02ad = 685 (client -> server)plNetMsgServerToClient= 0x02b2 = 690 (abstract)plNetMsgGroupOwner= 0x0264 = 612 (server -> client)plNetMsgMembersList= 0x02ae = 686 (server -> client)plNetMsgMemberUpdate= 0x02b1 = 689 (server -> client)plNetMsgInitialAgeStateSent= 0x02b8 = 696 (server -> client)
plNetMsgListenListUpdate= 0x02c8 = 712 (client <-> server, unused, but client theoretically handles it)plNetMsgRelevanceRegions= 0x03ac = 940 (client -> server)plNetMsgPlayerPage= 0x03b4 = 948 (client -> server)
Common data types
Assorted data types used by the message classes below.
See also
-
class plGenericType
Type: 1-byte
pnGenericType::Types. Indicates the type and meaning of the following data field.Data: Varies depending on the type field (see below).
-
enum Types
-
enumerator kInt = 0
4-byte signed int.
-
enumerator kFloat = 1
4-byte floating-point number. Not used in the open-sourced client code.
-
enumerator kBool = 2
1-byte boolean.
-
enumerator kString = 3
-
enumerator kChar = 4
A single 8-bit character. Not used in the open-sourced client code.
-
enumerator kAny = 4
An arbitrary untyped value. Stored as a SafeString, but may be implicitly converted to any of the other data types. Not used in the open-sourced client code.
Converting to string returns the string as-is.
Converting to char returns the first character of the string (or a zero byte if the string is empty).
Converting to any of the integer or floating-point types parses the string as a decimal literal of that number type. The open-sourced client code performs the conversion using the standard C functions
atoiandatof, so any leading whitespace and trailing non-number characters are ignored (though it’s probably best not to rely on this). H’uru clients usestrtol/strtoulfor integer parsing, meaning that out-of-range integer values are clamped to the minimum/maximum 32-bit integer value (unlike in OpenUru clients, where such values wrap around in two’s complement fashion) and C octal and hexadecimal prefixes are understood (this is probably not intentional).Converting to bool returns true if the string is
trueor a valid non-zero integer (see above), or false in all other cases.
-
enumerator kUInt = 5
4-byte unsigned int. Not used in the open-sourced client code.
-
enumerator kDouble = 6
8-byte floating-point number.
-
enumerator kNone = 255
No value. “Stored” as 0 bytes of data. Not used in the open-sourced client code.
-
enumerator kInt = 0
-
class plNetGroupId
ID: 6-byte
plLocation.Flags: 1-byte unsigned int. See
NetGroupConstantsfor details.
-
class plClientGuid : public plCreatable
Flags: 2-byte unsigned int. See
Flagsfor details.Account UUID: 16-byte UUID. Only present if the
kAccountUUIDflag is set. Always unset in practice and not used by client or servers. Unclear if Cyan’s server software does anything with it.Player ID: 4-byte unsigned int. Only present if the
kPlayerIDflag is set. The avatar’s KI number.Temp player ID: 4-byte unsigned int. Only present if the
kTempPlayerIDflag is set. Always unset in practice and not used by fan servers. Unclear if Cyan’s server software does anything with it. The open-sourced client code treats this identically to the regular player ID field.Player name length: 2-byte unsigned int. Only present if the
kPlayerNameflag is set. Byte count for the following player name.Player name: Variable-length byte string. Only present if the
kPlayerNameflag is set. The avatar’s display name.CCR level: 1-byte unsigned int. Only present if the
kCCRLevelflag is set. The avatar’s current CCR level. MOSS hardcodes this field to 0, whereas DIRTSAND doesn’t set it at all.Protected login: 1-byte boolean. Only present if the
kProtectedLoginflag is set. Always unset in practice and not used by client or servers. Unclear if Cyan’s server software does anything with it.Build type: 1-byte unsigned int. Only present if the
kBuildTypeflag is set. Always unset in practice and not used by client or servers. Unclear if Cyan’s server software does anything with it.Source IP address: 4-byte packed IPv4 address. Only present if the
kSrcAddrflag is set. Always unset in practice and not used by client or servers. Unclear if Cyan’s server software does anything with it.Source port: 2-byte unsigned int. Only present if the
kSrcPortflag is set. Always unset in practice and not used by client or servers. Unclear if Cyan’s server software does anything with it.Reserved: 1-byte boolean. Only present if the
kReservedflag is set. Always unset in practice and not used by client or servers. Unclear if Cyan’s server software does anything with it.Client key length: 2-byte unsigned int. Only present if the
kClientKeyflag is set. Byte count for the following client key.Client key: Variable-length byte string. Only present if the
kClientKeyflag is set. Always unset in practice and not used by client or servers. Unclear if Cyan’s server software does anything with it.
-
enum Flags
-
enumerator kAccountUUID = 1 << 0
-
enumerator kPlayerID = 1 << 1
-
enumerator kTempPlayerID = 1 << 2
-
enumerator kCCRLevel = 1 << 3
-
enumerator kProtectedLogin = 1 << 4
-
enumerator kBuildType = 1 << 5
-
enumerator kPlayerName = 1 << 6
-
enumerator kSrcAddr = 1 << 7
-
enumerator kSrcPort = 1 << 8
-
enumerator kReserved = 1 << 9
-
enumerator kClientKey = 1 << 10
-
enumerator kAccountUUID = 1 << 0
-
class plNetMsgStreamHelper : public plCreatable
Uncompressed length: 4-byte unsigned int. Byte length of the stream data after decompression. If the stream data is not compressed, this field is set to 0.
Compression type: 1-byte
plNetMessage::CompressionType.Stream length: 4-byte unsigned int. Byte length of the following stream data field.
Stream data: Variable-length byte array. The format of this data depends on the class of the containing message. Additionally, the data may be compressed, depending on the compression type field.
-
enum plNetMember::Flags
-
enumerator kWaitingForLinkQuery = 1 << 0
“only used server side”
-
enumerator kIndirectMember = 1 << 1
“this guy is behind a firewall of some sort”
-
enumerator kRequestP2P = 1 << 2
“wants to play peer to peer”
-
enumerator kWaitingForChallengeResponse = 1 << 3
“waiting for client response”
-
enumerator kIsServer = 1 << 4
“used by transport member”
-
enumerator kAllowTimeOut = 1 << 5
“used by gameserver”
-
enumerator kWaitingForLinkQuery = 1 << 0
-
class plNetMsgMemberInfoHelper : public plCreatable
Flags: 4-byte unsigned int. See
plNetMember::Flagsfor details. MOSS and DIRTSAND always set this field to 0.Client info:
plClientGuid.Avatar UOID:
plUoid.
plNetMessage
-
class plNetMessage : public plCreatable
Class index = 0x025e = 606
The serialized format has the following common header structure, with any subclass-specific data directly after the header.
Class index: 2-byte unsigned int. Identifies the specific
plNetMessagesubclass that this message is an instance of.Flags: 4-byte unsigned int. Collection of various boolean flags, some of which affect the format of the remaining message data. See
BitVectorFlagsfor details.Protocol version: 2 bytes. Only present if the
kHasVersionflag is set. Always unset in practice and not used by client or servers. Not supported by MOSS. Unclear if Cyan’s server software does anything with it. According to comments in the open-sourced client code, this version number has remained unchanged since 2003-12-01.Major version: 1-byte unsigned int. Always set to 12.
Minor version: 1-byte unsigned int. Always set to 6.
Time sent: 8-byte
plUnifiedTime. Only present if thekHasTimeSentflag is set. Timestamp indicating when this message was sent. Used by the client to adjust for differences between the client and server clocks. The client sets this field for every message it sends, and so does every server implementation apparently.Context: 4-byte unsigned int. Only present if the
kHasContextflag is set. Always unset in practice and not used by client or servers. Not supported by MOSS. Unclear if Cyan’s server software does anything with it.Transaction ID: 4-byte unsigned int. Only present if the
kHasTransactionIDflag is set. Always unset in practice and not used by client or servers (the MOSS source code says “should never happen” about the code that reads this field). Unclear if Cyan’s server software does anything with it.Player ID: 4-byte unsigned int. Only present if the
kHasPlayerIDflag is set. KI number of the avatar being played by the client that sent the message. The client sets this field for every message it sends, but messages originating from the server usually leave it unset.Account UUID: 16-byte UUID. Only present if the
kHasAcctUUIDflag is set. Always unset in practice and not used by client or servers (the MOSS source code says “should never happen” about the code that reads this field). Unclear if Cyan’s server software does anything with it.
-
enum BitVectorFlags
-
enumerator kHasTimeSent = 1 << 0
Whether the time sent field is present. Always set in practice.
-
enumerator kHasGameMsgRcvrs = 1 << 1
Set for
plNetMsgGameMessage(or subclass) messages if the wrappedplMessagehas at least one receiver whoseplLocationis not virtual or reserved. Should never be set for other message types. According to comments in the open-sourced client code, this flag is meant to allow some server-side optimization. MOSS and DIRTSAND ignore it though.
-
enumerator kEchoBackToSender = 1 << 2
Request that the server sends the message back to the client that sent it. DIRTSAND implements this flag for broadcast and propagate messages MOSS doesn’t implement it and silently ignores it. The open-sourced client code sets this flag in two cases:
If
plNetClientRecorderis enabled using the console commandDemo.RecordNet, this flag is set on allplNetMsgSDLState,plNetMsgSDLStateBCast,plNetMsgGameMessage, andplNetMsgLoadClonemessages.If voice chat echo has been enabled using the console command
Net.Voice.Echo, this flag is set on allplNetMsgVoicemessages (this is broken in OpenUru clients if compression is disabled using the console commandAudio.EnableVoiceCompression).
Because both of these features can only be enabled via console commands, this flag is almost never set in practice.
-
enumerator kRequestP2P = 1 << 3
Unused and always unset.
-
enumerator kAllowTimeOut = 1 << 4
Unused and always unset. MOSS has some incomplete code that handles this flag, which interprets it as adding an extra 6 bytes to the message size, supposedly for IP address and port fields. This interpretation seems incorrect though, especially because it’s based on what Alcugs does, and it seems that this bit had a different meaning in pre-MOUL Uru.
-
enumerator kIndirectMember = 1 << 5
Unused and always unset.
-
enumerator kPublicIPClient = 1 << 6
Unused and always unset.
-
enumerator kHasContext = 1 << 7
Whether the context field is present. Always unset in practice. Not supported by MOSS.
-
enumerator kAskVaultForGameState = 1 << 8
Unused and always unset.
-
enumerator kHasTransactionID = 1 << 9
Whether the transaction ID field is present. Always unset in practice.
-
enumerator kNewSDLState = 1 << 10
Set by the client when sending a
plNetMsgSDLState(or subclass) message for an object that the server doesn’t know about yet. Once a message with this flag has been sent for an object, or if the client receives a state for an object from the server, this flag will be unset for all furtherplNetMsgSDLStatemessages for that object. Should never be set for other message types. Ignored by MOSS and DIRTSAND.
-
enumerator kInitialAgeStateRequest = 1 << 11
Set by the client for all
plNetMsgGameStateRequestmessages. Should never be set for other message types. Ignored by MOSS and DIRTSAND.
-
enumerator kHasPlayerID = 1 << 12
Whether the player ID field is present.
-
enumerator kUseRelevanceRegions = 1 << 13
Whether the message should be filtered by relevance regions. The client sets this flag for
plNetMsgGameMessage(or subclass) messages if the wrappedplMessagehas thekNetUseRelevanceRegionsflag set, and for someplNetMsgSDLState(or subclass) messages caused byplArmatureMod. Ignored by MOSS and DIRTSAND.
-
enumerator kHasAcctUUID = 1 << 14
Whether the account UUID field is present. Always unset in practice.
-
enumerator kInterAgeRouting = 1 << 15
Whether the message should also be sent across age instances, not just within the current age instance as usual. Set for
plNetMsgGameMessage(or subclass) messages if the wrappedplMessagehas thekNetAllowInterAgeflag set (unlesskRouteToAllPlayers/kCCRSendToAllPlayersis also set). This should only happen forplNetMsgGameMessageDirectedmessages. Should never be set for other message types. Ignored by MOSS and DIRTSAND.
-
enumerator kHasVersion = 1 << 16
Whether the protocol version field is present. Always unset in practice.
-
enumerator kIsSystemMessage = 1 << 17
Set for all
plNetMsgRoomsList,plNetMsgObjStateRequest,plNetMsgMembersListReq, andplNetMsgServerToClientmessages (including subclasses, if any). DIRTSAND also sets it for someplNetMsgSDLStateBCastmessages. MOSS, DIRTSAND, and the client never use this flag for anything. Unclear if Cyan’s server software does anything with it.
-
enumerator kNeedsReliableSend = 1 << 18
The client sets this flag for all messages other than
plNetMsgVoice,plNetMsgObjectUpdateFilter, andplNetMsgListenListUpdate. DIRTSAND sets it for all messages it creates, whereas MOSS never sets it for its own messages. MOSS, DIRTSAND, and the client never use this flag for anything. Unclear if Cyan’s server software does anything with it.
-
enumerator kRouteToAllPlayers = 1 << 19
Whether the message should be sent to all players in all age instances. If this flag is set,
kInterAgeRoutingshould be unset. The client sets this flag forplNetMsgGameMessage(or subclass) messages if the client is internal and the wrappedplMessagehas thekCCRSendToAllPlayersflag set. Should never be set for other message types. DIRTSAND implements this flag, but only respects it if the sender is permitted to send unsafeplMessages (i. e. if the sender’s account has thekAccountRoleAdminflag set). MOSS doesn’t implement this flag at all and always ignores it.
-
enumerator kHasTimeSent = 1 << 0
-
enum CompressionType
Only used within the subclass
plNetMsgStreamedObject.-
enumerator kCompressionNone = 0
The stream data is not compressed because it didn’t meet the length threshold for compression.
-
enumerator kCompressionFailed = 1
This is an internal error value used when the stream data could not be (de)compressed. It should never appear in a serialized message.
-
enumerator kCompressionZlib = 2
The stream data is partially compressed: the first two bytes are uncompressed, followed by the remaining data, which is zlib-compressed. The open-sourced client code uses zlib compression iff the stream data is at least 256 bytes long.
-
enumerator kCompressionDont = 3
The stream data is not compressed because compression has been explicitly disabled. The open-sourced client code does this iff the
kHasGameMsgRcvrsflag is set on the message.
-
enumerator kCompressionNone = 0
plNetMsgRoomsList
-
class plNetMsgRoomsList : public plNetMessage
Class index = 0x0263 = 611
Header:
plNetMessage.Room count: 4-byte unsigned int (or signed in the original/OpenUru code for some reason). Element count for the following array of rooms.
Rooms: Variable-length array. Each element has the following structure:
Location: 6-byte
plLocation.Name length: 2-byte unsigned int. Byte count for the following name string.
Name: Variable-length byte string.
plNetMsgPagingRoom
-
class plNetMsgPagingRoom : public plNetMsgRoomsList
Class index = 0x0218 = 536
Header:
plNetMsgRoomsList.Flags: 1-byte unsigned int. See
PageFlagsfor details.
Sent by the client after loading and before unloading a page. The rooms array (from
plNetMsgRoomsList) contains the pages that are being (un)loaded. It should never be empty and in practice always contains exactly one element.The open-sourced client code sends page-in messages only for pages loaded during the initial age loading process, not for ones loaded later on demand — it’s unclear if this is intentional. Page-out messages are sent for all page unloads.
The server can theoretically use these messages to track which clients have which pages loaded, but because of the inconsistent page-in messages, this would be unreliable in practice. MOSS ignores this message type completely. DIRTSAND only broadcasts it to other clients (even though the client doesn’t support receiving it) and otherwise also ignores it. Unclear if Cyan’s server software does anything with it.
-
enum PageFlags
-
enumerator kPagingOut = 1 << 0
Set if the pages in question are being unloaded, or unset if they are being loaded.
-
enumerator kResetList = 1 << 1
Unused and always unset.
-
enumerator kRequestState = 1 << 2
Unused and always unset.
-
enumerator kFinalRoomInAge = 1 << 3
Unused and always unset.
-
enumerator kPagingOut = 1 << 0
plNetMsgGameStateRequest
-
class plNetMsgGameStateRequest : public plNetMsgRoomsList
Class index = 0x0265 = 613
Identical structure to its superclass
plNetMsgRoomsList.Request the current state of the age instance. Sent by the client exactly once as part of the link-in/loading process, immediately after the
plNetMsgMembersListReq.The rooms list can be used to limit the request only to objects in certain rooms, but in practice the client always sends an empty list, which requests the state for all rooms loaded by the client. MOSS has code for handling both empty and non-empty rooms lists. DIRTSAND ignores the rooms list and unconditionally sends all states.
The server replies immediately with the following messages:
One
plNetMsgLoadClonefor every clone currently in the age instance. DIRTSAND actually sends these messages in response toplNetMsgMembersListReqalready, but this makes no practical difference, because the messages are sent in the same order either way. DIRTSAND also doesn’t count them towards the total number of sent states (see below), but this is also not a problem, because the communication is TCP-based and so there’s no possibility of any state messages getting lost.One
plNetMsgSDLStatefor the state of every object currently in the age instance. This notably includes the age SDL state (if any), which is sent as the SDL state for the AgeSDLHook object.A single
plNetMsgInitialAgeStateSentcontaining the number of state messages that were sent. DIRTSAND doesn’t includeplNetMsgLoadClonemessages in this count.
After sending the request, the client blocks the link-in/loading process until it has received the
plNetMsgInitialAgeStateSentmessage and the indicated number of state mesages.The exact order of the reply messages doesn’t matter, except that an object’s
plNetMsgLoadClonemessage must be sent before any other messages referring to that object.DIRTSAND uses the following order:
plNetMsgLoadClones for all non-avatar clonesplNetMsgLoadClones for all avatarsplNetMsgSDLStatefor the AgeSDLHook (if any)plNetMsgSDLStates for all other objects
MOSS sends all clones and states in the order in which they were first received, except that the clone of the requester’s avatar is skipped. The
plNetMsgInitialAgeStateSentmessage is sent last.
plNetMsgObject
-
class plNetMsgObject : public plNetMessage
Class index = 0x0268 = 616
Header:
plNetMessage.UOID:
plUoid.
plNetMsgStreamedObject
-
class plNetMsgStreamedObject : public plNetMsgObject
Class index = 0x027b = 635
Header:
plNetMsgObject.Stream:
plNetMsgStreamHelper. The format of the stream data depends on the subclass.
plNetMsgTestAndSet
-
class plNetMsgTestAndSet : public plNetMsgSharedState
Class index = 0x027d = 637
Identical structure to its superclass
plNetMsgSharedState.Update a server-side shared state variable attached to an object.
In practice, this is only used to implement simple mutexes. Almost all fields have fixed or restricted values:
UOID: Always has class type 0x002d (
plLogicModifier).Stream data:
State name: Always the string
TrigState.Variable count: Always 1.
Server may delete: False if triggering, or true if un-triggering.
Variables:
Variable name: Always the string
Triggered.Variable value:
Type: Always
pnGenericType::Types::kBool.Data: True if triggering, or false if un-triggering.
Lock request: True if triggering, or false if un-triggering.
MOSS only accepts this exact structure. DIRTSAND implements a full parser for the stream data that accepts any state name and variables, but then ignores the parsed data entirely and only acts based on the lock request field.
If the lock request field is true, the server tries to lock the object and then replies with a
plNetMsgGameMessagecontaining aplServerReplyMsg. The message has no sender and its only receiver is the UOID sent by the client in theplNetMsgTestAndSetmessage. The reply’s type field is set to 1 (affirm) if the lock request succeeded (i. e. the client now has the lock) or 0 (deny) if it failed (i. e. another client already has the lock at the moment).If the lock request field is false, the server clears the client’s lock on the object. DIRTSAND also sends a
plServerReplyMsgin this case, with its type field set to -1 (uninitialized). MOSS doesn’t send any reply at all. (TODO What does Cyan’s server software do?)
plNetMsgSDLState
-
class plNetMsgSDLState : public plNetMsgStreamedObject
Class index = 0x02cd = 717
Header:
plNetMsgStreamedObject.Is initial state: 1-byte boolean. Set to true by the server when replying to a
plNetMsgGameStateRequest. The client always sets it to false. When the client receives a message with this flag set, it initializes all variables not present in the received SDL record to their default values and sets their dirty flag. (See also thekNewSDLStateflag.)Persist on server: 1-byte boolean. Normally always set to true. The client sets it to false for SDL states that shouldn’t be saved permanently on the server. In that case, the state is only broadcast to other clients (if requested) and saved temporarily as long as the game server is running so that it can be sent to newly joining clients. MOSS ignores this flag and instead only saves states if the blob doesn’t have the volatile flag set and the UOID doesn’t have clone IDs (this filters out all avatar states).
Is avatar state: 1-byte boolean. Set to true by the client for SDL states related/attached to an avatar. If true, the persist on server flag should be false. MOSS and DIRTSAND ignore this flag.
Notifies the other side about the SDL state of an object in the age instance. The UOID field (from
plNetMsgObject) identifies the object to which the state belongs. The stream data is an SDL blob, including its stream header.This message can be sent both from the client to the server and the other way around. In most cases, the message is sent from the client and then possibly broadcast by the server to other clients. When a client joins an age instance, the server sends it the states of all objects in the age instance (see
plNetMsgGameStateRequest). In a few cases, the server also sends this message to clients unprompted, especially for the AgeSDLHook.The SDL blob often doesn’t contain the object’s entire state, but only the variable values that were actually changed. The complete state is only sent if the other side doesn’t know the object’s state yet — that is, when the server sends the initial SDL states to a client joining the age instance, or when the client sends the SDL state for an object that the server doesn’t know about yet.
If the receiver already has an SDL state for the object in question, it updates that state using the received SDL blob. For every variable in the received SDL blob:
Copy the notification info from the received variable to the existing variable. The client does this only if the received variable has notification info at all and its hint string is non-empty — i. e. it avoids overwriting an existing non-empty string with an empty one. MOSS and DIRTSAND overwrite the string unconditionally.
If it’s a simple variable:
If the received variable does not have the dirty flag set, MOSS skips the variable entirely. DIRTSAND and the client don’t interpret the dirty flag this way. (This makes no difference in practice, because the client only sends variables with the dirty flag set.)
Copy the variable value from the received variable to the existing variable.
Update the timestamp and flags of the existing (now updated) variable.
MOSS unconditionally sets the timestamp to the current time and sets the “has timestamp” flag. All other flags are copied as-is from the received variable.
DIRTSAND checks if the dirty and “want timestamp” flags are set and if so, sets the timestamp to the current time, sets the “has timestamp” flag and unsets the “want timestamp” flag. All other flags are copied as-is from the received variable.
The client unconditionally unsets the timestamp and all flags, except for the “same as default” flag, which is updated based on whether the variable value is equal to the default.
If it’s a nested SDL variable, copy the variable value from the received variable to the existing variable. The client recursively updates every record in the existing variable using the corresponding record in the received variable. MOSS and DIRTSAND instead overwrite the existing records without any recursive updates.
If the receiver doesn’t have an SDL state for the object in question yet, the received SDL blob must contain a complete SDL record with all variables set. In this case, the client still follows the update process above, but using a new SDL record with no variables set as the “existing” state. DIRTSAND skips most of the update process and uses the received SDL record mostly unchanged, except for updating its timestamps as described above. MOSS skips the update process entirely and uses the received SDL record as-is.
As a special case, the AgeSDLHook object stands for the state of the age instance itself. The AgeSDLHook state is handled differently on the server side than all other object states: it isn’t stored directly as an SDL record, but is actually a combination of the age instance’s SDL vault node and any shard-wide settings for the age. For any SDL variables set in both places, the age instance vault node takes priority over the shard-wide settings. If a client changes the AgeSDLHook state, the changed values are always stored in the age instance vault node — even when changing a variable whose value came from the shard-wide settings and wasn’t previously set in the age instance!
plNetMsgSDLStateBCast
-
class plNetMsgSDLStateBCast : public plNetMsgSDLState
Class index = 0x0329 = 809
Identical structure to its superclass
plNetMsgSDLState.Handled the same way as
plNetMsgSDLState, except that on the server side, the state change is additionally broadcast to all other clients in the same age instance.MOSS broadcasts the received SDL blob as-is, which most likely contains an incomplete SDL record. DIRTSAND instead always broadcasts a complete SDL record, serialized from its own version of the object’s state after it was updated with the SDL record received from the client.
plNetMsgObjStateRequest
-
class plNetMsgObjStateRequest : public plNetMsgObject
Class index = 0x0286 = 646
Identical structure to its superclass
plNetMsgObject.
plNetMsgStream
-
class plNetMsgStream : public plNetMessage
Class index = 0x026c = 620
Header:
plNetMessage.Stream:
plNetMsgStreamHelper. The format of the stream data depends on the subclass.
plNetMsgGameMessage
-
class plNetMsgGameMessage : public plNetMsgStream
Class index = 0x026b = 619
Header:
plNetMsgStream.Delivery time present: 1-byte boolean. Whether the following delivery time field is set.
Delivery time: 8-byte
plUnifiedTime. Only present if the preceding boolean field is true, otherwise defaults to all zeroes (i. e. the Unix epoch). Set by the client when sending based on the wrappedplMessage’s timestamp field: if the timestamp lies in the future, it’s converted from local game time to an absolute time and stored in this field, otherwise the timestamp is set to zero and this field is left unset. It seems that all server implementations ignore this field and pass it on unmodified. If this field is set when received by the client, it’s converted to the client’s local game time and stored in theplMessage’s timestamp field.
Wraps a
plMessageto be sent between clients. The stream data contains theplMessageas a serializedplCreatablewith header. See Plasma messages for details on that format.If the contained
plMessageis an instance ofplLoadCloneMsg, then the wrapper message must have the classplNetMsgLoadCloneinstead.When the client sends (locally) a
plMessagethat has thekNetPropagateflag set, it wraps theplMessagein aplNetMsgGameMessage(or one of its subclasses) and sends it to the game server. Afterwards, theplMessageis also sent locally on the client side if it has thekLocalPropagateflag set (which is the case by default).When the server receives this message, by default it forwards it to all other clients in the same age instance.
If the message has the
kEchoBackToSenderflag set, it’s also repeated back to the sender. MOSS doesn’t support this flag. DIRTSAND ignores it forplNetMsgGameMessageDirectedmessages.If the message has the
kRouteToAllPlayersflag set, it’s forwarded to all clients on the entire shard, even ones in other age instances. MOSS doesn’t support this flag. DIRTSAND only allows it if the sender’s account has thekAccountRoleAdminflag set and the message class is exactlyplNetMsgGameMessage.DIRTSAND by default blocks forwarding of certain
plMessages that cannot occur during normal gameplay and should only be used by CCRs and developers. Such messages are silently dropped and not forwarded to anyone, unless the sender’s account has thekAccountRoleAdminflag set, which allows bypassing this check.The server forwards the message completely unmodified, with the following exceptions:
If the message has the
kHasTimeSentflag set (which is always the case in practice), MOSS updates the time sent to the current time when forwarding the message. DIRTSAND leaves it untouched and keeps the time sent that was originally set by the sending client. (TODO What does Cyan’s server software do?) This difference shouldn’t be noticeable in practice.DIRTSAND sets the
kNetNonLocalflag on the wrappedplMessagebefore forwarding it (unless the wrapper message is aplNetMsgLoadClone— but that might just be a bug). MOSS never sets this flag and that apparently has no negative effect on gameplay. (TODO What does Cyan’s server software do?)
plNetMsgGameMessageDirected
-
class plNetMsgGameMessageDirected : public plNetMsgGameMessage
Class index = 0x032e = 814
Header:
plNetMsgGameMessage.Receiver count: 1-byte unsigned int. Element count of the following receiver array.
Receivers: Variable-length array of 4-byte unsigned ints, each a KI number of an avatar that should receive this message. Note that this is independent of the wrapped
plMessage’s list of receiver keys — in fact, the latter is usually empty for directed messages.
Behaves like its superclass
plNetMsgGameMessage, except that the server only forwards it to the specified list of receivers, not all avatars in the age instance. The wrappedplMessageshould be an instance ofpfKIMsg,plCCRCommunicationMsg,plAvatarInputStateMsg,plInputIfaceMgrMsg, orplNotifyMsg.By default, the message is only forwarded to receivers that are in the same age instance as the sender. If the
kInterAgeRoutingflag is set, it’s also forwarded to receivers in other age instances. MOSS and DIRTSAND ignore this flag though and always forward the message to all receivers, regardless of which age instance they’re in.DIRTSAND ignores the
kEchoBackToSenderfor directed messages and instead repeats the message back if and only if the sender’s KI number is in the list of receivers.The
kRouteToAllPlayersflag shouldn’t be used with directed messages.
plNetMsgLoadClone
-
class plNetMsgLoadClone : public plNetMsgGameMessage
Class index = 0x03b3 = 947
Header:
plNetMsgGameMessage.UOID:
plUoid.Is player: 1-byte boolean. If the wrapped message is a
plLoadAvatarMsg, this field matches its “is player” field, otherwise it’s always set to false.Is loading: 1-byte boolean. Matches the “is loading” field of the wrapped
plLoadCloneMsg.Is initial state: 1-byte boolean. Set to true by the server when replying to a
plNetMsgGameStateRequest. The client always sets it to false. This field is only used by the client to count how many initial state messages it has received — it has no effect on how the message itself is handled.
Special case of
plNetMsgGameMessageused if the wrappedplMessageis an instance ofplLoadCloneMsg(or its only subclassplLoadAvatarMsg).These messages are always forwarded to all clients within the same age instance — they cannot be directed and should never have the
kInterAgeRoutingorkRouteToAllPlayersflags set.
plNetMsgVoice
-
enum plVoiceFlags
-
enumerator kEncoded = 1 << 0
Whether the voice data is compressed/encoded. This flag is normally always set, because all clients use some kind of compression by default — although the exact codec depends on the other flags explained below. Compression can be disabled using the console command
Audio.EnableVoiceCompression(OpenUru) orAudio.SetVoiceCodec(H’uru), in which case this flag is left unset and the voice data is transmitted as uncompressed PCM data, mono, 16-bit, 8 kHz. OpenUru defines this flag as the macroVOICE_ENCODED.
-
enumerator kEncodedSpeex = 1 << 1
Whether the voice data is encoded using the Speex codec. If set, then
kEncodedmust also be set. Only set by H’uru clients if Speex is manually selected using theAudio.SetVoiceCodecconsole command. OpenUru defines this flag as the macroVOICE_NARROWBAND, but ignores it and never sets it — OpenUru clients don’t support any codecs other than Speex and assume that all messages withkEncoded/VOICE_ENCODEDset use Speex. For compatibility, H’uru clients also assume Speex compression if onlykEncodedand no other codec flags are set.The voice data has the following format:
Frames: Variable-length array. The number of elements is stored in the message’s frame count field. Each element has the following structure:
Byte count: 1-byte unsigned int. Byte count for the following data field.
Data: Variable-length byte array. A single Speex frame.
-
enumerator kEncodedOpus = 1 << 2
Whether the voice data is encoded using the Opus codec. If set, then
kEncodedmust also be set. H’uru clients use Opus by default, but this can be changed using theAudio.SetVoiceCodecconsole command. OpenUru defines this flag as the macroVOICE_ENH, but ignores it and never sets it — OpenUru clients don’t support Opus compression.The voice data has the following format:
Ignored: 4-byte unsigned int. Set to 0 when writing and ignored when reading. This is a compatibility measure for clients that assume that all encoded/compressed voice data is Speex-encoded.
Data: Variable-length byte array (the entire remaining data). A single Opus packet.
-
enumerator kEncoded = 1 << 0
-
class plNetMsgVoice : public plNetMessage
Class index = 0x0279 = 633
Header:
plNetMessage.Flags: 1-byte unsigned int. Describes the format/codec of the voice data. See
plVoiceFlagsfor details.Frame count: 1-byte unsigned int. Number of compressed audio frames in the voice data. For OpenUru clients, this seems to be always set to 10. For H’uru clients, it’s usually very low (1 or 2 frames), regardless of whether Opus or Speex is used. Set to 0 for uncompressed voice data.
Voice data length: 2-byte unsigned int. Byte count for the following voice data.
Voice data: Variable-length byte array. The actual voice data. The format depends on the flags — see
plVoiceFlagsfor details.Receiver count: 1-byte unsigned int. Element count for the following receiver array.
Receivers: Variable-length array of 4-byte unsigned ints, each a KI number of an avatar that should receive this voice chat. Contains the sender’s KI number iff the message has the
kEchoBackToSenderflag set. Left empty if no avatars are in voice range of the sender and echo is disabled.
plNetMsgObjectUpdateFilter
-
class plNetMsgObjectUpdateFilter : public plNetMessage
Class index = 0x029d = 669
Header:
plNetMessage.UOID count: 2-byte signed int (yes, it’s signed for some reason, even though it can never be negative). Element count for the following UOID array.
UOIDs: Variable-length array of
plUoids.Maximum update frequency: 4-byte floating-point number.
plNetMsgMembersListReq
-
class plNetMsgMembersListReq : public plNetMessage
Class index = 0x02ad = 685
Identical structure to its superclass
plNetMessage.Request a list of all other clients/avatars currently in the age instance. Sent by the client exactly once as part of the link-in/loading process, after it has finished loading the age and avatar.
The server replies immediately with a
plNetMsgMembersListmessage. DIRTSAND also uses this as the trigger for sendingplNetMsgLoadClonemessages for all existing clones in the age instance. MOSS sends theplNetMsgLoadClonemessages in response toplNetMsgGameStateRequestinstead. This makes practically no difference, because the two request messages are sent immediately after one another.
plNetMsgServerToClient
-
class plNetMsgServerToClient : public plNetMessage
Class index = 0x02b2 = 690
Identical structure to its superclass
plNetMessage.
plNetMsgGroupOwner
-
class plNetMsgGroupOwner : public plNetMsgServerToClient
Class index = 0x0264 = 612
Header:
plNetMsgServerToClient.Group count: 4-byte signed int (yes, it’s signed for some reason, even though it can never be negative). Element count for the following group array.
Groups: Variable-length array of:
ID: 7-byte
plNetGroupId.Owned: 1-byte boolean.
plNetMsgMembersList
-
class plNetMsgMembersList : public plNetMsgServerToClient
Class index = 0x02ae = 686
Header:
plNetMsgServerToClient.Member count: 2-byte signed int (yes, it’s signed for some reason, even though it can never be negative). Element count for the following member array.
Members: Variable-length array of
plNetMsgMemberInfoHelpers.
Reply to a
plNetMsgMembersListReq. The members list contains information about every other client currently in the age instance and the UOID of each corresponding avatar. The state of each of these avatars is sent separately, in response to theplNetMsgGameStateRequest.
plNetMsgMemberUpdate
-
class plNetMsgMemberUpdate : public plNetMsgServerToClient
Class index = 0x02b1 = 689
Header:
plNetMsgServerToClient.Member:
plNetMsgMemberInfoHelper.Was added: 1-byte boolean. Set to true if the member in question was added, or set to false if it was removed.
plNetMsgInitialAgeStateSent
-
class plNetMsgInitialAgeStateSent : public plNetMsgServerToClient
Class index = 0x02b8 = 696
Header:
plNetMsgServerToClient.Initial SDL state count: 4-byte unsigned int. The number of
plNetMsgLoadCloneandplNetMsgSDLStatemessages sent by the server.
Reply to a
plNetMsgGameStateRequest. Once the client has received this message and the expected number of clone/state messages, it finishes the link-in/loading process, hides the loading screen, and finally allows the player to interact with the game again.
plNetMsgListenListUpdate
-
class plNetMsgListenListUpdate : public plNetMessage
Class index = 0x02c8 = 712
Header:
plNetMessage.Adding: 1-byte boolean. Set to true if the avatars in question should be added, or set to false if they should be removed.
Receiver count: 1-byte unsigned int. Element count for the following receiver array.
Receivers: Variable-length array of 4-byte unsigned ints, each a KI number of another avatar.
plNetMsgRelevanceRegions
-
class plNetMsgRelevanceRegions : public plNetMessage
Class index = 0x03ac = 940
Header:
plNetMessage.Regions I care about:
hsBitVector. Bit mask of regions for which the client wants to receive messages. The least significant bit (region 0) and all bits from the “regions I’m in” field should always be set.Regions I’m in:
hsBitVector. Bit mask of regions in which the client’s avatar is currently located. At least one bit should always be set — if the avatar isn’t in any region, it’s considered to be in region 0, so the least significant bit should be set in that case.
Sent by the client when its avatar enters or leaves a relevance region.
Only a few ages define relevance regions, namely Ae’gura (city) and Minkata. The regions themselves are defined using
plRelevanceRegionobjects in the .prp files. The “care about” relationships between regions are defined in a separate .csv file for the age.There is always an implicit relevance region 0, which represents everything not contained in any explicit relevance region. All avatars implicitly care about region 0 and avatars in region 0 care about all regions. For ages with no relevance regions defined, region 0 is the only region and contains everything.
Based on this information, the server can theoretically reduce network traffic by delivering broadcast messages only to clients for which they are currently relevant — see the
kNetUseRelevanceRegionsflag. MOSS and DIRTSAND ignore this message though and deliver all broadcast messages to all clients. Unclear if Cyan’s server software uses this message or respects thekNetUseRelevanceRegionsin any way.
plNetMsgPlayerPage
-
class plNetMsgPlayerPage : public plNetMessage
Class index = 0x03b4 = 948
Header:
plNetMessage.Unload: 1-byte boolean. True if the avatar object was unloaded or false if it was loaded. Always false in practice.
UOID:
plUoid. Identifies the avatar object that was un-/loaded.
Sent by the client after it has loaded its own avatar object.
This message is only sent when an avatar is loaded for the first time, i. e. on the first link-in after selecting an avatar on the avatar selection screen, or when changing to another avatar via the console. When an already loaded avatar links to another age instance, this message is not sent again to the new game server. Although this message could also be sent when the avatar is unloaded again, the open-sourced client code never does this.
MOSS broadcasts this message to other clients (even though the client doesn’t support receiving it), but otherwise ignores it. DIRTSAND ignores it completely. Unclear if Cyan’s server software does anything with it.