diff --git a/README.md b/README.md index 935b30b..976efa0 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,10 @@ gradle desktop:dist To turn dev mode on, please run the game with `DESKTOP_DEV_MODE=true` environment variable. Dev mode features: - Smaller window size, so you can run multiple game windows at the same time and see IDE logs on-fly - Cursor gets "un-caught" when "Esc" is pressed during death-match, so you can use the mouse to go back to IDE or run one more game window -- Basic network metrics are rendered on the screen during death-match + +#### Debug controls +- N - see network metrics +- P - log current player position ## Distribution diff --git a/build.gradle b/build.gradle index bfdd0ac..9ddef1b 100644 --- a/build.gradle +++ b/build.gradle @@ -95,7 +95,7 @@ project(":core") { implementation 'commons-io:commons-io:2.8.0' api 'org.apache.commons:commons-lang3:3.13.0' api 'ch.qos.logback:logback-classic:1.4.12' - api "com.github.beverly-hills-money-gangster.Daikombat-server:net-client:8.0.2" + api "com.github.beverly-hills-money-gangster.Daikombat-server:net-client:9f29c6360b" api 'commons-validator:commons-validator:1.7' api 'org.apache.commons:commons-math3:3.6.1' compileOnly 'org.projectlombok:lombok:1.18.30' diff --git a/core/src/com/beverly/hills/money/gang/entities/enemies/Enemy.java b/core/src/com/beverly/hills/money/gang/entities/enemies/Enemy.java index 1bddd8b..af94ab6 100644 --- a/core/src/com/beverly/hills/money/gang/entities/enemies/Enemy.java +++ b/core/src/com/beverly/hills/money/gang/entities/enemies/Enemy.java @@ -117,7 +117,7 @@ public void getHit() { } protected long getAnimationTimeoutMls() { - return System.currentTimeMillis() + 100; + return System.currentTimeMillis() + 150; } diff --git a/core/src/com/beverly/hills/money/gang/entities/enemies/EnemyPlayer.java b/core/src/com/beverly/hills/money/gang/entities/enemies/EnemyPlayer.java index cc59e70..3873b52 100644 --- a/core/src/com/beverly/hills/money/gang/entities/enemies/EnemyPlayer.java +++ b/core/src/com/beverly/hills/money/gang/entities/enemies/EnemyPlayer.java @@ -12,7 +12,6 @@ import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.math.Vector3; -import com.beverly.hills.money.gang.Configs; import com.beverly.hills.money.gang.Constants; import com.beverly.hills.money.gang.assets.managers.registry.TexturesRegistry; import com.beverly.hills.money.gang.entities.player.Player; @@ -20,7 +19,6 @@ import com.beverly.hills.money.gang.rect.RectanglePlus; import com.beverly.hills.money.gang.rect.filters.RectanglePlusFilter; import com.beverly.hills.money.gang.screens.GameScreen; -import com.beverly.hills.money.gang.screens.ui.selection.SkinUISelection; import java.util.ArrayDeque; import java.util.Queue; import lombok.Getter; @@ -33,6 +31,8 @@ public class EnemyPlayer extends Enemy { private final EnemyTextures enemyTextures; + private int lastEventSequenceId = -1; + private static final int MAX_ACTION_QUEUE_CLOGGING = 30; private final Queue actions = new ArrayDeque<>(); @@ -93,20 +93,43 @@ public void queueAction(EnemyPlayerAction enemyPlayerAction) { } else { this.currentSpeed = getSpeed(actions, this.defaultSpeed); } - actions.add(enemyPlayerAction); - } + // if in order + if (enemyPlayerAction.getEventSequenceId() > lastEventSequenceId) { + lastEventSequenceId = enemyPlayerAction.getEventSequenceId(); + actions.add(enemyPlayerAction); + return; + } + // fix out-of-order case + switch (enemyPlayerAction.getEnemyPlayerActionType()) { + case MOVE -> LOG.warn( + "MOVE event is out of order. Last event sequence id {} but given {}. Skip event.", + lastEventSequenceId, enemyPlayerAction.getEventSequenceId()); + case SHOOT, PUNCH -> { + LOG.warn( + "SHOOT/PUNCH event is out of order. Last event sequence id {} but given {}", + lastEventSequenceId, enemyPlayerAction.getEventSequenceId()); + actions.add(EnemyPlayerAction.builder() + .enemyPlayerActionType(enemyPlayerAction.getEnemyPlayerActionType()) + .eventSequenceId(enemyPlayerAction.getEventSequenceId()) + .onComplete(enemyPlayerAction.getOnComplete()) + .direction(enemyPlayerAction.getDirection()) + .route(getRect().getOldPosition()) + .build()); + } + } + } static float getSpeed(final Queue actions, final float defaultSpeed) { if (actions.size() > 15) { - LOG.info("Action queue is super clogged. Size {}", actions.size()); - return defaultSpeed * 3f; + LOG.warn("Action queue is super clogged. Size {}", actions.size()); + return defaultSpeed * 3f; } else if (actions.size() > 10) { - LOG.info("Action queue is very clogged. Size {}", actions.size()); + LOG.warn("Action queue is very clogged. Size {}", actions.size()); return defaultSpeed * 2f; } else if (actions.size() >= 5) { - LOG.info("Action queue is clogged. Size {}", actions.size()); + LOG.warn("Action queue is clogged. Size {}", actions.size()); return defaultSpeed * 1.25f; } else if (actions.size() > 2) { return defaultSpeed * 1.15f; @@ -130,7 +153,7 @@ public void render3D(final ModelBatch mdlBatch, final Environment env, final flo getMdlInst().transform.setToLookAt( getScreen().getCurrentCam().direction.cpy().rotate(Vector3.Z, 180f), Vector3.Y); getMdlInst().transform.setTranslation(getPosition().cpy().add(0, Constants.HALF_UNIT, 0)); - // otherwise, it the enemy is too "fat" + // otherwise, the enemy is too "fat" getMdlInst().transform.scale(0.8f, 1f, 1); super.render3D(mdlBatch, env, delta); } @@ -169,7 +192,7 @@ public void tick(final float delta) { } } - } else { + } else if (System.currentTimeMillis() >= movingAnimationUntil) { isIdle = true; } getRect().setX(getRect().getNewPosition().x); @@ -198,41 +221,41 @@ private TextureRegion getTextureToUse() { float angle = playerDirection.angleDeg(lastDirection); if (Constants.FRONT_RANGE.contains(angle)) { - if (keepShootingAnimation()) { + if (isShootingAnimation()) { return enemyTextures.getEnemyPlayerTextureRegion(EnemyTextureRegistry.SHOOTINGTEXFRONTREG); } else if (isIdle) { return enemyTextures.getEnemyPlayerTextureRegion(EnemyTextureRegistry.IDLETEXFRONTREG); - } else if (keepMovingAnimation()) { + } else if (isNextStepAnimation()) { movingAnimationUntil = getAnimationTimeoutMls(); currentStep++; return enemyTextures.getEnemyPlayerMoveFrontTextureRegion(currentStep); } } else if (Constants.LEFT_RANGE.contains(angle)) { - if (keepShootingAnimation()) { + if (isShootingAnimation()) { return enemyTextures.getEnemyPlayerTextureRegion(EnemyTextureRegistry.SHOOTINGTEXLEFTREG); } else if (isIdle) { return enemyTextures.getEnemyPlayerTextureRegion(EnemyTextureRegistry.IDLETEXLEFTREG); - } else if (keepMovingAnimation()) { + } else if (isNextStepAnimation()) { movingAnimationUntil = getAnimationTimeoutMls(); currentStep++; return enemyTextures.getEnemyPlayerMoveLeftTextureRegion(currentStep); } } else if (Constants.RIGHT_RANGE.contains(angle)) { - if (keepShootingAnimation()) { + if (isShootingAnimation()) { return enemyTextures.getEnemyPlayerTextureRegion(EnemyTextureRegistry.SHOOTINGTEXRIGHTREG); } else if (isIdle) { return enemyTextures.getEnemyPlayerTextureRegion(EnemyTextureRegistry.IDLETEXRIGHTREG); - } else if (keepMovingAnimation()) { + } else if (isNextStepAnimation()) { movingAnimationUntil = getAnimationTimeoutMls(); currentStep++; return enemyTextures.getEnemyPlayerMoveRightTextureRegion(currentStep); } } else { - if (keepShootingAnimation()) { + if (isShootingAnimation()) { return enemyTextures.getEnemyPlayerTextureRegion(EnemyTextureRegistry.SHOOTINGTEXBACKTREG); } else if (isIdle) { return enemyTextures.getEnemyPlayerTextureRegion(EnemyTextureRegistry.IDLETEXBACKREG); - } else if (keepMovingAnimation()) { + } else if (isNextStepAnimation()) { movingAnimationUntil = getAnimationTimeoutMls(); currentStep += 1; return enemyTextures.getEnemyPlayerMoveBackTextureRegion(currentStep); @@ -241,11 +264,11 @@ private TextureRegion getTextureToUse() { return null; } - private boolean keepShootingAnimation() { + private boolean isShootingAnimation() { return System.currentTimeMillis() <= shootingAnimationUntil; } - private boolean keepMovingAnimation() { + private boolean isNextStepAnimation() { return System.currentTimeMillis() >= movingAnimationUntil; } diff --git a/core/src/com/beverly/hills/money/gang/entities/enemies/EnemyPlayerAction.java b/core/src/com/beverly/hills/money/gang/entities/enemies/EnemyPlayerAction.java index b2c9085..475184a 100644 --- a/core/src/com/beverly/hills/money/gang/entities/enemies/EnemyPlayerAction.java +++ b/core/src/com/beverly/hills/money/gang/entities/enemies/EnemyPlayerAction.java @@ -4,6 +4,7 @@ import lombok.Builder; import lombok.Builder.Default; import lombok.Getter; +import lombok.NonNull; @Getter @Builder @@ -12,6 +13,8 @@ public class EnemyPlayerAction { private final Vector2 route; private final Vector2 direction; private final EnemyPlayerActionType enemyPlayerActionType; + @NonNull + private final Integer eventSequenceId; @Default private final Runnable onComplete = () -> { }; diff --git a/core/src/com/beverly/hills/money/gang/entities/ui/UINetworkStats.java b/core/src/com/beverly/hills/money/gang/entities/ui/UINetworkStats.java new file mode 100644 index 0000000..b7adf69 --- /dev/null +++ b/core/src/com/beverly/hills/money/gang/entities/ui/UINetworkStats.java @@ -0,0 +1,61 @@ +package com.beverly.hills.money.gang.entities.ui; + +import com.beverly.hills.money.gang.stats.NetworkStatsReader; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.apache.commons.io.FileUtils; + +@RequiredArgsConstructor +public class UINetworkStats { + + private final NetworkStatsReader primaryNetworkStats; + private final Iterable secondaryNetworkStats; + + private String networkStatsToString(int receivedMsg, int sentMsg, long inboundBytes, + long outboundBytes, Integer pingMls) { + return String.format(Locale.ENGLISH, + "RCV %s MSG | SENT %s MSG | IN %s | OUT %s | PING %s MS", + receivedMsg, + sentMsg, + FileUtils.byteCountToDisplaySize(inboundBytes), + FileUtils.byteCountToDisplaySize(outboundBytes), + Objects.toString(pingMls, "-")) + .toUpperCase(); + } + + private String networkStatsToString(NetworkStatsReader networkStatsReader) { + return networkStatsToString(networkStatsReader.getReceivedMessages(), + networkStatsReader.getSentMessages(), networkStatsReader.getInboundPayloadBytes(), + networkStatsReader.getOutboundPayloadBytes(), networkStatsReader.getPingMls()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + int totalReceivedMsg = primaryNetworkStats.getReceivedMessages(); + int totalSentMsg = primaryNetworkStats.getSentMessages(); + long totalInboundBytes = primaryNetworkStats.getInboundPayloadBytes(); + long totalOutboundBytes = primaryNetworkStats.getOutboundPayloadBytes(); + sb.append("PRIMARY ").append(networkStatsToString(primaryNetworkStats)).append("\n"); + int totalPing = Optional.ofNullable(primaryNetworkStats.getPingMls()).orElse(0); + int totalConnections = 1; + for (NetworkStatsReader networkStatsReader : secondaryNetworkStats) { + sb.append("SECONDARY ").append(networkStatsToString(networkStatsReader)) + .append("\n"); + totalReceivedMsg += networkStatsReader.getReceivedMessages(); + totalSentMsg += networkStatsReader.getSentMessages(); + totalInboundBytes += networkStatsReader.getInboundPayloadBytes(); + totalOutboundBytes += networkStatsReader.getOutboundPayloadBytes(); + totalPing += Optional.ofNullable(networkStatsReader.getPingMls()).orElse(0); + totalConnections++; + } + sb.append("TOTAL ").append( + networkStatsToString(totalReceivedMsg, totalSentMsg, totalInboundBytes, totalOutboundBytes, + totalPing / totalConnections)) + .append("\n"); + return sb.toString(); + } + +} diff --git a/core/src/com/beverly/hills/money/gang/handler/PlayScreenGameConnectionHandler.java b/core/src/com/beverly/hills/money/gang/handler/PlayScreenGameConnectionHandler.java index e062031..a643d5e 100644 --- a/core/src/com/beverly/hills/money/gang/handler/PlayScreenGameConnectionHandler.java +++ b/core/src/com/beverly/hills/money/gang/handler/PlayScreenGameConnectionHandler.java @@ -31,8 +31,6 @@ public class PlayScreenGameConnectionHandler { private static final Logger LOG = LoggerFactory.getLogger(PlayScreenGameConnectionHandler.class); - private static final int EVENTS_TO_POLL = 10; - private final PlayScreen playScreen; private boolean initialRequestHandled; @@ -44,7 +42,8 @@ public void handle() { LOG.info("Stop handling"); return; } - playScreen.getGameConnection().getResponse().poll(EVENTS_TO_POLL).forEach(serverResponse -> { + // TODO sort game responses + playScreen.getGameConnection().pollResponses().forEach(serverResponse -> { if (serverResponse.hasChatEvents()) { handleChat(serverResponse.getChatEvents()); } else if (serverResponse.hasGameEvents()) { @@ -57,9 +56,7 @@ public void handle() { handlePowerUpSpawn(serverResponse.getPowerUpSpawn()); } }); - - playScreen.getGameConnection().getErrors().poll() - .ifPresent(this::handleException); + playScreen.getGameConnection().pollErrors().forEach(this::handleException); if (!initialRequestHandled) { initialRequestHandled = true; } @@ -107,6 +104,7 @@ private void handleSpawn(ServerResponse.GameEvent gameEvent) { LOG.info("Player already spawned {}", gameEvent); return; } + LOG.info("Spawn player {}", gameEvent.getPlayer().getPlayerName()); EnemyPlayer enemyPlayer = new EnemyPlayer(playScreen.getPlayer(), gameEvent.getPlayer().getPlayerId(), new Vector3(gameEvent.getPlayer().getPosition().getX(), @@ -179,6 +177,7 @@ private void handleMove(ServerResponse.GameEvent gameEvent) { gameEvent.getPlayer().getActivePowerUpsList().forEach(gamePowerUp -> activateEnemyPowerUp(enemyPlayer, gamePowerUp)); enemyPlayer.queueAction(EnemyPlayerAction.builder() + .eventSequenceId(gameEvent.getSequence()) .enemyPlayerActionType(EnemyPlayerActionType.MOVE) .direction(Converter.convertToVector2(gameEvent.getPlayer().getDirection())) .route(Converter.convertToVector2(gameEvent.getPlayer().getPosition())).build()); @@ -236,6 +235,7 @@ private void handleAttackMiss(ServerResponse.GameEvent gameEvent) { }; enemiesRegistry.getEnemy(gameEvent.getPlayer().getPlayerId()) .ifPresent(enemyPlayer -> enemyPlayer.queueAction(EnemyPlayerAction.builder() + .eventSequenceId(gameEvent.getSequence()) .enemyPlayerActionType(enemyPlayerActionType) .direction(Converter.convertToVector2(gameEvent.getPlayer().getDirection())) .route(Converter.convertToVector2(gameEvent.getPlayer().getPosition())).build())); @@ -258,6 +258,7 @@ private void handleGetHit(ServerResponse.GameEvent gameEvent) { enemiesRegistry.getEnemy(gameEvent.getPlayer().getPlayerId()) .ifPresent(enemyPlayer -> { enemyPlayer.queueAction(EnemyPlayerAction.builder() + .eventSequenceId(gameEvent.getSequence()) .enemyPlayerActionType(enemyPlayerActionType) .direction(Converter.convertToVector2(gameEvent.getPlayer().getDirection())) .route(Converter.convertToVector2(gameEvent.getPlayer().getPosition())) @@ -274,6 +275,7 @@ private void handleGetHit(ServerResponse.GameEvent gameEvent) { // enemies hitting each other enemiesRegistry.getEnemy(gameEvent.getPlayer().getPlayerId()) .ifPresent(enemyPlayer -> enemyPlayer.queueAction(EnemyPlayerAction.builder() + .eventSequenceId(gameEvent.getSequence()) .enemyPlayerActionType(enemyPlayerActionType) .direction(Converter.convertToVector2(gameEvent.getPlayer().getDirection())) .route(Converter.convertToVector2(gameEvent.getPlayer().getPosition())) @@ -304,6 +306,7 @@ private void handleDeath(ServerResponse.GameEvent gameEvent) { enemiesRegistry.getEnemy(gameEvent.getPlayer().getPlayerId()) .ifPresent(enemyPlayer -> enemyPlayer.queueAction(EnemyPlayerAction.builder() + .eventSequenceId(gameEvent.getSequence()) .enemyPlayerActionType(enemyPlayerActionType) .direction(Converter.convertToVector2(gameEvent.getPlayer().getDirection())) .route(Converter.convertToVector2(gameEvent.getPlayer().getPosition())) @@ -340,6 +343,7 @@ private void handleDeath(ServerResponse.GameEvent gameEvent) { } else { enemiesRegistry.getEnemy(gameEvent.getPlayer().getPlayerId()) .ifPresent(enemyPlayer -> enemyPlayer.queueAction(EnemyPlayerAction.builder() + .eventSequenceId(gameEvent.getSequence()) .enemyPlayerActionType(enemyPlayerActionType) .direction(Converter.convertToVector2(gameEvent.getPlayer().getDirection())) .route(Converter.convertToVector2(gameEvent.getPlayer().getPosition())) diff --git a/core/src/com/beverly/hills/money/gang/screens/ErrorScreen.java b/core/src/com/beverly/hills/money/gang/screens/ErrorScreen.java index 6528b9c..18a692f 100644 --- a/core/src/com/beverly/hills/money/gang/screens/ErrorScreen.java +++ b/core/src/com/beverly/hills/money/gang/screens/ErrorScreen.java @@ -8,9 +8,12 @@ import com.beverly.hills.money.gang.DaiKombatGame; import com.beverly.hills.money.gang.assets.managers.registry.FontRegistry; import java.util.Locale; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ErrorScreen extends AbstractMainMenuScreen { + private static final Logger LOG = LoggerFactory.getLogger(JoinGameScreen.class); private final String errorMessage; private final BitmapFont guiFont32; @@ -20,6 +23,7 @@ public class ErrorScreen extends AbstractMainMenuScreen { public ErrorScreen(final DaiKombatGame game, final String errorMessage) { super(game); this.errorMessage = errorMessage.toUpperCase(Locale.ENGLISH); + LOG.error("Go to error screen with error '{}'", errorMessage); guiFont32 = game.getAssMan().getFont(FontRegistry.FONT_32); guiFont64 = game.getAssMan().getFont(FontRegistry.FONT_64); } diff --git a/core/src/com/beverly/hills/money/gang/screens/GameOverScreen.java b/core/src/com/beverly/hills/money/gang/screens/GameOverScreen.java index f442230..90c630d 100644 --- a/core/src/com/beverly/hills/money/gang/screens/GameOverScreen.java +++ b/core/src/com/beverly/hills/money/gang/screens/GameOverScreen.java @@ -24,7 +24,6 @@ public class GameOverScreen extends AbstractMainMenuScreen { private final UserSettingSound boomSound1; private final UserSettingSound dingSound1; private final UserSettingSound youWinMusic; - private final UserSettingSound youWinVoice; private final GlyphLayout glyphLayoutWinner; private final UILeaderBoard uiLeaderBoard; private boolean showLeaderBoard; @@ -37,13 +36,13 @@ public class GameOverScreen extends AbstractMainMenuScreen { public GameOverScreen(final DaiKombatGame game, final UILeaderBoard uiLeaderBoard, final JoinGameData joinGameData) { + // TODO show winner player skin instead of logo super(game); guiFont64 = game.getAssMan().getFont(FontRegistry.FONT_64); guiFont32 = game.getAssMan().getFont(FontRegistry.FONT_32); boomSound1 = game.getAssMan().getUserSettingSound(SoundRegistry.BOOM_1); dingSound1 = game.getAssMan().getUserSettingSound(SoundRegistry.DING_1); youWinMusic = game.getAssMan().getUserSettingSound(SoundRegistry.WIN_MUSIC); - youWinVoice = game.getAssMan().getUserSettingSound(SoundRegistry.YOU_WIN); this.joinGameData = joinGameData; this.uiLeaderBoard = uiLeaderBoard; leaderMessage = "WINNER IS " + uiLeaderBoard.getFirstPlaceStats(); @@ -51,7 +50,6 @@ public GameOverScreen(final DaiKombatGame game, if (uiLeaderBoard.getMyPlace() == 1) { stopBgMusic(); youWinMusic.play(DEFAULT_MUSIC_VOLUME * 1.2f); - youWinVoice.play(Constants.QUAKE_NARRATOR_FX_VOLUME); } } diff --git a/core/src/com/beverly/hills/money/gang/screens/GetServerInfoScreen.java b/core/src/com/beverly/hills/money/gang/screens/GetServerInfoScreen.java index a7fdcc4..f5a052b 100644 --- a/core/src/com/beverly/hills/money/gang/screens/GetServerInfoScreen.java +++ b/core/src/com/beverly/hills/money/gang/screens/GetServerInfoScreen.java @@ -4,9 +4,12 @@ import com.beverly.hills.money.gang.entity.GameServerCreds; import com.beverly.hills.money.gang.entity.HostPort; import com.beverly.hills.money.gang.network.GameConnection; +import com.beverly.hills.money.gang.network.LoadBalancedGameConnection; +import com.beverly.hills.money.gang.network.SecondaryGameConnection; import com.beverly.hills.money.gang.proto.GetServerInfoCommand; import com.beverly.hills.money.gang.screens.data.JoinGameData; import com.beverly.hills.money.gang.screens.data.PlayerConnectionContextData; +import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -20,7 +23,7 @@ public class GetServerInfoScreen extends AbstractLoadingScreen { private final AtomicReference errorMessage = new AtomicReference<>(); private final JoinGameData joinGameData; - private final AtomicReference gameConnectionRef = new AtomicReference<>(); + private final AtomicReference gameConnectionRef = new AtomicReference<>(); private final PlayerConnectionContextData.PlayerConnectionContextDataBuilder playerContextDataBuilder; @@ -36,14 +39,25 @@ public GetServerInfoScreen(final DaiKombatGame game, public void show() { new Thread(() -> { try { - gameConnectionRef.set(new GameConnection(GameServerCreds.builder() + var creds = GameServerCreds.builder() .hostPort(HostPort.builder() .host(joinGameData.getServerHost()) .port(joinGameData.getServerPort()).build()) .password(joinGameData.getServerPassword()) - .build(), - gameConnection -> gameConnection.write(GetServerInfoCommand.newBuilder().build()))); - + .build(); + var connection = new GameConnection(creds); + List secondaryGameConnections + = List.of(new SecondaryGameConnection(creds), new SecondaryGameConnection(creds)); + LoadBalancedGameConnection loadBalancedGameConnection + = new LoadBalancedGameConnection( + connection, secondaryGameConnections); + if (!loadBalancedGameConnection.waitUntilAllConnected(5_000)) { + errorMessage.set("Connection timeout"); + loadBalancedGameConnection.disconnect(); + return; + } + gameConnectionRef.set(loadBalancedGameConnection); + connection.write(GetServerInfoCommand.newBuilder().build()); } catch (Throwable e) { LOG.error("Can't create connection", e); errorMessage.set(ExceptionUtils.getMessage(e)); @@ -54,40 +68,46 @@ public void show() { @Override protected void onEscape() { - Optional.ofNullable(gameConnectionRef.get()).ifPresent(GameConnection::disconnect); + Optional.ofNullable(gameConnectionRef.get()).ifPresent(LoadBalancedGameConnection::disconnect); } @Override protected void onLoadingRender(final float delta) { if (errorMessage.get() != null) { removeAllEntities(); + LOG.error("Got error '{}'", errorMessage.get()); + Optional.ofNullable(gameConnectionRef.get()).ifPresent(LoadBalancedGameConnection::disconnect); getGame().setScreen(new ErrorScreen(getGame(), errorMessage.get())); return; } - Optional.ofNullable(gameConnectionRef.get()) - .ifPresent(conn -> conn.getResponse().poll().ifPresentOrElse(response -> { - if (response.hasErrorEvent()) { - errorMessage.set(response.getErrorEvent().getMessage()); - } else if (response.hasServerInfo()) { - var serverInfo = response.getServerInfo(); - playerContextDataBuilder.movesUpdateFreqMls(serverInfo.getMovesUpdateFreqMls()); - playerContextDataBuilder.fragsToWin(serverInfo.getFragsToWin()); - playerContextDataBuilder.speed(serverInfo.getPlayerSpeed()); - removeAllEntities(); - getGame().setScreen(new JoinGameScreen(getGame(), - playerContextDataBuilder, joinGameData, conn)); - } - }, () -> conn.getErrors().poll().ifPresent(throwable -> { - LOG.error("Error while loading", throwable); - conn.disconnect(); - errorMessage.set(ExceptionUtils.getMessage(throwable)); - }))); + Optional.ofNullable(gameConnectionRef.get()).ifPresent( + connection -> { + connection.pollPrimaryConnectionResponse().ifPresent(response -> { + if (response.hasErrorEvent()) { + + errorMessage.set(response.getErrorEvent().getMessage()); + } else if (response.hasServerInfo()) { + var serverInfo = response.getServerInfo(); + playerContextDataBuilder.movesUpdateFreqMls(serverInfo.getMovesUpdateFreqMls()); + playerContextDataBuilder.fragsToWin(serverInfo.getFragsToWin()); + playerContextDataBuilder.speed(serverInfo.getPlayerSpeed()); + removeAllEntities(); + getGame().setScreen(new JoinGameScreen(getGame(), + playerContextDataBuilder, joinGameData, connection)); + } + }); + connection.pollErrors().stream().findFirst().ifPresent(throwable -> { + LOG.error("Error while loading", throwable); + connection.disconnect(); + errorMessage.set(ExceptionUtils.getMessage(throwable)); + }); + }); } @Override protected void onTimeout() { - Optional.ofNullable(gameConnectionRef.get()).ifPresent(GameConnection::disconnect); + Optional.ofNullable(gameConnectionRef.get()).ifPresent(LoadBalancedGameConnection::disconnect); } } diff --git a/core/src/com/beverly/hills/money/gang/screens/JoinGameScreen.java b/core/src/com/beverly/hills/money/gang/screens/JoinGameScreen.java index f6a6d28..e373b70 100644 --- a/core/src/com/beverly/hills/money/gang/screens/JoinGameScreen.java +++ b/core/src/com/beverly/hills/money/gang/screens/JoinGameScreen.java @@ -3,15 +3,15 @@ import com.beverly.hills.money.gang.Configs; import com.beverly.hills.money.gang.DaiKombatGame; import com.beverly.hills.money.gang.config.ClientConfig; -import com.beverly.hills.money.gang.network.GameConnection; +import com.beverly.hills.money.gang.network.LoadBalancedGameConnection; import com.beverly.hills.money.gang.proto.JoinGameCommand; +import com.beverly.hills.money.gang.proto.MergeConnectionCommand; import com.beverly.hills.money.gang.proto.ServerResponse; import com.beverly.hills.money.gang.proto.SkinColorSelection; import com.beverly.hills.money.gang.screens.data.JoinGameData; import com.beverly.hills.money.gang.screens.data.PlayerConnectionContextData; import com.beverly.hills.money.gang.screens.ui.selection.SkinUISelection; import com.beverly.hills.money.gang.utils.Converter; -import java.util.Optional; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,7 +21,7 @@ public class JoinGameScreen extends AbstractLoadingScreen { private static final Logger LOG = LoggerFactory.getLogger(JoinGameScreen.class); private String errorMessage; - private final GameConnection gameConnection; + private final LoadBalancedGameConnection gameConnection; private final PlayerConnectionContextData.PlayerConnectionContextDataBuilder playerContextDataBuilder; private final JoinGameData joinGameData; @@ -29,7 +29,7 @@ public class JoinGameScreen extends AbstractLoadingScreen { public JoinGameScreen(final DaiKombatGame game, final PlayerConnectionContextData.PlayerConnectionContextDataBuilder playerContextDataBuilder, final JoinGameData joinGameData, - final GameConnection gameConnection) { + final LoadBalancedGameConnection gameConnection) { super(game); this.gameConnection = gameConnection; this.joinGameData = joinGameData; @@ -38,7 +38,7 @@ public JoinGameScreen(final DaiKombatGame game, @Override public void show() { - if (gameConnection.isDisconnected()) { + if (gameConnection.isAnyDisconnected()) { errorMessage = "Connection lost"; } else { gameConnection.write(JoinGameCommand.newBuilder() @@ -71,13 +71,15 @@ protected void onEscape() { @Override protected void onLoadingRender(final float delta) { if (errorMessage != null) { + LOG.error("Got error while loading {}", errorMessage); removeAllEntities(); + gameConnection.disconnect(); getGame().setScreen(new ErrorScreen(getGame(), errorMessage)); return; } - Optional.ofNullable(gameConnection).ifPresent(gameConnection - -> gameConnection.getResponse().poll().ifPresentOrElse(response -> { + gameConnection.pollPrimaryConnectionResponse().ifPresent(response -> { if (response.hasErrorEvent()) { + LOG.error("Error {}", errorMessage); errorMessage = response.getErrorEvent().getMessage(); } else if (response.hasGameEvents()) { removeAllEntities(); @@ -85,16 +87,22 @@ protected void onLoadingRender(final float delta) { getGame().setScreen( new PlayScreen(getGame(), gameConnection, createPlayerContextData(response))); } - }, () -> gameConnection.getErrors().poll().ifPresent(throwable -> { + }); + + gameConnection.pollErrors().stream().findAny().ifPresent(throwable -> { LOG.error("Error while loading", throwable); gameConnection.disconnect(); errorMessage = ExceptionUtils.getMessage(throwable); - }))); + }); } private PlayerConnectionContextData createPlayerContextData(ServerResponse response) { var mySpawnEvent = response.getGameEvents().getEvents(0); int playerId = mySpawnEvent.getPlayer().getPlayerId(); + gameConnection.getSecondaryGameConnections().forEach( + secondaryGameConnection -> secondaryGameConnection.write( + MergeConnectionCommand.newBuilder().setGameId(Configs.GAME_ID).setPlayerId(playerId) + .build())); return playerContextDataBuilder .playerId(playerId) .playersOnline(response.getGameEvents().getPlayersOnline()) diff --git a/core/src/com/beverly/hills/money/gang/screens/PlayScreen.java b/core/src/com/beverly/hills/money/gang/screens/PlayScreen.java index 870337f..a36e898 100644 --- a/core/src/com/beverly/hills/money/gang/screens/PlayScreen.java +++ b/core/src/com/beverly/hills/money/gang/screens/PlayScreen.java @@ -26,11 +26,12 @@ import com.beverly.hills.money.gang.entities.item.PowerUpType; import com.beverly.hills.money.gang.entities.player.Player; import com.beverly.hills.money.gang.entities.ui.UILeaderBoard; +import com.beverly.hills.money.gang.entities.ui.UINetworkStats; import com.beverly.hills.money.gang.handler.PlayScreenGameConnectionHandler; import com.beverly.hills.money.gang.input.TextInputProcessor; import com.beverly.hills.money.gang.log.ChatLog; import com.beverly.hills.money.gang.log.MyPlayerKillLog; -import com.beverly.hills.money.gang.network.GameConnection; +import com.beverly.hills.money.gang.network.LoadBalancedGameConnection; import com.beverly.hills.money.gang.proto.PushChatEventCommand; import com.beverly.hills.money.gang.proto.PushGameEventCommand; import com.beverly.hills.money.gang.screens.data.PlayerConnectionContextData; @@ -38,13 +39,12 @@ import com.beverly.hills.money.gang.screens.ui.selection.DeadPlayUISelection; import com.beverly.hills.money.gang.screens.ui.selection.UISelection; import java.util.HashMap; -import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import lombok.Getter; import lombok.Setter; -import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,6 +56,7 @@ public class PlayScreen extends GameScreen { private static final int DEAD_SCREEN_INPUT_DELAY_MLS = 1_000; private GameScreen screenToTransition; + private boolean showNetworkStats; private final SoundQueue narratorSoundQueue = new SoundQueue(1_250, Constants.QUAKE_NARRATOR_FX_VOLUME); private static final int MAX_CHAT_MSG_LEN = 32; @@ -78,6 +79,7 @@ public class PlayScreen extends GameScreen { private final UserSettingSound oneFragLeftSound; private final UserSettingSound twoFragsLeftSound; private final UserSettingSound threeFragsLeftSound; + private int actionSequence; @Getter @Setter @@ -107,16 +109,20 @@ public class PlayScreen extends GameScreen { private final ChatLog chatLog; private final MyPlayerKillLog myPlayerKillLog; - private final GameConnection gameConnection; + private final LoadBalancedGameConnection gameConnection; private final PlayerConnectionContextData playerConnectionContextData; + private final UINetworkStats uiNetworkStats; + private final Map powerUps = new HashMap<>(); public PlayScreen(final DaiKombatGame game, - final GameConnection gameConnection, + final LoadBalancedGameConnection gameConnection, final PlayerConnectionContextData playerConnectionContextData) { super(game, new StretchViewport(Gdx.graphics.getWidth(), Gdx.graphics.getHeight())); this.gameConnection = gameConnection; + this.uiNetworkStats = new UINetworkStats(gameConnection.getPrimaryNetworkStats(), + gameConnection.getSecondaryNetworkStats()); dingSound1 = getGame().getAssMan().getUserSettingSound(SoundRegistry.DING_1); this.playerConnectionContextData = playerConnectionContextData; this.playersOnline = playerConnectionContextData.getPlayersOnline(); @@ -168,7 +174,11 @@ public PlayScreen(final DaiKombatGame game, playerWeapon.getPlayer().playWeaponHitSound(playerWeapon.getWeapon()); gameConnection.write(PushGameEventCommand.newBuilder() .setGameId(Configs.GAME_ID) + .setPingMls( + Optional.ofNullable(gameConnection.getPrimaryNetworkStats().getPingMls()) + .orElse(0)) .setPlayerId(playerConnectionContextData.getPlayerId()) + .setSequence(actionSequence++) .setDirection( PushGameEventCommand.Vector.newBuilder().setX(direction.x).setY(direction.y) .build()) @@ -183,6 +193,10 @@ public PlayScreen(final DaiKombatGame game, // if we haven't hit anybody gameConnection.write(PushGameEventCommand.newBuilder() .setGameId(Configs.GAME_ID) + .setSequence(actionSequence++) + .setPingMls( + Optional.ofNullable(gameConnection.getPrimaryNetworkStats().getPingMls()) + .orElse(0)) .setPlayerId(playerConnectionContextData.getPlayerId()) .setDirection( PushGameEventCommand.Vector.newBuilder().setX(direction.x).setY(direction.y) @@ -201,7 +215,7 @@ public PlayScreen(final DaiKombatGame game, }, player -> { if (System.currentTimeMillis() < nextTimeToFlushPlayerActions - || gameConnection.isDisconnected()) { + || gameConnection.isAnyDisconnected()) { return; } sendCurrentPlayerPosition(); @@ -266,6 +280,9 @@ public void spawnPowerUp(PowerUpType powerUpType, Vector2 position) { var currentPosition = getPlayer().getCurrent2DPosition(); var currentDirection = getPlayer().getCurrent2DDirection(); gameConnection.write(PushGameEventCommand.newBuilder() + .setSequence(actionSequence++) + .setPingMls(Optional.ofNullable(gameConnection.getPrimaryNetworkStats().getPingMls()) + .orElse(0)) .setPlayerId(playerConnectionContextData.getPlayerId()) .setEventType(powerUpType.getPickType()) .setGameId(Configs.GAME_ID) @@ -326,6 +343,9 @@ public void handleInput(final float delta) { if (Gdx.input.isKeyJustPressed(Keys.P)) { LOG.info("Player position is {}", getPlayer().getCurrent2DPosition()); } + if (Gdx.input.isKeyJustPressed(Keys.N)) { + showNetworkStats = !showNetworkStats; + } getPlayer().handleInput(delta); } } @@ -407,6 +427,9 @@ private void sendCurrentPlayerPosition() { var currentPosition = getPlayer().getCurrent2DPosition(); var currentDirection = getPlayer().getCurrent2DDirection(); gameConnection.write(PushGameEventCommand.newBuilder() + .setPingMls( + Optional.ofNullable(gameConnection.getPrimaryNetworkStats().getPingMls()).orElse(0)) + .setSequence(actionSequence++) .setPlayerId(playerConnectionContextData.getPlayerId()) .setEventType(PushGameEventCommand.GameEventType.MOVE) .setGameId(Configs.GAME_ID) @@ -545,11 +568,8 @@ leaderBoard, getViewport().getWorldWidth() / 2f - glyphLayoutRecSentMessages.wid if (gameOver) { screenToTransition = new GameOverScreen(getGame(), uiLeaderBoard, playerConnectionContextData.getJoinGameData()); - } else if (gameConnection.isDisconnected()) { - while (gameConnection.getErrors().size() != 0) { - gameConnection.getErrors().poll() - .ifPresent(playScreenGameConnectionHandler::handleException); - } + } else if (gameConnection.isAnyDisconnected()) { + gameConnection.pollErrors().forEach(playScreenGameConnectionHandler::handleException); screenToTransition = new ErrorScreen(getGame(), StringUtils.defaultIfBlank(errorMessage, "Connection lost")); } else { @@ -559,7 +579,7 @@ leaderBoard, getViewport().getWorldWidth() / 2f - glyphLayoutRecSentMessages.wid LOG.error("Can't handle screen actions", e); screenToTransition = new ErrorScreen(getGame(), StringUtils.defaultIfEmpty(e.getMessage(), "Can't handle connection") - + ". Check internet signal. Last ping " + gameConnection.getNetworkStats() + + ". Check internet signal. Last ping " + gameConnection.getPrimaryNetworkStats() .getPingMls() + " mls."); } } @@ -580,9 +600,7 @@ private void renderGameTechStats() { StringBuilder gameTechStats = new StringBuilder(); gameTechStats.append(playersOnline).append(" ONLINE "); gameTechStats.append("| PING ") - .append(Optional.of(gameConnection.getNetworkStats().getPingMls()) - .filter(ping -> ping >= 0) - .map(String::valueOf).orElse("-")) + .append(Objects.toString(gameConnection.getPrimaryNetworkStats().getPingMls(), "-")) .append(" MLS | "); gameTechStats.append(Gdx.graphics.getFramesPerSecond()).append(" FPS"); @@ -591,24 +609,18 @@ private void renderGameTechStats() { getViewport().getWorldWidth() - 32 - gameTechStatsGlyph.width, getViewport().getWorldHeight() - 32 - gameTechStatsGlyph.height); - if (Configs.DEV_MODE) { + if (showNetworkStats) { renderDevModeGameTechStats(); } } private void renderDevModeGameTechStats() { - String networkStats = String.format(Locale.ENGLISH, - "NETWORK: RECV %s MSG | SENT %s MSG | INBOUND %s | OUTBOUND %s", - gameConnection.getNetworkStats().getReceivedMessages(), - gameConnection.getNetworkStats().getSentMessages(), - FileUtils.byteCountToDisplaySize(gameConnection.getNetworkStats().getInboundPayloadBytes()), - FileUtils.byteCountToDisplaySize( - gameConnection.getNetworkStats().getOutboundPayloadBytes())) - .toUpperCase(); - var glyphNetStatsMessages = new GlyphLayout(guiFont32, networkStats); + String networkStats = uiNetworkStats.toString(); + GlyphLayout glyphNetworkStats = new GlyphLayout(guiFont32, networkStats); guiFont32.draw(getGame().getBatch(), - networkStats, getViewport().getWorldWidth() / 2f - glyphNetStatsMessages.width / 2f, - getViewport().getWorldHeight() - 32); + networkStats, Constants.DEFAULT_SELECTION_INDENT, + getViewport().getWorldHeight() - getViewport().getWorldHeight() / 3 + + glyphNetworkStats.height / 2); } diff --git a/core/src/com/beverly/hills/money/gang/screens/RespawnScreen.java b/core/src/com/beverly/hills/money/gang/screens/RespawnScreen.java index f81620d..d606fc6 100644 --- a/core/src/com/beverly/hills/money/gang/screens/RespawnScreen.java +++ b/core/src/com/beverly/hills/money/gang/screens/RespawnScreen.java @@ -4,11 +4,12 @@ import com.badlogic.gdx.Input; import com.beverly.hills.money.gang.Configs; import com.beverly.hills.money.gang.DaiKombatGame; -import com.beverly.hills.money.gang.network.GameConnection; +import com.beverly.hills.money.gang.network.LoadBalancedGameConnection; import com.beverly.hills.money.gang.proto.RespawnCommand; import com.beverly.hills.money.gang.proto.ServerResponse; import com.beverly.hills.money.gang.screens.data.PlayerConnectionContextData; import com.beverly.hills.money.gang.utils.Converter; +import java.util.function.Consumer; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,11 +23,11 @@ public class RespawnScreen extends AbstractLoadingScreen { private final PlayerConnectionContextData oldPlayerConnectionContextData; - private final GameConnection gameConnection; + private final LoadBalancedGameConnection gameConnection; public RespawnScreen(final DaiKombatGame game, final PlayerConnectionContextData oldPlayerConnectionContextData, - final GameConnection gameConnection) { + final LoadBalancedGameConnection gameConnection) { super(game); this.oldPlayerConnectionContextData = oldPlayerConnectionContextData; this.gameConnection = gameConnection; @@ -34,7 +35,7 @@ public RespawnScreen(final DaiKombatGame game, @Override public void show() { - if (gameConnection.isDisconnected()) { + if (gameConnection.isAnyDisconnected()) { errorMessage = "Connection lost"; } else { gameConnection.write(RespawnCommand.newBuilder() @@ -60,7 +61,7 @@ protected void onLoadingRender(final float delta) { getGame().setScreen(new ErrorScreen(getGame(), errorMessage)); return; } - gameConnection.getResponse().poll().ifPresentOrElse(response -> { + gameConnection.pollPrimaryConnectionResponse().ifPresent(response -> { if (response.hasErrorEvent()) { errorMessage = response.getErrorEvent().getMessage(); } else if (response.hasGameEvents()) { @@ -76,11 +77,13 @@ protected void onLoadingRender(final float delta) { getGame().setScreen( new PlayScreen(getGame(), gameConnection, createPlayerContextData(response))); } - }, () -> gameConnection.getErrors().poll().ifPresent(throwable -> { + }); + + gameConnection.pollErrors().stream().findFirst().ifPresent(throwable -> { LOG.error("Error while loading", throwable); gameConnection.disconnect(); errorMessage = ExceptionUtils.getMessage(throwable); - })); + }); } private PlayerConnectionContextData createPlayerContextData(ServerResponse response) { diff --git a/core/test/com/beverly/hills/money/gang/entities/enemies/EnemyPlayerTest.java b/core/test/com/beverly/hills/money/gang/entities/enemies/EnemyPlayerTest.java index 7a540e8..134faf5 100644 --- a/core/test/com/beverly/hills/money/gang/entities/enemies/EnemyPlayerTest.java +++ b/core/test/com/beverly/hills/money/gang/entities/enemies/EnemyPlayerTest.java @@ -8,6 +8,8 @@ public class EnemyPlayerTest { + private int sequence = 0; + private final int defaultSpeed = 5; @Test @@ -43,7 +45,7 @@ public void testGetSpeedEmptyQueueSuperClogged() { private Queue createActions(int numberOfActions) { Queue enemyPlayerActions = new ArrayDeque<>(); for (int i = 0; i < numberOfActions; i++) { - enemyPlayerActions.add(EnemyPlayerAction.builder().build()); + enemyPlayerActions.add(EnemyPlayerAction.builder().eventSequenceId(sequence++).build()); } return enemyPlayerActions; }