Skip to content

Commit

Permalink
Update and debug database logic on player dis/connection
Browse files Browse the repository at this point in the history
  • Loading branch information
Xharos committed Jun 26, 2024
1 parent a820897 commit b9afd14
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 43 deletions.
9 changes: 7 additions & 2 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ services:
- ./data:/data
networks:
- islands_network
depends_on:
mongo:
condition: service_healthy
redis:
condition: service_healthy

mongo:
build: mongodb/.
Expand All @@ -63,8 +68,8 @@ services:
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/iswars --quiet
interval: 2s
timeout: 12s
retries: 5
timeout: 5s
retries: 2
start_period: 10s
volumes:
- ./mongodb/docker-entrypoint-initdb.d/:/docker-entrypoint-initdb.d/
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/fr/islandswars/ineundo/lang/IneundoError.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public IneundoError(String message) {
super(message);
}

public IneundoError(Throwable cause) {
super(cause.getMessage(), cause);
}

public IneundoError(String message, Throwable cause) {
super(message, cause);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.proxy.Player;
import fr.islandswars.commons.service.collection.Collection;
import fr.islandswars.commons.service.mongodb.MongoDBConnection;
import fr.islandswars.commons.service.mongodb.ObservableSubscriber;
import fr.islandswars.commons.service.mongodb.OperationSubscriber;
import fr.islandswars.commons.service.redis.RedisConnection;
import fr.islandswars.commons.utils.ReflectionUtil;
import fr.islandswars.ineundo.Ineundo;
import fr.islandswars.ineundo.lang.IneundoError;
import fr.islandswars.ineundo.log.internal.PlayerConnectionLog;
import fr.islandswars.ineundo.player.IslandsPlayer;
import fr.islandswars.ineundo.utils.MongoConstants;
import fr.islandswars.ineundo.utils.RedisConstants;
import net.kyori.adventure.text.Component;
import org.apache.logging.log4j.Level;
import org.bson.Document;

import java.lang.reflect.Field;
Expand All @@ -27,10 +31,7 @@
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;

/**
* File <b>PlayerDataListener</b> located on fr.islandswars.ineundo.listener
Expand All @@ -55,11 +56,10 @@
* @author Jangliu, {@literal <[email protected]>}
* Created the 24/06/2024 at 16:10
* @since 0.1
* TODO proper logging
*/
public class PlayerDataListener extends LazyListener {

private final long mongoTimeout = 2L;
private final long mongoTimeout = 1L;
private final TimeUnit mongoTimeoutUnit = TimeUnit.SECONDS;
private final List<OperationSubscriber<UpdateResult>> pendingResults;
private final Collection<IslandsPlayer> playersCollection;
Expand All @@ -72,10 +72,13 @@ public PlayerDataListener(Ineundo ineundo, MongoDBConnection mongo, RedisConnect
this.playersCollection = mongo.getCollection(MongoConstants.PLAYER_COLLECTION, IslandsPlayer.class);
}

@Subscribe
@Subscribe(order = PostOrder.FIRST)
public void onLogin(PreLoginEvent event) {
var uuid = event.getUniqueId();
fetchPlayerData(uuid);
getIneundo().getPlayer(uuid).ifPresentOrElse(p -> {
event.setResult(PreLoginEvent.PreLoginComponentResult.denied(Component.translatable("event.join.data.error")));
new PlayerConnectionLog(Level.ERROR, "Player not saved in database").withEvent(event).log();
}, () -> fetchPlayerData(uuid, event));
}

@Subscribe(order = PostOrder.FIRST)
Expand All @@ -86,64 +89,75 @@ public EventTask onServerPreconnectEvent(ServerPreConnectEvent event) {
return null;
}

@Subscribe
@Subscribe(order = PostOrder.LAST)
public void onPlayerDisconnect(DisconnectEvent event) {
var uuid = event.getPlayer().getUniqueId();
var optPlayer = getPlayer(uuid);
optPlayer.ifPresent(this::savePlayerData);
}

@Subscribe
@Subscribe(order = PostOrder.FIRST)
public void onProxyShutdown(ProxyShutdownEvent event) {
while (!pendingResults.isEmpty()) {
}
}

private void savePlayerData(IslandsPlayer player) {
updateFromRedis(player).whenCompleteAsync((p, thr) -> {
if (thr != null)
error(new IneundoError("Error when saving player data in MongoDB...", thr));
updateFromRedis(player).whenCompleteAsync((p, th) -> {
if (th != null)
error(new IneundoError("Error when retrieving player data from Redis", th));

var subscriber = playersCollection.replace(player, MongoConstants.PLAYER_ID_FILTER(player.getUUID()));
var subscriber = playersCollection.replace(p, MongoConstants.PLAYER_ID_FILTER(p.getUUID()));
pendingResults.add(subscriber);

CompletableFuture<UpdateResult> result = new CompletableFuture<>();
result.completeAsync(subscriber::first);
result.whenCompleteAsync((re, th) -> {
if (th != null)
error(new IneundoError("Error when saving player data in MongoDB...", th));
result.completeAsync(subscriber::first).orTimeout(mongoTimeout, mongoTimeoutUnit);
result.whenCompleteAsync((re, thr) -> {
if (thr != null) {
error(new IneundoError("Error when saving player data in MongoDB.", thr));
getIneundo().getInfraLogger().log(Level.ERROR, playersCollection.serialize(p).toJson()); //manual save in case of problem
}
getIneundo().removePlayer(player);
pendingResults.remove(subscriber);
});
//TODO maybe add a timeout here in case the player wants to connect back to the server
//TODO or check if a player with this uuid is already existing when joining
});
}

private void synchroniseData(ServerPreConnectEvent event) {
getPlayerAsync(event.getPlayer().getUniqueId()).whenCompleteAsync((optPlayer, th) -> {
if (th != null || optPlayer.isEmpty()) {
error(new IneundoError("Player " + event.getPlayer().getUsername() + " cannot be retrieved in time", th));
//error(new IneundoError("Player " + event.getPlayer().getUsername() + " cannot be retrieved in time", th));
event.getPlayer().disconnect(Component.translatable("event.join.data.error"));
new PlayerConnectionLog(Level.ERROR, "Cannot retrieve data from mongodb in time").withEvent(event).log();
} else {
var player = optPlayer.get();
var player = optPlayer.get();
injectGameProfile(player, event.getPlayer());
var sanction = player.isKick();
sanction.ifPresent(s -> event.getPlayer().disconnect(s.getKickMessage()));
if (getIneundo().getSTAFF_ONLY().get() && !player.getMainRank().isStaff()) event.getPlayer().disconnect(Component.translatable("event.join.staff"));
else {
sanction.ifPresent(s -> {
event.getPlayer().disconnect(s.getKickMessage());
new PlayerConnectionLog(Level.INFO, "Kicked player attempt to login").withEvent(event).log();
});
if (getIneundo().getSTAFF_ONLY().get() && !player.getMainRank().isStaff()) {
event.getPlayer().disconnect(Component.translatable("event.join.staff"));
new PlayerConnectionLog(Level.INFO, "Connection attempt when the server is in maintenance").withEvent(event).log();
} else {
//TODO server offline
redis.getConnection().set(RedisConstants.PLAYER_KEY(player.getUUID()), playersCollection.serialize(player).toJson());
redis.getConnection().set(RedisConstants.PLAYER_KEY(player.getUUID()), playersCollection.serialize(player).toJson()).whenCompleteAsync((re, thr) -> {
if (thr != null) {
event.getPlayer().disconnect(Component.translatable("event.join.data.error"));
new PlayerConnectionLog(Level.ERROR, "Cannot save data in redis").withEvent(event).log();
} else
new PlayerConnectionLog(Level.INFO, "Successful connection").withEvent(event).log();
});
}
}
});
}

private CompletionStage<IslandsPlayer> updateFromRedis(IslandsPlayer current) {
return redis.getConnection().get(current.getUUID().toString() + ":player").handleAsync((json, th) -> {
if (th != null)
error(new IneundoError("Cannot retrieve player data on redis...", th));

return redis.getConnection().get(RedisConstants.PLAYER_KEY(current.getUUID())).thenApply((json) -> {
if (json != null) {
log(json);
var retrieved = playersCollection.deserialize(Document.parse(json));
Field[] fields = retrieved.getClass().getDeclaredFields();
for (Field field : fields) {
Expand All @@ -160,18 +174,27 @@ private CompletionStage<IslandsPlayer> updateFromRedis(IslandsPlayer current) {
});
}

private void fetchPlayerData(UUID uuid) {
private void fetchPlayerData(UUID uuid, PreLoginEvent event) {
var publisher = playersCollection.findOne(MongoConstants.PLAYER_ID_FILTER(uuid));
publisher.thenApplyAsync(player -> {
if (!event.getConnection().getProtocolVersion().isSupported()) {
new PlayerConnectionLog(Level.WARN, "Minecraft version not supported!").withEvent(event).log();
throw new UnsupportedOperationException("Outdated client version");
}
PlayerConnectionLog log;
if (player == null) {
player = new IslandsPlayer();
player.firstConection(uuid);
log = new PlayerConnectionLog(Level.INFO, "First login attempt to join the server");
} else {
player.welcomeBack();
log = new PlayerConnectionLog(Level.INFO, "Login attempt to join the server");
}
log.withEvent(event).log();
return player;
}).thenAcceptAsync(player -> getIneundo().addPlayer(player)).orTimeout(mongoTimeout, mongoTimeoutUnit).exceptionallyAsync(th -> {
error(new IneundoError("MongoDB timeout when retrieving player data", th));
if (!(th instanceof UnsupportedOperationException))//check if mongo can throw this error
error(new IneundoError(th));
return null;
});
}
Expand All @@ -192,4 +215,13 @@ private CompletableFuture<Optional<IslandsPlayer>> getPlayerAsync(UUID uuid) {
});
return future;
}

private void injectGameProfile(IslandsPlayer isPlayer, Player player) {
var profileProperty = player.getGameProfile().getProperties().stream().filter(prop -> prop.getName().equals("textures")).findFirst();
profileProperty.ifPresent(prop ->{
if (isPlayer.getProfile() == null || !isPlayer.getProfile().equals(prop))
isPlayer.setProfile(prop);
});

}
}
10 changes: 7 additions & 3 deletions src/main/java/fr/islandswars/ineundo/log/InternalLogger.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.apache.logging.log4j.core.config.Configurator;

import java.net.URISyntaxException;
import java.security.KeyPair;

/**
* File <b>InternalLogger</b> located on fr.islandswars.ineundo.log
Expand Down Expand Up @@ -44,11 +45,13 @@ public class InternalLogger {
private final boolean debug;

public InternalLogger() {
this.gson = new GsonBuilder().registerTypeAdapter(Level.class, new Log4jLevelSerializer())
.registerTypeAdapter(StackTraceElement.class, new StackTraceElementTypeAdapter()).create();
this.gson = new GsonBuilder()
.registerTypeAdapter(Level.class, new Log4jLevelSerializer())
.registerTypeAdapter(StackTraceElement.class, new StackTraceElementTypeAdapter())
.create();
this.debug = Boolean.parseBoolean(System.getenv("DEBUG"));
this.rootLogger = (Logger) LogManager.getRootLogger();
//overrideDefault(); TODO update
//overrideDefault(); //TODO update
}

private void overrideDefault() {
Expand Down Expand Up @@ -83,6 +86,7 @@ public void logError(Exception e) {
}

protected void sysout(Log object) {
System.out.print(object.msg);
if (object.getLevel() == Level.DEBUG) {
if (debug)
rootLogger.log(object.getLevel(), gson.toJson(object));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package fr.islandswars.ineundo.log.internal;

import com.google.common.base.Preconditions;
import com.google.gson.annotations.SerializedName;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.InboundConnection;
import fr.islandswars.ineundo.log.Log;
import org.apache.logging.log4j.Level;

import java.net.InetSocketAddress;
import java.util.UUID;

/**
* File <b>PlayerConnectionLog</b> located on fr.islandswars.ineundo.log.internal
* PlayerConnectionLog is a part of ineundo.
* <p>
* Copyright (c) 2017 - 2024 Islands Wars.
* <p>
* ineundo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p>
* This program 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <a href="http://www.gnu.org/licenses/">GNU license</a>.
* <p>
*
* @author Jangliu, {@literal <[email protected]>}
* Created the 26/06/2024 at 22:23
* @since 0.1
*/
public class PlayerConnectionLog extends Log {

private UUID uuid;
private String name;
@SerializedName("protocol_state")
private String protocolState;
@SerializedName("protocol_version")
private ProtocolVersion protocolVersion;
private String address;

public PlayerConnectionLog(Level level, String msg) {
super(level, msg);
}

@Override
protected void checkValue() {
Preconditions.checkNotNull(uuid);
Preconditions.checkNotNull(name);
Preconditions.checkNotNull(protocolState);
Preconditions.checkNotNull(protocolVersion);
Preconditions.checkNotNull(address);
}

public PlayerConnectionLog withEvent(PreLoginEvent event) {
this.protocolState = event.getConnection().getProtocolState().name();
this.protocolVersion = event.getConnection().getProtocolVersion();
this.address = event.getConnection().getRemoteAddress().toString();
this.uuid = event.getUniqueId();
this.name = event.getUsername();
return this;
}

public PlayerConnectionLog withEvent(ServerPreConnectEvent event) {
this.protocolState = event.getPlayer().getProtocolState().name();
this.protocolVersion = event.getPlayer().getProtocolVersion();
this.address = event.getPlayer().getRemoteAddress().toString();
this.uuid = event.getPlayer().getUniqueId();
this.name = event.getPlayer().getUsername();
return this;
}
}
17 changes: 12 additions & 5 deletions src/main/java/fr/islandswars/ineundo/player/IslandsPlayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import com.velocitypowered.api.util.GameProfile;
import fr.islandswars.ineundo.Ineundo;
import fr.islandswars.ineundo.player.sanction.IslandsSanction;
import fr.islandswars.ineundo.utils.ProxyConstants;
import fr.islandswars.ineundo.utils.TimeUtils;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.*;

/**
* File <b>IslandsPlayer</b> located on fr.islandswars.ineundo.player
Expand Down Expand Up @@ -51,10 +49,11 @@ public class IslandsPlayer {
private String lastConnection;
@Expose
private List<IslandsSanction> sanctions;
@Expose
private GameProfile.Property profile;

public IslandsPlayer() {
this.ranks = new ArrayList<>();

this.sanctions = new ArrayList<>();
}

Expand Down Expand Up @@ -107,5 +106,13 @@ public Optional<IslandsSanction> isKick() {
}
return Optional.empty();
}

public GameProfile.Property getProfile() {
return profile;
}

public void setProfile(GameProfile.Property profile) {
this.profile = profile;
}
}

Loading

0 comments on commit b9afd14

Please sign in to comment.