/* * Aphelion * Copyright (c) 2013 Joris van der Wel * * This file is part of Aphelion * * Aphelion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, version 3 of the License. * * Aphelion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Aphelion. If not, see <http://www.gnu.org/licenses/>. * * In addition, the following supplemental terms apply, based on section 7 of * the GNU Affero General Public License (version 3): * a) Preservation of all legal notices and author attributions * b) Prohibition of misrepresentation of the origin of this material, and * modified versions are required to be marked in reasonable ways as * different from the original version (for example by appending a copyright notice). * * Linking this library statically or dynamically with other modules is making a * combined work based on this library. Thus, the terms and conditions of the * GNU Affero General Public License cover the whole combination. * * As a special exception, the copyright holders of this library give you * permission to link this library with independent modules to produce an * executable, regardless of the license terms of these independent modules, * and to copy and distribute the resulting executable under terms of your * choice, provided that you also meet, for each linked independent module, * the terms and conditions of the license of that module. An independent * module is a module which is not derived from or based on this library. */ package aphelion.server.game; import aphelion.shared.resource.Asset; import aphelion.shared.gameconfig.GCBoolean; import aphelion.shared.gameconfig.GCStringList; import aphelion.shared.net.game.GameProtocolConnection; import aphelion.shared.net.game.NetworkedActor; import aphelion.shared.net.protobuf.GameOperation; import aphelion.shared.net.protobuf.GameS2C; import aphelion.shared.net.protobuf.GameS2C.ArenaLoad; import aphelion.shared.physics.*; import aphelion.shared.physics.entities.ActorPublic; import aphelion.shared.physics.entities.ProjectilePublic; import aphelion.shared.physics.operations.pub.OperationPublic; import aphelion.shared.physics.operations.pub.ActorNewPublic; import aphelion.shared.physics.valueobjects.PhysicsMovement; import aphelion.shared.physics.valueobjects.PhysicsPoint; import aphelion.shared.swissarmyknife.MySecureRandom; import aphelion.shared.swissarmyknife.RollingHistory; import aphelion.shared.swissarmyknife.SwissArmyKnife; import java.util.HashMap; import java.util.Iterator; import java.util.List; /** * * @author Joris */ public class ClientState { public static enum STATE { // the order in this enum is also the order the states will move through. ESTABLISHED (0), // The client has a session token and atleast 1 websocket WAIT_FOR_AUTHENTICATE (1), RECEIVED_AUTHENTICATE (2), WAIT_FOR_CONNECTION_READY (3), // waiting for ConnectionReady RECEIVED_CONNECTION_READY (4), WAIT_FOR_ARENA_LOADED (5), RECEIVED_ARENA_LOADED (6), SEND_ARENA_SYNC (7), READY (8), // ready to play the game DISCONNECTED (9); final int sequence; private STATE(int sequence) { this.sequence = sequence; } public boolean hasOccurred(STATE other) { return this.sequence >= other.sequence; } } private final ServerGame serverGame; private final SimpleEnvironment physicsEnv; public final GameProtocolConnection gameConn; /** Used to track duplicate moves, and to prevent them from being forwarded if unequal. * Make sure to check MAX_FUTURE_TICKS before you use the setter of this object. * Otherwise it will push out the history of older ticks. */ public final RollingHistory<PhysicsMovement> receivedMove; /** Used to track duplicate weapons, and to prevent them from being forwarded if unequal. * Make sure to check MAX_FUTURE_TICKS before you use the setter of this object. * Otherwise it will push out the history of older ticks. */ public final RollingHistory<WEAPON_SLOT> receivedWeapon; /** Do not accept operations from this client * that are more than this many ticks into the future. */ public final int MAX_FUTURE_TICKS = 10; public int pid; public STATE state; public String nickname; private GCStringList ships; public long lastActorSyncBroadcast_nanos; private final long WARNING_DROPPED_OPERATION_INTERVAL = 30_000_000_000L; // 30s private long lastDroppedWarning_nanos; private long nextWeaponSyncKey = 0; public NetworkedActor myNetActor; ClientState(ServerGame serverGame, GameProtocolConnection gameConn) { this.serverGame = serverGame; this.physicsEnv = serverGame.physicsEnv; this.gameConn = gameConn; this.ships = physicsEnv.getGlobalConfigStringList("ships"); this.receivedMove = new RollingHistory<>(physicsEnv.getTick(), physicsEnv.getConfig().HIGHEST_DELAY + MAX_FUTURE_TICKS + 1); this.receivedWeapon = new RollingHistory<>(physicsEnv.getTick(), physicsEnv.getConfig().HIGHEST_DELAY + MAX_FUTURE_TICKS + 1); } public void setNickname(String nickname) { this.nickname = nickname; if (myNetActor != null) { myNetActor.name = nickname; } } public void nextState(STATE state) { //System.out.println("Server new state: " + state); this.state = state; switch(state) { case ESTABLISHED: nextState(STATE.WAIT_FOR_AUTHENTICATE); break; case WAIT_FOR_AUTHENTICATE: break; case RECEIVED_AUTHENTICATE: nextState(STATE.WAIT_FOR_CONNECTION_READY); break; case WAIT_FOR_CONNECTION_READY: break; case RECEIVED_CONNECTION_READY: sendInitialResourceRequirements(); nextState(STATE.WAIT_FOR_ARENA_LOADED); break; case WAIT_FOR_ARENA_LOADED: break; case RECEIVED_ARENA_LOADED: nextState(STATE.SEND_ARENA_SYNC); break; case SEND_ARENA_SYNC: // never a 0 pid this.pid = serverGame.generatePid(); this.myNetActor = new NetworkedActor(pid, false, nickname); doArenaSync(); nextState(STATE.READY); break; case READY: serverGame.addReadyPlayer(gameConn); serverGame.addActor(myNetActor); break; case DISCONNECTED: serverGame.removeReadyPlayer(gameConn); if (myNetActor != null) { serverGame.removeActor(myNetActor); } if (pid > 0) { physicsEnv.actorRemove(physicsEnv.getTick(), pid); GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder(); GameOperation.ActorRemove.Builder actorRemove = s2c.addActorRemoveBuilder(); actorRemove.setTick(physicsEnv.getTick()); actorRemove.setPid(pid); serverGame.broadcast(s2c); } // else the connection was dropped before the player reached the SEND_ARENA_SYNC state break; } } private void sendInitialResourceRequirements() { GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder(); ArenaLoad.Builder arenaLoad = s2c.addArenaLoadBuilder(); for (Asset ass : serverGame.assets) { ass.toProtoBuf(arenaLoad.addResourceRequirementBuilder()); } arenaLoad.setMap(serverGame.mapResource); arenaLoad.addAllGameGonfig(serverGame.gameConfigResources); arenaLoad.addAllNiftyGui(serverGame.niftyGuiResources); gameConn.send(s2c); } private void doArenaSync() { assert nickname != null; // random ship String ship; { int s = ships.getValuesLength(); if (s > 0) { s = SwissArmyKnife.random.nextInt(s); ship = ships.get(s); } else { ship = ""; } } long seed = 0; while (seed == 0) // make sure seed is not 0 so that 0 is reserved for errors { seed = MySecureRandom.nextLong(); } physicsEnv.actorNew(physicsEnv.getTick(), pid, seed, ship); // Send ArenaSync to the new player { GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder(); GameS2C.ArenaSync.Builder inArena = s2c.addArenaSyncBuilder(); inArena.setCurrentTicks(physicsEnv.getTick()); // currentNano returns the time at which the tick began, not System.nanoTime() inArena.setCurrentNanoTime(serverGame.loop.currentNano()); inArena.setName(nickname); inArena.setYourPid(pid); inArena.setYourSeed(seed); inArena.setShip(ship); gameConn.send(s2c); } { // Send ActorNew to all other players GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder(); GameOperation.ActorNew.Builder actorNew = s2c.addActorNewBuilder(); actorNew.setTick(physicsEnv.getTick()); actorNew.setPid(pid); actorNew.setName(nickname); actorNew.setSeed(seed); actorNew.setShip(ship); // There is no actorSync here. The player will initialize with the same default // values for everyone. serverGame.broadcast(s2c, this.gameConn); } // Send the state of all actors to the player int oldestState = physicsEnv.econfig.TRAILING_STATES-1; long oldestStateTick = physicsEnv.getTick(oldestState); Iterator<ActorPublic> actorIt = physicsEnv.actorIterator(oldestState); int actors = 0; actorloop: while (actorIt.hasNext()) { ActorPublic actor = actorIt.next(); int actorPid = actor.getPid(); NetworkedActor netActor = serverGame.getActor(actorPid); if (actorPid == this.pid) { // Already sent all the data we need to send for the actor we just spawned continue; } // should never happen because we are running in the same thread // as the physics environment (so it has no change to loose its // reference to the interal actor. If we get an actor from actorIterator // the weak reference return null if used immediatel. assert actor.hasReference(); if (actor.isNonExistent()) { continue; } ++actors; GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder(); GameOperation.ActorNew.Builder actorNew = s2c.addActorNewBuilder(); actorNew.setTick(oldestStateTick); actorNew.setPid(actorPid); actorNew.setName(netActor == null ? "???????" : netActor.name); actorNew.setSeed(actor.getSeed()); actorNew.setShip(actor.getShip()); GameOperation.ActorSync.Builder actorSync = s2c.addActorSyncBuilder(); boolean r = actor.getSync(actorSync); assert r; // should not fail if isDeleted() == false assert actorSync.getTick() == oldestStateTick; assert actorSync.getPid() == actorPid; Iterator<ProjectilePublic> projectileIt = actor.projectileIterator(); while (projectileIt.hasNext()) { ProjectilePublic projectile = projectileIt.next(); if (projectile.isNonExistent()) { continue; } assert projectile.getOwner() == actorPid; if (projectile.getProjectileIndex() != 0) { continue; } GameOperation.WeaponSync.Builder weaponSync = s2c.addWeaponSyncBuilder(); weaponSync.setTick(oldestStateTick); weaponSync.setPid(projectile.getOwner()); weaponSync.setWeaponKey(projectile.getWeaponKey()); nextWeaponSyncKey++; if (nextWeaponSyncKey == 0) { nextWeaponSyncKey = 1; } // reserve 0 as a special value weaponSync.setKey(nextWeaponSyncKey); Iterator<ProjectilePublic> projIt = projectile.getCoupledProjectiles(); while (projIt.hasNext()) { ProjectilePublic projCoupled = projIt.next(); if (projCoupled.isNonExistent()) { continue; } assert projectile.getOwner() == projCoupled.getOwner(); assert projectile.getWeaponKey().equals(projCoupled.getWeaponKey()); GameOperation.WeaponSync.Projectile.Builder weaponSyncProjectile = weaponSync.addProjectilesBuilder(); projCoupled.getSync(weaponSyncProjectile); } } gameConn.send(s2c); } HashMap<Integer, GameS2C.S2C.Builder> s2cMessages = new HashMap<>(actors); // play back all the operations in the todo list of the oldest state Iterator<OperationPublic> opIt = physicsEnv.todoListIterator(oldestState); // the todo list is ordered by tick while (opIt.hasNext()) { OperationPublic op = opIt.next(); if (op.getPid() == this.pid && op instanceof ActorNewPublic) { // already sent this stuff (few lines up) continue; } GameS2C.S2C.Builder s2c = s2cMessages.get(op.getPid()); if (s2c == null) { s2c = GameS2C.S2C.newBuilder(); s2cMessages.put(op.getPid(), s2c); } serverGame.addPhysicsOperationToMessage(s2c, op); } for (GameS2C.S2C.Builder val : s2cMessages.values()) { gameConn.send(val); } lastActorSyncBroadcast_nanos = System.nanoTime(); } public void broadcastActorSync() { // send the sync of my actor to all players (including myself) int oldestState = physicsEnv.econfig.TRAILING_STATES-1; ActorPublic actor = physicsEnv.getActor(pid, oldestState, false); if (actor == null) { return; } long oldestStateTick = physicsEnv.getTick(oldestState); GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder(); GameOperation.ActorSync.Builder actorSync = s2c.addActorSyncBuilder(); if (actor.getSync(actorSync)) { assert actorSync.getPid() == pid; assert actorSync.getTick() == oldestStateTick; serverGame.broadcast(s2c); } lastActorSyncBroadcast_nanos = System.nanoTime(); } public void sendCommandResponse(boolean error, String message) { sendCommandResponse(error, 0, message); } public void sendCommandResponse(boolean error, int responseCode, String message) { GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder(); GameS2C.LocalChatMessage.Builder chat = s2c.addLocalChatMessageBuilder(); chat.setMessage((error ? "\\#de3108#" : "\\#73ff63#") + message); if (responseCode != 0) { chat.setCommandResponseCode(responseCode); } gameConn.send(s2c); } public void warnDroppedOperation() { long now = serverGame.loop.getLoopSystemNanoTime(); if (lastDroppedWarning_nanos == 0 || now - lastDroppedWarning_nanos >= WARNING_DROPPED_OPERATION_INTERVAL) { GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder(); GameS2C.LocalChatMessage.Builder chat = s2c.addLocalChatMessageBuilder(); chat.setMessage("\\#de3108#You are experiencing very high lag, please check your network connection. (server)"); gameConn.send(s2c); lastDroppedWarning_nanos = now; } } public void parseCommand(String name, int responseCode, List<String> argumentsList) { // todo move these somewhere else? switch (name) { case "ship": ActorPublic actor = physicsEnv.getActor(this.pid); if (argumentsList.size() < 1) { sendCommandResponse(true, "You must specify a ship name."); break; } if (actor == null) { sendCommandResponse(true, "You are not in a ship."); break; } if (!actor.canChangeShip()) { sendCommandResponse(true, "You need full energy to change your ship."); break; } String ship = argumentsList.get(0); if (!ships.hasValue(ship)) { sendCommandResponse(true, "The ship '"+ship+"' is not a valid ship."); break; } long tick = physicsEnv.getTick() + 50; // todo 50 to config GCBoolean respawn = actor.getActorConfigBoolean("ship-change-respawn"); physicsEnv.actorModification(tick, this.pid, ship); GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder(); GameOperation.ActorModification.Builder actorMod = s2c.addActorModificationBuilder(); actorMod.setTick(tick); actorMod.setPid(this.pid); actorMod.setShip(ship); if (respawn.get()) { GameOperation.ActorWarp.Builder actorWarp = s2c.addActorWarpBuilder(); actorWarp.setTick(tick); actorWarp.setPid(this.pid); actorWarp.setHint(false); PhysicsPoint spawn = new PhysicsPoint(); actor.findSpawnPoint(spawn, tick); int rot = actor.randomRotation(actor.getSeed()); int x = spawn.x * PhysicsMap.TILE_PIXELS + PhysicsMap.TILE_PIXELS/2; int y = spawn.y * PhysicsMap.TILE_PIXELS + PhysicsMap.TILE_PIXELS/2; int x_vel = 0; int y_vel = 0; physicsEnv.actorWarp(tick, pid, false, x, y, x_vel, y_vel, rot); actorWarp.setX(x); actorWarp.setY(y); actorWarp.setXVel(x_vel); actorWarp.setYVel(y_vel); actorWarp.setRotation(rot); } serverGame.broadcast(s2c); break; default: // todo: unknown command } } }