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();
/**
* The {@link ServerBootstrap} for the service listener.
*/
private final ServerBootstrap serviceBootstrap = new ServerBootstrap();
/**
* The event loop group.
*/
private final EventLoopGroup loopGroup = new NioEventLoopGroup();
/**
* The {@link ServerBootstrap} for the service listener.
*/
private final ServerBootstrap serviceBootstrap = new ServerBootstrap();
/**
* Creates the Apollo server.
*/
@@ -99,18 +99,17 @@ public final class Server {
* @param service The service address to bind to.
* @param http The HTTP 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
* reason.
* @throws BindException If the ServerBootstrap fails to bind to the SocketAddress.
*/
public void bind(SocketAddress service, SocketAddress http, SocketAddress jaggrab) throws BindException {
logger.fine("Binding service listener to address: " + service + "...");
bind(serviceBootstrap, service);
try {
logger.fine("Binding HTTP listener to address: " + http + "...");
bind(httpBootstrap, http);
logger.fine("Binding HTTP listener to address: " + http + "...");
bind(httpBootstrap, http);
} 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 + "...");
@@ -119,22 +118,6 @@ public final class Server {
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.
*
@@ -176,4 +159,19 @@ public final class Server {
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;
import java.util.List;
import java.util.Set;
import org.apollo.game.model.Position;
import org.apollo.game.model.area.RegionCoordinates;
@@ -18,24 +19,25 @@ public final class GroupedRegionUpdateMessage extends Message {
*/
private final Position lastKnownRegion;
/**
* The Set of RegionUpdateMessages to be sent.
*/
private final Set<RegionUpdateMessage> messages;
/**
* The Position of the Region being updated.
*/
private final Position region;
/**
* The List of RegionUpdateMessages to be sent.
*/
private final List<RegionUpdateMessage> messages;
/**
* Creates the GroupedRegionUpdateMessage.
*
* @param lastKnownRegion The last known region {@link Position} of the Player.
* @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;
region = new Position(coordinates.getAbsoluteX(), coordinates.getAbsoluteY());
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;
}
@@ -2,7 +2,6 @@ package org.apollo.game.model.area;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
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.Preconditions;
import com.google.common.collect.ImmutableList;
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);
/**
* 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.
@@ -108,8 +109,8 @@ public final class Region {
listeners.add(new UpdateRegionListener());
for (int height = 0; height < Position.HEIGHT_LEVELS; height++) {
snapshots.add(new HashMap<>());
updates.add(new ArrayList<>(DEFAULT_LIST_SIZE));
removedObjects.add(new HashSet<>());
updates.add(new HashSet<>(DEFAULT_LIST_SIZE));
}
}
@@ -154,7 +155,6 @@ public final class Region {
* @param entity The Entity.
* @return {@code true} if this Region contains the Entity, otherwise {@code false}.
*/
public boolean contains(Entity entity) {
Position position = entity.getPosition();
Set<Entity> local = entities.get(position);
@@ -162,6 +162,22 @@ public final class Region {
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}.
*
@@ -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.
*/
public List<RegionUpdateMessage> getSnapshot(int height) {
List<RegionUpdateMessage> copy = new ArrayList<>(snapshots.get(height).values());
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);
public Set<RegionUpdateMessage> getUpdates(int height) {
return ImmutableSet.copyOf(updates.get(height));
}
/**
@@ -261,15 +260,14 @@ public final class Region {
* @param entity The Entity.
* @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();
checkPosition(position);
Set<Entity> local = entities.get(position);
if (local == null || !local.remove(entity)) {
throw new IllegalArgumentException("Entity (" + entity + ") belongs in this Region (" + this
+ ") but does not exist.");
throw new IllegalArgumentException("Entity (" + entity + ") belongs in (" + this + ") but does not exist.");
}
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.
*/
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();
Set<RegionUpdateMessage> updates = this.updates.get(height);
Map<Entity, RegionUpdateMessage> snapshot = snapshots.get(height);
updates.get(height).add(message);
snapshot.remove(entity);
if ((entity.getEntityType() == EntityType.STATIC_OBJECT && type == EntityUpdateType.REMOVE)
|| (entity.getEntityType() != EntityType.STATIC_OBJECT && type == EntityUpdateType.ADD)) {
snapshot.put(entity, message);
if (entity.getEntityType() == EntityType.STATIC_OBJECT && type == EntityUpdateType.REMOVE) {
removedObjects.get(height).add(message);
updates.add(message);
updates.remove(inverse);
} else {
updates.add(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,
* removed, or moved.
*
* @author Major
* @param <E> The type of {@link Entity} in this type.
* @author Major
*/
public abstract class UpdateOperation<E extends Entity> {
@@ -47,6 +47,24 @@ public abstract class UpdateOperation<E extends 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}.
*
@@ -26,8 +26,7 @@ public final class GroupedRegionUpdateMessageEncoder extends MessageEncoder<Grou
/**
* Creates the GroupedRegionUpdateMessageEncoder.
*
* @param release The {@link Release} containing the {@link MessageEncoder}s for the {@link RegionUpdateMessage}
* s.
* @param release The {@link Release} containing the {@link MessageEncoder}s for the {@link RegionUpdateMessage}s.
*/
public GroupedRegionUpdateMessageEncoder(Release release) {
this.release = release;
@@ -81,10 +81,8 @@ public final class GameService extends Service {
*
* @param player The player.
*/
public void finalizePlayerUnregistration(Player player) {
synchronized (this) {
world.unregister(player);
}
public synchronized void finalizePlayerUnregistration(Player player) {
world.unregister(player);
}
/**
@@ -73,7 +73,7 @@ public final class LoginService extends Service {
public void submitLoadRequest(LoginSession session, LoginRequest request) throws IOException {
int response = LoginConstants.STATUS_OK;
if (requiresUpdate(session, request)) {
if (requiresUpdate(request)) {
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.
*
* @param session The login session.
* @param request The login request.
* @return {@code true} if an update is required, otherwise return {@code false}.
* @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();
if (release.getReleaseNumber() != request.getReleaseNumber()) {
return true;
@@ -139,11 +138,7 @@ public final class LoginService extends Service {
int[] clientCrcs = request.getArchiveCrcs();
int[] serverCrcs = context.getFileSystem().getCrcs();
if (Arrays.equals(clientCrcs, serverCrcs)) {
return false;
}
return true;
return !Arrays.equals(clientCrcs, serverCrcs);
}
}
@@ -1,12 +1,5 @@
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.model.area.RegionCoordinates;
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.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
* 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
* {@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.
*
* @author Graham
@@ -58,12 +59,12 @@ public final class ParallelClientSynchronizer extends ClientSynchronizer {
int playerCount = players.size();
int npcCount = npcs.size();
Map<RegionCoordinates, List<RegionUpdateMessage>> updates = new HashMap<>();
Map<RegionCoordinates, List<RegionUpdateMessage>> snapshots = new HashMap<>();
Map<RegionCoordinates, Set<RegionUpdateMessage>> encodes = new ConcurrentHashMap<>();
Map<RegionCoordinates, Set<RegionUpdateMessage>> updates = new ConcurrentHashMap<>();
phaser.bulkRegister(playerCount);
for (Player player : players) {
SynchronizationTask task = new PrePlayerSynchronizationTask(player, updates, snapshots);
SynchronizationTask task = new PrePlayerSynchronizationTask(player, encodes, updates);
executor.submit(new PhasedSynchronizationTask(phaser, task));
}
phaser.arriveAndAwaitAdvance();
@@ -3,6 +3,7 @@ package org.apollo.game.sync;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apollo.game.message.impl.RegionUpdateMessage;
import org.apollo.game.model.area.RegionCoordinates;
@@ -31,11 +32,10 @@ public final class SequentialClientSynchronizer extends ClientSynchronizer {
@Override
public void synchronize(MobRepository<Player> players, MobRepository<Npc> npcs) {
Map<RegionCoordinates, List<RegionUpdateMessage>> updates = new HashMap<>();
Map<RegionCoordinates, List<RegionUpdateMessage>> snapshots = new HashMap<>();
Map<RegionCoordinates, Set<RegionUpdateMessage>> encodes = new HashMap<>(), updates = new HashMap<>();
for (Player player : players) {
SynchronizationTask task = new PrePlayerSynchronizationTask(player, updates, snapshots);
SynchronizationTask task = new PrePlayerSynchronizationTask(player, encodes, updates);
task.run();
}
@@ -1,10 +1,7 @@
package org.apollo.game.sync.task;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
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.entity.Player;
import com.google.common.collect.ImmutableSet;
/**
* 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 {
/**
* The update mode used when sending a {@link GroupedRegionUpdateMessage}.
* The radius of viewable regions.
*/
private enum RegionUpdateMode {
/**
* 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);
private static final int VIEWABLE_REGION_RADIUS = 3;
/**
* The width of the viewport of every Player, in tiles.
*/
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.
*/
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
* can
* already view.
* can 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}.
*
* @param player The Player.
* @param encodes The Map containing Region encodes.
* @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,
Map<RegionCoordinates, List<RegionUpdateMessage>> snapshots) {
public PrePlayerSynchronizationTask(Player player, Map<RegionCoordinates, Set<RegionUpdateMessage>> encodes,
Map<RegionCoordinates, Set<RegionUpdateMessage>> updates) {
this.player = player;
this.updates = updates;
this.snapshots = snapshots;
this.encodes = encodes;
}
@Override
@@ -91,71 +68,49 @@ public final class PrePlayerSynchronizationTask extends SynchronizationTask {
Position old = player.getPosition();
player.getWalkingQueue().pulse();
RegionUpdateMode mode = RegionUpdateMode.DIFFERENCE;
if (player.isTeleporting()) {
player.resetViewingDistance();
mode = RegionUpdateMode.FULL;
}
boolean hasKnownRegion = player.hasLastKnownRegion();
if (!hasKnownRegion) {
mode = RegionUpdateMode.FULL;
}
if (!hasKnownRegion || isRegionUpdateRequired()) {
Position position = player.getPosition();
if (!player.hasLastKnownRegion() || isRegionUpdateRequired()) {
player.setRegionChanged(true);
Position position = player.getPosition();
player.setLastKnownRegion(position);
player.send(new RegionChangeMessage(position));
}
Set<RegionCoordinates> newRegions = getNewRegions(old, player.getPosition());
sendRegionUpdates(mode, newRegions);
Set<RegionCoordinates> oldViewable = getViewableRegions(old), newViewable = getViewableRegions(position);
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
* only just became able to view.
* Gets the {@link Set} of {@link RegionCoordinates} of Regions that are viewable from the specified {@link
* Position}.
*
* @param old The old {@link Position} of the Player.
* @param next The new Position of the Player.
* @return The Set of RegionCoordinates. Will not be {@code null}, but may be empty.
* @param position The Position.
* @return The Set of RegionCoordinates.
*/
private Set<RegionCoordinates> getNewRegions(Position old, Position next) {
RegionCoordinates oldRegion = old.getRegionCoordinates();
RegionCoordinates nextRegion = next.getRegionCoordinates();
private Set<RegionCoordinates> getViewableRegions(Position position) { // TODO possibly more complicated than this
RegionCoordinates local = position.getRegionCoordinates();
int localX = local.getX(), localY = local.getY();
int maxX = localX + VIEWABLE_REGION_RADIUS, maxY = localY + VIEWABLE_REGION_RADIUS;
if (oldRegion.equals(nextRegion)) {
return ImmutableSet.of();
}
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));
Set<RegionCoordinates> viewable = new HashSet<>();
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));
}
}
if (dy != 0) {
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;
return viewable;
}
/**
@@ -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 newRegions The {@link Set} of {@link RegionCoordinates} that should be sent as a full update.
* @param position The {@link Position} of the last known region.
* @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) {
Position position = player.getPosition();
RegionCoordinates base = position.getRegionCoordinates();
int baseX = base.getX(), baseY = base.getY();
private void sendUpdates(Position position, Set<RegionCoordinates> differences, Set<RegionCoordinates> full) {
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++) {
for (int y = baseY - REGION_COUNT; y <= baseY + REGION_COUNT; y++) {
RegionCoordinates coordinates = new RegionCoordinates(x, y);
differences.stream().map(coordinates -> {
Set<RegionUpdateMessage> messages = updates.computeIfAbsent(coordinates,
coords -> repository.get(coords).getUpdates(height));
RegionUpdateMode local = mode;
if (mode == RegionUpdateMode.DIFFERENCE && newRegions.contains(coordinates)) {
local = RegionUpdateMode.FULL;
return new GroupedRegionUpdateMessage(position, coordinates, messages);
}).forEach(player::send);
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);
}
}
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));
player.send(new ClearRegionMessage(position, coordinates));
return new GroupedRegionUpdateMessage(position, coordinates, messages);
}).forEach(player::send);
}
}