|
When I was writing the player updating for the proof of concept actor server I'm writing, I stumbled onto something that got me curious whilst referencing a part of Hyperion's protocol. This part of adding players to the update list is what I'm talking about:
This code is part of the method called addNewPlayer(...). The comment says the second bit sent is a flag to let the client discard the client sided walking queues. When I looked into the method that does something with this flag I noticed it's actually quite some different. It's actually has to do with cached players that were not in the current client's viewport but within 8 tiles from the current client's viewport. This led to an understanding of a misconception we have been carrying with us for quite some time. Allow me to explain how this current conception is wrong and what I think is the correct way.Code:/* * Write two flags here: the first indicates an update is required * (this is always true as we add the appearance after adding a player) * and the second to indicate we should discard client-side walk * queues. */ packet.putBits(1, 1); packet.putBits(1, 1);
The client's viewport is 32 x 32 tiles big. This is exactly 4 x 4 segments big. Segments are blocks of 8 x 8 tiles. You need to build your viewport from a block of 5 x 5 segments covering 40 x 40 tiles and ignore tiles/players you don't want to consider because of the client's 32 x 32 viewport. The center segment of this 5 x 5 area would be the segment you're currently in. You would use the 5 x 5 segments area to take the players within to queue for adding to the local update list of the client and still have a distance check to make sure you only send players within the 32 x 32 viewport when trying to add players to the client. Assume you add a player to the local viewport and this player is on the edge of the 32 x 32 area. If you walk away one tile, he will be outside the 32 x 32 viewport. The code mentioned above allows us to not remove the player from the client yet as explicitly as long as the player stays within the 5 x 5 segment. Instead it caches the player and if the player walks one tile into the same direction as you did, or you walk back, we can notify the client through that code above and let it know it's not a 'new' player but instead a player that was still within the 5 x 5 segments area and let it update its walking queue.
Consider this code:
This gives us an idea how to handle this concept. Players that are in the 5 x 5 segment area are added to the additionList list. From this list we add players to the localList, these are the players that are within the 32 x 32 client's viewport. All players that leave the 5 x 5 segment area or you move to a neighbouring segment and are left that way, are on the removalList list and those players are removed form the localList. Players that are not within the viewport anymore are still considered being on the localList but don't get updates, instead for them the client receives false flags for updating. They are stored on the cachedList and are checked upon in the procedure of adding players to the viewport, to see if they are back in the viewport. If they are, the first bit mentioned above is set to 0 or 1 according to whether an appearance update needs to be forced and or other masks have been flagged for updating. The second bit referenced above is set to 0 if a cached player is added back or to 1 if it's a new player.Code:protected Set<Integer> localList = new LinkedHashSet<Integer>(); protected Set<Integer> additionList = new LinkedHashSet<Integer>(); protected Set<Integer> removalList = new LinkedHashSet<Integer>(); protected Set<Integer> cachedList = new LinkedHashSet<Integer>();
This is a concept implementation:
The reason I believe Jagex made it this way is because I believe Jagex handled the viewport with the fixed 8 x 8 segments regions are build with. But 4 x 4 segments are only sufficient if you're right in the middle of them. Instead they used 5 x 5 segments, making it easy to grab the players without having to check a too big area and limit the use of bandwidth by ignoring anything thats further away than absolute 15 tiles.Code:package maximemeire.phantom.model.entity.player.update; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import maximemeire.phantom.model.entity.MovementNode; import maximemeire.phantom.model.entity.player.Player; import maximemeire.phantom.model.entity.player.update.PlayerUpdateFlags.PlayerUpdateFlag; import maximemeire.phantom.network.DynamicOutboundBuffer; /** * @author Maxime Meire * */ public class PlayerUpdate { protected PlayerUpdateNode updateNode; protected Set<Integer> localList = new LinkedHashSet<Integer>(); protected Set<Integer> additionList = new LinkedHashSet<Integer>(); protected Set<Integer> removalList = new LinkedHashSet<Integer>(); protected Set<Integer> cachedList = new LinkedHashSet<Integer>(); protected Map<Integer, PlayerUpdateNode> otherUpdates = new HashMap<Integer, PlayerUpdateNode>(); private int movementSize = 512; private int generalSize = 256; protected static DynamicOutboundBuffer encodePayload(Player player) { try { PlayerUpdate update = player.getUpdate(); int movementSize = update.movementSize + (1 / 4) * update.movementSize; DynamicOutboundBuffer payload = new DynamicOutboundBuffer(movementSize); int generalSize = update.generalSize + (1 / 2) * update.generalSize; DynamicOutboundBuffer masks = new DynamicOutboundBuffer(generalSize); payload.start_bit_access(); encodeThisMovement(player, payload); encodeGeneralUpdate(update.updateNode, masks, false); payload.put_bits(8, update.localList.size()); encodeOtherMovementUpdates(player, payload, masks); encodeAddFOVPlayer(player, payload, masks); if (masks.readable()) { payload.put_bits(11, 2047); payload.end_bit_access(); payload.writeBytes(masks); } else { payload.end_bit_access(); } return payload; } catch (Exception e) { e.printStackTrace(); return null; } } protected static void encodeThisMovement(Player player, DynamicOutboundBuffer movement) { PlayerUpdate update = player.getUpdate(); if (update.updateNode.teleported || update.updateNode.changedRegion) { movement.put_bits(1, 1); movement.put_bits(2, 3); movement.put_bits(2, player.getLocation().getZ()); movement.put_bits(1, update.updateNode.teleported ? 1 : 0); movement.put_bits(1, update.updateNode.requireUpdate() ? 1 : 0); movement.put_bits(7, player.getClientLoadedBase().deltaY(player.getLocation())); movement.put_bits(7, player.getClientLoadedBase().deltaX(player.getLocation())); } else { if (update.updateNode.movement.notMoving()) { if (update.updateNode.requireUpdate()) { movement.put_bits(1, 1); movement.put_bits(2, 0); } else { movement.put_bits(1, 0); } } else if (update.updateNode.movement.walking()) { movement.put_bits(1, 1); movement.put_bits(2, 1); movement.put_bits(3, update.updateNode.movement.getFirst()); movement.put_bits(1, update.updateNode.requireUpdate() ? 1 : 0); } else { movement.put_bits(1, 1); movement.put_bits(2, 2); movement.put_bits(3, update.updateNode.movement.getFirst()); movement.put_bits(3, update.updateNode.movement.getSecond()); movement.put_bits(1, update.updateNode.requireUpdate() ? 1 : 0); } } } protected static void encodeOtherMovementUpdates(Player player, DynamicOutboundBuffer movement, DynamicOutboundBuffer general) throws Exception { PlayerUpdate update = player.getUpdate(); for (int id : update.localList) { if (update.additionList.contains(id)) throw new Exception("Player is on the addition list but already on the local list"); if (update.removalList.contains(id)) { movement.put_bits(1, 1); movement.put_bits(2, 3); update.localList.remove(id); update.otherUpdates.remove(id); update.cachedList.remove(id); continue; } else if (player.getLocation().absDeltaX(update.updateNode.location) > 15 && player.getLocation().absDeltaX(update.updateNode.location) > 15 && player.getLocation().getZ() == update.updateNode.location.getZ()) { movement.put_bits(1, 0); update.cachedList.add(id); continue; } else { PlayerUpdateNode updateNode = update.otherUpdates.get(id); MovementNode movementNode = updateNode.movement; if (movementNode.notMoving()) { if (updateNode.requireUpdate()) { movement.put_bits(1, 1); movement.put_bits(2, 0); } else movement.put_bits(1, 0); } else if (movementNode.walking()) { movement.put_bits(1, 1); movement.put_bits(2, 1); movement.put_bits(3, movementNode.getFirst()); movement.put_bits(1, updateNode.requireUpdate() ? 1 : 0); } else { movement.put_bits(1, 1); movement.put_bits(2, 2); movement.put_bits(3, movementNode.getFirst()); movement.put_bits(3, movementNode.getSecond()); movement.put_bits(1, updateNode.requireUpdate() ? 1 : 0); } if (updateNode.requireUpdate()) { encodeGeneralUpdate(updateNode, general, false); } } } } protected static void encodeGeneralUpdate(PlayerUpdateNode updateNode, DynamicOutboundBuffer general, boolean forceAppearance) { if (!updateNode.requireUpdate() && !forceAppearance) { return; } if (updateNode.hasCachedMaskBlock() && !forceAppearance) { general.writeBytes(updateNode.cachedMaskBlock); return; } DynamicOutboundBuffer cacheBlock = new DynamicOutboundBuffer(100); int mask = updateNode.mask; if (mask >= 0x100) { mask |= 0x40; cacheBlock.put8(mask & 0xff); cacheBlock.put8(mask >> 8); } else cacheBlock.put8(mask); if (updateNode.requireGraphicsUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.GRAPHICS_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireAnimationUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.ANIMATION_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireForcedChatUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.FORCED_CHAT_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireChatUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.CHAT_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireFaceEntityUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.FACE_ENTITY_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireAppearanceUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.APPEARANCE_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireFaceCoordinateUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.FACE_COORDINATE_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireHitUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.HIT_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireHit2Update()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.HIT2_UPDATE_MASK.ordinal()].payload); } if (!forceAppearance) { updateNode.cachedMaskBlock = cacheBlock; } general.writeBytes(cacheBlock); } protected static void encodeAddFOVPlayer(Player player, DynamicOutboundBuffer movement, DynamicOutboundBuffer general) throws Exception { PlayerUpdate update = player.getUpdate(); for (int id : update.additionList) { if (update.localList.contains(id)) throw new Exception("Player is already on the local list although scheduled to be added to it"); if (update.localList.size() >= 255) return; if (update.cachedList.contains(id)) continue; if (player.getLocation().absDeltaX(update.updateNode.location) <= 15 && player.getLocation().absDeltaX(update.updateNode.location) <= 15 && player.getLocation().getZ() == update.updateNode.location.getZ()) { PlayerUpdateNode updateNode = update.otherUpdates.get(id); movement.put_bits(11, updateNode.index); movement.put_bits(1, 1); movement.put_bits(1, 1); int deltaX = updateNode.location.deltaX(player.getLocation()); int deltaY = updateNode.location.deltaY(player.getLocation()); movement.put_bits(5, deltaY); movement.put_bits(5, deltaX); encodeGeneralUpdate(updateNode, general, true); update.localList.add(id); update.additionList.remove(id); } else { continue; } } for (int id : update.cachedList) { if (update.localList.contains(id)) throw new Exception("Player is already on the local list although also cached."); if (update.localList.size() >= 255) return; if (player.getLocation().absDeltaX(update.updateNode.location) <= 15 && player.getLocation().absDeltaX(update.updateNode.location) <= 15 && player.getLocation().getZ() == update.updateNode.location.getZ()) { PlayerUpdateNode updateNode = update.otherUpdates.get(id); movement.put_bits(11, updateNode.index); movement.put_bits(1, 1); // force appearance until having the logic in place to check for it movement.put_bits(1, 0); int deltaX = updateNode.location.deltaX(player.getLocation()); int deltaY = updateNode.location.deltaY(player.getLocation()); movement.put_bits(5, deltaY); movement.put_bits(5, deltaX); encodeGeneralUpdate(updateNode, general, true); update.localList.add(id); update.cachedList.remove(id); } } } }
Question: Actually I remain unsure about the second bit. Because all it does is pretty much is shifting every element in the walking queue one index higher. What could this be useful for?
interesting find, nice one
I bet on a horse says Harlan has no idea what you said
Added a concept implementation:
Code:package maximemeire.phantom.model.entity.player.update; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import maximemeire.phantom.model.entity.MovementNode; import maximemeire.phantom.model.entity.player.Player; import maximemeire.phantom.model.entity.player.update.PlayerUpdateFlags.PlayerUpdateFlag; import maximemeire.phantom.network.DynamicOutboundBuffer; /** * @author Maxime Meire * */ public class PlayerUpdate { protected PlayerUpdateNode updateNode; protected Set<Integer> localList = new LinkedHashSet<Integer>(); protected Set<Integer> additionList = new LinkedHashSet<Integer>(); protected Set<Integer> removalList = new LinkedHashSet<Integer>(); protected Set<Integer> cachedList = new LinkedHashSet<Integer>(); protected Map<Integer, PlayerUpdateNode> otherUpdates = new HashMap<Integer, PlayerUpdateNode>(); private int movementSize = 512; private int generalSize = 256; protected static DynamicOutboundBuffer encodePayload(Player player) { try { PlayerUpdate update = player.getUpdate(); int movementSize = update.movementSize + (1 / 4) * update.movementSize; DynamicOutboundBuffer payload = new DynamicOutboundBuffer(movementSize); int generalSize = update.generalSize + (1 / 2) * update.generalSize; DynamicOutboundBuffer masks = new DynamicOutboundBuffer(generalSize); payload.start_bit_access(); encodeThisMovement(player, payload); encodeGeneralUpdate(update.updateNode, masks, false); payload.put_bits(8, update.localList.size()); encodeOtherMovementUpdates(player, payload, masks); encodeAddFOVPlayer(player, payload, masks); if (masks.readable()) { payload.put_bits(11, 2047); payload.end_bit_access(); payload.writeBytes(masks); } else { payload.end_bit_access(); } return payload; } catch (Exception e) { e.printStackTrace(); return null; } } protected static void encodeThisMovement(Player player, DynamicOutboundBuffer movement) { PlayerUpdate update = player.getUpdate(); if (update.updateNode.teleported || update.updateNode.changedRegion) { movement.put_bits(1, 1); movement.put_bits(2, 3); movement.put_bits(2, player.getLocation().getZ()); movement.put_bits(1, update.updateNode.teleported ? 1 : 0); movement.put_bits(1, update.updateNode.requireUpdate() ? 1 : 0); movement.put_bits(7, player.getClientLoadedBase().deltaY(player.getLocation())); movement.put_bits(7, player.getClientLoadedBase().deltaX(player.getLocation())); } else { if (update.updateNode.movement.notMoving()) { if (update.updateNode.requireUpdate()) { movement.put_bits(1, 1); movement.put_bits(2, 0); } else { movement.put_bits(1, 0); } } else if (update.updateNode.movement.walking()) { movement.put_bits(1, 1); movement.put_bits(2, 1); movement.put_bits(3, update.updateNode.movement.getFirst()); movement.put_bits(1, update.updateNode.requireUpdate() ? 1 : 0); } else { movement.put_bits(1, 1); movement.put_bits(2, 2); movement.put_bits(3, update.updateNode.movement.getFirst()); movement.put_bits(3, update.updateNode.movement.getSecond()); movement.put_bits(1, update.updateNode.requireUpdate() ? 1 : 0); } } } protected static void encodeOtherMovementUpdates(Player player, DynamicOutboundBuffer movement, DynamicOutboundBuffer general) throws Exception { PlayerUpdate update = player.getUpdate(); for (int id : update.localList) { if (update.additionList.contains(id)) throw new Exception("Player is on the addition list but already on the local list"); if (update.removalList.contains(id)) { movement.put_bits(1, 1); movement.put_bits(2, 3); update.localList.remove(id); update.otherUpdates.remove(id); update.cachedList.remove(id); continue; } else if (player.getLocation().absDeltaX(update.updateNode.location) > 15 && player.getLocation().absDeltaX(update.updateNode.location) > 15 && player.getLocation().getZ() == update.updateNode.location.getZ()) { movement.put_bits(1, 0); update.cachedList.add(id); continue; } else { PlayerUpdateNode updateNode = update.otherUpdates.get(id); MovementNode movementNode = updateNode.movement; if (movementNode.notMoving()) { if (updateNode.requireUpdate()) { movement.put_bits(1, 1); movement.put_bits(2, 0); } else movement.put_bits(1, 0); } else if (movementNode.walking()) { movement.put_bits(1, 1); movement.put_bits(2, 1); movement.put_bits(3, movementNode.getFirst()); movement.put_bits(1, updateNode.requireUpdate() ? 1 : 0); } else { movement.put_bits(1, 1); movement.put_bits(2, 2); movement.put_bits(3, movementNode.getFirst()); movement.put_bits(3, movementNode.getSecond()); movement.put_bits(1, updateNode.requireUpdate() ? 1 : 0); } if (updateNode.requireUpdate()) { encodeGeneralUpdate(updateNode, general, false); } } } } protected static void encodeGeneralUpdate(PlayerUpdateNode updateNode, DynamicOutboundBuffer general, boolean forceAppearance) { if (!updateNode.requireUpdate() && !forceAppearance) { return; } if (updateNode.hasCachedMaskBlock() && !forceAppearance) { general.writeBytes(updateNode.cachedMaskBlock); return; } DynamicOutboundBuffer cacheBlock = new DynamicOutboundBuffer(100); int mask = updateNode.mask; if (mask >= 0x100) { mask |= 0x40; cacheBlock.put8(mask & 0xff); cacheBlock.put8(mask >> 8); } else cacheBlock.put8(mask); if (updateNode.requireGraphicsUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.GRAPHICS_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireAnimationUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.ANIMATION_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireForcedChatUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.FORCED_CHAT_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireChatUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.CHAT_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireFaceEntityUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.FACE_ENTITY_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireAppearanceUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.APPEARANCE_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireFaceCoordinateUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.FACE_COORDINATE_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireHitUpdate()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.HIT_UPDATE_MASK.ordinal()].payload); } if (updateNode.requireHit2Update()) { cacheBlock.writeBytes(updateNode.masks[PlayerUpdateFlag.HIT2_UPDATE_MASK.ordinal()].payload); } if (!forceAppearance) { updateNode.cachedMaskBlock = cacheBlock; } general.writeBytes(cacheBlock); } protected static void encodeAddFOVPlayer(Player player, DynamicOutboundBuffer movement, DynamicOutboundBuffer general) throws Exception { PlayerUpdate update = player.getUpdate(); for (int id : update.additionList) { if (update.localList.contains(id)) throw new Exception("Player is already on the local list although scheduled to be added to it"); if (update.localList.size() >= 255) return; if (update.cachedList.contains(id)) continue; if (player.getLocation().absDeltaX(update.updateNode.location) <= 15 && player.getLocation().absDeltaX(update.updateNode.location) <= 15 && player.getLocation().getZ() == update.updateNode.location.getZ()) { PlayerUpdateNode updateNode = update.otherUpdates.get(id); movement.put_bits(11, updateNode.index); movement.put_bits(1, 1); movement.put_bits(1, 1); int deltaX = updateNode.location.deltaX(player.getLocation()); int deltaY = updateNode.location.deltaY(player.getLocation()); movement.put_bits(5, deltaY); movement.put_bits(5, deltaX); encodeGeneralUpdate(updateNode, general, true); update.localList.add(id); update.additionList.remove(id); } else { continue; } } for (int id : update.cachedList) { if (update.localList.contains(id)) throw new Exception("Player is already on the local list although also cached."); if (update.localList.size() >= 255) return; if (player.getLocation().absDeltaX(update.updateNode.location) <= 15 && player.getLocation().absDeltaX(update.updateNode.location) <= 15 && player.getLocation().getZ() == update.updateNode.location.getZ()) { PlayerUpdateNode updateNode = update.otherUpdates.get(id); movement.put_bits(11, updateNode.index); movement.put_bits(1, 1); // force appearance until having the logic in place to check for it movement.put_bits(1, 0); int deltaX = updateNode.location.deltaX(player.getLocation()); int deltaY = updateNode.location.deltaY(player.getLocation()); movement.put_bits(5, deltaY); movement.put_bits(5, deltaX); encodeGeneralUpdate(updateNode, general, true); update.localList.add(id); update.cachedList.remove(id); } } } }
To actually take advantage of this, wouldn't you need to keep informing the client about the players' movements (and probably the 'general update' stuff) when they're still nearby but not in the player's 32x32 FOV? If you just tell the client to remove them like normal, it will forget about their information.
I didn't look at the removal code, I should have done that. I looked at it and you're right, the client discards the player when sending the removal bits. I looked into it a bit more and came to the following conclusion:
As long as the player is within the 5 x 5 segments and has been added to the viewport and has left the viewport, the player should not be removed and should be kept sending movement updates for. However, it should not be updating its position, so all that should be sending for updating the player movement is 1 bit being set to 0. If it enters the viewport again, the masks updating flag (first bit referenced above) is set to 0 or 1 according whether there has been an appearance change and or other masks are flagged. The second flag (second bit referenced above) should be set to 0 if it is a cached player I believe.
I'll change my thread to display this information correctly.
I was actually recently discussing this with someone when I came across this thread.
To clarify for people who may have a hard time understanding... If a player is within a 32 tile radius of your current position, you apply a full update as normal, but if the player is within a 40 yard radius, to save all the useless adding/removing of the players, you want to simply use this update type to update only the player's position (so that the client knows where they are and stays in sync)
« Previous Thread | Next Thread » |
Thread Information |
Users Browsing this ThreadThere are currently 1 users browsing this thread. (0 members and 1 guests) |