diff --git a/src/main/java/io/graversen/rust/rcon/DefaultRustRconService.java b/src/main/java/io/graversen/rust/rcon/DefaultRustRconService.java index 01b7d10..09f4d00 100644 --- a/src/main/java/io/graversen/rust/rcon/DefaultRustRconService.java +++ b/src/main/java/io/graversen/rust/rcon/DefaultRustRconService.java @@ -10,6 +10,8 @@ import io.graversen.rust.rcon.protocol.dto.ServerInfoDTO; import io.graversen.rust.rcon.protocol.oxide.DefaultOxideManagement; import io.graversen.rust.rcon.protocol.oxide.OxideManagement; +import io.graversen.rust.rcon.protocol.player.DefaultPlayerManagement; +import io.graversen.rust.rcon.protocol.player.PlayerManagement; import io.graversen.rust.rcon.tasks.RconTask; import io.graversen.rust.rcon.tasks.RustPlayersEmitTask; import io.graversen.rust.rcon.tasks.ServerInfoEmitTask; @@ -39,7 +41,7 @@ public class DefaultRustRconService implements RustRconService { private final AtomicBoolean isRconLogEnabled = new AtomicBoolean(false); private final AtomicReference diagnostics = new AtomicReference<>(null); - private final AtomicReference> rustPlayers = new AtomicReference<>(List.of()); + private final AtomicReference> rustPlayers = new AtomicReference<>(List.of()); private final @NonNull RustRconConfiguration configuration; @@ -52,6 +54,7 @@ public class DefaultRustRconService implements RustRconService { private final Lazy rustRconRouter = Lazy.of(this::createRustRconRouter); private final Lazy codec = Lazy.of(this::createCodec); private final Lazy oxideManagement = Lazy.of(this::createOxideManagement); + private final Lazy playerManagement = Lazy.of(this::createPlayerManagement); @Override public Codec codec() { @@ -86,6 +89,11 @@ public OxideManagement oxideManagement() { return oxideManagement.get(); } + @Override + public PlayerManagement playerManagement() { + return playerManagement.get(); + } + @Override public void schedule(@NonNull RconTask task, @NonNull Duration fixedDelay, @Nullable Duration initialDelay) { final var wrappedTask = wrapRconTask(task); @@ -103,7 +111,7 @@ public Optional diagnostics() { } @Override - public List rustPlayers() { + public List players() { return rustPlayers.get(); } @@ -153,6 +161,10 @@ protected OxideManagement createOxideManagement() { return new DefaultOxideManagement(codec().oxide()); } + protected PlayerManagement createPlayerManagement() { + return new DefaultPlayerManagement(codec().admin()); + } + protected RustWebSocketClient createWebSocketClient() { return new ReconnectingRustWebSocketClient( configuration.getHostname(), diff --git a/src/main/java/io/graversen/rust/rcon/FullRustPlayer.java b/src/main/java/io/graversen/rust/rcon/FullRustPlayer.java new file mode 100644 index 0000000..75182cf --- /dev/null +++ b/src/main/java/io/graversen/rust/rcon/FullRustPlayer.java @@ -0,0 +1,40 @@ +package io.graversen.rust.rcon; + +import io.graversen.rust.rcon.protocol.util.PlayerName; +import io.graversen.rust.rcon.protocol.util.SteamId64; +import io.graversen.rust.rcon.util.CommonUtils; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.ZonedDateTime; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class FullRustPlayer extends RustPlayer { + private final @NonNull String ping; + private final @NonNull String ipAddress; + private final @NonNull Duration connectedDuration; + private final @NonNull BigDecimal health; + + public FullRustPlayer( + @NonNull SteamId64 steamId, + @NonNull PlayerName playerName, + @NonNull String ping, + @NonNull String ipAddress, + @NonNull Duration connectedDuration, + @NonNull BigDecimal health + ) { + super(steamId, playerName); + this.ping = ping; + this.ipAddress = ipAddress; + this.connectedDuration = connectedDuration; + this.health = health; + } + + public ZonedDateTime connectedAt() { + return CommonUtils.now().minus(connectedDuration); + } +} diff --git a/src/main/java/io/graversen/rust/rcon/RustPlayer.java b/src/main/java/io/graversen/rust/rcon/RustPlayer.java index 4125c31..8fa7ef4 100644 --- a/src/main/java/io/graversen/rust/rcon/RustPlayer.java +++ b/src/main/java/io/graversen/rust/rcon/RustPlayer.java @@ -2,27 +2,15 @@ import io.graversen.rust.rcon.protocol.util.PlayerName; import io.graversen.rust.rcon.protocol.util.SteamId64; -import io.graversen.rust.rcon.util.CommonUtils; -import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import lombok.Value; -import java.math.BigDecimal; -import java.time.Duration; -import java.time.ZonedDateTime; - -@Value -@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +@Getter +@EqualsAndHashCode +@RequiredArgsConstructor public class RustPlayer { - @NonNull SteamId64 steamId; - @NonNull PlayerName playerName; - @NonNull String ping; - @NonNull String ipAddress; - @NonNull Duration connectedDuration; - @NonNull BigDecimal health; - - public ZonedDateTime connectedAt() { - return CommonUtils.now().minus(connectedDuration); - } + private final @NonNull SteamId64 steamId; + private final @NonNull PlayerName playerName; } diff --git a/src/main/java/io/graversen/rust/rcon/RustPlayerEventListener.java b/src/main/java/io/graversen/rust/rcon/RustPlayerEventListener.java index 87d37a7..8f65265 100644 --- a/src/main/java/io/graversen/rust/rcon/RustPlayerEventListener.java +++ b/src/main/java/io/graversen/rust/rcon/RustPlayerEventListener.java @@ -17,7 +17,7 @@ @RequiredArgsConstructor(access = AccessLevel.PACKAGE) public class RustPlayerEventListener { - private final @NonNull Consumer> rustPlayersConsumer; + private final @NonNull Consumer> rustPlayersConsumer; @Subscribe public void onServerInfo(RustPlayersEvent rustPlayersEvent) { @@ -25,7 +25,7 @@ public void onServerInfo(RustPlayersEvent rustPlayersEvent) { rustPlayersConsumer.accept(rustPlayers); } - Function> mapRustPlayers() { + Function> mapRustPlayers() { return rustPlayersEvent -> { final var rustPlayers = rustPlayersEvent.getRustPlayers(); return rustPlayers.stream() @@ -34,8 +34,8 @@ Function> mapRustPlayers() { }; } - Function mapRustPlayer() { - return rustPlayerDTO -> new RustPlayer( + Function mapRustPlayer() { + return rustPlayerDTO -> new FullRustPlayer( SteamId64.parseOrFail(rustPlayerDTO.getSteamId()), new PlayerName(rustPlayerDTO.getPlayerName()), rustPlayerDTO.getPing(), diff --git a/src/main/java/io/graversen/rust/rcon/RustRconService.java b/src/main/java/io/graversen/rust/rcon/RustRconService.java index e095581..2a81058 100644 --- a/src/main/java/io/graversen/rust/rcon/RustRconService.java +++ b/src/main/java/io/graversen/rust/rcon/RustRconService.java @@ -3,6 +3,7 @@ import io.graversen.rust.rcon.protocol.Codec; import io.graversen.rust.rcon.protocol.dto.ServerInfoDTO; import io.graversen.rust.rcon.protocol.oxide.OxideManagement; +import io.graversen.rust.rcon.protocol.player.PlayerManagement; import io.graversen.rust.rcon.tasks.RconTask; import io.graversen.rust.rcon.util.EventEmitter; import lombok.NonNull; @@ -24,9 +25,11 @@ public interface RustRconService extends EventEmitter { OxideManagement oxideManagement(); + PlayerManagement playerManagement(); + void schedule(@NonNull RconTask task, @NonNull Duration fixedDelay, @Nullable Duration initialDelay); Optional diagnostics(); - List rustPlayers(); + List players(); } diff --git a/src/main/java/io/graversen/rust/rcon/protocol/AdminCodec.java b/src/main/java/io/graversen/rust/rcon/protocol/AdminCodec.java index 3efc214..207bd69 100644 --- a/src/main/java/io/graversen/rust/rcon/protocol/AdminCodec.java +++ b/src/main/java/io/graversen/rust/rcon/protocol/AdminCodec.java @@ -28,4 +28,6 @@ public interface AdminCodec { CompletableFuture serverInfo(); CompletableFuture playerList(); + + CompletableFuture sleepingPlayers(); } diff --git a/src/main/java/io/graversen/rust/rcon/protocol/DefaultAdminCodec.java b/src/main/java/io/graversen/rust/rcon/protocol/DefaultAdminCodec.java index ea85274..b897f53 100644 --- a/src/main/java/io/graversen/rust/rcon/protocol/DefaultAdminCodec.java +++ b/src/main/java/io/graversen/rust/rcon/protocol/DefaultAdminCodec.java @@ -120,4 +120,10 @@ public CompletableFuture playerList() { final var rconMessage = compile(PLAYER_LIST); return send(rconMessage); } + + @Override + public CompletableFuture sleepingPlayers() { + final var rconMessage = compile(SLEEPING_PLAYERS); + return send(rconMessage); + } } diff --git a/src/main/java/io/graversen/rust/rcon/protocol/RustProtocolTemplates.java b/src/main/java/io/graversen/rust/rcon/protocol/RustProtocolTemplates.java index e6ef030..9c8fab9 100644 --- a/src/main/java/io/graversen/rust/rcon/protocol/RustProtocolTemplates.java +++ b/src/main/java/io/graversen/rust/rcon/protocol/RustProtocolTemplates.java @@ -57,6 +57,7 @@ public static class AdminProtocol { public static final String UNMUTE_PLAYER = "global.unmute \"" + STEAM_ID_64 + "\""; public static final String SERVER_INFO = "global.serverinfo"; public static final String PLAYER_LIST = "playerlist"; + public static final String SLEEPING_PLAYERS = "global.sleepingusers"; } public static class OxideProtocol { diff --git a/src/main/java/io/graversen/rust/rcon/protocol/oxide/DefaultOxideManagement.java b/src/main/java/io/graversen/rust/rcon/protocol/oxide/DefaultOxideManagement.java index 4471f24..7b4c8bf 100644 --- a/src/main/java/io/graversen/rust/rcon/protocol/oxide/DefaultOxideManagement.java +++ b/src/main/java/io/graversen/rust/rcon/protocol/oxide/DefaultOxideManagement.java @@ -15,7 +15,6 @@ public class DefaultOxideManagement implements OxideManagement { private static final String PLUGINS_REGEX_STRING = "\\d+\\s+\"(.*?)\"\\s+\\((.*?)\\)\\s+by\\s+(.*?)\\s+\\(.*?\\)\\s+-\\s+(.*?)$"; private static final Pattern PLUGINS_REGEX = Pattern.compile(PLUGINS_REGEX_STRING, Pattern.MULTILINE); - private final @NonNull OxideCodec oxideCodec; @Override diff --git a/src/main/java/io/graversen/rust/rcon/protocol/player/DefaultPlayerManagement.java b/src/main/java/io/graversen/rust/rcon/protocol/player/DefaultPlayerManagement.java new file mode 100644 index 0000000..c1abac9 --- /dev/null +++ b/src/main/java/io/graversen/rust/rcon/protocol/player/DefaultPlayerManagement.java @@ -0,0 +1,43 @@ +package io.graversen.rust.rcon.protocol.player; + +import io.graversen.rust.rcon.RustPlayer; +import io.graversen.rust.rcon.RustRconResponse; +import io.graversen.rust.rcon.protocol.AdminCodec; +import io.graversen.rust.rcon.protocol.util.PlayerName; +import io.graversen.rust.rcon.protocol.util.SteamId64; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +@Slf4j +@RequiredArgsConstructor +public class DefaultPlayerManagement implements PlayerManagement { + private final @NonNull AdminCodec adminCodec; + + @Override + public CompletableFuture> sleepingPlayers() { + return adminCodec.sleepingPlayers().thenApply(mapSleepingPlayers()); + } + + Function> mapSleepingPlayers() { + return rconResponse -> { + try { + final var message = rconResponse.getMessage(); + return Arrays.stream(message.split("\n")) + .filter(line -> !line.isBlank()) + .filter(line -> !line.contains("sleeping users")) + .map(line -> line.split(":")) + .map(parts -> new RustPlayer(SteamId64.parseOrFail(parts[0].trim()), new PlayerName(parts[1].trim()))) + .toList(); + } catch (Exception e) { + log.error(e.getMessage(), e); + return List.of(); + } + }; + } +} diff --git a/src/main/java/io/graversen/rust/rcon/protocol/player/PlayerManagement.java b/src/main/java/io/graversen/rust/rcon/protocol/player/PlayerManagement.java new file mode 100644 index 0000000..f6ea0d9 --- /dev/null +++ b/src/main/java/io/graversen/rust/rcon/protocol/player/PlayerManagement.java @@ -0,0 +1,10 @@ +package io.graversen.rust.rcon.protocol.player; + +import io.graversen.rust.rcon.RustPlayer; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public interface PlayerManagement { + CompletableFuture> sleepingPlayers(); +} diff --git a/src/test/java/io/graversen/rust/rcon/protocol/player/DefaultPlayerManagementTest.java b/src/test/java/io/graversen/rust/rcon/protocol/player/DefaultPlayerManagementTest.java new file mode 100644 index 0000000..57c2a4a --- /dev/null +++ b/src/test/java/io/graversen/rust/rcon/protocol/player/DefaultPlayerManagementTest.java @@ -0,0 +1,48 @@ +package io.graversen.rust.rcon.protocol.player; + +import io.graversen.rust.rcon.RustPlayer; +import io.graversen.rust.rcon.TestRustRconResponse; +import io.graversen.rust.rcon.protocol.AdminCodec; +import io.graversen.rust.rcon.protocol.util.PlayerName; +import io.graversen.rust.rcon.protocol.util.SteamId64; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class DefaultPlayerManagementTest { + @Mock + private AdminCodec adminCodec; + + @InjectMocks + private DefaultPlayerManagement defaultPlayerManagement; + + @Test + void mapSleepingPlayers() { + final var sleepingPlayersOutput = "\n" + + "76561198127947780:DKautobahnTV\n" + + "76561198813879070:MrCool88\n" + + "76561198072271313:Trey\n" + + "76561198203638647:cuzzy\n" + + "76561198088326077:Hirabayashi\n" + + "76561198436002452:Arska\n" + + "76561198973297741:Nikke\n" + + "76561198251899848:Klarius\n" + + "76561198307063094:IX\n" + + "76561198046357656:DarkDouchebag\n" + + "76561198075300933:[RTA]PenetrationsKonsulten\n" + + "76561198154164007:Hubbi3000\n" + + "76561198201936914:Bosthief\n" + + "13 sleeping users\n"; + + final var sleepingPlayers = defaultPlayerManagement.mapSleepingPlayers().apply(new TestRustRconResponse(sleepingPlayersOutput)); + assertFalse(sleepingPlayers.isEmpty()); + assertEquals(13, sleepingPlayers.size()); + assertEquals(new RustPlayer(SteamId64.parseOrFail("76561198127947780"), new PlayerName("DKautobahnTV")), sleepingPlayers.get(0)); + assertEquals(new RustPlayer(SteamId64.parseOrFail("76561198201936914"), new PlayerName("Bosthief")), sleepingPlayers.get(12)); + } +} \ No newline at end of file