This commit is contained in:
Major-
2015-08-29 22:30:59 +01:00
5 changed files with 175 additions and 89 deletions
+34 -24
View File
@@ -33,9 +33,11 @@ import org.apollo.util.NameUtil;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
/** /**
* The world class is a singleton which contains objects like the {@link MobRepository} for players and NPCs. It should * The world class is a singleton which contains objects like the
* only contain things relevant to the in-game world and not classes which deal with I/O and such (these may be better * {@link MobRepository} for players and NPCs. It should only contain things
* off inside some custom {@link Service} or other code, however, the circumstances are rare). * relevant to the in-game world and not classes which deal with I/O and such
* (these may be better off inside some custom {@link Service} or other code,
* however, the circumstances are rare).
* *
* @author Graham * @author Graham
*/ */
@@ -139,8 +141,8 @@ public final class World {
} }
/** /**
* Gets the {@link Player} with the specified username. Note that this will return {@code null} if the player is * Gets the {@link Player} with the specified username. Note that this will
* offline. * return {@code null} if the player is offline.
* *
* @param username The username. * @param username The username.
* @return The player. * @return The player.
@@ -186,12 +188,14 @@ public final class World {
} }
/** /**
* Initialises the world by loading definitions from the specified file system. * Initialises the world by loading definitions from the specified file
* system.
* *
* @param release The release number. * @param release The release number.
* @param fs The file system. * @param fs The file system.
* @param manager The plugin manager. TODO move this. * @param manager The plugin manager. TODO move this.
* @throws Exception If any definitions could not be loaded or there was a failure when loading plugins. * @throws Exception If any definitions could not be loaded or there was a
* failure when loading plugins.
*/ */
public void init(int release, IndexedFileSystem fs, PluginManager manager) throws Exception { public void init(int release, IndexedFileSystem fs, PluginManager manager) throws Exception {
releaseNumber = release; releaseNumber = release;
@@ -202,7 +206,9 @@ public final class World {
decoder.block(); decoder.block();
npcMovement = new NpcMovementTask(regions); // Must be exactly here because of ordering issues. npcMovement = new NpcMovementTask(regions); // Must be exactly here
// because of ordering
// issues.
scheduler.schedule(npcMovement); scheduler.schedule(npcMovement);
manager.start(); manager.start();
@@ -221,7 +227,8 @@ public final class World {
} }
/** /**
* Adds an {@link EventListener}, listening for an {@link Event} of the specified type. * Adds an {@link EventListener}, listening for an {@link Event} of the
* specified type.
* *
* @param type The type of the Event. * @param type The type of the Event.
* @param listener The EventListener. * @param listener The EventListener.
@@ -241,7 +248,8 @@ public final class World {
* Registers the specified npc. * Registers the specified npc.
* *
* @param npc The npc. * @param npc The npc.
* @return {@code true} if the npc registered successfully, otherwise {@code false}. * @return {@code true} if the npc registered successfully, otherwise
* {@code false}.
*/ */
public boolean register(Npc npc) { public boolean register(Npc npc) {
boolean success = npcRepository.add(npc); boolean success = npcRepository.add(npc);
@@ -264,24 +272,14 @@ public final class World {
* Registers the specified player. * Registers the specified player.
* *
* @param player The player. * @param player The player.
* @return A {@link RegistrationStatus}.
*/ */
public RegistrationStatus register(Player player) { public void register(Player player) {
String username = player.getUsername(); String username = player.getUsername();
if (isPlayerOnline(username)) {
return RegistrationStatus.ALREADY_ONLINE;
}
boolean success = playerRepository.add(player); playerRepository.add(player);
if (success) {
players.put(NameUtil.encodeBase37(username), player); players.put(NameUtil.encodeBase37(username), player);
logger.info("Registered player: " + player + " [count=" + playerRepository.size() + "]"); logger.info("Registered player: " + player + " [count=" + playerRepository.size() + "]");
return RegistrationStatus.OK;
}
logger.warning("Failed to register player: " + player + " [count=" + playerRepository.size() + "]");
return RegistrationStatus.WORLD_FULL;
} }
/** /**
@@ -295,8 +293,8 @@ public final class World {
} }
/** /**
* Spawns the specified {@link Entity}, which must not be a {@link Player} or an {@link Npc}, which have their own * Spawns the specified {@link Entity}, which must not be a {@link Player}
* register methods. * or an {@link Npc}, which have their own register methods.
* *
* @param entity The Entity. * @param entity The Entity.
*/ */
@@ -350,4 +348,16 @@ public final class World {
} }
} }
<<<<<<< HEAD
=======
/**
* Adds entities to regions in the {@link RegionRepository}. By default, we
* do not notify listeners.
*
* @param entities The entities.
*/
private void placeEntities(Entity... entities) {
Arrays.stream(entities).forEach(entity -> regions.fromPosition(entity.getPosition()).addEntity(entity, false));
}
>>>>>>> c3443521f43dde6310e77de16637503af54805c2
} }
@@ -19,6 +19,7 @@ import org.apollo.game.message.impl.UpdateRunEnergyMessage;
import org.apollo.game.model.Appearance; import org.apollo.game.model.Appearance;
import org.apollo.game.model.Position; import org.apollo.game.model.Position;
import org.apollo.game.model.World; import org.apollo.game.model.World;
import org.apollo.game.model.World.RegistrationStatus;
import org.apollo.game.model.entity.attr.Attribute; import org.apollo.game.model.entity.attr.Attribute;
import org.apollo.game.model.entity.attr.AttributeDefinition; import org.apollo.game.model.entity.attr.AttributeDefinition;
import org.apollo.game.model.entity.attr.AttributeMap; import org.apollo.game.model.entity.attr.AttributeMap;
@@ -217,7 +218,8 @@ public final class Player extends Mob {
} }
/** /**
* Adds a click, represented by a {@link Point}, to the {@link List} of clicks. * Adds a click, represented by a {@link Point}, to the {@link List} of
* clicks.
* *
* @param point The point. * @param point The point.
* @return {@code true} if the point was added successfully. * @return {@code true} if the point was added successfully.
@@ -245,7 +247,8 @@ public final class Player extends Mob {
} }
/** /**
* Adds the specified {@link DynamicGameObject} to this Player's {@link Set} of visible objects. * Adds the specified {@link DynamicGameObject} to this Player's {@link Set}
* of visible objects.
* *
* @param object The DynamicGameObject. * @param object The DynamicGameObject.
*/ */
@@ -288,10 +291,12 @@ public final class Player extends Mob {
} }
/** /**
* Indicates whether this player is friends with the player with the specified username or not. * Indicates whether this player is friends with the player with the
* specified username or not.
* *
* @param username The username of the other player. * @param username The username of the other player.
* @return {@code true} if the specified username is on this player's friend list, otherwise {@code false}. * @return {@code true} if the specified username is on this player's friend
* list, otherwise {@code false}.
*/ */
public boolean friendsWith(String username) { public boolean friendsWith(String username) {
return friends.contains(username.toLowerCase()); return friends.contains(username.toLowerCase());
@@ -409,7 +414,8 @@ public final class Player extends Mob {
/** /**
* Gets the last known region. * Gets the last known region.
* *
* @return The last known region, or {@code null} if the player has never known a region. * @return The last known region, or {@code null} if the player has never
* known a region.
*/ */
public Position getLastKnownRegion() { public Position getLastKnownRegion() {
return lastKnownRegion; return lastKnownRegion;
@@ -507,7 +513,8 @@ public final class Player extends Mob {
} }
/** /**
* Indicates whether or not the player with the specified username is on this player's ignore list. * Indicates whether or not the player with the specified username is on
* this player's ignore list.
* *
* @param username The username of the player. * @param username The username of the player.
* @return {@code true} if the player is ignored, {@code false} if not. * @return {@code true} if the player is ignored, {@code false} if not.
@@ -540,7 +547,8 @@ public final class Player extends Mob {
} }
/** /**
* Increments this player's viewing distance if it is less than the maximum viewing distance. * Increments this player's viewing distance if it is less than the maximum
* viewing distance.
*/ */
public void incrementViewingDistance() { public void incrementViewingDistance() {
if (viewingDistance < Position.MAX_DISTANCE) { if (viewingDistance < Position.MAX_DISTANCE) {
@@ -596,12 +604,31 @@ public final class Player extends Mob {
/** /**
* Checks if this player is withdrawing noted items. * Checks if this player is withdrawing noted items.
* *
* @return {@code true} if the player is currently withdrawing notes, otherwise {@code false}. * @return {@code true} if the player is currently withdrawing notes,
* otherwise {@code false}.
*/ */
public boolean isWithdrawingNotes() { public boolean isWithdrawingNotes() {
return withdrawingNotes; return withdrawingNotes;
} }
/**
* Determines the {@link RegistrationStatus} for this player. This method
* can remain lock-free since writes to the player {@link MobRepository} are
* only happening on the game thread.
*
* @return The status.
*/
public RegistrationStatus getRegistrationStatus() {
MobRepository<Player> repository = world.getPlayerRepository();
if (world.isPlayerOnline(getUsername())) {
return RegistrationStatus.ALREADY_ONLINE;
} else if (repository.capacity() == repository.size()) {
return RegistrationStatus.WORLD_FULL;
}
return RegistrationStatus.OK;
}
/** /**
* Logs the player out, if possible. * Logs the player out, if possible.
*/ */
@@ -642,7 +669,8 @@ public final class Player extends Mob {
* Removes the specified username from this player's friend list. * Removes the specified username from this player's friend list.
* *
* @param username The username. * @param username The username.
* @return {@code true} if the player's friend list contained the specified user, {@code false} if not. * @return {@code true} if the player's friend list contained the specified
* user, {@code false} if not.
*/ */
public boolean removeFriend(String username) { public boolean removeFriend(String username) {
return friends.remove(username.toLowerCase()); return friends.remove(username.toLowerCase());
@@ -652,14 +680,16 @@ public final class Player extends Mob {
* Removes the specified username from this player's ignore list. * Removes the specified username from this player's ignore list.
* *
* @param username The username. * @param username The username.
* @return {@code true} if the player's ignore list contained the specified user, {@code false} if not. * @return {@code true} if the player's ignore list contained the specified
* user, {@code false} if not.
*/ */
public boolean removeIgnore(String username) { public boolean removeIgnore(String username) {
return ignores.remove(username.toLowerCase()); return ignores.remove(username.toLowerCase());
} }
/** /**
* Removes the specified {@link DynamicGameObject} from this Player's {@link Set} of visible objects. * Removes the specified {@link DynamicGameObject} from this Player's
* {@link Set} of visible objects.
* *
* @param object The DynamicGameObject. * @param object The DynamicGameObject.
*/ */
@@ -911,7 +941,8 @@ public final class Player extends Mob {
/** /**
* Sets whether or not the player is withdrawing notes from the bank. * Sets whether or not the player is withdrawing notes from the bank.
* *
* @param withdrawingNotes Whether or not the player is withdrawing noted items. * @param withdrawingNotes Whether or not the player is withdrawing noted
* items.
*/ */
public void setWithdrawingNotes(boolean withdrawingNotes) { public void setWithdrawingNotes(boolean withdrawingNotes) {
this.withdrawingNotes = withdrawingNotes; this.withdrawingNotes = withdrawingNotes;
@@ -932,8 +963,7 @@ public final class Player extends Mob {
@Override @Override
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this).add("username", getUsername()).add("privilege", privilegeLevel) return MoreObjects.toStringHelper(this).add("username", getUsername()).add("privilege", privilegeLevel).toString();
.toString();
} }
/** /**
@@ -966,16 +996,13 @@ public final class Player extends Mob {
* Initialises the player's inventories. * Initialises the player's inventories.
*/ */
private void initInventories() { private void initInventories() {
InventoryListener fullInventoryListener = new FullInventoryListener(this, InventoryListener fullInventoryListener = new FullInventoryListener(this, FullInventoryListener.FULL_INVENTORY_MESSAGE);
FullInventoryListener.FULL_INVENTORY_MESSAGE);
InventoryListener fullBankListener = new FullInventoryListener(this, FullInventoryListener.FULL_BANK_MESSAGE); InventoryListener fullBankListener = new FullInventoryListener(this, FullInventoryListener.FULL_BANK_MESSAGE);
InventoryListener appearanceListener = new AppearanceInventoryListener(this); InventoryListener appearanceListener = new AppearanceInventoryListener(this);
InventoryListener syncInventoryListener = new SynchronizationInventoryListener(this, InventoryListener syncInventoryListener = new SynchronizationInventoryListener(this, SynchronizationInventoryListener.INVENTORY_ID);
SynchronizationInventoryListener.INVENTORY_ID);
InventoryListener syncBankListener = new SynchronizationInventoryListener(this, BankConstants.BANK_INVENTORY_ID); InventoryListener syncBankListener = new SynchronizationInventoryListener(this, BankConstants.BANK_INVENTORY_ID);
InventoryListener syncEquipmentListener = new SynchronizationInventoryListener(this, InventoryListener syncEquipmentListener = new SynchronizationInventoryListener(this, SynchronizationInventoryListener.EQUIPMENT_ID);
SynchronizationInventoryListener.EQUIPMENT_ID);
inventory.addListener(syncInventoryListener); inventory.addListener(syncInventoryListener);
inventory.addListener(fullInventoryListener); inventory.addListener(fullInventoryListener);
@@ -15,7 +15,6 @@ import org.apollo.game.GamePulseHandler;
import org.apollo.game.io.MessageHandlerChainSetParser; import org.apollo.game.io.MessageHandlerChainSetParser;
import org.apollo.game.message.handler.MessageHandlerChainSet; import org.apollo.game.message.handler.MessageHandlerChainSet;
import org.apollo.game.model.World; import org.apollo.game.model.World;
import org.apollo.game.model.World.RegistrationStatus;
import org.apollo.game.model.area.Region; import org.apollo.game.model.area.Region;
import org.apollo.game.model.entity.MobRepository; import org.apollo.game.model.entity.MobRepository;
import org.apollo.game.model.entity.Player; import org.apollo.game.model.entity.Player;
@@ -27,18 +26,25 @@ import org.apollo.util.xml.XmlParser;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
/** /**
* The {@link GameService} class schedules and manages the execution of the {@link GamePulseHandler} class. * The {@link GameService} class schedules and manages the execution of the
* {@link GamePulseHandler} class.
* *
* @author Graham * @author Graham
*/ */
public final class GameService extends Service { public final class GameService extends Service {
/** /**
* The number of times to unregister players per cycle. This is to ensure the saving threads don't get swamped with * The number of times to unregister players per cycle. This is to ensure
* requests and slow everything down. * the saving threads don't get swamped with requests and slow everything
* down.
*/ */
private static final int UNREGISTERS_PER_CYCLE = 50; private static final int UNREGISTERS_PER_CYCLE = 50;
/**
* The number of times to register players per cycle.
*/
private static final int REGISTERS_PER_CYCLE = 25;
/** /**
* The World this Service is for. * The World this Service is for.
*/ */
@@ -49,11 +55,15 @@ public final class GameService extends Service {
*/ */
private final Queue<Player> oldPlayers = new ConcurrentLinkedQueue<>(); private final Queue<Player> oldPlayers = new ConcurrentLinkedQueue<>();
/**
* A queue of players to add.
*/
private final Queue<Player> newPlayers = new ConcurrentLinkedQueue<>();
/** /**
* The scheduled executor service. * The scheduled executor service.
*/ */
private final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(ThreadUtil private final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(ThreadUtil.create("GameService"));
.create("GameService"));
/** /**
* The {@link MessageHandlerChainSet}. * The {@link MessageHandlerChainSet}.
@@ -85,6 +95,21 @@ public final class GameService extends Service {
world.unregister(player); world.unregister(player);
} }
/**
* Finalizes the registration of a player.
*
* @param player The player.
*/
public void finalizePlayerRegistration(Player player) {
world.register(player);
Region region = world.getRegionRepository().fromPosition(player.getPosition());
region.addEntity(player);
if (player.getSession().isReconnecting()) {
player.sendInitialMessages();
}
}
/** /**
* Gets the MessageHandlerChainSet * Gets the MessageHandlerChainSet
* *
@@ -97,7 +122,8 @@ public final class GameService extends Service {
/** /**
* Called every pulse. * Called every pulse.
*/ */
public synchronized void pulse() { public void pulse() {
finalizeRegistrations();
finalizeUnregistrations(); finalizeUnregistrations();
MobRepository<Player> players = world.getPlayerRepository(); MobRepository<Player> players = world.getPlayerRepository();
@@ -112,25 +138,6 @@ public final class GameService extends Service {
synchronizer.synchronize(players, world.getNpcRepository()); synchronizer.synchronize(players, world.getNpcRepository());
} }
/**
* Registers a {@link Player} (may block!).
*
* @param player The Player.
* @param session The {@link GameSession} of the Player.
* @return A {@link RegistrationStatus}.
*/
public synchronized RegistrationStatus registerPlayer(Player player, GameSession session) {
RegistrationStatus status = world.register(player);
if (status == RegistrationStatus.OK) {
player.setSession(session);
Region region = world.getRegionRepository().fromPosition(player.getPosition());
region.addEntity(player);
}
return status;
}
/** /**
* Shuts down this game service. * Shuts down this game service.
* *
@@ -138,17 +145,19 @@ public final class GameService extends Service {
*/ */
public void shutdown(boolean natural) { public void shutdown(boolean natural) {
scheduledExecutor.shutdownNow(); scheduledExecutor.shutdownNow();
// TODO: Other events that should happen upon natural or unexpected shutdown. // TODO: Other events that should happen upon natural or unexpected
// shutdown.
} }
@Override @Override
public void start() { public void start() {
scheduledExecutor.scheduleAtFixedRate(new GamePulseHandler(this), GameConstants.PULSE_DELAY, scheduledExecutor.scheduleAtFixedRate(new GamePulseHandler(this), GameConstants.PULSE_DELAY, GameConstants.PULSE_DELAY,
GameConstants.PULSE_DELAY, TimeUnit.MILLISECONDS); TimeUnit.MILLISECONDS);
} }
/** /**
* Unregisters a player. Returns immediately. The player is unregistered at the start of the next cycle. * Unregisters a player. Returns immediately. The player is unregistered at
* the start of the next cycle.
* *
* @param player The player. * @param player The player.
*/ */
@@ -156,6 +165,16 @@ public final class GameService extends Service {
oldPlayers.add(player); oldPlayers.add(player);
} }
/**
* Registers a player. Returns immediately. The player is registered at the
* start of the next cycle.
*
* @param player The player.
*/
public void registerPlayer(Player player) {
newPlayers.add(player);
}
/** /**
* Finalizes the unregistration of Player's queued to be unregistered. * Finalizes the unregistration of Player's queued to be unregistered.
*/ */
@@ -172,12 +191,27 @@ public final class GameService extends Service {
} }
} }
/**
* Finalizes the registration of Player's queued to be registered.
*/
private void finalizeRegistrations() {
for (int count = 0; count < REGISTERS_PER_CYCLE; count++) {
Player player = newPlayers.poll();
if (player == null) {
break;
}
finalizePlayerRegistration(player);
}
}
/** /**
* Initializes the game service. * Initializes the game service.
* *
* @throws IOException If there is an error accessing the file. * @throws IOException If there is an error accessing the file.
* @throws SAXException If there is an error parsing the file. * @throws SAXException If there is an error parsing the file.
* @throws ReflectiveOperationException If a MessageHandler could not be created. * @throws ReflectiveOperationException If a MessageHandler could not be
* created.
*/ */
private void init() throws IOException, SAXException, ReflectiveOperationException { private void init() throws IOException, SAXException, ReflectiveOperationException {
try (InputStream input = new FileInputStream("data/messages.xml")) { try (InputStream input = new FileInputStream("data/messages.xml")) {
@@ -43,17 +43,24 @@ public final class GameSession extends Session {
*/ */
private final Player player; private final Player player;
/**
* If the player was reconnecting.
*/
private final boolean reconnecting;
/** /**
* Creates a login session for the specified channel. * Creates a login session for the specified channel.
* *
* @param channel The channel. * @param channel The channel.
* @param context The server context. * @param context The server context.
* @param player The player. * @param player The player.
* @param reconnecting If the player was reconnecting.
*/ */
public GameSession(Channel channel, ServerContext context, Player player) { public GameSession(Channel channel, ServerContext context, Player player, boolean reconnecting) {
super(channel); super(channel);
this.context = context; this.context = context;
this.player = player; this.player = player;
this.reconnecting = reconnecting;
} }
@Override @Override
@@ -111,4 +118,13 @@ public final class GameSession extends Session {
} }
} }
/**
* Determines if this player is reconnecting.
*
* @return {@code true} if reconnecting, {@code false} otherwise.
*/
public boolean isReconnecting() {
return reconnecting;
}
} }
@@ -80,14 +80,18 @@ public final class LoginSession extends Session {
Player player = optional.get(); Player player = optional.get();
rights = player.getPrivilegeLevel().toInteger(); rights = player.getPrivilegeLevel().toInteger();
GameSession session = new GameSession(channel, context, player); RegistrationStatus registration = player.getRegistrationStatus();
RegistrationStatus registration = service.registerPlayer(player, session);
if (registration != RegistrationStatus.OK) { if (registration != RegistrationStatus.OK) {
optional = Optional.empty(); optional = Optional.empty();
rights = 0; rights = 0;
status = registration == RegistrationStatus.ALREADY_ONLINE ? LoginConstants.STATUS_ACCOUNT_ONLINE : LoginConstants.STATUS_SERVER_FULL; status = registration == RegistrationStatus.ALREADY_ONLINE ? LoginConstants.STATUS_ACCOUNT_ONLINE
: LoginConstants.STATUS_SERVER_FULL;
} else {
GameSession session = new GameSession(channel, context, player, request.isReconnecting());
channel.attr(ApolloHandler.SESSION_KEY).set(session);
player.setSession(session);
} }
} }
@@ -102,20 +106,16 @@ public final class LoginSession extends Session {
channel.pipeline().addFirst("messageEncoder", new GameMessageEncoder(release)); channel.pipeline().addFirst("messageEncoder", new GameMessageEncoder(release));
channel.pipeline().addBefore("messageEncoder", "gameEncoder", new GamePacketEncoder(randomPair.getEncodingRandom())); channel.pipeline().addBefore("messageEncoder", "gameEncoder", new GamePacketEncoder(randomPair.getEncodingRandom()));
channel.pipeline().addBefore("handler", "gameDecoder", new GamePacketDecoder(randomPair.getDecodingRandom(), context.getRelease())); channel.pipeline().addBefore("handler", "gameDecoder",
new GamePacketDecoder(randomPair.getDecodingRandom(), context.getRelease()));
channel.pipeline().addAfter("gameDecoder", "messageDecoder", new GameMessageDecoder(release)); channel.pipeline().addAfter("gameDecoder", "messageDecoder", new GameMessageDecoder(release));
channel.pipeline().remove("loginDecoder"); channel.pipeline().remove("loginDecoder");
channel.pipeline().remove("loginEncoder"); channel.pipeline().remove("loginEncoder");
service.registerPlayer(optional.get());
channel.attr(ApolloHandler.SESSION_KEY).set(optional.get().getSession());
} else { } else {
future.addListener(ChannelFutureListener.CLOSE); future.addListener(ChannelFutureListener.CLOSE);
} }
if (optional.isPresent() && !request.isReconnecting()) {
optional.get().sendInitialMessages();
}
} }
@Override @Override
@@ -124,5 +124,4 @@ public final class LoginSession extends Session {
handleLoginRequest((LoginRequest) message); handleLoginRequest((LoginRequest) message);
} }
} }
} }