diff --git a/src/main/java/io/graversen/rust/rcon/DefaultRustRconService.java b/src/main/java/io/graversen/rust/rcon/DefaultRustRconService.java index b14cf5a..01b7d10 100644 --- a/src/main/java/io/graversen/rust/rcon/DefaultRustRconService.java +++ b/src/main/java/io/graversen/rust/rcon/DefaultRustRconService.java @@ -11,6 +11,7 @@ import io.graversen.rust.rcon.protocol.oxide.DefaultOxideManagement; import io.graversen.rust.rcon.protocol.oxide.OxideManagement; import io.graversen.rust.rcon.tasks.RconTask; +import io.graversen.rust.rcon.tasks.RustPlayersEmitTask; import io.graversen.rust.rcon.tasks.ServerInfoEmitTask; import io.graversen.rust.rcon.util.DefaultJsonMapper; import io.graversen.rust.rcon.util.Lazy; @@ -23,6 +24,7 @@ import javax.annotation.Nullable; import java.time.Duration; import java.time.LocalDateTime; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -37,6 +39,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 @NonNull RustRconConfiguration configuration; @@ -99,6 +102,11 @@ public Optional diagnostics() { return Optional.ofNullable(diagnostics.get()); } + @Override + public List rustPlayers() { + return rustPlayers.get(); + } + @Override public void registerEvents(@NonNull Object subscriber) { eventBus.get().register(subscriber); @@ -117,7 +125,14 @@ protected void configure() { eventBus.get()::post ); registerInternalTask(serverInfoEmitTask, Duration.ofSeconds(5)); + final var rustPlayersEmitTask = new RustPlayersEmitTask( + rustRconClient.get().rustServer(), + () -> codec().admin().playerList().thenApply(rustDtoMappers.get().mapRustPlayers()), + eventBus.get()::post + ); + registerInternalTask(rustPlayersEmitTask, Duration.ofSeconds(3)); registerRustDiagnosticsListener(); + registerRustPlayerEventListener(); } protected ScheduledExecutorService createScheduledExecutorService() { @@ -199,6 +214,11 @@ private void registerRustDiagnosticsListener() { registerEvents(new ServerInfoDiagnosticsEventListener(diagnostics::set)); } + private void registerRustPlayerEventListener() { + log.info("Registering {}", RustPlayerEventListener.class.getSimpleName()); + registerEvents(new RustPlayerEventListener(rustPlayers::set)); + } + private void runConfigure() { log.info("Running internal configuration hook"); configure(); diff --git a/src/main/java/io/graversen/rust/rcon/RustPlayer.java b/src/main/java/io/graversen/rust/rcon/RustPlayer.java new file mode 100644 index 0000000..4125c31 --- /dev/null +++ b/src/main/java/io/graversen/rust/rcon/RustPlayer.java @@ -0,0 +1,28 @@ +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.AccessLevel; +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) +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); + } +} diff --git a/src/main/java/io/graversen/rust/rcon/RustPlayerEventListener.java b/src/main/java/io/graversen/rust/rcon/RustPlayerEventListener.java new file mode 100644 index 0000000..87d37a7 --- /dev/null +++ b/src/main/java/io/graversen/rust/rcon/RustPlayerEventListener.java @@ -0,0 +1,47 @@ +package io.graversen.rust.rcon; + +import com.google.common.eventbus.Subscribe; +import io.graversen.rust.rcon.event.server.RustPlayersEvent; +import io.graversen.rust.rcon.protocol.dto.RustPlayerDTO; +import io.graversen.rust.rcon.protocol.util.PlayerName; +import io.graversen.rust.rcon.protocol.util.SteamId64; +import lombok.AccessLevel; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.math.BigDecimal; +import java.time.Duration; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public class RustPlayerEventListener { + private final @NonNull Consumer> rustPlayersConsumer; + + @Subscribe + public void onServerInfo(RustPlayersEvent rustPlayersEvent) { + final var rustPlayers = mapRustPlayers().apply(rustPlayersEvent); + rustPlayersConsumer.accept(rustPlayers); + } + + Function> mapRustPlayers() { + return rustPlayersEvent -> { + final var rustPlayers = rustPlayersEvent.getRustPlayers(); + return rustPlayers.stream() + .map(mapRustPlayer()) + .toList(); + }; + } + + Function mapRustPlayer() { + return rustPlayerDTO -> new RustPlayer( + SteamId64.parseOrFail(rustPlayerDTO.getSteamId()), + new PlayerName(rustPlayerDTO.getPlayerName()), + rustPlayerDTO.getPing(), + rustPlayerDTO.getIpAddress().split(":")[0], + Duration.ofSeconds(rustPlayerDTO.getConnectedSeconds()), + BigDecimal.valueOf(rustPlayerDTO.getHealth()) + ); + } +} diff --git a/src/main/java/io/graversen/rust/rcon/RustRconService.java b/src/main/java/io/graversen/rust/rcon/RustRconService.java index aa83ba5..e095581 100644 --- a/src/main/java/io/graversen/rust/rcon/RustRconService.java +++ b/src/main/java/io/graversen/rust/rcon/RustRconService.java @@ -9,6 +9,7 @@ import javax.annotation.Nullable; import java.time.Duration; +import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -26,4 +27,6 @@ public interface RustRconService extends EventEmitter { void schedule(@NonNull RconTask task, @NonNull Duration fixedDelay, @Nullable Duration initialDelay); Optional diagnostics(); + + List rustPlayers(); } diff --git a/src/main/java/io/graversen/rust/rcon/event/server/RustPlayersEvent.java b/src/main/java/io/graversen/rust/rcon/event/server/RustPlayersEvent.java new file mode 100644 index 0000000..9868b92 --- /dev/null +++ b/src/main/java/io/graversen/rust/rcon/event/server/RustPlayersEvent.java @@ -0,0 +1,20 @@ +package io.graversen.rust.rcon.event.server; + +import io.graversen.rust.rcon.RustServer; +import io.graversen.rust.rcon.protocol.dto.RustPlayerDTO; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +import java.util.List; + +@Getter +@ToString(callSuper = true) +public class RustPlayersEvent extends ServerEvent { + private final @NonNull List rustPlayers; + + public RustPlayersEvent(@NonNull RustServer server, @NonNull List rustPlayers) { + super(server); + this.rustPlayers = rustPlayers; + } +} 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 dece03e..3efc214 100644 --- a/src/main/java/io/graversen/rust/rcon/protocol/AdminCodec.java +++ b/src/main/java/io/graversen/rust/rcon/protocol/AdminCodec.java @@ -26,4 +26,6 @@ public interface AdminCodec { CompletableFuture unmutePlayer(@NonNull SteamId64 steamId64); CompletableFuture serverInfo(); + + CompletableFuture playerList(); } 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 0566d08..ea85274 100644 --- a/src/main/java/io/graversen/rust/rcon/protocol/DefaultAdminCodec.java +++ b/src/main/java/io/graversen/rust/rcon/protocol/DefaultAdminCodec.java @@ -114,4 +114,10 @@ public CompletableFuture serverInfo() { final var rconMessage = compile(SERVER_INFO); return send(rconMessage); } + + @Override + public CompletableFuture playerList() { + final var rconMessage = compile(PLAYER_LIST); + 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 cfcb812..e6ef030 100644 --- a/src/main/java/io/graversen/rust/rcon/protocol/RustProtocolTemplates.java +++ b/src/main/java/io/graversen/rust/rcon/protocol/RustProtocolTemplates.java @@ -56,6 +56,7 @@ public static class AdminProtocol { public static final String MUTE_PLAYER = "global.mute \"" + STEAM_ID_64 + "\""; 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 class OxideProtocol { diff --git a/src/main/java/io/graversen/rust/rcon/protocol/dto/RustDtoMappers.java b/src/main/java/io/graversen/rust/rcon/protocol/dto/RustDtoMappers.java index 5e7761a..1b5a88c 100644 --- a/src/main/java/io/graversen/rust/rcon/protocol/dto/RustDtoMappers.java +++ b/src/main/java/io/graversen/rust/rcon/protocol/dto/RustDtoMappers.java @@ -27,4 +27,8 @@ public Function mapBuildInfo() { public Function mapServerInfo() { return rconResponse -> jsonMapper.fromJson(rconResponse.getMessage(), ServerInfoDTO.class); } + + public Function> mapRustPlayers() { + return rconResponse -> jsonMapper.fromJsonArray(rconResponse.getMessage(), RustPlayerDTO.class); + } } diff --git a/src/main/java/io/graversen/rust/rcon/protocol/dto/RustPlayerDTO.java b/src/main/java/io/graversen/rust/rcon/protocol/dto/RustPlayerDTO.java new file mode 100644 index 0000000..eaab048 --- /dev/null +++ b/src/main/java/io/graversen/rust/rcon/protocol/dto/RustPlayerDTO.java @@ -0,0 +1,30 @@ +package io.graversen.rust.rcon.protocol.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) +public class RustPlayerDTO { + @JsonProperty("SteamID") + private final String steamId; + + @JsonProperty("DisplayName") + private final String playerName; + + @JsonProperty("Ping") + private final String ping; + + @JsonProperty("Address") + private final String ipAddress; + + @JsonProperty("ConnectedSeconds") + private final Integer connectedSeconds; + + @JsonProperty("Health") + private final Double health; +} diff --git a/src/main/java/io/graversen/rust/rcon/tasks/RustPlayersEmitTask.java b/src/main/java/io/graversen/rust/rcon/tasks/RustPlayersEmitTask.java new file mode 100644 index 0000000..d613c29 --- /dev/null +++ b/src/main/java/io/graversen/rust/rcon/tasks/RustPlayersEmitTask.java @@ -0,0 +1,34 @@ +package io.graversen.rust.rcon.tasks; + +import io.graversen.rust.rcon.RustServer; +import io.graversen.rust.rcon.event.server.RustPlayersEvent; +import io.graversen.rust.rcon.protocol.dto.RustPlayerDTO; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +@Slf4j +@RequiredArgsConstructor +public class RustPlayersEmitTask implements RconTask { + private final @NonNull RustServer server; + private final @NonNull Supplier>> rustPlayersGetter; + private final @NonNull Consumer rustPlayersEventEmitter; + + @Override + public void run() { + rustPlayersGetter.get() + .thenApply(rustPlayersEventMapper()) + .thenAccept(rustPlayersEventEmitter); + } + + Function, RustPlayersEvent> rustPlayersEventMapper() { + return rustPlayers -> new RustPlayersEvent(server, rustPlayers); + } + +} diff --git a/src/main/java/io/graversen/rust/rcon/util/DefaultJsonMapper.java b/src/main/java/io/graversen/rust/rcon/util/DefaultJsonMapper.java index 0acf937..70c1bf0 100644 --- a/src/main/java/io/graversen/rust/rcon/util/DefaultJsonMapper.java +++ b/src/main/java/io/graversen/rust/rcon/util/DefaultJsonMapper.java @@ -6,6 +6,8 @@ import lombok.SneakyThrows; import lombok.Synchronized; +import java.util.List; + public class DefaultJsonMapper implements JsonMapper { private ObjectMapper objectMapper; @@ -21,6 +23,13 @@ public T fromJson(@NonNull String json, @NonNull Class toClass) { return getObjectMapper().readValue(json, toClass); } + @Override + @SneakyThrows + public List fromJsonArray(@NonNull String json, @NonNull Class toClass) { + final T[] jsonArray = (T[]) getObjectMapper().readValue(json, toClass.arrayType()); + return List.of(jsonArray); + } + @Synchronized private ObjectMapper getObjectMapper() { if (objectMapper == null) { diff --git a/src/main/java/io/graversen/rust/rcon/util/JsonMapper.java b/src/main/java/io/graversen/rust/rcon/util/JsonMapper.java index 90ab27c..a4bc2a1 100644 --- a/src/main/java/io/graversen/rust/rcon/util/JsonMapper.java +++ b/src/main/java/io/graversen/rust/rcon/util/JsonMapper.java @@ -2,8 +2,12 @@ import lombok.NonNull; +import java.util.List; + public interface JsonMapper { String toJson(@NonNull Object object); T fromJson(@NonNull String json, @NonNull Class toClass); + + List fromJsonArray(@NonNull String json, @NonNull Class toClass); }