/*
* 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.server.game.ClientState.STATE;
import aphelion.shared.event.*;
import aphelion.shared.net.game.GameProtoListener;
import aphelion.shared.net.game.GameProtocolConnection;
import aphelion.shared.net.WS_CLOSE_STATUS;
import aphelion.shared.net.game.NetworkedActor;
import aphelion.shared.net.protobuf.GameC2S;
import aphelion.shared.net.protobuf.GameC2S.Authenticate;
import aphelion.shared.net.protobuf.GameOperation;
import aphelion.shared.net.protobuf.GameS2C;
import aphelion.shared.net.protobuf.GameS2C.AuthenticateResponse;
import aphelion.shared.physics.operations.pub.ActorModificationPublic;
import aphelion.shared.physics.operations.pub.ActorMovePublic;
import aphelion.shared.physics.operations.pub.ActorNewPublic;
import aphelion.shared.physics.operations.pub.ActorRemovePublic;
import aphelion.shared.physics.operations.pub.ActorWarpPublic;
import aphelion.shared.physics.operations.pub.ActorWeaponFirePublic;
import aphelion.shared.physics.operations.pub.OperationPublic;
import aphelion.shared.physics.PhysicsEnvironment;
import aphelion.shared.physics.SimpleEnvironment;
import aphelion.shared.physics.valueobjects.PhysicsMovement;
import aphelion.shared.physics.WEAPON_SLOT;
import aphelion.shared.physics.events.Event;
import aphelion.shared.physics.events.pub.ActorDiedPublic;
import aphelion.shared.physics.events.pub.EventPublic;
import aphelion.shared.swissarmyknife.AttachmentConsumer;
import aphelion.shared.swissarmyknife.SwissArmyKnife;
import com.google.protobuf.ByteString;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
/**
*
* @author Joris
*/
public class ServerGame implements LoopEvent, TickEvent, GameProtoListener
{
private static final Logger log = Logger.getLogger("aphelion.server.game");
private static final AttachmentConsumer<GameProtocolConnection, ClientState> stateAttachment
= new AttachmentConsumer<>(GameProtocolConnection.attachmentManager);
private final static long SYNC_ACTOR_EVERY_NANOS = 15 * 1_000_000_000L; // 15 sec
private final static int SYNC_SOME_ACTOR_EVERY_TICKS = 25; // 0.25 sec
public final SimpleEnvironment physicsEnv;
public final TickedEventLoop loop;
public final List<Asset> assets;
public final String mapResource;
public final List<String> gameConfigResources;
public final List<String> niftyGuiResources;
// Simply raise this value to get the next pid, do not reuse them for now.
// This way we would go into negative pids after 248 days if we would get a new player every 10ms.
private int next_pid = 1;
// all players that are in STATE.READY
private LinkedList<GameProtocolConnection> readyPlayers = new LinkedList<>();
private Map<Integer, NetworkedActor> actors = new HashMap<>(); // actor pid -> client state
private static final AttachmentConsumer<EventPublic, Boolean> hasHandledEventConsumer
= new AttachmentConsumer<>(Event.attachmentManager);
public ServerGame(SimpleEnvironment physicsEnv,
TickedEventLoop loop,
List<Asset> assets,
String mapResource,
List<String> gameConfigResources,
List<String> niftyGuiResources)
{
this.physicsEnv = physicsEnv;
this.loop = loop;
this.assets = assets;
this.mapResource = mapResource;
this.gameConfigResources = Collections.unmodifiableList(gameConfigResources);
this.niftyGuiResources = Collections.unmodifiableList(niftyGuiResources);
loop.addTimerEvent(SYNC_SOME_ACTOR_EVERY_TICKS, actorSyncTimer);
}
public void addReadyPlayer(GameProtocolConnection gameConn)
{
readyPlayers.add(gameConn);
}
public void removeReadyPlayer(GameProtocolConnection gameConn)
{
readyPlayers.remove(gameConn);
}
public void addActor(NetworkedActor netActor)
{
actors.put(netActor.pid, netActor);
}
public void removeActor(NetworkedActor netActor)
{
actors.remove(netActor.pid);
}
public NetworkedActor getActor(int pid)
{
return actors.get(pid);
}
public int getPlayerCount()
{
return readyPlayers.size();
}
@Override
public void loop(long systemNanoTime, long sourceNanoTime)
{
}
public int generatePid()
{
if (next_pid == 0) { next_pid = 1; }
return next_pid++;
}
@Override
public void tick(long tick)
{
physicsEnv.tick();
for (EventPublic event_ : physicsEnv.eventIterable())
{
if (hasHandledEventConsumer.get(event_) == Boolean.TRUE)
{
continue;
}
if (event_ instanceof ActorDiedPublic)
{
ActorDiedPublic event = (ActorDiedPublic) event_;
if (event.hasOccurred(0))
{
log.log(Level.INFO, "{0} was killed by {1}", new Object[]
{
event.getDied(0),
event.getKiller(0)
});
hasHandledEventConsumer.set(event_, Boolean.TRUE);
GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder();
GameS2C.ActorDied.Builder actorDied = s2c.addActorDiedBuilder();
actorDied.setTick(event.getOccurredAt(0));
actorDied.setDied(event.getDied(0));
if (event.getKiller(0) != 0)
{
actorDied.setKiller(event.getKiller(0));
}
broadcast(s2c);
}
}
}
}
@Override
public void gameNewClient(GameProtocolConnection game)
{
ClientState state = new ClientState(this, game);
stateAttachment.set(game, state);
state.nextState(STATE.ESTABLISHED);
}
@Override
public void gameEstablishFailure(WS_CLOSE_STATUS code, String reason)
{
assert false;
}
@Override
public void gameRemovedClient(GameProtocolConnection game)
{
ClientState state = stateAttachment.get(game);
state.nextState(STATE.DISCONNECTED);
}
@Override
public void gameNewConnection(GameProtocolConnection game)
{
}
@Override
public void gameDropConnection(GameProtocolConnection game, WS_CLOSE_STATUS code, String reason)
{
}
@Override
public void gameC2SMessage(GameProtocolConnection game, GameC2S.C2S c2s, long receivedAt)
{
ClientState state = stateAttachment.get(game);
assert state != null;
// Do not handle time requests here!
// They are already handled in the thread that received them
for (GameC2S.Authenticate msg : c2s.getAuthenticateList())
{
if (state.state != STATE.WAIT_FOR_AUTHENTICATE)
{
log.log(Level.WARNING, "Received a duplicate Authenticate or in an invalid state");
break;
}
AuthenticateResponse.ERROR error = AuthenticateResponse.ERROR.OK;
String errorMessage = null;
String nickname = msg.getNickname();
if (!SwissArmyKnife.isValidNickname(nickname))
{
error = AuthenticateResponse.ERROR.INVALID_NICKNAME;
errorMessage = "Your nickname contains invalid characters.";
}
else if (msg.getAuthMethod() == Authenticate.AUTH_METHOD.NONE)
{
// todo: add prefix if someone connects with NONE auth
for (GameProtocolConnection otherGame : readyPlayers)
{
ClientState otherState = stateAttachment.get(otherGame);
if (SwissArmyKnife.nicknameCompare(nickname, otherState.nickname) == 0)
{
error = AuthenticateResponse.ERROR.NICKNAME_IN_USE;
errorMessage = "Someone else is already using that nickname.";
break;
}
}
// todo kick the existing player if password authentication is used
}
GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder();
GameS2C.AuthenticateResponse.Builder response = s2c.addAuthenticateResponseBuilder();
response.setError(error);
if (errorMessage != null) response.setErrorDescription(errorMessage);
game.send(s2c);
if (error == AuthenticateResponse.ERROR.OK)
{
state.setNickname(nickname);
state.nextState(STATE.RECEIVED_AUTHENTICATE);
}
else
{
state.nextState(STATE.WAIT_FOR_AUTHENTICATE);
}
}
for (GameC2S.ConnectionReady msg : c2s.getConnectionReadyList())
{
if (state.state != STATE.WAIT_FOR_CONNECTION_READY)
{
log.log(Level.WARNING, "Received a duplicate ConnectionReady or in an invalid state");
break;
}
state.nextState(STATE.RECEIVED_CONNECTION_READY);
}
for (GameC2S.ArenaLoaded msg : c2s.getArenaLoadedList())
{
if (state.state != STATE.WAIT_FOR_ARENA_LOADED)
{
log.log(Level.WARNING, "Received a duplicate ArenaLoaded or in an invalid state");
break;
}
state.nextState(STATE.RECEIVED_ARENA_LOADED);
}
for (GameC2S.Command msg : c2s.getCommandList())
{
state.parseCommand(msg.getName(), msg.getResponseCode(), msg.getArgumentsList());
}
for (GameOperation.ActorMove msg : c2s.getActorMoveList())
{
if (state.state != STATE.READY)
{
log.log(Level.WARNING, "Received an ActorMove in an invalid state");
break;
}
if (msg.getPid() != state.pid)
{
log.log(Level.WARNING, "Received an invalid pid in ActorMove");
break;
}
List<PhysicsMovement> moves = PhysicsMovement.unserializeListLE(msg.getMove().asReadOnlyByteBuffer());
if (moves.isEmpty())
{
log.log(Level.WARNING, "Received no actual moves in ActorMove");
break;
}
// Forward the move operation
GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder();
GameOperation.ActorMove.Builder moveBuilder = s2c.addActorMoveBuilder();
moveBuilder.setPid(msg.getPid());
moveBuilder.setDirect(true);
for (int i = 0; i < moves.size(); ++i)
{
long move_tick = msg.getTick() + i;
if (move_tick > physicsEnv.getTick() + state.MAX_FUTURE_TICKS)
{
// too far into the future, ignore it.
// This also prevents history from being lost in state.receivedMove
continue;
}
PhysicsMovement messageMove = moves.get(i);
PhysicsMovement existingMove = state.receivedMove.get(move_tick);
if (existingMove != null && !existingMove.equals(messageMove))
{
// duplicate move which is unequal, ignore it
log.log(Level.WARNING, "Received a duplicate move from a client which does not match what he sent previously! pid={0}", state.pid);
continue;
}
state.receivedMove.setHistory(move_tick, messageMove);
boolean valid = physicsEnv.actorMove(
move_tick,
msg.getPid(),
moves.get(i));
if (!valid)
{
state.warnDroppedOperation();
moves.set(i, PhysicsMovement.NONE);
}
}
// find end
int end;
for (end = moves.size() - 1; end >= 0; --end)
{
if (moves.get(end).hasEffect())
{
break;
}
}
int start;
// find start
for (start = 0; start < moves.size(); ++start)
{
if (moves.get(start).hasEffect())
{
break;
}
}
moves = moves.subList(start, end + 1);
if (!moves.isEmpty())
{
moveBuilder.setMove(ByteString.copyFrom(PhysicsMovement.serializeListLE(moves)));
moveBuilder.setTick(msg.getTick() + start);
broadcast(s2c, game); // forward to all other clients
}
}
for (GameOperation.ActorWeapon msg : c2s.getActorWeaponList())
{
if (msg.getPid() != state.pid)
{
log.log(Level.WARNING, "Received an invalid pid in ActorWeapon");
continue;
}
if (!WEAPON_SLOT.isValidId(msg.getSlot()))
{
log.log(Level.SEVERE, "Received an ActorWeapon with an invalid slot id {0}", msg.getSlot());
continue;
}
if (msg.getTick() > physicsEnv.getTick() + state.MAX_FUTURE_TICKS)
{
// too far into the future, ignore it
// This also prevents history from being lost in state.receivedWeapon
continue;
}
WEAPON_SLOT messageSlut = WEAPON_SLOT.byId(msg.getSlot());
WEAPON_SLOT existingSlut = state.receivedWeapon.get(msg.getTick());
if (existingSlut != null && !existingSlut.equals(messageSlut))
{
// duplicate weapon which is unequal, ignore it
log.log(Level.WARNING, "Received a duplicate weapon from a client which does not match what he sent previously! pid={0}", state.pid);
continue;
}
state.receivedWeapon.setHistory(msg.getTick(), messageSlut);
GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder();
GameOperation.ActorWeapon.Builder weaponBuilder = s2c.addActorWeaponBuilder();
weaponBuilder.setPid(msg.getPid());
weaponBuilder.setTick(msg.getTick());
weaponBuilder.setSlot(msg.getSlot());
// Doing nothing with the weapon hint for now.
// It might be useful for trusted clients (bots etc)
boolean valid = physicsEnv.actorWeapon(msg.getTick(), msg.getPid(), messageSlut);
if (!valid)
{
state.warnDroppedOperation();
}
broadcast(s2c, game); // forward to all other clients
}
for (GameC2S.SendLocalChat msg : c2s.getSendLocalChatList())
{
String message = msg.getMessage();
if (state.nickname == null || message == null || message.isEmpty())
{
continue;
}
message = annoyance.matcher(message).replaceAll("");
GameS2C.S2C.Builder s2c = GameS2C.S2C.newBuilder();
GameS2C.LocalChatMessage.Builder chat = s2c.addLocalChatMessageBuilder();
chat.setSender(state.nickname);
chat.setMessage(message);
broadcast(s2c); // forward to all clients
}
}
private static final Pattern annoyance = Pattern.compile("n[e|3]wb|n[o0]{2,}b", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
@Override
public void gameS2CMessage(GameProtocolConnection game, GameS2C.S2C s2c, long receivedAt)
{
// should not occur
}
public void broadcast(GameS2C.S2COrBuilder s2c)
{
broadcast(s2c, null);
}
public void broadcast(GameS2C.S2COrBuilder s2c, GameProtocolConnection except)
{
for (GameProtocolConnection conn : readyPlayers)
{
ClientState state = stateAttachment.get(conn);
if (state.state == STATE.READY && conn != except)
{
conn.send(s2c);
}
}
}
public boolean addPhysicsOperationToMessage(GameS2C.S2C.Builder s2c, OperationPublic op)
{
if (op instanceof ActorNewPublic)
{
ActorNewPublic opNew = (ActorNewPublic) op;
NetworkedActor netActor = getActor(opNew.getPid());
GameOperation.ActorNew.Builder actorNew = s2c.addActorNewBuilder();
actorNew.setTick(opNew.getTick());
actorNew.setPid(opNew.getPid());
actorNew.setName(netActor == null ? "??????" : netActor.name);
actorNew.setSeed(opNew.getSeed());
actorNew.setShip(opNew.getShip());
return true;
}
else if (op instanceof ActorRemovePublic)
{
GameOperation.ActorRemove.Builder actorRemove = s2c.addActorRemoveBuilder();
actorRemove.setTick(op.getTick());
actorRemove.setPid(op.getPid());
return true;
}
else if (op instanceof ActorMovePublic)
{
ActorMovePublic opMove = (ActorMovePublic) op;
PhysicsMovement move = opMove.getMove();
if (move.hasEffect())
{
GameOperation.ActorMove.Builder actorMove = s2c.addActorMoveBuilder();
actorMove.setTick(op.getTick());
actorMove.setPid(op.getPid());
actorMove.setMove(ByteString.copyFrom(PhysicsMovement.serializeListLE(Arrays.asList(move))));
return true;
}
return false;
}
else if (op instanceof ActorWarpPublic)
{
ActorWarpPublic opWarp = (ActorWarpPublic) op;
GameOperation.ActorWarp.Builder actorWarp = s2c.addActorWarpBuilder();
actorWarp.setTick(op.getTick());
actorWarp.setPid(op.getPid());
actorWarp.setHint(false);
opWarp.getWarp().toProtobuf(actorWarp);
return true;
}
else if (op instanceof ActorWeaponFirePublic)
{
ActorWeaponFirePublic opWeaponFire = (ActorWeaponFirePublic) op;
GameOperation.ActorWeapon.Builder actorWeapon = s2c.addActorWeaponBuilder();
actorWeapon.setTick(op.getTick());
actorWeapon.setPid(op.getPid());
actorWeapon.setSlot(opWeaponFire.getWeaponSlot().id);
return true;
}
else if (op instanceof ActorModificationPublic)
{
ActorModificationPublic opActorMod = (ActorModificationPublic) op;
GameOperation.ActorModification.Builder actorMod = s2c.addActorModificationBuilder();
actorMod.setTick(op.getTick());
actorMod.setPid(op.getPid());
if (opActorMod.getShip() != null)
{
actorMod.setShip(opActorMod.getShip());
}
return true;
}
else
{
log.log(Level.WARNING, "Unknown operation {0}. IMPLEMENT ME!", op);
return false;
}
}
private final TimerEvent actorSyncTimer = new TimerEvent()
{
@Override
public boolean timerElapsed(long tick)
{
long now = System.nanoTime();
long highest = 0;
ClientState lowest_client = null;
for (GameProtocolConnection game : readyPlayers)
{
ClientState state = stateAttachment.get(game);
long ago = now - state.lastActorSyncBroadcast_nanos;
if (ago < SYNC_ACTOR_EVERY_NANOS)
{
continue;
}
if (ago > highest)
{
highest = state.lastActorSyncBroadcast_nanos;
lowest_client = state;
}
}
if (lowest_client != null)
{
lowest_client.broadcastActorSync();
}
return true; // do not remove timer
}
};
}