mirror of
https://github.com/2006-Scape/apollo.git
synced 2026-07-06 08:40:03 +00:00
Fix issue #64.
This commit is contained in:
@@ -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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user