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).

Common data types

Assorted data types used by the message classes below.

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

SafeString.

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 atoi and atof, so any leading whitespace and trailing non-number characters are ignored (though it’s probably best not to rely on this). H’uru clients use strtol/strtoul for 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 true or 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.

class plNetGroupId
enum NetGroupConstants
enumerator kNetGroupConstant = 1 << 0
enumerator kNetGroupLocal = 1 << 1
class plClientGuid : public plCreatable
  • Flags: 2-byte unsigned int. See Flags for details.

  • Account UUID: 16-byte UUID. Only present if the kAccountUUID flag 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 kPlayerID flag is set. The avatar’s KI number.

  • Temp player ID: 4-byte unsigned int. Only present if the kTempPlayerID flag 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 kPlayerName flag is set. Byte count for the following player name.

  • Player name: Variable-length byte string. Only present if the kPlayerName flag is set. The avatar’s display name.

  • CCR level: 1-byte unsigned int. Only present if the kCCRLevel flag 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 kProtectedLogin flag 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 kBuildType flag 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 kSrcAddr flag 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 kSrcPort flag 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 kReserved flag 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 kClientKey flag is set. Byte count for the following client key.

  • Client key: Variable-length byte string. Only present if the kClientKey flag 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
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”

class plNetMsgMemberInfoHelper : public plCreatable

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 plNetMessage subclass 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 BitVectorFlags for details.

  • Protocol version: 2 bytes. Only present if the kHasVersion flag 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 the kHasTimeSent flag 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 kHasContext flag 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 kHasTransactionID flag 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 kHasPlayerID flag 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 kHasAcctUUID flag 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 wrapped plMessage has at least one receiver whose plLocation is 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 plNetClientRecorder is enabled using the console command Demo.RecordNet, this flag is set on all plNetMsgSDLState, plNetMsgSDLStateBCast, plNetMsgGameMessage, and plNetMsgLoadClone messages.

  • If voice chat echo has been enabled using the console command Net.Voice.Echo, this flag is set on all plNetMsgVoice messages (this is broken in OpenUru clients if compression is disabled using the console command Audio.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 further plNetMsgSDLState messages 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 plNetMsgGameStateRequest messages. 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 wrapped plMessage has the kNetUseRelevanceRegions flag set, and for some plNetMsgSDLState (or subclass) messages caused by plArmatureMod. 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 wrapped plMessage has the kNetAllowInterAge flag set (unless kRouteToAllPlayers/kCCRSendToAllPlayers is also set). This should only happen for plNetMsgGameMessageDirected messages. 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, and plNetMsgServerToClient messages (including subclasses, if any). DIRTSAND also sets it for some plNetMsgSDLStateBCast messages. 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, and plNetMsgListenListUpdate. 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, kInterAgeRouting should be unset. The client sets this flag for plNetMsgGameMessage (or subclass) messages if the client is internal and the wrapped plMessage has the kCCRSendToAllPlayers flag set. Should never be set for other message types. DIRTSAND implements this flag, but only respects it if the sender is permitted to send unsafe plMessages (i. e. if the sender’s account has the kAccountRoleAdmin flag set). MOSS doesn’t implement this flag at all and always ignores it.

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 kHasGameMsgRcvrs flag is set on the message.

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

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.

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 plNetMsgLoadClone for every clone currently in the age instance. DIRTSAND actually sends these messages in response to plNetMsgMembersListReq already, 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 plNetMsgSDLState for 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 plNetMsgInitialAgeStateSent containing the number of state messages that were sent. DIRTSAND doesn’t include plNetMsgLoadClone messages in this count.

After sending the request, the client blocks the link-in/loading process until it has received the plNetMsgInitialAgeStateSent message and the indicated number of state mesages.

The exact order of the reply messages doesn’t matter, except that an object’s plNetMsgLoadClone message must be sent before any other messages referring to that object.

DIRTSAND uses the following order:

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 plNetMsgInitialAgeStateSent message is sent last.

plNetMsgObject

class plNetMsgObject : public plNetMessage

Class index = 0x0268 = 616

plNetMsgStreamedObject

class plNetMsgStreamedObject : public plNetMsgObject

Class index = 0x027b = 635

plNetMsgSharedState

class plNetMsgSharedState : public plNetMsgStreamedObject

Class index = 0x027c = 636

The stream data has the following format (although in practice, only two specific combinations of values are used — see plNetMsgTestAndSet):

  • State name length: 2-byte unsigned int. Byte count for the following state name field.

  • State name: Variable-length byte string.

  • Variable count: 4-byte signed int (yes, it’s signed for some reason, even though it can never be negative). Element count for the following array of variables.

  • Server may delete: 1-byte boolean. Set to true if the state is being set to its default value, in which case the server doesn’t have to store the value anymore, or set to false if the state is being set to a non-default value.

  • Variables: Variable-length array of:

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 plNetMsgGameMessage containing a plServerReplyMsg. The message has no sender and its only receiver is the UOID sent by the client in the plNetMsgTestAndSet message. 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 plServerReplyMsg in 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 the kNewSDLState flag.)

  • 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.

plNetMsgGetSharedState

class plNetMsgGetSharedState : public plNetMsgObject

Class index = 0x027e = 638

  • Header: plNetMsgObject.

  • Shared state name: 32-byte zero-terminated string.

plNetMsgObjStateRequest

class plNetMsgObjStateRequest : public plNetMsgObject

Class index = 0x0286 = 646

Identical structure to its superclass plNetMsgObject.

plNetMsgStream

class plNetMsgStream : public plNetMessage

Class index = 0x026c = 620

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 wrapped plMessage’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 the plMessage’s timestamp field.

Wraps a plMessage to be sent between clients. The stream data contains the plMessage as a serialized plCreatable with header. See Plasma messages for details on that format.

If the contained plMessage is an instance of plLoadCloneMsg, then the wrapper message must have the class plNetMsgLoadClone instead.

When the client sends (locally) a plMessage that has the kNetPropagate flag set, it wraps the plMessage in a plNetMsgGameMessage (or one of its subclasses) and sends it to the game server. Afterwards, the plMessage is also sent locally on the client side if it has the kLocalPropagate flag 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 kEchoBackToSender flag set, it’s also repeated back to the sender. MOSS doesn’t support this flag. DIRTSAND ignores it for plNetMsgGameMessageDirected messages.

If the message has the kRouteToAllPlayers flag 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 the kAccountRoleAdmin flag set and the message class is exactly plNetMsgGameMessage.

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 the kAccountRoleAdmin flag set, which allows bypassing this check.

The server forwards the message completely unmodified, with the following exceptions:

  • If the message has the kHasTimeSent flag 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 kNetNonLocal flag on the wrapped plMessage before forwarding it (unless the wrapper message is a plNetMsgLoadClone — 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 wrapped plMessage should be an instance of pfKIMsg, plCCRCommunicationMsg, plAvatarInputStateMsg, plInputIfaceMgrMsg, or plNotifyMsg.

By default, the message is only forwarded to receivers that are in the same age instance as the sender. If the kInterAgeRouting flag 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 kEchoBackToSender for directed messages and instead repeats the message back if and only if the sender’s KI number is in the list of receivers.

The kRouteToAllPlayers flag 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 plNetMsgGameMessage used if the wrapped plMessage is an instance of plLoadCloneMsg (or its only subclass plLoadAvatarMsg).

These messages are always forwarded to all clients within the same age instance — they cannot be directed and should never have the kInterAgeRouting or kRouteToAllPlayers flags 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) or Audio.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 macro VOICE_ENCODED.

enumerator kEncodedSpeex = 1 << 1

Whether the voice data is encoded using the Speex codec. If set, then kEncoded must also be set. Only set by H’uru clients if Speex is manually selected using the Audio.SetVoiceCodec console command. OpenUru defines this flag as the macro VOICE_NARROWBAND, but ignores it and never sets it — OpenUru clients don’t support any codecs other than Speex and assume that all messages with kEncoded/VOICE_ENCODED set use Speex. For compatibility, H’uru clients also assume Speex compression if only kEncoded and 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 kEncoded must also be set. H’uru clients use Opus by default, but this can be changed using the Audio.SetVoiceCodec console command. OpenUru defines this flag as the macro VOICE_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.

class plNetMsgVoice : public plNetMessage

Class index = 0x0279 = 633

  • Header: plNetMessage.

  • Flags: 1-byte unsigned int. Describes the format/codec of the voice data. See plVoiceFlags for 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 plVoiceFlags for 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 kEchoBackToSender flag 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 plNetMsgMembersList message. DIRTSAND also uses this as the trigger for sending plNetMsgLoadClone messages for all existing clones in the age instance. MOSS sends the plNetMsgLoadClone messages in response to plNetMsgGameStateRequest instead. 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:

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 the plNetMsgGameStateRequest.

plNetMsgMemberUpdate

class plNetMsgMemberUpdate : public plNetMsgServerToClient

Class index = 0x02b1 = 689

plNetMsgInitialAgeStateSent

class plNetMsgInitialAgeStateSent : public plNetMsgServerToClient

Class index = 0x02b8 = 696

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 plRelevanceRegion objects 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 kNetUseRelevanceRegions flag. 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 the kNetUseRelevanceRegions in 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.