Fix issue #64.

This commit is contained in:
Major-
2015-08-28 16:32:20 +01:00
parent 0a3574eb20
commit 6f5a910d70
10 changed files with 174 additions and 260 deletions
+24 -26
View File
@@ -76,16 +76,16 @@ public final class Server {
*/ */
private final ServerBootstrap jaggrabBootstrap = new ServerBootstrap(); private final ServerBootstrap jaggrabBootstrap = new ServerBootstrap();
/**
* The {@link ServerBootstrap} for the service listener.
*/
private final ServerBootstrap serviceBootstrap = new ServerBootstrap();
/** /**
* The event loop group. * The event loop group.
*/ */
private final EventLoopGroup loopGroup = new NioEventLoopGroup(); private final EventLoopGroup loopGroup = new NioEventLoopGroup();
/**
* The {@link ServerBootstrap} for the service listener.
*/
private final ServerBootstrap serviceBootstrap = new ServerBootstrap();
/** /**
* Creates the Apollo server. * Creates the Apollo server.
*/ */
@@ -99,18 +99,17 @@ public final class Server {
* @param service The service address to bind to. * @param service The service address to bind to.
* @param http The HTTP address to bind to. * @param http The HTTP address to bind to.
* @param jaggrab The JAGGRAB address to bind to. * @param jaggrab The JAGGRAB address to bind to.
* @throws BindException If the ServerBootstrap fails to bind to the SocketAddress for any * @throws BindException If the ServerBootstrap fails to bind to the SocketAddress.
* reason.
*/ */
public void bind(SocketAddress service, SocketAddress http, SocketAddress jaggrab) throws BindException { public void bind(SocketAddress service, SocketAddress http, SocketAddress jaggrab) throws BindException {
logger.fine("Binding service listener to address: " + service + "..."); logger.fine("Binding service listener to address: " + service + "...");
bind(serviceBootstrap, service); bind(serviceBootstrap, service);
try { try {
logger.fine("Binding HTTP listener to address: " + http + "..."); logger.fine("Binding HTTP listener to address: " + http + "...");
bind(httpBootstrap, http); bind(httpBootstrap, http);
} catch (Exception cause) { } catch (Exception cause) {
logger.warning("Unable to bind to HTTP, JAGGRAB will be used as a fallback however this is not recommended."); logger.warning("Unable to bind to HTTP, JAGGRAB will be used as a fallback however this is not recommended.");
} }
logger.fine("Binding JAGGRAB listener to address: " + jaggrab + "..."); logger.fine("Binding JAGGRAB listener to address: " + jaggrab + "...");
@@ -119,22 +118,6 @@ public final class Server {
logger.info("Ready for connections."); logger.info("Ready for connections.");
} }
/**
* Attempts to bind the specified ServerBootstrap to the specified SocketAddress.
*
* @param bootstrap The ServerBootstrap.
* @param address The SocketAddress.
* @throws BindException If the ServerBootstrap fails to bind to the SocketAddress for any
* reason.
*/
private void bind(ServerBootstrap bootstrap, SocketAddress address) throws BindException {
try {
bootstrap.bind(address).sync();
} catch (Exception cause) {
throw new BindException("Failed to bind to: " + address);
}
}
/** /**
* Initialises the server. * Initialises the server.
* *
@@ -176,4 +159,19 @@ public final class Server {
world.init(version, fs, manager); world.init(version, fs, manager);
} }
/**
* Attempts to bind the specified ServerBootstrap to the specified SocketAddress.
*
* @param bootstrap The ServerBootstrap.
* @param address The SocketAddress.
* @throws BindException If the ServerBootstrap fails to bind to the SocketAddress.
*/
private void bind(ServerBootstrap bootstrap, SocketAddress address) throws BindException {
try {
bootstrap.bind(address).sync();
} catch (Exception cause) {
throw new BindException("Failed to bind to: " + address);
}
}
} }
@@ -1,6 +1,7 @@
package org.apollo.game.message.impl; package org.apollo.game.message.impl;
import java.util.List; import java.util.List;
import java.util.Set;
import org.apollo.game.model.Position; import org.apollo.game.model.Position;
import org.apollo.game.model.area.RegionCoordinates; import org.apollo.game.model.area.RegionCoordinates;
@@ -18,24 +19,25 @@ public final class GroupedRegionUpdateMessage extends Message {
*/ */
private final Position lastKnownRegion; private final Position lastKnownRegion;
/**
* The Set of RegionUpdateMessages to be sent.
*/
private final Set<RegionUpdateMessage> messages;
/** /**
* The Position of the Region being updated. * The Position of the Region being updated.
*/ */
private final Position region; private final Position region;
/**
* The List of RegionUpdateMessages to be sent.
*/
private final List<RegionUpdateMessage> messages;
/** /**
* Creates the GroupedRegionUpdateMessage. * Creates the GroupedRegionUpdateMessage.
* *
* @param lastKnownRegion The last known region {@link Position} of the Player. * @param lastKnownRegion The last known region {@link Position} of the Player.
* @param coordinates The {@link RegionCoordinates} of the Region being updated. * @param coordinates The {@link RegionCoordinates} of the Region being updated.
* @param messages The {@link List} of {@link RegionUpdateMessage}s. * @param messages The {@link Set} of {@link RegionUpdateMessage}s.
*/ */
public GroupedRegionUpdateMessage(Position lastKnownRegion, RegionCoordinates coordinates, List<RegionUpdateMessage> messages) { public GroupedRegionUpdateMessage(Position lastKnownRegion, RegionCoordinates coordinates,
Set<RegionUpdateMessage> messages) {
this.lastKnownRegion = lastKnownRegion; this.lastKnownRegion = lastKnownRegion;
region = new Position(coordinates.getAbsoluteX(), coordinates.getAbsoluteY()); region = new Position(coordinates.getAbsoluteX(), coordinates.getAbsoluteY());
this.messages = messages; this.messages = messages;
@@ -51,11 +53,11 @@ public final class GroupedRegionUpdateMessage extends Message {
} }
/** /**
* Gets the {@link List} of {@link RegionUpdateMessage}s. * Gets the {@link Set} of {@link RegionUpdateMessage}s.
* *
* @return The Collection. * @return The Set.
*/ */
public List<RegionUpdateMessage> getMessages() { public Set<RegionUpdateMessage> getMessages() {
return messages; return messages;
} }
@@ -2,7 +2,6 @@ package org.apollo.game.model.area;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -21,7 +20,6 @@ import org.apollo.game.model.entity.EntityType;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
/** /**
@@ -79,14 +77,17 @@ public final class Region {
private final CollisionMatrix[] matrices = CollisionMatrix.createMatrices(Position.HEIGHT_LEVELS, SIZE, SIZE); private final CollisionMatrix[] matrices = CollisionMatrix.createMatrices(Position.HEIGHT_LEVELS, SIZE, SIZE);
/** /**
* The Set containing RegionUpdateMessages which can be sent to add every non-Mob Entity in this Region. * The List of Sets containing RegionUpdateMessages that specifically remove StaticGameObjects. The
* List is ordered based on the height level the RegionUpdateMessages concern.
*/ */
private final List<Map<Entity, RegionUpdateMessage>> snapshots = new ArrayList<>(Position.HEIGHT_LEVELS); private final List<Set<RegionUpdateMessage>> removedObjects = new ArrayList<>(Position.HEIGHT_LEVELS);
/** /**
* The Set containing UpdateOperations. * The List of Sets containing RegionUpdateMessages. The List is ordered based on the height level the
* RegionUpdateMessages concern. This only contains the updates to this Region that have occurred in the last
* pulse.
*/ */
private final List<List<RegionUpdateMessage>> updates = new ArrayList<>(Position.HEIGHT_LEVELS); private final List<Set<RegionUpdateMessage>> updates = new ArrayList<>(Position.HEIGHT_LEVELS);
/** /**
* Creates a new Region. * Creates a new Region.
@@ -108,8 +109,8 @@ public final class Region {
listeners.add(new UpdateRegionListener()); listeners.add(new UpdateRegionListener());
for (int height = 0; height < Position.HEIGHT_LEVELS; height++) { for (int height = 0; height < Position.HEIGHT_LEVELS; height++) {
snapshots.add(new HashMap<>()); removedObjects.add(new HashSet<>());
updates.add(new ArrayList<>(DEFAULT_LIST_SIZE)); updates.add(new HashSet<>(DEFAULT_LIST_SIZE));
} }
} }
@@ -154,7 +155,6 @@ public final class Region {
* @param entity The Entity. * @param entity The Entity.
* @return {@code true} if this Region contains the Entity, otherwise {@code false}. * @return {@code true} if this Region contains the Entity, otherwise {@code false}.
*/ */
public boolean contains(Entity entity) { public boolean contains(Entity entity) {
Position position = entity.getPosition(); Position position = entity.getPosition();
Set<Entity> local = entities.get(position); Set<Entity> local = entities.get(position);
@@ -162,6 +162,22 @@ public final class Region {
return local != null && local.contains(entity); return local != null && local.contains(entity);
} }
/**
* Encodes the contents of this Region into a {@link Set} of {@link RegionUpdateMessage}s, to be sent to a client.
*
* @return The Set of RegionUpdateMessages.
*/
public Set<RegionUpdateMessage> encode(int height) {
Set<RegionUpdateMessage> additions = entities.values().stream()
.flatMap(Set::stream).filter(entity -> entity instanceof GroupableEntity)
.map(entity -> ((GroupableEntity) entity).toUpdateOperation(this, EntityUpdateType.ADD).toMessage())
.collect(Collectors.toSet());
ImmutableSet.Builder<RegionUpdateMessage> builder = ImmutableSet.builder();
builder.addAll(additions).addAll(updates.get(height)).addAll(removedObjects.get(height));
return builder.build();
}
/** /**
* Gets this Region's {@link RegionCoordinates}. * Gets this Region's {@link RegionCoordinates}.
* *
@@ -218,31 +234,14 @@ public final class Region {
} }
/** /**
* Gets a {@link Set} containing {@link RegionUpdateMessage}s that add every {@link Entity} in this Region. * Gets the {@link Set} of {@link RegionUpdateMessage}s that have occurred in the last pulse. This method can
* only be called <strong>once</strong> per pulse.
* *
* @param height The height level to get the Set of RegionUpdateMessages for. * @param height The height level to get the RegionUpdateMessages for.
* @return The Set of RegionUpdateMessages. * @return The Set of RegionUpdateMessages.
*/ */
public List<RegionUpdateMessage> getSnapshot(int height) { public Set<RegionUpdateMessage> getUpdates(int height) {
List<RegionUpdateMessage> copy = new ArrayList<>(snapshots.get(height).values()); return ImmutableSet.copyOf(updates.get(height));
Collections.sort(copy);
return ImmutableList.copyOf(copy);
}
/**
* Gets the updates that have occurred in the last tick in this Region, as a {@link Set} of
* {@link RegionUpdateMessage}s.
*
* @param height The height level to get the Set of RegionUpdateMessages for.
* @return The Set of RegionUpdateMessages.
*/
public List<RegionUpdateMessage> getUpdates(int height) {
List<RegionUpdateMessage> original = this.updates.get(height);
List<RegionUpdateMessage> updates = new ArrayList<>(original);
original.clear();
Collections.sort(updates);
return ImmutableList.copyOf(updates);
} }
/** /**
@@ -261,15 +260,14 @@ public final class Region {
* @param entity The Entity. * @param entity The Entity.
* @throws IllegalArgumentException If the Entity does not belong in this Region, or if it was never added. * @throws IllegalArgumentException If the Entity does not belong in this Region, or if it was never added.
*/ */
public void removeEntity(Entity entity) { public void removeEntity(Entity entity) { // TODO entity update stuff
Position position = entity.getPosition(); Position position = entity.getPosition();
checkPosition(position); checkPosition(position);
Set<Entity> local = entities.get(position); Set<Entity> local = entities.get(position);
if (local == null || !local.remove(entity)) { if (local == null || !local.remove(entity)) {
throw new IllegalArgumentException("Entity (" + entity + ") belongs in this Region (" + this throw new IllegalArgumentException("Entity (" + entity + ") belongs in (" + this + ") but does not exist.");
+ ") but does not exist.");
} }
notifyListeners(entity, EntityUpdateType.REMOVE); notifyListeners(entity, EntityUpdateType.REMOVE);
@@ -315,16 +313,19 @@ public final class Region {
* @throws UnsupportedOperationException If the specified Entity cannot be operated on in this manner. * @throws UnsupportedOperationException If the specified Entity cannot be operated on in this manner.
*/ */
private <T extends Entity & GroupableEntity> void record(T entity, EntityUpdateType type) { private <T extends Entity & GroupableEntity> void record(T entity, EntityUpdateType type) {
RegionUpdateMessage message = entity.toUpdateOperation(this, type).toMessage(); UpdateOperation<?> operation = entity.toUpdateOperation(this, type);
RegionUpdateMessage message = operation.toMessage(), inverse = operation.inverse();
int height = entity.getPosition().getHeight(); int height = entity.getPosition().getHeight();
Set<RegionUpdateMessage> updates = this.updates.get(height);
Map<Entity, RegionUpdateMessage> snapshot = snapshots.get(height); if (entity.getEntityType() == EntityType.STATIC_OBJECT && type == EntityUpdateType.REMOVE) {
updates.get(height).add(message); removedObjects.get(height).add(message);
updates.add(message);
snapshot.remove(entity); updates.remove(inverse);
if ((entity.getEntityType() == EntityType.STATIC_OBJECT && type == EntityUpdateType.REMOVE) } else {
|| (entity.getEntityType() != EntityType.STATIC_OBJECT && type == EntityUpdateType.ADD)) { updates.add(message);
snapshot.put(entity, message); updates.remove(inverse);
} }
} }
@@ -14,8 +14,8 @@ import com.google.common.base.Preconditions;
* An type that is contained in the snapshot of a {@link Region}, which consists of an {@link Entity} being added, * An type that is contained in the snapshot of a {@link Region}, which consists of an {@link Entity} being added,
* removed, or moved. * removed, or moved.
* *
* @author Major
* @param <E> The type of {@link Entity} in this type. * @param <E> The type of {@link Entity} in this type.
* @author Major
*/ */
public abstract class UpdateOperation<E extends Entity> { public abstract class UpdateOperation<E extends Entity> {
@@ -47,6 +47,24 @@ public abstract class UpdateOperation<E extends Entity> {
this.entity = entity; this.entity = entity;
} }
/**
* Gets a {@link RegionUpdateMessage} that would counteract the effect of this UpdateOperation.
*
* @return The RegionUpdateMessage.
*/
public final RegionUpdateMessage inverse() {
int offset = getPositionOffset(entity.getPosition());
switch (type) {
case ADD:
return remove(offset);
case REMOVE:
return add(offset);
default:
throw new IllegalStateException("Unsupported EntityUpdateType " + type + ".");
}
}
/** /**
* Returns this UpdateOperation as a {@link Message}. * Returns this UpdateOperation as a {@link Message}.
* *
@@ -26,8 +26,7 @@ public final class GroupedRegionUpdateMessageEncoder extends MessageEncoder<Grou
/** /**
* Creates the GroupedRegionUpdateMessageEncoder. * Creates the GroupedRegionUpdateMessageEncoder.
* *
* @param release The {@link Release} containing the {@link MessageEncoder}s for the {@link RegionUpdateMessage} * @param release The {@link Release} containing the {@link MessageEncoder}s for the {@link RegionUpdateMessage}s.
* s.
*/ */
public GroupedRegionUpdateMessageEncoder(Release release) { public GroupedRegionUpdateMessageEncoder(Release release) {
this.release = release; this.release = release;
@@ -81,10 +81,8 @@ public final class GameService extends Service {
* *
* @param player The player. * @param player The player.
*/ */
public void finalizePlayerUnregistration(Player player) { public synchronized void finalizePlayerUnregistration(Player player) {
synchronized (this) { world.unregister(player);
world.unregister(player);
}
} }
/** /**
@@ -73,7 +73,7 @@ public final class LoginService extends Service {
public void submitLoadRequest(LoginSession session, LoginRequest request) throws IOException { public void submitLoadRequest(LoginSession session, LoginRequest request) throws IOException {
int response = LoginConstants.STATUS_OK; int response = LoginConstants.STATUS_OK;
if (requiresUpdate(session, request)) { if (requiresUpdate(request)) {
response = LoginConstants.STATUS_GAME_UPDATED; response = LoginConstants.STATUS_GAME_UPDATED;
} }
@@ -125,12 +125,11 @@ public final class LoginService extends Service {
/** /**
* Checks if an update is required whenever a {@link Player} submits a login request. * Checks if an update is required whenever a {@link Player} submits a login request.
* *
* @param session The login session.
* @param request The login request. * @param request The login request.
* @return {@code true} if an update is required, otherwise return {@code false}. * @return {@code true} if an update is required, otherwise return {@code false}.
* @throws IOException If some I/O exception occurs. * @throws IOException If some I/O exception occurs.
*/ */
private boolean requiresUpdate(LoginSession session, LoginRequest request) throws IOException { private boolean requiresUpdate(LoginRequest request) throws IOException {
Release release = context.getRelease(); Release release = context.getRelease();
if (release.getReleaseNumber() != request.getReleaseNumber()) { if (release.getReleaseNumber() != request.getReleaseNumber()) {
return true; return true;
@@ -139,11 +138,7 @@ public final class LoginService extends Service {
int[] clientCrcs = request.getArchiveCrcs(); int[] clientCrcs = request.getArchiveCrcs();
int[] serverCrcs = context.getFileSystem().getCrcs(); int[] serverCrcs = context.getFileSystem().getCrcs();
if (Arrays.equals(clientCrcs, serverCrcs)) { return !Arrays.equals(clientCrcs, serverCrcs);
return false;
}
return true;
} }
} }
@@ -1,12 +1,5 @@
package org.apollo.game.sync; package org.apollo.game.sync;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Phaser;
import org.apollo.game.message.impl.RegionUpdateMessage; import org.apollo.game.message.impl.RegionUpdateMessage;
import org.apollo.game.model.area.RegionCoordinates; import org.apollo.game.model.area.RegionCoordinates;
import org.apollo.game.model.entity.MobRepository; import org.apollo.game.model.entity.MobRepository;
@@ -23,11 +16,19 @@ import org.apollo.game.sync.task.PrePlayerSynchronizationTask;
import org.apollo.game.sync.task.SynchronizationTask; import org.apollo.game.sync.task.SynchronizationTask;
import org.apollo.util.ThreadUtil; import org.apollo.util.ThreadUtil;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Phaser;
/** /**
* An implementation of {@link ClientSynchronizer} which runs in a thread pool. A {@link Phaser} is used to ensure that * An implementation of {@link ClientSynchronizer} which runs in a thread pool. A {@link Phaser} is used to ensure that
* the synchronization is complete, allowing control to return to the {@link GameService} that started the * the synchronization is complete, allowing control to return to the {@link GameService} that started the
* synchronization. This class will scale well with machines that have multiple cores/processors. The * synchronization. This class will scale well with machines that have multiple cores/processors. The
* {@link SequentialClientSynchronizer} will work better on machines with a single core/processor, however, both classes * {@link SequentialClientSynchronizer} will work better on machines with a single core/processor, however, both
* classes
* will work. * will work.
* *
* @author Graham * @author Graham
@@ -58,12 +59,12 @@ public final class ParallelClientSynchronizer extends ClientSynchronizer {
int playerCount = players.size(); int playerCount = players.size();
int npcCount = npcs.size(); int npcCount = npcs.size();
Map<RegionCoordinates, List<RegionUpdateMessage>> updates = new HashMap<>(); Map<RegionCoordinates, Set<RegionUpdateMessage>> encodes = new ConcurrentHashMap<>();
Map<RegionCoordinates, List<RegionUpdateMessage>> snapshots = new HashMap<>(); Map<RegionCoordinates, Set<RegionUpdateMessage>> updates = new ConcurrentHashMap<>();
phaser.bulkRegister(playerCount); phaser.bulkRegister(playerCount);
for (Player player : players) { for (Player player : players) {
SynchronizationTask task = new PrePlayerSynchronizationTask(player, updates, snapshots); SynchronizationTask task = new PrePlayerSynchronizationTask(player, encodes, updates);
executor.submit(new PhasedSynchronizationTask(phaser, task)); executor.submit(new PhasedSynchronizationTask(phaser, task));
} }
phaser.arriveAndAwaitAdvance(); phaser.arriveAndAwaitAdvance();
@@ -3,6 +3,7 @@ package org.apollo.game.sync;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.apollo.game.message.impl.RegionUpdateMessage; import org.apollo.game.message.impl.RegionUpdateMessage;
import org.apollo.game.model.area.RegionCoordinates; import org.apollo.game.model.area.RegionCoordinates;
@@ -31,11 +32,10 @@ public final class SequentialClientSynchronizer extends ClientSynchronizer {
@Override @Override
public void synchronize(MobRepository<Player> players, MobRepository<Npc> npcs) { public void synchronize(MobRepository<Player> players, MobRepository<Npc> npcs) {
Map<RegionCoordinates, List<RegionUpdateMessage>> updates = new HashMap<>(); Map<RegionCoordinates, Set<RegionUpdateMessage>> encodes = new HashMap<>(), updates = new HashMap<>();
Map<RegionCoordinates, List<RegionUpdateMessage>> snapshots = new HashMap<>();
for (Player player : players) { for (Player player : players) {
SynchronizationTask task = new PrePlayerSynchronizationTask(player, updates, snapshots); SynchronizationTask task = new PrePlayerSynchronizationTask(player, encodes, updates);
task.run(); task.run();
} }
@@ -1,10 +1,7 @@
package org.apollo.game.sync.task; package org.apollo.game.sync.task;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import org.apollo.game.message.impl.ClearRegionMessage; import org.apollo.game.message.impl.ClearRegionMessage;
@@ -17,8 +14,6 @@ import org.apollo.game.model.area.RegionCoordinates;
import org.apollo.game.model.area.RegionRepository; import org.apollo.game.model.area.RegionRepository;
import org.apollo.game.model.entity.Player; import org.apollo.game.model.entity.Player;
import com.google.common.collect.ImmutableSet;
/** /**
* A {@link SynchronizationTask} which does pre-synchronization work for the specified {@link Player}. * A {@link SynchronizationTask} which does pre-synchronization work for the specified {@link Player}.
* *
@@ -28,62 +23,44 @@ import com.google.common.collect.ImmutableSet;
public final class PrePlayerSynchronizationTask extends SynchronizationTask { public final class PrePlayerSynchronizationTask extends SynchronizationTask {
/** /**
* The update mode used when sending a {@link GroupedRegionUpdateMessage}. * The radius of viewable regions.
*/ */
private enum RegionUpdateMode { private static final int VIEWABLE_REGION_RADIUS = 3;
/**
* The difference update mode, which only sends updates for changes that have occurred in the last pulse.
*/
DIFFERENCE,
/**
* The full update mode, which sends everything in the Region.
*/
FULL;
}
/**
* The amount of Regions that are in view of a player, in one direction.
*/
private static final int REGION_COUNT = (int) Math.ceil(Position.MAX_DISTANCE / Region.SIZE);
/** /**
* The width of the viewport of every Player, in tiles. * The width of the viewport of every Player, in tiles.
*/ */
private static final int VIEWPORT_WIDTH = Region.SIZE * 13; private static final int VIEWPORT_WIDTH = Region.SIZE * 13;
/**
* The Map of RegionCoordinates to Sets of RegionUpdateMessages, which contain all of the Entity information for a
* Region.
*/
private final Map<RegionCoordinates, Set<RegionUpdateMessage>> encodes;
/** /**
* The player. * The player.
*/ */
private final Player player; private final Player player;
/**
* The Map of RegionCoordinates to Sets of RegionUpdateMessages, which contain all of the Entity information for a
* Region.
*/
private final Map<RegionCoordinates, List<RegionUpdateMessage>> snapshots;
/** /**
* The Map of RegionCoordinates to Sets of RegionUpdateMessages, which contain the updates for a Region a Player * The Map of RegionCoordinates to Sets of RegionUpdateMessages, which contain the updates for a Region a Player
* can * can already view.
* already view.
*/ */
private final Map<RegionCoordinates, List<RegionUpdateMessage>> updates; private final Map<RegionCoordinates, Set<RegionUpdateMessage>> updates;
/** /**
* Creates the {@link PrePlayerSynchronizationTask} for the specified {@link Player}. * Creates the {@link PrePlayerSynchronizationTask} for the specified {@link Player}.
* *
* @param player The Player. * @param player The Player.
* @param encodes The Map containing Region encodes.
* @param updates The {@link Map} containing {@link Region} updates. * @param updates The {@link Map} containing {@link Region} updates.
* @param snapshots The Map containing Region snapshots.
*/ */
public PrePlayerSynchronizationTask(Player player, Map<RegionCoordinates, List<RegionUpdateMessage>> updates, public PrePlayerSynchronizationTask(Player player, Map<RegionCoordinates, Set<RegionUpdateMessage>> encodes,
Map<RegionCoordinates, List<RegionUpdateMessage>> snapshots) { Map<RegionCoordinates, Set<RegionUpdateMessage>> updates) {
this.player = player; this.player = player;
this.updates = updates; this.updates = updates;
this.snapshots = snapshots; this.encodes = encodes;
} }
@Override @Override
@@ -91,71 +68,49 @@ public final class PrePlayerSynchronizationTask extends SynchronizationTask {
Position old = player.getPosition(); Position old = player.getPosition();
player.getWalkingQueue().pulse(); player.getWalkingQueue().pulse();
RegionUpdateMode mode = RegionUpdateMode.DIFFERENCE;
if (player.isTeleporting()) { if (player.isTeleporting()) {
player.resetViewingDistance(); player.resetViewingDistance();
mode = RegionUpdateMode.FULL;
} }
boolean hasKnownRegion = player.hasLastKnownRegion(); Position position = player.getPosition();
if (!hasKnownRegion) { if (!player.hasLastKnownRegion() || isRegionUpdateRequired()) {
mode = RegionUpdateMode.FULL;
}
if (!hasKnownRegion || isRegionUpdateRequired()) {
player.setRegionChanged(true); player.setRegionChanged(true);
Position position = player.getPosition();
player.setLastKnownRegion(position); player.setLastKnownRegion(position);
player.send(new RegionChangeMessage(position)); player.send(new RegionChangeMessage(position));
} }
Set<RegionCoordinates> newRegions = getNewRegions(old, player.getPosition()); Set<RegionCoordinates> oldViewable = getViewableRegions(old), newViewable = getViewableRegions(position);
sendRegionUpdates(mode, newRegions);
Set<RegionCoordinates> differences = new HashSet<>(newViewable);
differences.retainAll(oldViewable);
Set<RegionCoordinates> full = new HashSet<>(newViewable);
full.removeAll(oldViewable);
sendUpdates(player.getLastKnownRegion(), differences, full);
} }
/** /**
* Gets the {@link Set} of {@link RegionCoordinates} of {@link Region}s that the {@link Player} in this task has * Gets the {@link Set} of {@link RegionCoordinates} of Regions that are viewable from the specified {@link
* only just became able to view. * Position}.
* *
* @param old The old {@link Position} of the Player. * @param position The Position.
* @param next The new Position of the Player. * @return The Set of RegionCoordinates.
* @return The Set of RegionCoordinates. Will not be {@code null}, but may be empty.
*/ */
private Set<RegionCoordinates> getNewRegions(Position old, Position next) { private Set<RegionCoordinates> getViewableRegions(Position position) { // TODO possibly more complicated than this
RegionCoordinates oldRegion = old.getRegionCoordinates(); RegionCoordinates local = position.getRegionCoordinates();
RegionCoordinates nextRegion = next.getRegionCoordinates(); int localX = local.getX(), localY = local.getY();
int maxX = localX + VIEWABLE_REGION_RADIUS, maxY = localY + VIEWABLE_REGION_RADIUS;
if (oldRegion.equals(nextRegion)) { Set<RegionCoordinates> viewable = new HashSet<>();
return ImmutableSet.of(); for (int x = localX - VIEWABLE_REGION_RADIUS; x < maxX; x++) {
} for (int y = localY - VIEWABLE_REGION_RADIUS; y < maxY; y++) {
viewable.add(new RegionCoordinates(x, y));
Set<RegionCoordinates> coordinates = new HashSet<>(9);
int oldX = oldRegion.getX(), oldY = oldRegion.getY();
int dx = nextRegion.getX() - oldX;
int dy = nextRegion.getY() - oldY;
if (dx != 0) {
int x = oldX + dx;
int maxY = oldY + REGION_COUNT;
for (int y = oldY - REGION_COUNT; y <= maxY; y++) {
coordinates.add(new RegionCoordinates(x, y));
} }
} }
if (dy != 0) { return viewable;
int y = oldY + dy;
int maxX = oldX + REGION_COUNT;
for (int x = oldX - REGION_COUNT; x <= maxX; x++) {
coordinates.add(new RegionCoordinates(x, y));
}
}
return coordinates;
} }
/** /**
@@ -175,83 +130,30 @@ public final class PrePlayerSynchronizationTask extends SynchronizationTask {
} }
/** /**
* Gets the {@link List} of {@link GroupedRegionUpdateMessage}s. * Sends the updates for a {@link Region}
* *
* @param mode The {@link RegionUpdateMode} used when creating the Messages. * @param position The {@link Position} of the last known region.
* @param newRegions The {@link Set} of {@link RegionCoordinates} that should be sent as a full update. * @param differences The {@link Set} of {@link RegionCoordinates} of Regions that changed.
* @param full The {@link Set} of {@link RegionCoordinates} of Regions that require a full update.
*/ */
private void sendRegionUpdates(RegionUpdateMode mode, Set<RegionCoordinates> newRegions) { private void sendUpdates(Position position, Set<RegionCoordinates> differences, Set<RegionCoordinates> full) {
Position position = player.getPosition();
RegionCoordinates base = position.getRegionCoordinates();
int baseX = base.getX(), baseY = base.getY();
RegionRepository repository = player.getWorld().getRegionRepository(); RegionRepository repository = player.getWorld().getRegionRepository();
List<GroupedRegionUpdateMessage> messages = new ArrayList<>(); int height = position.getHeight();
for (int x = baseX - REGION_COUNT; x <= baseX + REGION_COUNT; x++) { differences.stream().map(coordinates -> {
for (int y = baseY - REGION_COUNT; y <= baseY + REGION_COUNT; y++) { Set<RegionUpdateMessage> messages = updates.computeIfAbsent(coordinates,
RegionCoordinates coordinates = new RegionCoordinates(x, y); coords -> repository.get(coords).getUpdates(height));
RegionUpdateMode local = mode; return new GroupedRegionUpdateMessage(position, coordinates, messages);
if (mode == RegionUpdateMode.DIFFERENCE && newRegions.contains(coordinates)) { }).forEach(player::send);
local = RegionUpdateMode.FULL;
player.send(new ClearRegionMessage(position, coordinates)); full.stream().map(coordinates -> {
} Set<RegionUpdateMessage> messages = encodes.computeIfAbsent(coordinates,
coords -> repository.get(coords).encode(height));
toUpdateMessage(local, player.getLastKnownRegion(), coordinates, repository).ifPresent(messages::add); player.send(new ClearRegionMessage(position, coordinates));
} return new GroupedRegionUpdateMessage(position, coordinates, messages);
} }).forEach(player::send);
messages.forEach(player::send);
}
/**
* Creates a {@link GroupedRegionUpdateMessage} using the specified {@link RegionUpdateMode}, returning
* {@link Optional#empty()} if no update message is required.
*
* @param mode The RegionUpdateMode for the Message.
* @param lastKnownRegion The last known region {@link Position} of the Player.
* @param coordinates The {@link RegionCoordinates} of the {@link Region}.
* @param repository The {@link RegionRepository} containing the Regions.
* @return The Optional containing the GroupedRegionUpdateMessage.
*/
private Optional<GroupedRegionUpdateMessage> toUpdateMessage(RegionUpdateMode mode, Position lastKnownRegion,
RegionCoordinates coordinates, RegionRepository repository) {
List<RegionUpdateMessage> messages;
/*
* Here we used Map#computeIfAbsent because the value may have been inserted into the Map by another thread
* after our Map#get call. This is done in two separate parts (rather than acquiring the lock every time, and
* just calling Map#computeIfAbsent immediately) for performance - once the List<RegionUpdateMessage> has been
* placed into the Map once, it will never be changed, and is therefore a read-only operation after this. The
* alternative, acquiring the lock for every access, would be very slow.
*/
switch (mode) {
case DIFFERENCE:
messages = updates.get(coordinates);
if (messages == null) {
synchronized (updates) {
messages = updates.computeIfAbsent(coordinates, coords -> repository.get(coords).getUpdates(lastKnownRegion.getHeight()));
}
}
break;
case FULL:
messages = snapshots.get(coordinates);
if (messages == null) {
synchronized (snapshots) {
messages = snapshots.computeIfAbsent(coordinates, coords -> repository.get(coords).getSnapshot(lastKnownRegion.getHeight()));
}
}
break;
default:
throw new IllegalArgumentException("Unrecognised RegionUpdateMode " + mode + ".");
}
return messages.isEmpty() ? Optional.empty() : Optional.of(new GroupedRegionUpdateMessage(lastKnownRegion, coordinates, messages));
} }
} }