I did this years ago, to correct this.
If for say you wanted to display a crown based on the rights of players around you; the client has no idea what the rights are of those players.
The only times it knows the rights of those players is when a player talks in chat. All other times the client only references the players rights (e.g private chat). You can see in the code below the server encodes the players rights after a user typed in chat, the rights are appended to the player updating via the chat mask which then the player updating gets sent to the client the client updates it then.
Code:
/**
* The [PlayerUpdateBlock] implementation encodes the [Player] chat mask.
*
* @author Seven
*/
class PlayerChatUpdateBlock : PlayerUpdateBlock(0x80, UpdateFlag.CHAT) {
override fun encode(player: Player, builder: GamePacketBuilder) {
val msg = player.chatMessage
val bytes = msg.text
builder.writeShort((msg.color and 0xFF shl 8) + (msg.effect and 0xFF), ByteOrder.LITTLE)
.write(player.rights.protocolValue)
.write(bytes!!.size, ByteModification.NEGATION)
.writeBytesReverse(bytes)
}
}
Here you can see the client actually assigns the rights after the chat mask has been received.
Code:
if ((mask & 0x80) != 0) {
int textInfo = buffer.readLEUShort();
int privilege = buffer.readUnsignedByte();
int offset = buffer.readNegUByte();
int off = buffer.currentPosition;
if (player.name != null && player.visible) {
long name = StringUtils.encodeBase37(player.name);
boolean ignored = false;
if (privilege <= 1) {
for (int count = 0; count < ignoreCount; count++) {
if (ignoreListAsLongs[count] != name)
continue;
ignored = true;
break;
}
}
if (!ignored && onTutorialIsland == 0)
try {
chatBuffer.currentPosition = 0;
buffer.readReverseData(chatBuffer.payload, offset, 0);
chatBuffer.currentPosition = 0;
String text = ChatMessageCodec.decode(offset, chatBuffer);
player.spokenText = text;
player.textColour = textInfo >> 8;
player.privelage = privilege;
player.textEffect = textInfo & 0xff;
player.textCycle = 150;
if (privilege == 2 || privilege == 3) {
displayChatMessage(text, 1, "@cr2@" + player.name);
} else if (privilege == 1) {
displayChatMessage(text, 1, "@cr1@" + player.name);
} else {
displayChatMessage(text, 2, player.name);
}
} catch (Exception exception) {
SignLink.reporterror("cde2");
}
}
buffer.currentPosition = off + offset;
}
To show you the private message rights reference. Server encodes the message.
Code:
class SendPrivateMessageOutgoingPacket(private val name: Long, private val rights: PlayerRights, private val message: ByteArray, private val messageLength: Int) : OutgoingPacket(196, PacketHeader.VARIABLE_BYTE) {
override fun writePacket(player: Player): GamePacketBuilder {
builder.writeLong(name)
.writeInt(player.lastMessage++)
.write(rights.protocolValue)
.writeBytes(message, messageLength)
return builder
}
}
Here you can see the client decodes this packet, the rights doesn't even get assigned it's only referenced.
Code:
if (opcode == PacketConstants.SEND_RECEIVED_PRIVATE_MESSAGE) {
long encodedName = incoming.readLong();
int messageId = incoming.readInt();
int rights = incoming.readUnsignedByte();
boolean ignoreRequest = false;
for (int index = 0; index < 100; index++) {
if (privateMessageIds[index] != messageId) {
continue;
}
ignoreRequest = true;
}
if (rights <= 1) {
for (int index = 0; index < ignoreCount; index++) {
if (ignoreListAsLongs[index] != encodedName) {
continue;
}
ignoreRequest = true;
}
}
if (!ignoreRequest && onTutorialIsland == 0)
try {
privateMessageIds[privateMessageCount] = messageId;
privateMessageCount = (privateMessageCount + 1) % 100;
String message = ChatMessageCodec.decode(packetSize - 13, incoming);
if (rights == 2 || rights == 3) {
pushMessage(message, 7, "@cr2@" + StringUtils.formatUsername(StringUtils.decodeBase37(encodedName)));
} else if (rights == 1) {
pushMessage(message, 7, "@cr1@" + StringUtils.formatUsername(StringUtils.decodeBase37(encodedName)));
} else {
pushMessage(message, 3, StringUtils.formatUsername(StringUtils.decodeBase37(encodedName)));
}
} catch (Exception ex) {
SignLink.reporterror("cde1");
}
opcode = -1;
return true;
}
Therefore in some cases you may not see the correct sprite on the map for a player. When players are updated via player updating, you can send the rights with player appearance. That is so the client updates the rights when a player is updated. So in some cases for example.. A moderator logged in, then you login you won't see their crown because they haven't talked in chat. Well now when you login you will see their crown right away because when you login your player updating gets all the information about your local players and sends it to the client so now the client will update it.
Code:
combatLevel = stream.readUnsignedByte();
// the index of the crown in the array to display
crownId= stream.readUnsignedByte();
skill = stream.readUShort();
visible = true;
appearanceHash = 0L;
Which means now in the server you will have to write this variable right below combatLevel in the player appearance.