Code:
package com.rs.game.player.masks;
import com.common.io.OutputStream;
import com.common.utils.Logger;
import com.rs.GameSettings;
import com.rs.game.World;
import com.rs.game.map.tiles.RegionHash;
import com.rs.game.map.tiles.WalkStep;
import com.rs.game.player.Player;
import com.rs.json.PacketDb;
import lombok.Getter;
public class LocalPlayerUpdate {
/**
* The maximum amount of local players being added per tick. This is to
* decrease time it takes to load crowded places (such as home).
*/
private static final int MAX_PLAYER_ADD = 10;
/**
* Hardcoded, cannot be changed
*/
private static final int PLAYERS_LIMIT = 2048;
private Player player;
/**
* both
*/
private byte[] skipFlags;
/**
* local
*/
private transient Player[] localPlayers;
private int[] localPlayersIndexes;
private int localPlayersIndexesCount;
@Getter
private int renderDataLength; // the total length of the render blocks updates
/**
* outside
*/
private int[] outPlayersIndexes;
private int outPlayersIndexesCount;
private RegionHash[] regionHashes;
private int localAddedPlayers; // The amount of local players added this tick.
public LocalPlayerUpdate(Player player) {
this.player = player;
skipFlags = new byte[PLAYERS_LIMIT];
outPlayersIndexes = new int[PLAYERS_LIMIT];
localPlayers = new Player[PLAYERS_LIMIT];
localPlayersIndexes = new int[PLAYERS_LIMIT];
regionHashes = new RegionHash[PLAYERS_LIMIT];
}
/**
* Initializes only player positions
*/
public void init(OutputStream stream) {
stream.initBitAccess();
stream.writeBits(30, player.getTileHash());
localPlayers[player.getIndex()] = player;
localPlayersIndexes[localPlayersIndexesCount++] = player.getIndex();
for (int playerIndex = 1; playerIndex < PLAYERS_LIMIT; playerIndex++) {
if (playerIndex == player.getIndex()) {
continue;
}
Player player = World.getPlayer(playerIndex);
regionHashes[playerIndex] = (player == null) ? null : player.getRegionHash();
int regionHash = regionHashes[playerIndex] == null ? 0 : regionHashes[playerIndex].getRegionHash();
stream.writeBits(18, regionHash);
outPlayersIndexes[outPlayersIndexesCount++] = playerIndex;
}
stream.finishBitAccess();
}
public OutputStream encode() {
OutputStream stream = new OutputStream();
OutputStream updateBlockData = new OutputStream();
stream.writePacketVarShort(player.getIsaacKeyPair(), PacketDb.getGameOut("player_update"));
processLocalPlayers(stream, updateBlockData, NsnBlock.NSN_0);
processLocalPlayers(stream, updateBlockData, NsnBlock.NSN_1);
processOutsidePlayers(stream, updateBlockData, NsnBlock.NSN_2);
processOutsidePlayers(stream, updateBlockData, NsnBlock.NSN_3);
stream.writeBytes(updateBlockData.getBuffer(), 0, updateBlockData.getOffset());
stream.endPacketVarShort();
renderDataLength = 0;
localPlayersIndexesCount = 0;
outPlayersIndexesCount = 0;
for (int playerIndex = 1; playerIndex < PLAYERS_LIMIT; playerIndex++) {
skipFlags[playerIndex] >>= 1;
Player player = localPlayers[playerIndex];
if (player == null) {
outPlayersIndexes[outPlayersIndexesCount++] = playerIndex;
} else {
localPlayersIndexes[localPlayersIndexesCount++] = playerIndex;
}
}
Logger.log("processing " + localPlayersIndexesCount + " local players");
return stream;
}
/**
*
* @param stream
* The main stream that will be used to update the player's basic information
*
* @param updateBlockData
* Used for player masks only, appended at the end to the main stream
*
* @param nsn
* Current nsn block being processed
* There's 2 nsn blocks (nsn2 + nsn3) used to save bandwidth by regrouping inactive players in one block,
* making skips more efficient
*/
private void processOutsidePlayers(OutputStream stream, OutputStream updateBlockData, NsnBlock nsn) {
stream.initBitAccess();
int skip = 0;
localAddedPlayers = 0;
for (int i = 0; i < outPlayersIndexesCount; i++) {
int playerIndex = outPlayersIndexes[i];
if (shouldSkipPlayer(nsn, playerIndex)) {
continue;
}
if (skip > 0) {
skip--;
skipPlayer(playerIndex);
continue;
}
Player p = World.getPlayer(playerIndex);
if (needsAdd(p)) {
// adds a new local player to the local players
stream.writeBits(1, 1); // is not skipped
stream.writeBits(2, 0); // request add to local players
writePosHash(stream, p); // writes and updates the region hash if necessary
stream.writeBits(6, p.getXInRegion());
stream.writeBits(6, p.getYInRegion());
appendUpdateBlock(p, updateBlockData, true);
stream.writeBits(1, 1); // needs update
addLocalPlayer(p);
} else {
// the player is either pos updated, or marked as skipped
writePosHash(stream, p);
if (p == null) {
for (int j = i + 1; j < outPlayersIndexesCount; j++) {
int p2Index = outPlayersIndexes[j];
if (shouldSkipPlayer(nsn, p2Index)) {
continue;
}
Player p2 = World.getPlayer(p2Index);
if (needsAdd(p2) || needsPositionUpdate(p2)) {
break;
}
skip++;
}
skipPlayers(stream, skip);
skipPlayer(playerIndex);
}
}
}
stream.finishBitAccess();
}
/**
*
* @param stream
* The main stream that will be used to update the player's basic information
*
* @param updateBlockData
* Used for player masks only, appended at the end to the main stream
*
* @param nsn
* Current nsn block being processed
* There's 2 nsn blocks (nsn0 + nsn1) used to save bandwidth by regrouping inactive players in one block,
* making skips more efficient
*/
private void processLocalPlayers(OutputStream stream, OutputStream updateBlockData, NsnBlock nsn) {
stream.initBitAccess();
int skip = 0;
for (int i = 0; i < localPlayersIndexesCount; i++) {
int playerIndex = localPlayersIndexes[i];
if (shouldSkipPlayer(nsn, playerIndex)) {
continue;
}
if (skip > 0) {
skip--;
skipPlayer(playerIndex);
continue;
}
Player p = localPlayers[playerIndex];
if (needsRemove(p)) {
stream.writeBits(1, 1); // is not skipped
stream.writeBits(1, 0); // no mask update
stream.writeBits(2, 0); // request remove
writePosHash(stream, p); // writes and updates the region hash if necessary
localPlayers[playerIndex] = null;
} else {
boolean needsMaskUpdate = p.getMaskHandler().needsUpdate();
if (needsLocalUpdate(p)) {
stream.writeBits(1, 1); // is not skipped
stream.writeBits(1, needsMaskUpdate ? 1 : 0); // needs mask update
}
if (needsMaskUpdate) {
appendUpdateBlock(p, updateBlockData, false);
}
if (p.getMaskHandler().hasTeleported()) {
stream.writeBits(2, 3); // teleporting
int xOffset = p.getX() - p.getLastWorldTile().getX();
int yOffset = p.getY() - p.getLastWorldTile().getY();
int planeOffset = p.getPlane() - p.getLastWorldTile().getPlane();
// -2 for safety measure
if (Math.abs(xOffset) <= GameSettings.NORMAL_VIEWPORT - 2 && Math.abs(yOffset) <= GameSettings.NORMAL_VIEWPORT - 2) {
stream.writeBits(1, 0);
if (xOffset < 0) {
xOffset += 32;
}
if (yOffset < 0) {
yOffset += 32;
}
stream.writeBits(12, yOffset + (xOffset << 5) + (planeOffset << 10));
} else {
stream.writeBits(1, 1);
stream.writeBits(30, (yOffset & 0x3fff) + ((xOffset & 0x3fff) << 14) + ((planeOffset & 0x3) << 28));
}
} else if (p.getMaskHandler().getCurrentMovementOpcode() != -1) {
int opcode = p.getMaskHandler().getCurrentMovementOpcode();
boolean running = p.getMaskHandler().isRunMovementOpcode();
stream.writeBits(2, running ? 2 : 1); // running or walking
stream.writeBits(running ? 4 : 3, opcode); // direction opcode
} else if (needsMaskUpdate) {
stream.writeBits(2, 0); // just update
} else {
stream.writeBits(1, 0); // skipped
for (int j = i + 1; j < localPlayersIndexesCount; j++) {
int p2Index = localPlayersIndexes[j];
if (shouldSkipPlayer(nsn, p2Index)) {
continue;
}
Player p2 = localPlayers[p2Index];
if (needsRemove(p2) || needsLocalUpdate(p2)) {
break;
}
skip++;
}
skipPlayers(stream, skip);
skipPlayer(playerIndex);
}
}
}
stream.finishBitAccess();
}
/**
* Writes the flag that says if the non-player needs a position update
* If he does, it also writes his new position
*/
private void writePosHash(OutputStream stream, Player p) {
if (p == null) {
stream.writeBits(1, 0); // no pos hash update
return;
}
if (needsPositionUpdate(p)) {
stream.writeBits(1, 0); // no pos hash update
} else {
RegionHash hash = p.getRegionHash();
RegionHash lastHash = regionHashes[p.getIndex()];
stream.writeBits(1, 1); // request pos hash update
updateRegionHash(stream, hash, lastHash);
regionHashes[p.getIndex()] = hash;
}
}
/**
* Checks if the player needs his position updated
*/
private boolean needsPositionUpdate(Player p) {
if (p == null) {
return false;
}
RegionHash hash = p.getRegionHash();
RegionHash lastHash = regionHashes[p.getIndex()];
return hash.equals(lastHash);
}
/**
* Checks if the player needs any type of local update
*/
private boolean needsLocalUpdate(Player p) {
return p.getMaskHandler().needsUpdate() || p.getMaskHandler().hasTeleported() ||
p.getMaskHandler().getCurrentMovementOpcode() != -1;
}
/**
* Sets the new chunk position of the player
*/
private void updateRegionHash(OutputStream stream, RegionHash currentRegionHash, RegionHash lastRegionHash) {
if (lastRegionHash == null) {
lastRegionHash = new RegionHash(0);
}
int lastRegionX = lastRegionHash.getRegionX();
int lastRegionY = lastRegionHash.getRegionY();
int lastPlane = lastRegionHash.getPlane();
int currentRegionX = currentRegionHash.getRegionX();
int currentRegionY = currentRegionHash.getRegionY();
int currentPlane = currentRegionHash.getPlane();
int planeOffset = currentPlane - lastPlane;
if (lastRegionX == currentRegionX && lastRegionY == currentRegionY) {
stream.writeBits(2, 1); // plane update
stream.writeBits(2, planeOffset); // sends the plane
} else if (Math.abs(currentRegionX - lastRegionX) <= 1 && Math.abs(currentRegionY - lastRegionY) <= 1) {
int dx = currentRegionX - lastRegionX;
int dy = currentRegionY - lastRegionY;
int opcode = WalkStep.getDirectionFromOffset(dx, dy); // usually used for walk, but same thing
stream.writeBits(2, 2); // moved +-1 region update
stream.writeBits(5, (planeOffset << 3) + (opcode & 0x7)); // new region move direction (and plane)
} else {
int xOffset = currentRegionX - lastRegionX;
int yOffset = currentRegionY - lastRegionY;
stream.writeBits(2, 3); // new region update (more than +-1)
stream.writeBits(18, (yOffset & 0xff) + ((xOffset & 0xff) << 8) + (planeOffset << 16)); // new region offset
}
}
/**
* Adds a local player from the non-local players
*/
private void addLocalPlayer(Player p) {
localAddedPlayers++;
localPlayers[p.getIndex()] = p;
skipPlayer(p.getIndex());
}
/**
* Skips X players of the 2048
*/
private void skipPlayers(OutputStream stream, int amount) {
int skipSize = (amount == 0) ? 0 : (amount > 255) ? 3 : (amount > 31) ? 2 : 1;
stream.writeBits(2, skipSize);
if (amount > 0) {
int bitsAmt = (amount > 255) ? 11 : (amount > 31) ? 8 : 5;
stream.writeBits(bitsAmt, amount);
}
}
/**
* Flags the player as "skipped" so he can be processed in the other nsn block next tick
*/
public void skipPlayer(int index) {
skipFlags[index] |= SKIPPED_THIS_CYCLE;
}
/**
* Checks if we should skip this player based on the nsn and which flag the player has
*/
private boolean shouldSkipPlayer(NsnBlock nsn, int index) {
int skipFlag = skipFlags[index];
return (nsn == NsnBlock.NSN_0 || nsn == NsnBlock.NSN_3) ? (skipFlag & SKIPPED_LAST_CYCLE) != 0 :
(skipFlag & SKIPPED_LAST_CYCLE) == 0;
}
/**
* Checks if a player needs to be added to the local players
*/
private boolean needsAdd(Player p) {
return p != null && player.withinDistance(p, player.getViewSceneDistance()) && localAddedPlayers < MAX_PLAYER_ADD;
}
/**
* Checks if a plyer needs to be removed from the local players
*/
private boolean needsRemove(Player p) {
return !player.withinDistance(p, player.getViewSceneDistance());
}
private enum NsnBlock {
NSN_0, NSN_1, NSN_2, NSN_3
}
private final byte SKIPPED_THIS_CYCLE = 0x2;
private final byte SKIPPED_LAST_CYCLE = 0x1;
private final byte NOT_SKIPPED = 0;
private void appendUpdateBlock(Player p, OutputStream stream, boolean firstTime) {
p.getMaskHandler().encodeUpdateBlock(stream, firstTime);
}
public void increaseRenderDataLength(int length) {
renderDataLength += length;
}
}