diff --git a/RemotelyMod/gradle/libs.versions.toml b/RemotelyMod/gradle/libs.versions.toml index cd77bafd..0b58770e 100644 --- a/RemotelyMod/gradle/libs.versions.toml +++ b/RemotelyMod/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -textile = "1.0.0-beta.13" -omnicore = "1.0.0-beta.40" +textile = "1.0.0-beta.8" +omnicore = "1.0.0-beta.38" [libraries] textile = { group = "dev.deftu", name = "textile", version.ref = "textile" } diff --git a/RemotelyMod/libs/ReScreen-1.0.jar b/RemotelyMod/libs/ReScreen-1.0.jar index f054595d..89a8e3f7 100644 Binary files a/RemotelyMod/libs/ReScreen-1.0.jar and b/RemotelyMod/libs/ReScreen-1.0.jar differ diff --git a/RemotelyMod/libs/Rebase-1.0-SNAPSHOT.jar b/RemotelyMod/libs/Rebase-1.0-SNAPSHOT.jar index df1bb433..0ebed386 100644 Binary files a/RemotelyMod/libs/Rebase-1.0-SNAPSHOT.jar and b/RemotelyMod/libs/Rebase-1.0-SNAPSHOT.jar differ diff --git a/RemotelyMod/libs/Remotely-App.jar b/RemotelyMod/libs/Remotely-App.jar index 6055cc98..fd422fce 100644 Binary files a/RemotelyMod/libs/Remotely-App.jar and b/RemotelyMod/libs/Remotely-App.jar differ diff --git a/libs/ReScreen-1.0.jar b/libs/ReScreen-1.0.jar index f054595d..89a8e3f7 100644 Binary files a/libs/ReScreen-1.0.jar and b/libs/ReScreen-1.0.jar differ diff --git a/libs/Rebase-1.0-SNAPSHOT.jar b/libs/Rebase-1.0-SNAPSHOT.jar index df1bb433..0ebed386 100644 Binary files a/libs/Rebase-1.0-SNAPSHOT.jar and b/libs/Rebase-1.0-SNAPSHOT.jar differ diff --git a/src/main/java/redxax/oxy/remotely/RemotelyManager.java b/src/main/java/redxax/oxy/remotely/RemotelyManager.java index 0562798d..8a68eef9 100644 --- a/src/main/java/redxax/oxy/remotely/RemotelyManager.java +++ b/src/main/java/redxax/oxy/remotely/RemotelyManager.java @@ -1,6 +1,7 @@ package redxax.oxy.remotely; import redxax.oxy.remotely.config.RemotelyConfigManager; +import restudio.rebase.backend.impl.ReStudioBackend; import restudio.rebase.update.ApplicationUpdateManager; import restudio.rebase.IRebaseManager; import restudio.rebase.account.AccountManager; @@ -13,7 +14,6 @@ import restudio.rebase.preset.ResourceListManager; import restudio.rebase.resource.InstanceResourceManager; import restudio.rebase.resource.ResourceMetadataManager; -import restudio.rebase.resource.ResourceStateManager; import restudio.rebase.resource.UpdateManager; import restudio.rebase.resource.provider.*; @@ -111,6 +111,7 @@ private void init() { javaManager.refreshRuntimes(); BackendFactory.register("LOCAL", (cfg, inst) -> new LocalBackend(cfg != null ? cfg : new BackendConfig("LOCAL", new java.util.HashMap<>()), inst)); BackendFactory.register("SSH", SshBackend::new); + BackendFactory.register("RESTUDIO", ReStudioBackend::new); if (configManager.isUpdateCheckOnStartup()) { applicationUpdateManager.checkForUpdates().thenAccept(updateOpt -> updateOpt.ifPresent(releaseInfo -> restudio.rescreen.ui.core.ScreenManager.getInstance().execute(() -> UpdateAvailablePopup.show(releaseInfo, applicationUpdateManager)))); diff --git a/src/main/java/redxax/oxy/remotely/data/managed/ManagedPlayer.java b/src/main/java/redxax/oxy/remotely/data/managed/ManagedPlayer.java deleted file mode 100644 index 9f8ac0f1..00000000 --- a/src/main/java/redxax/oxy/remotely/data/managed/ManagedPlayer.java +++ /dev/null @@ -1,39 +0,0 @@ -package redxax.oxy.remotely.data.managed; - -import java.util.UUID; - -public class ManagedPlayer { - public final UUID uuid; - public String name; - public boolean isOnline = false; - public int ping = -1; - public String address; - public long lastSeen = 0; - - public boolean isOp = false; - public int opLevel = 0; - - public boolean isBanned = false; - public BanEntry banInfo; - - public boolean isIpBanned = false; - public IpBanEntry ipBanInfo; - - public ManagedPlayer(UUID uuid, String name) { - this.uuid = uuid; - this.name = name; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ManagedPlayer that = (ManagedPlayer) o; - return uuid.equals(that.uuid); - } - - @Override - public int hashCode() { - return uuid.hashCode(); - } -} diff --git a/src/main/java/redxax/oxy/remotely/data/player/IPlayerActionProvider.java b/src/main/java/redxax/oxy/remotely/data/player/IPlayerActionProvider.java deleted file mode 100644 index 1aa47b05..00000000 --- a/src/main/java/redxax/oxy/remotely/data/player/IPlayerActionProvider.java +++ /dev/null @@ -1,18 +0,0 @@ -package redxax.oxy.remotely.data.player; - -import redxax.oxy.remotely.data.managed.ManagedPlayer; -import redxax.oxy.remotely.data.managed.PlayerAction; - -import java.util.List; -import java.util.concurrent.CompletableFuture; - -public interface IPlayerActionProvider { - void initialize(); - void shutdown(); - CompletableFuture kickPlayer(ManagedPlayer player, String reason); - CompletableFuture banPlayer(ManagedPlayer player, String reason, boolean ipBan); - CompletableFuture unbanPlayer(ManagedPlayer player); - CompletableFuture toggleOp(ManagedPlayer player); - CompletableFuture runCustomCommand(ManagedPlayer player, String commandTemplate); - CompletableFuture> getCustomActions(); -} \ No newline at end of file diff --git a/src/main/java/redxax/oxy/remotely/data/player/IPlayerDataProvider.java b/src/main/java/redxax/oxy/remotely/data/player/IPlayerDataProvider.java deleted file mode 100644 index 4d3fe3a9..00000000 --- a/src/main/java/redxax/oxy/remotely/data/player/IPlayerDataProvider.java +++ /dev/null @@ -1,18 +0,0 @@ -package redxax.oxy.remotely.data.player; - -import redxax.oxy.remotely.data.managed.ManagedPlayer; - -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; - -public interface IPlayerDataProvider { - void initialize(); - void shutdown(); - CompletableFuture fullRefresh(); - Map getCachedPlayers(); - void addUpdateListener(Consumer> listener); - void removeUpdateListener(Consumer> listener); -} \ No newline at end of file diff --git a/src/main/java/redxax/oxy/remotely/data/player/PlayerRegistry.java b/src/main/java/redxax/oxy/remotely/data/player/PlayerRegistry.java new file mode 100644 index 00000000..7bd78643 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/PlayerRegistry.java @@ -0,0 +1,30 @@ +package redxax.oxy.remotely.data.player; + +import redxax.oxy.remotely.data.player.model.UnifiedPlayer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class PlayerRegistry { + private final Map players = new ConcurrentHashMap<>(); + + public UnifiedPlayer getOrCreate(UUID uuid, String name) { + return players.computeIfAbsent(uuid, k -> new UnifiedPlayer(k, name)); + } + + public UnifiedPlayer get(UUID uuid) { + return players.get(uuid); + } + + public List getAll() { + return new ArrayList<>(players.values()); + } + + public void cleanup() { + + } +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/PlayerService.java b/src/main/java/redxax/oxy/remotely/data/player/PlayerService.java new file mode 100644 index 00000000..77163ac6 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/PlayerService.java @@ -0,0 +1,111 @@ +package redxax.oxy.remotely.data.player; + +import redxax.oxy.remotely.data.player.action.IActionExecutor; +import redxax.oxy.remotely.data.player.model.UnifiedPlayer; +import redxax.oxy.remotely.data.player.source.IPlayerSource; +import restudio.rescreen.debug.DebugManager; + +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +public class PlayerService { + private final PlayerRegistry registry = new PlayerRegistry(); + private final List sources = new CopyOnWriteArrayList<>(); + private final List executors = new CopyOnWriteArrayList<>(); + private final List>> listeners = new CopyOnWriteArrayList<>(); + + public void registerSource(IPlayerSource source) { + sources.add(source); + source.init(this); + source.enable(); + } + + public void registerExecutor(IActionExecutor executor) { + executors.add(executor); + } + + public void refreshSources() { + for (IPlayerSource source : sources) { + source.refresh(); + } + } + + public void submitUpdate(PlayerUpdateBatch batch) { + for (PlayerUpdateBatch.PlayerUpdate update : batch.getUpdates()) { + UnifiedPlayer player = registry.getOrCreate(update.getUuid(), update.getName()); + + if (update.getName() != null && !update.getName().isEmpty()) { + player.setName(update.getName()); + } + + String src = batch.getSource(); + int prio = batch.getPriority(); + + if (update.getOnline() != null) player.updateOnline(update.getOnline(), src, prio); + if (update.getPing() != null) player.updatePing(update.getPing(), src, prio); + if (update.getOp() != null) player.updateOp(update.getOp(), src, prio); + if (update.getLastSeen() != null) player.updateLastSeen(update.getLastSeen(), src, prio); + + if (update.shouldClearBan()) player.updateBan(null, src, prio); + else if (update.getBan() != null) player.updateBan(update.getBan(), src, prio); + + if (update.shouldClearIp()) player.updateIp(null, src, prio); + else if (update.getIp() != null) player.updateIp(update.getIp(), src, prio); + } + + notifyListeners(); + } + + public CompletableFuture executeAction(UnifiedPlayer player, String actionType, Object... args) { + DebugManager.getInstance().log("PlayerService", "Executing action: " + actionType + " for " + player.getName()); + List candidates = executors.stream() + .filter(e -> { + boolean can = e.canExecute(actionType); + DebugManager.getInstance().log("PlayerService", "Executor " + e.getClass().getSimpleName() + " canExecute(" + actionType + "): " + can); + return can; + }) + .sorted(Comparator.comparingInt(IActionExecutor::getPriority).reversed()) + .toList(); + + if (candidates.isEmpty()) { + DebugManager.getInstance().log("PlayerService", "No executors found for " + actionType); + return CompletableFuture.failedFuture(new IllegalStateException("No executor found for action: " + actionType)); + } + + DebugManager.getInstance().log("PlayerService", "Candidates: " + candidates.size() + ". Starting chain."); + return executeChain(candidates, 0, player, actionType, args); + } + + private CompletableFuture executeChain(List executors, int index, UnifiedPlayer player, String actionType, Object... args) { + if (index >= executors.size()) { + DebugManager.getInstance().log("PlayerService", "Chain exhausted. All failed."); + return CompletableFuture.failedFuture(new IllegalStateException("All executors failed for action: " + actionType)); + } + + IActionExecutor current = executors.get(index); + DebugManager.getInstance().log("PlayerService", "Trying executor: " + current.getClass().getSimpleName()); + return current.execute(player, actionType, args) + .exceptionallyCompose(ex -> { + DebugManager.getInstance().log("PlayerService", "Executor " + current.getClass().getSimpleName() + " failed: " + ex.getMessage()); + return executeChain(executors, index + 1, player, actionType, args); + }); + } + + public void addListener(Consumer> listener) { + listeners.add(listener); + } + + private void notifyListeners() { + List allPlayers = registry.getAll(); + for (Consumer> listener : listeners) { + listener.accept(allPlayers); + } + } + + public PlayerRegistry getRegistry() { + return registry; + } +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/PlayerUpdateBatch.java b/src/main/java/redxax/oxy/remotely/data/player/PlayerUpdateBatch.java new file mode 100644 index 00000000..4564bf4e --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/PlayerUpdateBatch.java @@ -0,0 +1,78 @@ +package redxax.oxy.remotely.data.player; + +import redxax.oxy.remotely.data.player.model.BanInfo; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class PlayerUpdateBatch { + private final String source; + private final int priority; + private final List updates = new ArrayList<>(); + + public PlayerUpdateBatch(String source, int priority) { + this.source = source; + this.priority = priority; + } + + public void add(PlayerUpdate update) { + updates.add(update); + } + + public String getSource() { + return source; + } + + public int getPriority() { + return priority; + } + + public List getUpdates() { + return updates; + } + + public static class PlayerUpdate { + private final UUID uuid; + private final String name; + + private Boolean online; + private Integer ping; + private Boolean op; + private BanInfo ban; + private String ip; + private Long lastSeen; + + public PlayerUpdate(UUID uuid, String name) { + this.uuid = uuid; + this.name = name; + } + + public UUID getUuid() { return uuid; } + public String getName() { return name; } + + public Boolean getOnline() { return online; } + public void setOnline(Boolean online) { this.online = online; } + + public Integer getPing() { return ping; } + public void setPing(Integer ping) { this.ping = ping; } + + public Boolean getOp() { return op; } + public void setOp(Boolean op) { this.op = op; } + + public BanInfo getBan() { return ban; } + public void setBan(BanInfo ban) { this.ban = ban; } + public boolean shouldClearBan() { return clearBan; } + public void clearBan() { this.clearBan = true; } + + public String getIp() { return ip; } + public void setIp(String ip) { this.ip = ip; } + public boolean shouldClearIp() { return clearIp; } + public void clearIp() { this.clearIp = true; } + + public Long getLastSeen() { return lastSeen; } + public void setLastSeen(Long lastSeen) { this.lastSeen = lastSeen; } + + private boolean clearBan = false; + private boolean clearIp = false; + } +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/action/BackendActionExecutor.java b/src/main/java/redxax/oxy/remotely/data/player/action/BackendActionExecutor.java new file mode 100644 index 00000000..54c7ddb4 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/action/BackendActionExecutor.java @@ -0,0 +1,46 @@ +package redxax.oxy.remotely.data.player.action; + +import redxax.oxy.remotely.data.player.model.UnifiedPlayer; +import restudio.rebase.backend.feature.PlayerManagementFeature; + +import java.util.concurrent.CompletableFuture; + +public class BackendActionExecutor implements IActionExecutor { + private final PlayerManagementFeature feature; + + public BackendActionExecutor(PlayerManagementFeature feature) { + this.feature = feature; + } + + @Override + public boolean canExecute(String actionType) { + return switch (actionType) { + case "kick", "ban", "unban", "op", "deop" -> true; + default -> false; + }; + } + + @Override + public CompletableFuture execute(UnifiedPlayer player, String actionType, Object... args) { + return switch (actionType) { + case "kick" -> { + String reason = args.length > 0 ? (String) args[0] : "Kicked by operator"; + yield feature.kick(player.getUuid(), reason); + } + case "ban" -> { + String reason = args.length > 0 ? (String) args[0] : "Banned by operator"; + boolean ipBan = args.length > 1 && (boolean) args[1]; + yield feature.ban(player.getUuid(), reason, ipBan); + } + case "unban" -> feature.unban(player.getUuid()); + case "op" -> feature.setOp(player.getUuid(), true); + case "deop" -> feature.setOp(player.getUuid(), false); + default -> CompletableFuture.failedFuture(new UnsupportedOperationException("Unknown action: " + actionType)); + }; + } + + @Override + public int getPriority() { + return 20; + } +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/action/IActionExecutor.java b/src/main/java/redxax/oxy/remotely/data/player/action/IActionExecutor.java new file mode 100644 index 00000000..207207bd --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/action/IActionExecutor.java @@ -0,0 +1,10 @@ +package redxax.oxy.remotely.data.player.action; + +import redxax.oxy.remotely.data.player.model.UnifiedPlayer; +import java.util.concurrent.CompletableFuture; + +public interface IActionExecutor { + boolean canExecute(String actionType); + CompletableFuture execute(UnifiedPlayer player, String actionType, Object... args); + int getPriority(); +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/action/MsmpActionExecutor.java b/src/main/java/redxax/oxy/remotely/data/player/action/MsmpActionExecutor.java new file mode 100644 index 00000000..92150d2c --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/action/MsmpActionExecutor.java @@ -0,0 +1,60 @@ +package redxax.oxy.remotely.data.player.action; + +import redxax.oxy.remotely.data.player.model.UnifiedPlayer; +import restudio.rebase.msmp.IMSMPApi; +import restudio.rebase.msmp.MSMPManager; + +import java.util.concurrent.CompletableFuture; + +public class MsmpActionExecutor implements IActionExecutor { + private final MSMPManager msmpManager; + + public MsmpActionExecutor(MSMPManager msmpManager) { + this.msmpManager = msmpManager; + } + + @Override + public boolean canExecute(String actionType) { + if (msmpManager.getApi() == null || !msmpManager.isConnected) return false; + return switch (actionType) { + case "kick", "ban", "unban", "op", "deop" -> true; + default -> false; + }; + } + + @Override + public CompletableFuture execute(UnifiedPlayer player, String actionType, Object... args) { + IMSMPApi api = msmpManager.getApi(); + if (api == null || !msmpManager.isConnected) return CompletableFuture.failedFuture(new IllegalStateException("Not connected to MSMP")); + + String uuid = player.getUuid().toString(); + String name = player.getName(); + + CompletableFuture future = switch (actionType) { + case "kick" -> { + String reason = args.length > 0 ? (String) args[0] : "Kicked by operator"; + yield api.kickPlayer(uuid, reason); + } + case "ban" -> { + String reason = args.length > 0 ? (String) args[0] : "Banned by operator"; + boolean ipBan = args.length > 1 && (boolean) args[1]; + if (ipBan && player.getIp().getValue() != null) { + yield api.banIp(player.getIp().getValue(), uuid, reason, null); + } else { + yield api.banPlayer(uuid, name, reason, null); + } + } + case "unban" -> api.unbanPlayer(uuid); + case "op" -> api.opPlayer(uuid, 4); + case "deop" -> api.deopPlayer(uuid); + default -> CompletableFuture.failedFuture(new UnsupportedOperationException("Unknown action: " + actionType)); + }; + + return future.thenApply(v -> null); + } + + @Override + public int getPriority() { + return 30; + } +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/action/StandardActionExecutor.java b/src/main/java/redxax/oxy/remotely/data/player/action/StandardActionExecutor.java new file mode 100644 index 00000000..e806d796 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/action/StandardActionExecutor.java @@ -0,0 +1,75 @@ +package redxax.oxy.remotely.data.player.action; + +import redxax.oxy.remotely.data.player.model.UnifiedPlayer; +import restudio.rebase.ui.widgets.TerminalWidget; +import restudio.rescreen.debug.DebugManager; + +import java.util.concurrent.CompletableFuture; + +public class StandardActionExecutor implements IActionExecutor { + private final TerminalWidget terminal; + + public StandardActionExecutor(TerminalWidget terminal) { + this.terminal = terminal; + } + + @Override + public boolean canExecute(String actionType) { + return switch (actionType) { + case "kick", "ban", "unban", "op", "deop", "command" -> true; + default -> false; + }; + } + + @Override + public CompletableFuture execute(UnifiedPlayer player, String actionType, Object... args) { + if (terminal == null) { + DebugManager.getInstance().log("StandardActionExecutor", "Terminal is null!"); + return CompletableFuture.completedFuture(null); + } + String name = player.getName(); + if (name == null && !actionType.equals("command")) { + DebugManager.getInstance().log("StandardActionExecutor", "Player name is null!"); + return CompletableFuture.completedFuture(null); + } + + DebugManager.getInstance().log("StandardActionExecutor", "Executing " + actionType + " for " + name); + + switch (actionType) { + case "kick" -> { + String reason = args.length > 0 ? (String) args[0] : "Kicked by operator"; + terminal.executeCommand("kick " + name + " " + reason); + } + case "ban" -> { + String reason = args.length > 0 ? (String) args[0] : "Banned by operator"; + boolean ipBan = args.length > 1 && (boolean) args[1]; + if (ipBan) { + terminal.executeCommand("ban-ip " + name + " " + reason); + } else { + terminal.executeCommand("ban " + name + " " + reason); + } + } + case "unban" -> { + terminal.executeCommand("pardon " + name); + } + case "op" -> { + terminal.executeCommand("op " + name); + } + case "deop" -> { + terminal.executeCommand("deop " + name); + } + case "command" -> { + String command = args.length > 0 ? (String) args[0] : null; + if (command != null) { + terminal.executeCommand(command); + } + } + } + return CompletableFuture.completedFuture(null); + } + + @Override + public int getPriority() { + return 10; + } +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/model/BanInfo.java b/src/main/java/redxax/oxy/remotely/data/player/model/BanInfo.java new file mode 100644 index 00000000..863d34b3 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/model/BanInfo.java @@ -0,0 +1,10 @@ +package redxax.oxy.remotely.data.player.model; + +public record BanInfo( + String uuid, + String name, + String created, + String source, + String expires, + String reason +) {} diff --git a/src/main/java/redxax/oxy/remotely/data/player/model/PlayerAttribute.java b/src/main/java/redxax/oxy/remotely/data/player/model/PlayerAttribute.java new file mode 100644 index 00000000..e6ddf926 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/model/PlayerAttribute.java @@ -0,0 +1,31 @@ +package redxax.oxy.remotely.data.player.model; + +public class PlayerAttribute { + private final T value; + private final String source; + private final long timestamp; + private final int priority; + + public PlayerAttribute(T value, String source, int priority) { + this.value = value; + this.source = source; + this.priority = priority; + this.timestamp = System.currentTimeMillis(); + } + + public T getValue() { + return value; + } + + public String getSource() { + return source; + } + + public long getTimestamp() { + return timestamp; + } + + public int getPriority() { + return priority; + } +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/model/UnifiedPlayer.java b/src/main/java/redxax/oxy/remotely/data/player/model/UnifiedPlayer.java new file mode 100644 index 00000000..7c6159ae --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/model/UnifiedPlayer.java @@ -0,0 +1,76 @@ +package redxax.oxy.remotely.data.player.model; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + +public class UnifiedPlayer { + private final UUID uuid; + private volatile String name; + + private volatile PlayerAttribute online = new PlayerAttribute<>(false, "default", -1); + private volatile PlayerAttribute ping = new PlayerAttribute<>(-1, "default", -1); + private volatile PlayerAttribute op = new PlayerAttribute<>(false, "default", -1); + private volatile PlayerAttribute ban = new PlayerAttribute<>(null, "default", -1); + private volatile PlayerAttribute ip = new PlayerAttribute<>(null, "default", -1); + private volatile PlayerAttribute lastSeen = new PlayerAttribute<>(0L, "default", -1); + + public UnifiedPlayer(UUID uuid, String name) { + this.uuid = uuid; + this.name = name; + } + + public UUID getUuid() { + return uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public PlayerAttribute getOnline() { return online; } + public PlayerAttribute getPing() { return ping; } + public PlayerAttribute getOp() { return op; } + public PlayerAttribute getBan() { return ban; } + public PlayerAttribute getIp() { return ip; } + public PlayerAttribute getLastSeen() { return lastSeen; } + + public boolean isOnline() { return online.getValue() != null && online.getValue(); } + public int getPingValue() { return ping.getValue() == null ? -1 : ping.getValue(); } + public boolean isOp() { return op.getValue() != null && op.getValue(); } + public long getLastSeenValue() { return lastSeen.getValue() == null ? 0L : lastSeen.getValue(); } + + private PlayerAttribute resolve(PlayerAttribute current, T newValue, String source, int priority) { + if (priority >= current.getPriority() || source.equals(current.getSource())) { + return new PlayerAttribute<>(newValue, source, priority); + } + return current; + } + + public void updateOnline(boolean value, String source, int priority) { + this.online = resolve(this.online, value, source, priority); + } + + public void updatePing(int value, String source, int priority) { + this.ping = resolve(this.ping, value, source, priority); + } + + public void updateOp(boolean value, String source, int priority) { + this.op = resolve(this.op, value, source, priority); + } + + public void updateBan(BanInfo value, String source, int priority) { + this.ban = resolve(this.ban, value, source, priority); + } + + public void updateIp(String value, String source, int priority) { + this.ip = resolve(this.ip, value, source, priority); + } + + public void updateLastSeen(long value, String source, int priority) { + this.lastSeen = resolve(this.lastSeen, value, source, priority); + } +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/msmp/MsmpPlayerProvider.java b/src/main/java/redxax/oxy/remotely/data/player/msmp/MsmpPlayerProvider.java deleted file mode 100644 index 3e447878..00000000 --- a/src/main/java/redxax/oxy/remotely/data/player/msmp/MsmpPlayerProvider.java +++ /dev/null @@ -1,273 +0,0 @@ -package redxax.oxy.remotely.data.player.msmp; - -import redxax.oxy.remotely.data.managed.BanEntry; -import redxax.oxy.remotely.data.managed.IpBanEntry; -import redxax.oxy.remotely.data.managed.ManagedPlayer; -import redxax.oxy.remotely.data.managed.PlayerAction; -import redxax.oxy.remotely.data.player.IPlayerActionProvider; -import redxax.oxy.remotely.data.player.IPlayerDataProvider; -import restudio.rebase.msmp.IMSMPApi; -import restudio.rebase.msmp.MSMPManager; -import restudio.rebase.msmp.dto.OpEntry; -import restudio.rebase.msmp.dto.Player; -import restudio.rescreen.debug.DebugManager; -import restudio.rescreen.ui.core.ScreenManager; - -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; - -public class MsmpPlayerProvider implements IPlayerDataProvider, IPlayerActionProvider { - - private final MSMPManager msmpManager; - private final Map playerCache = new ConcurrentHashMap<>(); - private final List>> updateListeners = new CopyOnWriteArrayList<>(); - private boolean initialized = false; - - public MsmpPlayerProvider(MSMPManager msmpManager) { - this.msmpManager = msmpManager; - } - - public boolean isConnectionActive() { - IMSMPApi api = msmpManager.getApi(); - return api != null && api.isConnected(); - } - - @Override - public void initialize() { - if (initialized) return; - initialized = true; - msmpManager.setOnPlayersChange(this::onMsmpPlayersUpdate); - if (msmpManager.isConnected) { - fullRefresh(); - } else { - msmpManager.connect(); - } - } - - @Override - public void shutdown() { - initialized = false; - msmpManager.setOnPlayersChange(null); - updateListeners.clear(); - playerCache.clear(); - } - - @Override - public CompletableFuture fullRefresh() { - IMSMPApi api = msmpManager.getApi(); - if (api == null || !api.isConnected()) { - msmpManager.connect(); - return CompletableFuture.completedFuture(null); - } - - CompletableFuture> playersF = api.getPlayers(); - CompletableFuture> bansF = api.getBans(); - CompletableFuture> ipBansF = api.getIpBans(); - CompletableFuture> opsF = api.getOps(); - - return CompletableFuture.allOf(playersF, bansF, ipBansF, opsF).thenRun(() -> { - try { - updateCache(playersF.join(), bansF.join(), ipBansF.join(), opsF.join()); - } catch (Exception e) { - DebugManager.getInstance().log("MsmpPlayerProvider", "Failed to refresh data: " + e.getMessage()); - } - }); - } - - @Override - public Map getCachedPlayers() { - return playerCache; - } - - @Override - public void addUpdateListener(Consumer> listener) { - updateListeners.add(listener); - } - - @Override - public void removeUpdateListener(Consumer> listener) { - updateListeners.remove(listener); - } - - private void onMsmpPlayersUpdate(List msmpPlayers) { - if (msmpPlayers != null) { - fullRefresh(); - } - } - - private void updateCache(List msmpPlayers, List bans, List ipBans, List ops) { - Set touched = new HashSet<>(); - - if (msmpPlayers != null) { - for (Player mp : msmpPlayers) { - touched.add(mp.uuid); - ManagedPlayer managed = playerCache.computeIfAbsent(mp.uuid, u -> new ManagedPlayer(u, mp.name)); - managed.name = mp.name; - managed.isOnline = true; - managed.ping = mp.ping; - managed.address = mp.address; - managed.lastSeen = System.currentTimeMillis(); - } - } - - if (bans != null) { - for (restudio.rebase.msmp.dto.BanEntry b : bans) { - if (b.uuid == null) continue; - try { - UUID u = UUID.fromString(b.uuid); - touched.add(u); - ManagedPlayer managed = playerCache.computeIfAbsent(u, uuid -> new ManagedPlayer(uuid, b.name != null ? b.name : "Unknown")); - managed.isBanned = true; - BanEntry legacyBan = new BanEntry(); - legacyBan.uuid = b.uuid; - legacyBan.name = b.name; - legacyBan.reason = b.reason; - legacyBan.source = b.source; - legacyBan.created = b.created; - legacyBan.expires = b.expires; - managed.banInfo = legacyBan; - } catch (Exception ignored) {} - } - } - - if (ipBans != null) { - for (restudio.rebase.msmp.dto.BanEntry b : ipBans) { - if (b.ip == null) continue; - for (ManagedPlayer p : playerCache.values()) { - if (p.address != null && p.address.startsWith(b.ip)) { - p.isIpBanned = true; - IpBanEntry legacy = new IpBanEntry(); - legacy.ip = b.ip; - legacy.reason = b.reason; - legacy.source = b.source; - legacy.created = b.created; - legacy.expires = b.expires; - p.ipBanInfo = legacy; - } - } - } - } - - if (ops != null) { - for (OpEntry op : ops) { - if (op.uuid == null) continue; - try { - UUID u = UUID.fromString(op.uuid); - touched.add(u); - ManagedPlayer managed = playerCache.computeIfAbsent(u, uuid -> new ManagedPlayer(uuid, op.name != null ? op.name : "Unknown")); - managed.isOp = true; - managed.opLevel = op.level; - } catch (Exception ignored) {} - } - } - - for (ManagedPlayer cached : playerCache.values()) { - if (msmpPlayers != null) { - boolean isOnline = false; - for(Player p : msmpPlayers) if(p.uuid.equals(cached.uuid)) { isOnline = true; break; } - if(!isOnline) { - cached.isOnline = false; - cached.ping = -1; - } - } - - if (bans != null) { - boolean isBanned = false; - for(restudio.rebase.msmp.dto.BanEntry b : bans) if(b.uuid != null && b.uuid.equals(cached.uuid.toString())) { isBanned = true; break; } - if (!isBanned) { - cached.isBanned = false; - cached.banInfo = null; - } - } - - if (ops != null) { - boolean isOp = false; - for(OpEntry o : ops) if(o.uuid != null && o.uuid.equals(cached.uuid.toString())) { isOp = true; break; } - if (!isOp) { - cached.isOp = false; - cached.opLevel = 0; - } - } - } - - notifyListeners(); - } - - private void notifyListeners() { - List snapshot = new ArrayList<>(playerCache.values()); - ScreenManager.getInstance().execute(() -> { - for (Consumer> listener : updateListeners) { - listener.accept(snapshot); - } - }); - } - - private void logAction(String action, String target) { - DebugManager.getInstance().recordEvent(msmpManager.getInstance().getInstanceId(), "Player Action", "MSMP", String.format("%s %s", action, target)); - } - - @Override - public CompletableFuture kickPlayer(ManagedPlayer player, String reason) { - IMSMPApi api = msmpManager.getApi(); - if (api == null) return failedFuture("Not connected to MSMP"); - logAction("Kicked", player.name); - return api.kickPlayer(player.uuid.toString(), reason).thenCompose(v -> fullRefresh()); - } - - @Override - public CompletableFuture banPlayer(ManagedPlayer player, String reason, boolean ipBan) { - IMSMPApi api = msmpManager.getApi(); - if (api == null) return failedFuture("Not connected to MSMP"); - CompletableFuture action; - if (ipBan && player.address != null) { - String ip = player.address.split(":")[0].replace("/", ""); - logAction("IP Banned", player.name + " (" + ip + ")"); - action = api.banIp(ip, player.uuid.toString(), reason, null); - } else { - logAction("Banned", player.name); - action = api.banPlayer(player.uuid.toString(), player.name, reason, null); - } - return action.thenCompose(v -> fullRefresh()); - } - - @Override - public CompletableFuture unbanPlayer(ManagedPlayer player) { - IMSMPApi api = msmpManager.getApi(); - if (api == null) return failedFuture("Not connected to MSMP"); - logAction("Unbanned", player.name); - return api.unbanPlayer(player.uuid.toString()).thenCompose(v -> fullRefresh()); - } - - @Override - public CompletableFuture toggleOp(ManagedPlayer player) { - IMSMPApi api = msmpManager.getApi(); - if (api == null) return failedFuture("Not connected to MSMP"); - CompletableFuture action; - if (player.isOp) { - logAction("De-opped", player.name); - action = api.deopPlayer(player.uuid.toString()); - } - else { - logAction("Opped", player.name); - action = api.opPlayer(player.uuid.toString(), 4); - } - return action.thenCompose(v -> fullRefresh()); - } - - @Override - public CompletableFuture runCustomCommand(ManagedPlayer player, String commandTemplate) { - return CompletableFuture.failedFuture(new UnsupportedOperationException("MSMP does not support arbitrary command execution.")); - } - - @Override - public CompletableFuture> getCustomActions() { - return CompletableFuture.completedFuture(Collections.emptyList()); - } - - private CompletableFuture failedFuture(String message) { - return CompletableFuture.failedFuture(new IllegalStateException(message)); - } -} diff --git a/src/main/java/redxax/oxy/remotely/data/player/source/BackendPlayerSource.java b/src/main/java/redxax/oxy/remotely/data/player/source/BackendPlayerSource.java new file mode 100644 index 00000000..402936a2 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/source/BackendPlayerSource.java @@ -0,0 +1,75 @@ +package redxax.oxy.remotely.data.player.source; + +import redxax.oxy.remotely.data.player.PlayerService; +import redxax.oxy.remotely.data.player.PlayerUpdateBatch; +import redxax.oxy.remotely.data.player.model.UnifiedPlayer; +import restudio.rebase.backend.feature.PlayerManagementFeature; +import restudio.rescreen.debug.DebugManager; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class BackendPlayerSource implements IPlayerSource { + private final PlayerManagementFeature feature; + private PlayerService service; + private boolean enabled = false; + + public BackendPlayerSource(PlayerManagementFeature feature) { + this.feature = feature; + } + + @Override + public void init(PlayerService context) { + this.service = context; + } + + @Override + public void enable() { + enabled = true; + refresh(); + } + + @Override + public void disable() { + enabled = false; + } + + @Override + public int getPriority() { + return 25; + } + + @Override + public void refresh() { + if (!enabled || service == null) return; + + feature.getOnlinePlayers().thenAccept(this::processPlayers) + .exceptionally(e -> { + DebugManager.getInstance().log("BackendPlayerSource", "Failed to fetch players: " + e.getMessage()); + return null; + }); + } + + private void processPlayers(List onlinePlayers) { + if (onlinePlayers == null) return; + + PlayerUpdateBatch batch = new PlayerUpdateBatch("backend", getPriority()); + + for (PlayerManagementFeature.SimplePlayer sp : onlinePlayers) { + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(sp.uuid(), sp.name()); + update.setOnline(true); + batch.add(update); + } + + service.getRegistry().getAll().stream() + .filter(UnifiedPlayer::isOnline) + .filter(p -> onlinePlayers.stream().noneMatch(sp -> sp.uuid().equals(p.getUuid()))) + .forEach(p -> { + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(p.getUuid(), p.getName()); + update.setOnline(false); + batch.add(update); + }); + + service.submitUpdate(batch); + } +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/source/IPlayerSource.java b/src/main/java/redxax/oxy/remotely/data/player/source/IPlayerSource.java new file mode 100644 index 00000000..55bdf0b8 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/source/IPlayerSource.java @@ -0,0 +1,11 @@ +package redxax.oxy.remotely.data.player.source; + +import redxax.oxy.remotely.data.player.PlayerService; + +public interface IPlayerSource { + void init(PlayerService context); + void enable(); + void disable(); + int getPriority(); + void refresh(); +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/source/MsmpPlayerSource.java b/src/main/java/redxax/oxy/remotely/data/player/source/MsmpPlayerSource.java new file mode 100644 index 00000000..50a242d2 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/source/MsmpPlayerSource.java @@ -0,0 +1,151 @@ +package redxax.oxy.remotely.data.player.source; + +import redxax.oxy.remotely.data.player.PlayerService; +import redxax.oxy.remotely.data.player.PlayerUpdateBatch; +import redxax.oxy.remotely.data.player.model.BanInfo; +import redxax.oxy.remotely.data.player.model.UnifiedPlayer; +import restudio.rebase.msmp.IMSMPApi; +import restudio.rebase.msmp.MSMPManager; +import restudio.rebase.msmp.dto.OpEntry; +import restudio.rebase.msmp.dto.Player; +import restudio.rescreen.debug.DebugManager; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class MsmpPlayerSource implements IPlayerSource { + private final MSMPManager msmpManager; + private PlayerService service; + private boolean enabled = false; + + public MsmpPlayerSource(MSMPManager msmpManager) { + this.msmpManager = msmpManager; + } + + @Override + public void init(PlayerService context) { + this.service = context; + } + + @Override + public void enable() { + if (!enabled) { + enabled = true; + msmpManager.setOnPlayersChange(this::onMsmpPlayersUpdate); + if (msmpManager.isConnected) { + refreshAll(); + } else { + msmpManager.connect(); + } + } + } + + @Override + public void disable() { + enabled = false; + msmpManager.setOnPlayersChange(null); + } + + @Override + public void refresh() { + refreshAll(); + } + + @Override + public int getPriority() { + return 30; + } + + private void onMsmpPlayersUpdate(List msmpPlayers) { + if (msmpPlayers != null) { + refreshAll(); + } + } + + public void refreshAll() { + IMSMPApi api = msmpManager.getApi(); + if (api == null || !api.isConnected()) return; + + CompletableFuture> playersF = api.getPlayers(); + CompletableFuture> bansF = api.getBans(); + CompletableFuture> ipBansF = api.getIpBans(); + CompletableFuture> opsF = api.getOps(); + + CompletableFuture.allOf(playersF, bansF, ipBansF, opsF).thenRun(() -> { + try { + updateCache(playersF.join(), bansF.join(), ipBansF.join(), opsF.join()); + } catch (Exception e) { + DebugManager.getInstance().log("MsmpPlayerSource", "Failed to refresh data: " + e.getMessage()); + } + }); + } + + private void updateCache(List msmpPlayers, List bans, List ipBans, List ops) { + if (service == null) return; + PlayerUpdateBatch batch = new PlayerUpdateBatch("msmp", getPriority()); + + if (msmpPlayers != null) { + for (Player mp : msmpPlayers) { + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(mp.uuid, mp.name); + update.setOnline(true); + update.setPing(mp.ping); + update.setIp(mp.address); + batch.add(update); + } + + service.getRegistry().getAll().stream() + .filter(UnifiedPlayer::isOnline) + .filter(p -> msmpPlayers.stream().noneMatch(mp -> mp.uuid.equals(p.getUuid()))) + .forEach(p -> { + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(p.getUuid(), p.getName()); + update.setOnline(false); + update.setPing(-1); + batch.add(update); + }); + } + + if (bans != null) { + for (restudio.rebase.msmp.dto.BanEntry b : bans) { + if (b.uuid == null) continue; + try { + UUID u = UUID.fromString(b.uuid); + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(u, b.name); + BanInfo banInfo = new BanInfo(b.uuid, b.name, b.created, b.source, b.expires, b.reason); + update.setBan(banInfo); + batch.add(update); + } catch (Exception ignored) {} + } + service.getRegistry().getAll().stream() + .filter(p -> p.getBan().getValue() != null) + .filter(p -> bans.stream().noneMatch(b -> b.uuid != null && b.uuid.equalsIgnoreCase(p.getUuid().toString()))) + .forEach(p -> { + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(p.getUuid(), p.getName()); + update.clearBan(); + batch.add(update); + }); + } + + if (ops != null) { + for (OpEntry op : ops) { + if (op.uuid == null) continue; + try { + UUID u = UUID.fromString(op.uuid); + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(u, op.name); + update.setOp(true); + batch.add(update); + } catch (Exception ignored) {} + } + service.getRegistry().getAll().stream() + .filter(UnifiedPlayer::isOp) + .filter(p -> ops.stream().noneMatch(o -> o.uuid != null && o.uuid.equalsIgnoreCase(p.getUuid().toString()))) + .forEach(p -> { + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(p.getUuid(), p.getName()); + update.setOp(false); + batch.add(update); + }); + } + + service.submitUpdate(batch); + } +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/source/StandardFileSource.java b/src/main/java/redxax/oxy/remotely/data/player/source/StandardFileSource.java new file mode 100644 index 00000000..431f6e39 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/source/StandardFileSource.java @@ -0,0 +1,159 @@ +package redxax.oxy.remotely.data.player.source; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import redxax.oxy.remotely.data.managed.BanEntry; +import redxax.oxy.remotely.data.managed.IpBanEntry; +import redxax.oxy.remotely.data.managed.OpEntry; +import redxax.oxy.remotely.data.player.PlayerService; +import redxax.oxy.remotely.data.player.PlayerUpdateBatch; +import redxax.oxy.remotely.data.player.model.BanInfo; +import redxax.oxy.remotely.data.player.model.UnifiedPlayer; +import restudio.rebase.api.RebaseAPI; +import restudio.rebase.instance.Instance; + +import java.lang.reflect.Type; +import java.nio.file.Path; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class StandardFileSource implements IPlayerSource { + private final RebaseAPI api; + private PlayerService service; + private final Gson gson = new Gson(); + private boolean enabled = false; + + private final Path opsPath; + private final Path bannedPlayersPath; + private final Path bannedIpsPath; + + public StandardFileSource(Instance instance, RebaseAPI api) { + this.api = api; + Path instancePath = Path.of(instance.getPath()); + this.opsPath = instancePath.resolve("ops.json"); + this.bannedPlayersPath = instancePath.resolve("banned-players.json"); + this.bannedIpsPath = instancePath.resolve("banned-ips.json"); + } + + @Override + public void init(PlayerService context) { + this.service = context; + } + + @Override + public void enable() { + if (!enabled) { + enabled = true; + refreshAll(); + } + } + + @Override + public void disable() { + enabled = false; + } + + @Override + public void refresh() { + refreshAll(); + } + + @Override + public int getPriority() { + return 20; + } + + public void refreshAll() { + loadJsonFile(opsPath, new TypeToken>() {}).thenAccept(this::processOps); + loadJsonFile(bannedPlayersPath, new TypeToken>() {}).thenAccept(this::processBans); + loadJsonFile(bannedIpsPath, new TypeToken>() {}).thenAccept(this::processIpBans); + } + + public void updateFromContent(String fileName, String content) { + if (!enabled || content == null || content.isEmpty()) return; + try { + if (fileName.endsWith("ops.json")) { + List ops = gson.fromJson(content, new TypeToken>() {}.getType()); + processOps(ops); + } else if (fileName.endsWith("banned-players.json")) { + List bans = gson.fromJson(content, new TypeToken>() {}.getType()); + processBans(bans); + } else if (fileName.endsWith("banned-ips.json")) { + List ipBans = gson.fromJson(content, new TypeToken>() {}.getType()); + processIpBans(ipBans); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void processOps(List ops) { + if (ops == null || service == null) return; + PlayerUpdateBatch batch = new PlayerUpdateBatch("files", getPriority()); + for (OpEntry op : ops) { + UUID uuid = UUID.fromString(op.uuid); + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(uuid, op.name); + update.setOp(true); + batch.add(update); + } + + service.getRegistry().getAll().stream() + .filter(UnifiedPlayer::isOp) + .filter(p -> ops.stream().noneMatch(o -> o.uuid.equalsIgnoreCase(p.getUuid().toString()))) + .forEach(p -> { + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(p.getUuid(), p.getName()); + update.setOp(false); + batch.add(update); + }); + + service.submitUpdate(batch); + } + + private void processBans(List bans) { + if (bans == null || service == null) return; + PlayerUpdateBatch batch = new PlayerUpdateBatch("files", getPriority()); + for (BanEntry ban : bans) { + UUID uuid = UUID.fromString(ban.uuid); + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(uuid, ban.name); + BanInfo banInfo = new BanInfo(ban.uuid, ban.name, ban.created, ban.source, ban.expires, ban.reason); + update.setBan(banInfo); + batch.add(update); + } + + service.getRegistry().getAll().stream() + .filter(p -> p.getBan().getValue() != null) + .filter(p -> bans.stream().noneMatch(b -> b.uuid.equalsIgnoreCase(p.getUuid().toString()))) + .forEach(p -> { + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(p.getUuid(), p.getName()); + update.clearBan(); + batch.add(update); + }); + + service.submitUpdate(batch); + } + + private void processIpBans(List ipBans) { + if (ipBans == null || service == null) return; + PlayerUpdateBatch batch = new PlayerUpdateBatch("files", getPriority()); + + List bannedIps = ipBans.stream().map(b -> b.ip).toList(); + + service.getRegistry().getAll().stream() + .filter(p -> p.getIp().getValue() != null) + .forEach(p -> { + boolean isBanned = bannedIps.contains(p.getIp().getValue()); + }); + } + + private CompletableFuture> loadJsonFile(Path path, TypeToken> typeToken) { + return api.fileExists(path).thenCompose(exists -> { + if (!exists) return CompletableFuture.completedFuture(null); + return api.readFile(path).thenApply(content -> { + if (content == null || content.isEmpty()) return null; + Type type = typeToken.getType(); + return gson.fromJson(content, type); + }); + }); + } +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/source/StandardLogSource.java b/src/main/java/redxax/oxy/remotely/data/player/source/StandardLogSource.java new file mode 100644 index 00000000..860fe980 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/data/player/source/StandardLogSource.java @@ -0,0 +1,194 @@ +package redxax.oxy.remotely.data.player.source; + +import redxax.oxy.remotely.data.managed.SessionEventType; +import redxax.oxy.remotely.data.player.IPlayerHistoryCollector; +import redxax.oxy.remotely.data.player.PlayerService; +import redxax.oxy.remotely.data.player.PlayerUpdateBatch; +import redxax.oxy.remotely.data.player.model.BanInfo; +import restudio.rebase.instance.Instance; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class StandardLogSource implements IPlayerSource { + private final Instance instance; + private final IPlayerHistoryCollector historyCollector; + private PlayerService service; + private boolean enabled = false; + + private final Map nameToUuid = new ConcurrentHashMap<>(); + + private static final Pattern PLAYER_JOIN_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*?(\\w+)\\[/([0-9.:]+)] logged in with entity id \\d+ at .*"); + private static final Pattern PLAYER_LEAVE_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*?(\\w+) left the game"); + private static final Pattern PLAYER_UUID_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*?UUID of player (\\w+) is ([0-9a-f\\-]+)"); + private static final Pattern ANSI_PATTERN = Pattern.compile("\u001B\\[[0-9;]*[A-Za-z]"); + + private static final Pattern PLAYER_OP_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*Made (\\w+) a server operator.*"); + private static final Pattern PLAYER_DEOP_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*Made (\\w+) no longer a server operator.*"); + + private static final Pattern OP_ALREADY_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*?Nothing changed\\. The player is already an operator"); + private static final Pattern OP_NOT_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*?Nothing changed\\. The player is not an operator"); + + private static final Pattern PLAYER_BAN_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*?Banned (\\w+): (.*)"); + private static final Pattern PLAYER_UNBAN_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*?Unbanned (\\w+)"); + + public StandardLogSource(Instance instance, IPlayerHistoryCollector historyCollector) { + this.instance = instance; + this.historyCollector = historyCollector; + } + + + @Override + public void init(PlayerService context) { + this.service = context; + } + + @Override + public void enable() { + if (!enabled) { + instance.addLogListener(this::processLogLine); + enabled = true; + } + } + + @Override + public void disable() { + if (enabled) { + instance.removeLogListener(this::processLogLine); + enabled = false; + } + } + + @Override + public int getPriority() { + return 25; + } + + @Override + public void refresh() { + } + + private void processLogLine(int lineNum, String line) { + if (line == null) return; + line = ANSI_PATTERN.matcher(line).replaceAll("").trim(); + if (line.isEmpty()) return; + + Matcher uuidMatcher = PLAYER_UUID_PATTERN.matcher(line); + if (uuidMatcher.matches()) { + String name = uuidMatcher.group(1); + UUID uuid = UUID.fromString(uuidMatcher.group(2)); + nameToUuid.put(name, uuid); + PlayerUpdateBatch batch = new PlayerUpdateBatch("log", getPriority()); + batch.add(new PlayerUpdateBatch.PlayerUpdate(uuid, name)); + service.submitUpdate(batch); + return; + } + + Matcher joinMatcher = PLAYER_JOIN_PATTERN.matcher(line); + if (joinMatcher.matches()) { + String name = joinMatcher.group(1); + String ip = joinMatcher.group(2); + UUID uuid = nameToUuid.get(name); + if (uuid != null) { + PlayerUpdateBatch batch = new PlayerUpdateBatch("log", getPriority()); + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(uuid, name); + update.setOnline(true); + update.setIp(ip); + update.setLastSeen(System.currentTimeMillis()); + batch.add(update); + service.submitUpdate(batch); + + historyCollector.startSession(uuid, name, ip, System.currentTimeMillis()); + } + return; + } + + Matcher leaveMatcher = PLAYER_LEAVE_PATTERN.matcher(line); + if (leaveMatcher.matches()) { + String name = leaveMatcher.group(1); + UUID uuid = nameToUuid.get(name); + if (uuid != null) { + PlayerUpdateBatch batch = new PlayerUpdateBatch("log", getPriority()); + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(uuid, name); + update.setOnline(false); + update.setLastSeen(System.currentTimeMillis()); + batch.add(update); + service.submitUpdate(batch); + + historyCollector.endSession(uuid, System.currentTimeMillis()); + } + return; + } + + Matcher opMatcher = PLAYER_OP_PATTERN.matcher(line); + if (opMatcher.matches()) { + String name = opMatcher.group(1); + UUID uuid = nameToUuid.get(name); + if (uuid != null) { + PlayerUpdateBatch batch = new PlayerUpdateBatch("log", getPriority()); + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(uuid, name); + update.setOp(true); + batch.add(update); + service.submitUpdate(batch); + historyCollector.recordAccessChange(uuid, name, SessionEventType.OP_CHANGE, "op=true", System.currentTimeMillis()); + } + return; + } + + Matcher deopMatcher = PLAYER_DEOP_PATTERN.matcher(line); + if (deopMatcher.matches()) { + String name = deopMatcher.group(1); + UUID uuid = nameToUuid.get(name); + if (uuid != null) { + PlayerUpdateBatch batch = new PlayerUpdateBatch("log", getPriority()); + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(uuid, name); + update.setOp(false); + batch.add(update); + service.submitUpdate(batch); + historyCollector.recordAccessChange(uuid, name, SessionEventType.OP_CHANGE, "op=false", System.currentTimeMillis()); + } + return; + } + + if (OP_ALREADY_PATTERN.matcher(line).find() || OP_NOT_PATTERN.matcher(line).find()) { + if (service != null) service.refreshSources(); + return; + } + + Matcher banMatcher = PLAYER_BAN_PATTERN.matcher(line); + if (banMatcher.matches()) { + String name = banMatcher.group(1); + String reason = banMatcher.group(2); + UUID uuid = nameToUuid.get(name); + if (uuid != null) { + PlayerUpdateBatch batch = new PlayerUpdateBatch("log", 15); + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(uuid, name); + String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").format(new Date()); + update.setBan(new BanInfo(uuid.toString(), name, now, "Console", "Forever", reason)); + batch.add(update); + service.submitUpdate(batch); + historyCollector.recordAccessChange(uuid, name, SessionEventType.BAN, reason, System.currentTimeMillis()); + } + return; + } + + Matcher unbanMatcher = PLAYER_UNBAN_PATTERN.matcher(line); + if (unbanMatcher.matches()) { + String name = unbanMatcher.group(1); + UUID uuid = nameToUuid.get(name); + if (uuid != null) { + PlayerUpdateBatch batch = new PlayerUpdateBatch("log", 15); + PlayerUpdateBatch.PlayerUpdate update = new PlayerUpdateBatch.PlayerUpdate(uuid, name); + update.clearBan(); + batch.add(update); + service.submitUpdate(batch); + historyCollector.recordAccessChange(uuid, name, SessionEventType.UNBAN, "", System.currentTimeMillis()); + } + } + } +} diff --git a/src/main/java/redxax/oxy/remotely/data/player/standard/StandardPlayerActionProvider.java b/src/main/java/redxax/oxy/remotely/data/player/standard/StandardPlayerActionProvider.java deleted file mode 100644 index 4593b081..00000000 --- a/src/main/java/redxax/oxy/remotely/data/player/standard/StandardPlayerActionProvider.java +++ /dev/null @@ -1,113 +0,0 @@ -package redxax.oxy.remotely.data.player.standard; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import redxax.oxy.remotely.data.managed.ManagedPlayer; -import redxax.oxy.remotely.data.managed.PlayerAction; -import redxax.oxy.remotely.data.player.IPlayerActionProvider; -import restudio.rebase.api.RebaseAPI; -import restudio.rebase.instance.Instance; -import restudio.rebase.ui.widgets.TerminalWidget; -import restudio.rescreen.util.Notification; - -import java.lang.reflect.Type; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; - -public class StandardPlayerActionProvider implements IPlayerActionProvider { - - private final RebaseAPI api; - private final TerminalWidget terminalWidget; - private final Path playerActionsPath; - private List cachedPlayerActions = new ArrayList<>(); - private final Gson gson = new Gson(); - - public StandardPlayerActionProvider(Instance instance, RebaseAPI api, TerminalWidget terminalWidget) { - this.api = api; - this.terminalWidget = terminalWidget; - this.playerActionsPath = Path.of(instance.getPath(), "Remotely", "player-actions.json"); - } - - @Override - public void initialize() { - ensureRemotelyDirectory(); - loadPlayerActions(); - } - - @Override - public void shutdown() { - } - - @Override - public CompletableFuture kickPlayer(ManagedPlayer player, String reason) { - return runCustomCommand(player, "kick " + player.name + " " + reason); - } - - @Override - public CompletableFuture banPlayer(ManagedPlayer player, String reason, boolean ipBan) { - String command = ipBan ? "ban-ip " : "ban "; - command += player.name + " " + reason; - return runCustomCommand(player, command); - } - - @Override - public CompletableFuture unbanPlayer(ManagedPlayer player) { - String command = player.isIpBanned && player.ipBanInfo != null ? "pardon-ip " + player.ipBanInfo.ip : "pardon " + player.name; - return runCustomCommand(player, command); - } - - @Override - public CompletableFuture toggleOp(ManagedPlayer player) { - String command = player.isOp ? "deop " : "op "; - return runCustomCommand(player, command + player.name); - } - - @Override - public CompletableFuture runCustomCommand(ManagedPlayer player, String commandTemplate) { - return CompletableFuture.runAsync(() -> { - if (terminalWidget == null) { - new Notification.Builder().message("Failed To Execute").type(Notification.Type.ERROR).description("Terminal is not available"); - return; - } - String command = commandTemplate.replace("$name", player.name).replace("$uuid", player.uuid.toString()); - terminalWidget.executeCommand(command); - }); - } - - @Override - public CompletableFuture> getCustomActions() { - return CompletableFuture.completedFuture(cachedPlayerActions); - } - - private void loadPlayerActions() { - loadJsonFile(playerActionsPath, new TypeToken>() {}).thenAccept(actions -> this.cachedPlayerActions = Objects.requireNonNullElseGet(actions, ArrayList::new)); - } - - private void ensureRemotelyDirectory() { - Path remotelyDir = playerActionsPath.getParent(); - api.fileExists(remotelyDir).thenAccept(exists -> { - if (!exists) { - api.createDirectory(remotelyDir); - } - }); - } - - private CompletableFuture> loadJsonFile(Path path, TypeToken> typeToken) { - return api.fileExists(path).thenCompose(exists -> { - if (!exists) { - return CompletableFuture.completedFuture(new ArrayList<>()); - } - return api.readFile(path).thenApply(content -> { - if (content == null || content.isEmpty()) { - return new ArrayList<>(); - } - Type type = typeToken.getType(); - List result = gson.fromJson(content, type); - return result != null ? result : new ArrayList<>(); - }); - }); - } -} diff --git a/src/main/java/redxax/oxy/remotely/data/player/standard/StandardPlayerDataProvider.java b/src/main/java/redxax/oxy/remotely/data/player/standard/StandardPlayerDataProvider.java deleted file mode 100644 index a10051b4..00000000 --- a/src/main/java/redxax/oxy/remotely/data/player/standard/StandardPlayerDataProvider.java +++ /dev/null @@ -1,357 +0,0 @@ -package redxax.oxy.remotely.data.player.standard; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import redxax.oxy.remotely.data.managed.*; -import redxax.oxy.remotely.data.player.IPlayerDataProvider; -import redxax.oxy.remotely.data.player.IPlayerHistoryCollector; -import restudio.rebase.api.RebaseAPI; -import restudio.rebase.instance.Instance; -import restudio.rebase.ui.widgets.TerminalWidget; -import restudio.rescreen.debug.DebugManager; -import restudio.rescreen.ui.core.ScreenManager; - -import java.lang.reflect.Type; -import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -public class StandardPlayerDataProvider implements IPlayerDataProvider { - - private final RebaseAPI api; - private final Instance instance; - private final TerminalWidget terminalWidget; - private final IPlayerHistoryCollector historyCollector; - private final Gson gson = new Gson(); - private final Map players = new LinkedHashMap<>(); - private final List>> updateListeners = new CopyOnWriteArrayList<>(); - private final String instanceId; - - private final Path remotelyDir; - private final Path playerLogPath; - private final Path opsPath; - private final Path bannedPlayersPath; - private final Path bannedIpsPath; - - private static final Pattern PLAYER_JOIN_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*?(\\w+)\\[/([0-9.:]+)] logged in with entity id \\d+ at .*"); - private static final Pattern PLAYER_LEAVE_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*?(\\w+) left the game"); - private static final Pattern PLAYER_UUID_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*?UUID of player (\\w+) is ([0-9a-f\\-]+)"); - private static final Pattern PLAYER_OP_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*Made (\\w+) a server operator.*"); - private static final Pattern PLAYER_DEOP_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*Made (\\w+) no longer a server operator.*"); - private static final Pattern PLAYER_BAN_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*?Banned (\\w+): (.*)"); - private static final Pattern PLAYER_UNBAN_PATTERN = Pattern.compile("(?:.*\\[INFO]: )?.*?Unbanned (\\w+)"); - private static final Pattern ANSI_PATTERN = Pattern.compile("\u001B\\[[0-9;]*[A-Za-z]"); - - private List cachedPlayerLog = new ArrayList<>(); - private List cachedOps = new ArrayList<>(); - private List cachedBans = new ArrayList<>(); - private List cachedIpBans = new ArrayList<>(); - - public StandardPlayerDataProvider(Instance instance, RebaseAPI api, TerminalWidget terminalWidget, IPlayerHistoryCollector historyCollector) { - this.instance = instance; - this.api = api; - this.terminalWidget = terminalWidget; - this.historyCollector = historyCollector; - this.instanceId = instance.getInstanceId(); - - Path instancePath = Path.of(instance.getPath()); - this.remotelyDir = instancePath.resolve("Remotely"); - this.playerLogPath = remotelyDir.resolve("player-log.json"); - this.opsPath = instancePath.resolve("ops.json"); - this.bannedPlayersPath = instancePath.resolve("banned-players.json"); - this.bannedIpsPath = instancePath.resolve("banned-ips.json"); - } - - @Override - public void initialize() { - ensureRemotelyDirectory(); - instance.addLogListener(this::processLogLine); - fullRefresh(); - } - - @Override - public void shutdown() { - instance.removeLogListener(this::processLogLine); - players.clear(); - updateListeners.clear(); - } - - @Override - public CompletableFuture fullRefresh() { - CompletableFuture> playerLogFuture = loadJsonFile(playerLogPath, new TypeToken<>() {}); - CompletableFuture> opsFuture = loadJsonFile(opsPath, new TypeToken<>() {}); - CompletableFuture> bannedPlayersFuture = loadJsonFile(bannedPlayersPath, new TypeToken<>() {}); - CompletableFuture> bannedIpsFuture = loadJsonFile(bannedIpsPath, new TypeToken<>() {}); - - return CompletableFuture.allOf(playerLogFuture, opsFuture, bannedPlayersFuture, bannedIpsFuture).thenAccept(v -> { - this.cachedPlayerLog = playerLogFuture.join(); - this.cachedOps = opsFuture.join(); - this.cachedBans = bannedPlayersFuture.join(); - this.cachedIpBans = bannedIpsFuture.join(); - rebuildPlayerCache(); - }).thenRun(this::notifyListeners); - } - - public void updateFromContent(String fileName, String content) { - if (content == null || content.isEmpty()) return; - try { - if (fileName.endsWith("ops.json")) { - this.cachedOps = gson.fromJson(content, new TypeToken>() {}.getType()); - } else if (fileName.endsWith("banned-players.json")) { - this.cachedBans = gson.fromJson(content, new TypeToken>() {}.getType()); - } else if (fileName.endsWith("banned-ips.json")) { - this.cachedIpBans = gson.fromJson(content, new TypeToken>() {}.getType()); - } - if (this.cachedOps == null) this.cachedOps = new ArrayList<>(); - if (this.cachedBans == null) this.cachedBans = new ArrayList<>(); - if (this.cachedIpBans == null) this.cachedIpBans = new ArrayList<>(); - - rebuildPlayerCache(); - notifyListeners(); - } catch (Exception e) { - DebugManager.getInstance().log("StandardPlayerDataProvider", "Failed to parse streamed content for " + fileName + ": " + e.getMessage()); - } - } - - private void rebuildPlayerCache() { - Map ops = cachedOps.stream().collect(Collectors.toMap(op -> UUID.fromString(op.uuid), Function.identity(), (a, b) -> a)); - Map bannedPlayersMap = cachedBans.stream().collect(Collectors.toMap(ban -> UUID.fromString(ban.uuid), Function.identity(), (a, b) -> a)); - Map bannedIps = cachedIpBans.stream().collect(Collectors.toMap(ban -> ban.ip, Function.identity(), (a, b) -> a)); - - synchronized (players) { - Map allKnownPlayers = new HashMap<>(); - cachedPlayerLog.forEach(p -> allKnownPlayers.put(p.uuid, p)); - ops.values().forEach(p -> allKnownPlayers.computeIfAbsent(UUID.fromString(p.uuid), u -> new PlayerLogEntry(u, p.name))); - bannedPlayersMap.values().forEach(p -> allKnownPlayers.computeIfAbsent(UUID.fromString(p.uuid), u -> new PlayerLogEntry(u, p.name))); - - allKnownPlayers.forEach((uuid, entry) -> { - ManagedPlayer p = players.computeIfAbsent(uuid, u -> new ManagedPlayer(u, entry.name)); - p.lastSeen = entry.lastSeen; - }); - - players.values().forEach(p -> { - p.isOp = ops.containsKey(p.uuid); - if (p.isOp) p.opLevel = ops.get(p.uuid).level; - p.isBanned = bannedPlayersMap.containsKey(p.uuid); - if (p.isBanned) p.banInfo = bannedPlayersMap.get(p.uuid); - if (p.isOnline && p.address != null) { - String playerIp = p.address.split(":")[0].replace("/", ""); - p.isIpBanned = bannedIps.containsKey(playerIp); - if (p.isIpBanned) p.ipBanInfo = bannedIps.get(playerIp); - } else { - p.isIpBanned = false; - } - }); - } - } - - @Override - public Map getCachedPlayers() { - return players; - } - - @Override - public void addUpdateListener(Consumer> listener) { - updateListeners.add(listener); - } - - @Override - public void removeUpdateListener(Consumer> listener) { - updateListeners.remove(listener); - } - - private void notifyListeners() { - List playerList; - synchronized (players) { - playerList = new ArrayList<>(players.values()); - } - ScreenManager.getInstance().execute(() -> { - for (Consumer> listener : updateListeners) { - listener.accept(playerList); - } - }); - } - - private void logAction(String action, String target) { - DebugManager.getInstance().recordEvent(instanceId, "Player Action", "Standard", String.format("%s %s", action, target)); - } - - private void processLogLine(int lineNum, String line) { - if (line == null) return; - line = ANSI_PATTERN.matcher(line).replaceAll("").trim(); - if (line.isEmpty()) return; - - Matcher uuidMatcher = PLAYER_UUID_PATTERN.matcher(line); - if (uuidMatcher.matches()) { - handlePlayerLogon(UUID.fromString(uuidMatcher.group(2)), uuidMatcher.group(1)); - return; - } - - Matcher joinMatcher = PLAYER_JOIN_PATTERN.matcher(line); - if (joinMatcher.matches()) { - handlePlayerJoin(joinMatcher.group(1), joinMatcher.group(2)); - return; - } - - Matcher leaveMatcher = PLAYER_LEAVE_PATTERN.matcher(line); - if (leaveMatcher.matches()) { - handlePlayerLeave(leaveMatcher.group(1)); - return; - } - - Matcher opMatcher = PLAYER_OP_PATTERN.matcher(line); - if (opMatcher.matches()) { - updatePlayerStatus(opMatcher.group(1), p -> p.isOp = true); - if (historyCollector instanceof StandardPlayerHistoryProvider history) { - history.recordAccessChange(getPlayerUUID(opMatcher.group(1)), opMatcher.group(1), SessionEventType.OP_CHANGE, "op=true", System.currentTimeMillis(), lineNum); - } else { - historyCollector.recordAccessChange(getPlayerUUID(opMatcher.group(1)), opMatcher.group(1), SessionEventType.OP_CHANGE, "op=true", System.currentTimeMillis()); - } - logAction("Opped", opMatcher.group(1)); - return; - } - - Matcher deopMatcher = PLAYER_DEOP_PATTERN.matcher(line); - if (deopMatcher.matches()) { - updatePlayerStatus(deopMatcher.group(1), p -> p.isOp = false); - if (historyCollector instanceof StandardPlayerHistoryProvider history) { - history.recordAccessChange(getPlayerUUID(deopMatcher.group(1)), deopMatcher.group(1), SessionEventType.OP_CHANGE, "op=false", System.currentTimeMillis(), lineNum); - } else { - historyCollector.recordAccessChange(getPlayerUUID(deopMatcher.group(1)), deopMatcher.group(1), SessionEventType.OP_CHANGE, "op=false", System.currentTimeMillis()); - } - logAction("De-opped", deopMatcher.group(1)); - return; - } - - Matcher banMatcher = PLAYER_BAN_PATTERN.matcher(line); - if (banMatcher.matches()) { - updatePlayerStatus(banMatcher.group(1), p -> p.isBanned = true); - if (historyCollector instanceof StandardPlayerHistoryProvider history) { - history.recordAccessChange(getPlayerUUID(banMatcher.group(1)), banMatcher.group(1), SessionEventType.BAN, "reason=" + banMatcher.group(2), System.currentTimeMillis(), lineNum); - } else { - historyCollector.recordAccessChange(getPlayerUUID(banMatcher.group(1)), banMatcher.group(1), SessionEventType.BAN, "reason=" + banMatcher.group(2), System.currentTimeMillis()); - } - logAction("Banned", banMatcher.group(1)); - return; - } - - Matcher unbanMatcher = PLAYER_UNBAN_PATTERN.matcher(line); - if (unbanMatcher.matches()) { - updatePlayerStatus(unbanMatcher.group(1), p -> { - p.isBanned = false; - p.isIpBanned = false; - }); - if (historyCollector instanceof StandardPlayerHistoryProvider history) { - history.recordAccessChange(getPlayerUUID(unbanMatcher.group(1)), unbanMatcher.group(1), SessionEventType.UNBAN, "", System.currentTimeMillis(), lineNum); - } else { - historyCollector.recordAccessChange(getPlayerUUID(unbanMatcher.group(1)), unbanMatcher.group(1), SessionEventType.UNBAN, "", System.currentTimeMillis()); - } - logAction("Unbanned", unbanMatcher.group(1)); - } - } - - private UUID getPlayerUUID(String name) { - synchronized (players) { - return players.values().stream().filter(p -> p.name.equalsIgnoreCase(name)).map(p -> p.uuid).findFirst().orElse(null); - } - } - - private void updatePlayerStatus(String name, Consumer updater) { - synchronized (players) { - players.values().stream().filter(p -> p.name.equalsIgnoreCase(name)).findFirst().ifPresent(updater); - } - notifyListeners(); - } - - private void handlePlayerLogon(UUID uuid, String name) { - synchronized (players) { - ManagedPlayer player = players.computeIfAbsent(uuid, u -> { - ManagedPlayer newPlayer = new ManagedPlayer(u, name); - savePlayerLog(); - return newPlayer; - }); - - if (!player.name.equals(name)) { - player.name = name; - savePlayerLog(); - } - } - } - - private void handlePlayerJoin(String name, String address) { - synchronized (players) { - Optional playerOpt = players.values().stream().filter(p -> p.name.equalsIgnoreCase(name)).findFirst(); - if (playerOpt.isPresent()) { - ManagedPlayer player = playerOpt.get(); - player.isOnline = true; - player.address = address; - historyCollector.startSession(player.uuid, player.name, address, System.currentTimeMillis()); - notifyListeners(); - DebugManager.getInstance().recordEvent(instanceId, "Player", "Standard", "Join: " + name); - } else { - if (terminalWidget != null) terminalWidget.executeCommand("uuid " + name); - } - } - } - - private void handlePlayerLeave(String name) { - synchronized (players) { - players.values().stream().filter(p -> p.name.equalsIgnoreCase(name)).findFirst().ifPresent(player -> { - player.isOnline = false; - player.address = null; - player.lastSeen = System.currentTimeMillis(); - savePlayerLog(); - historyCollector.endSession(player.uuid, player.lastSeen); - notifyListeners(); - DebugManager.getInstance().recordEvent(instanceId, "Player", "Standard", "Leave: " + name); - }); - } - } - - private void ensureRemotelyDirectory() { - api.fileExists(remotelyDir).thenAccept(exists -> { - if (!exists) { - api.createDirectory(remotelyDir); - } - }); - } - - private void savePlayerLog() { - List playerLog = new ArrayList<>(); - synchronized(players) { - for (ManagedPlayer p : players.values()) { - PlayerLogEntry ple = new PlayerLogEntry(p.uuid, p.name); - ple.lastSeen = p.lastSeen; - playerLog.add(ple); - } - } - saveJsonFile(playerLogPath, playerLog); - } - - private CompletableFuture> loadJsonFile(Path path, TypeToken> typeToken) { - return api.fileExists(path).thenCompose(exists -> { - if (!exists) { - return CompletableFuture.completedFuture(new ArrayList<>()); - } - return api.readFile(path).thenApply(content -> { - if (content == null || content.isEmpty()) { - return new ArrayList<>(); - } - Type type = typeToken.getType(); - List result = gson.fromJson(content, type); - return result != null ? result : new ArrayList<>(); - }); - }); - } - - private CompletableFuture saveJsonFile(Path path, Object data) { - String json = new Gson().newBuilder().setPrettyPrinting().create().toJson(data); - return api.writeFile(path, json); - } -} diff --git a/src/main/java/redxax/oxy/remotely/ui/server/ServerConfigurationScreen.java b/src/main/java/redxax/oxy/remotely/ui/server/ServerConfigurationScreen.java index a028dbbe..4e489703 100644 --- a/src/main/java/redxax/oxy/remotely/ui/server/ServerConfigurationScreen.java +++ b/src/main/java/redxax/oxy/remotely/ui/server/ServerConfigurationScreen.java @@ -13,6 +13,7 @@ import restudio.rebase.instance.InstanceRepairer; import restudio.rebase.instance.InstanceState; import restudio.rebase.instance.loaders.ModLoader; +import restudio.rebase.restudio.ReStudio; import restudio.rebase.settings.controllers.VersionSettingsController; import restudio.rebase.util.VersionUtil; import restudio.rescreen.theme.ThemeManager; @@ -33,6 +34,7 @@ import static restudio.rescreen.util.SoundUtils.playSound; +@SuppressWarnings("unchecked") public class ServerConfigurationScreen extends ReScreen { private final Screen parent; private final boolean isEditMode; @@ -41,6 +43,10 @@ public class ServerConfigurationScreen extends ReScreen { private final RemoteHost remoteHostContext; private final RemotelyClient remotelyClient; + private final Map remoteVariables = new HashMap<>(); + private final boolean isReStudioBackend; + private String serverIdentifier; + public ServerConfigurationScreen(Screen parent, Instance instance, RemoteHost remoteHostContext, RemotelyClient remotelyClient) { super(); this.parent = parent; @@ -52,6 +58,8 @@ public ServerConfigurationScreen(Screen parent, Instance instance, RemoteHost re if (isEditMode) { this.tempInstance = new Instance(instance, instance.getName()); boolean isRemote = instance.getBackendConfig() != null && !"LOCAL".equalsIgnoreCase(instance.getBackendConfig().type); + this.isReStudioBackend = instance.getBackendConfig() != null && "RESTUDIO".equalsIgnoreCase(instance.getBackendConfig().type); + this.serverIdentifier = isReStudioBackend ? instance.getBackendConfig().credentials.get("identifier") : null; if (isRemote || remoteHostContext != null) { if (remoteHostContext != null) { @@ -67,6 +75,7 @@ public ServerConfigurationScreen(Screen parent, Instance instance, RemoteHost re } } else { this.tempInstance = new Instance("New Server", remotelyClient.getHost().getGameVersion(), ""); + this.isReStudioBackend = false; if (remoteHostContext != null) { Map creds = new HashMap<>(); creds.put("host", remoteHostContext.getIp()); @@ -88,6 +97,7 @@ public void init() { CompletableFuture propertiesFuture; CompletableFuture settingsFuture; CompletableFuture> filesFuture; + CompletableFuture remoteConfigFuture; boolean isRemote = tempInstance.getBackendConfig() != null && !"LOCAL".equalsIgnoreCase(tempInstance.getBackendConfig().type); @@ -99,17 +109,36 @@ public void init() { propertiesFuture = CompletableFuture.runAsync(tempInstance::loadServerProperties); settingsFuture = CompletableFuture.completedFuture(null); } - filesFuture = RebaseApiFactory.get(tempInstance).listDirectory(Path.of(tempInstance.getPath())) - .thenApply(entries -> entries.stream().map(RebaseAPI.FileEntry::toString).toList()) - .exceptionally(e -> new ArrayList<>()); + filesFuture = RebaseApiFactory.get(tempInstance).listDirectory(Path.of(tempInstance.getPath())).thenApply(entries -> entries.stream().map(RebaseAPI.FileEntry::toString).toList()).exceptionally(e -> new ArrayList<>()); + + if (isReStudioBackend) { + remoteConfigFuture = ReStudio.getInstance().getApi().getServerStartupConfig(serverIdentifier).thenAccept(data -> { + if (data.containsKey("data")) { + List> vars = (List>) data.get("data"); + for (Map varWrapper : vars) { + Map attr = (Map) varWrapper.get("attributes"); + String key = (String) attr.get("env_variable"); + String val = (String) attr.get("server_value"); + remoteVariables.put(key, val); + } + } + }).exceptionally(e -> { + System.err.println("Failed to fetch startup config: " + e.getMessage()); + return null; + }); + } else { + remoteConfigFuture = CompletableFuture.completedFuture(null); + } + } else { tempInstance.loadServerProperties(); propertiesFuture = CompletableFuture.completedFuture(null); settingsFuture = CompletableFuture.completedFuture(null); filesFuture = CompletableFuture.completedFuture(new ArrayList<>()); + remoteConfigFuture = CompletableFuture.completedFuture(null); } - CompletableFuture.allOf(propertiesFuture, settingsFuture, filesFuture).thenRun(() -> { + CompletableFuture.allOf(propertiesFuture, settingsFuture, filesFuture, remoteConfigFuture).thenRun(() -> { List files = filesFuture.join(); ScreenManager.getInstance().execute(() -> setupSettingsUI(files)); }).exceptionally(e -> { @@ -125,7 +154,14 @@ private void setupSettingsUI(List extraFiles) { Map>> settingsByTab = new LinkedHashMap<>(); List cleanupActions = new ArrayList<>(); - VersionSettingsController versionController = new VersionSettingsController(tempInstance); + VersionSettingsController versionController; + if (isReStudioBackend) { + versionController = new VersionSettingsController(tempInstance); + versionController.bindToRemoteVariables(remoteVariables); + } else { + versionController = new VersionSettingsController(tempInstance); + } + ServerGeneralSettingsController generalController = new ServerGeneralSettingsController(tempInstance); settingsByTab.put("General", () -> { @@ -135,6 +171,11 @@ private void setupSettingsUI(List extraFiles) { return settings; }); + if (isReStudioBackend) { + ServerEggSettingsController eggController = new ServerEggSettingsController(remoteVariables, true); + settingsByTab.put("Installer", eggController::getSettings); + } + ServerAdvancedSettingsController advancedController = new ServerAdvancedSettingsController(tempInstance); settingsByTab.put("Advanced", advancedController::getSettings); @@ -144,7 +185,10 @@ private void setupSettingsUI(List extraFiles) { ServerPerformanceSettingsController performanceController = new ServerPerformanceSettingsController(tempInstance); settingsByTab.put("Performance", performanceController::getSettings); - ServerJvmSettingsController javaController = new ServerJvmSettingsController(tempInstance, (redxax.oxy.remotely.config.RemotelyConfigManager) Rebase.get().getConfigManager()); + ServerJvmSettingsController javaController = new ServerJvmSettingsController(tempInstance); + if (isReStudioBackend) { + javaController.bindToRemoteVariables(remoteVariables); + } settingsByTab.put("Java", javaController::getSettings); if (isEditMode) { @@ -263,13 +307,17 @@ private void editServer() { InstanceRepairer.createStartScript(originalInstance).join(); + if (isReStudioBackend) { + saveRemoteVariables(); + } + boolean versionChanged = oldLoader != originalInstance.getModLoader() || (oldVersion == null ? originalInstance.getVersionId() != null : !oldVersion.equals(originalInstance.getVersionId())); boolean isRemote = originalInstance.getBackendConfig() != null && !"LOCAL".equalsIgnoreCase(originalInstance.getBackendConfig().type); if (versionChanged) { Notification notification = new Notification.Builder().message("Applying Version Changes...").autoSlideOut(false).image(Identifier.animatedIcon("loadingGreen.png")).animateImage(true).accent(ThemeManager.getAccent("calm")).build(); - if (isRemote) { + if (isRemote && !isReStudioBackend) { RemoteHost host = remoteHostContext; if(host == null) { for(RemoteHost h : Rebase.get().getInstanceManager().getRemoteHosts()) { @@ -298,7 +346,7 @@ private void editServer() { } else { notification.update().message("Update Failed").description("Could not resolve remote host context").type(Notification.Type.ERROR); } - } else { + } else if (!isReStudioBackend) { Rebase.get().getInstanceManager().createInstance(originalInstance, notification).thenAccept(newInstance -> ScreenManager.getInstance().execute(() -> { notification.update().message("Server Updated Successfully!").description("Version changes applied.").type(Notification.Type.SUCCESS).loading(false).image(null); notification.loading = false; @@ -312,12 +360,29 @@ private void editServer() { }); return null; }); + } else { + notification.update().message("Server Configuration Saved").description("Settings updated on panel.").type(Notification.Type.SUCCESS).loading(false).image(null).autoSlideOut(true); } } else { new Notification(originalInstance.getName() + " Edited Successfully!", Notification.Type.SUCCESS); } } + private void saveRemoteVariables() { + if (!isReStudioBackend || remoteVariables.isEmpty()) return; + + List> futures = new ArrayList<>(); + + for (Map.Entry entry : remoteVariables.entrySet()) { + futures.add(ReStudio.getInstance().getApi().updateServerStartupVariable(serverIdentifier, entry.getKey(), entry.getValue())); + } + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).exceptionally(e -> { + ScreenManager.getInstance().execute(() -> new Notification("Save Warning", "Some startup variables failed to update.", Notification.Type.WARN)); + return null; + }); + } + @Override public void onDisplayed() { playSound(Sound.SCREEN); diff --git a/src/main/java/redxax/oxy/remotely/ui/server/ServerDetailsScreen.java b/src/main/java/redxax/oxy/remotely/ui/server/ServerDetailsScreen.java index 97fbfad9..f769815c 100644 --- a/src/main/java/redxax/oxy/remotely/ui/server/ServerDetailsScreen.java +++ b/src/main/java/redxax/oxy/remotely/ui/server/ServerDetailsScreen.java @@ -346,7 +346,13 @@ protected void onTabSelected(TabsManager.Tab tab) { } } if (info.playersContainer != null) info.playersContainer.fullRefresh(); - })); + })).exceptionally(e -> { + ScreenManager.getInstance().execute(() -> { + new Notification("Config Load Failed", e.getMessage(), Notification.Type.ERROR); + setupTerminalListeners(ctx.instance, info); + }); + return null; + }); if (VersionUtil.isMSMPCompatible(ctx.instance.getVersionId())) { ctx.instance.getMSMPManager().handleInstanceStateChange(ctx.instance.getState()); @@ -634,7 +640,7 @@ protected void onStateChanged(InstanceState newState) { TabContext ctx = getActiveContext(); ServerContextInfo info = contextInfos.get(ctx); if (ctx != null && !ctx.views.isEmpty()) onViewChanged(ctx, ctx.views.get(ctx.selectedViewIndex)); - if (info != null && info.playersContainer != null) info.playersContainer.rebuildPlayerWidgets(); + if (info != null && info.playersContainer != null) info.playersContainer.fullRefresh(); if (ctx != null && ctx.instance != null) ctx.instance.getMSMPManager().handleInstanceStateChange(newState); }); } diff --git a/src/main/java/redxax/oxy/remotely/ui/server/ServerManagerScreen.java b/src/main/java/redxax/oxy/remotely/ui/server/ServerManagerScreen.java index 4cf4cc7b..e6f03578 100644 --- a/src/main/java/redxax/oxy/remotely/ui/server/ServerManagerScreen.java +++ b/src/main/java/redxax/oxy/remotely/ui/server/ServerManagerScreen.java @@ -6,8 +6,12 @@ import redxax.oxy.remotely.config.RemotelyConfigManager; import redxax.oxy.remotely.config.SettingsScreenFactory; import redxax.oxy.remotely.ui.widgets.DesktopIconWidget; +import restudio.rebase.backend.BackendConfig; +import restudio.rebase.instance.InstanceState; +import restudio.rebase.instance.loaders.ModLoader; import restudio.rebase.restudio.AuthStateListener; import restudio.rebase.restudio.ReStudio; +import restudio.rebase.restudio.api.models.ServerModels; import restudio.rebase.ui.screens.auth.ReStudioLoginScreen; import restudio.rebase.ui.screens.feedback.FeedbackBrowserScreen; import restudio.rebase.ui.screens.notification.InboxScreen; @@ -39,6 +43,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; import static redxax.oxy.remotely.config.Config.remotelyDir; import static redxax.oxy.remotely.util.DevUtil.devPrint; @@ -64,6 +69,7 @@ public class ServerManagerScreen extends ReScreen implements AuthStateListener { private static BufferedImage unknown, serverIcon, paper, vanilla, fabric, forge, neoforge, waterfall, velocity, leaf, quilt, spigot, bukkit, purpur; private InstanceManager instanceManager; + private final List restudioInstances = new CopyOnWriteArrayList<>(); public ServerManagerScreen(Object parent, RemotelyClient remotelyClient) { super(); @@ -212,6 +218,14 @@ private void loadIcons() { private void populateHostTabs() { tabs().addTab("Local", activeContainer).setData(null); + + if (ReStudio.getInstance().isAuthenticated()) { + Container c = createContainer("desktop_restudio", 0, 0, width, height - 35); + DesktopLayout remoteLayout = new DesktopLayout(); + c.layout(remoteLayout).backgroundDrawing(false).enableSelecting(true).disableScissorRegion(true); + tabs().addTab("ReStudio", c).setData("RESTUDIO_MARKER"); + } + for (RemoteHost host : instanceManager.getRemoteHosts()) { Container c = createContainer("desktop_remote_" + host.name, 0, 0, width, height - 35); DesktopLayout remoteLayout = new DesktopLayout(); @@ -228,14 +242,20 @@ private void loadServersForCurrentTab() { if (activeContainer == null) return; activeContainer.clearWidgets(); - List instances = new ArrayList<>(getCurrentServers()); + List instances; + Object tabData = (tabs().getActiveTab() != null) ? tabs().getActiveTab().getData() : null; + + if ("RESTUDIO_MARKER".equals(tabData)) { + instances = new ArrayList<>(restudioInstances); + } else { + instances = new ArrayList<>(getCurrentServers()); + } String context = "local"; - if (tabs().getActiveTabIndex() > 0 && tabs().getActiveTab() != null) { - Object data = tabs().getActiveTab().getData(); - if (data instanceof RemoteHost host) { - context = "remote." + host.name; - } + if (tabData instanceof RemoteHost host) { + context = "remote." + host.name; + } else if ("RESTUDIO_MARKER".equals(tabData)) { + context = "remote.restudio"; } RemotelyConfigManager config = (RemotelyConfigManager) Rebase.get().getConfigManager(); @@ -257,7 +277,10 @@ private void loadServersForCurrentTab() { for (Instance server : instances) { addServerWidget(server, false); } - addServerWidget(null, true); + if (!"RESTUDIO_MARKER".equals(tabData)) { + addServerWidget(null, true); + } + activeContainer.updateWidgetPositions(); } @@ -272,6 +295,8 @@ private void saveServerOrder(Container container, RemoteHost host) { String context = "local"; if (host != null) { context = "remote." + host.name; + } else if (tabs().getActiveTab() != null && "RESTUDIO_MARKER".equals(tabs().getActiveTab().getData())) { + context = "remote.restudio"; } RemotelyConfigManager config = (RemotelyConfigManager) Rebase.get().getConfigManager(); @@ -302,14 +327,68 @@ private void onHostTabSelected(TabsManager.Tab tab) { instanceManager.fetchRemoteInstances(host) .whenComplete((v, e) -> ScreenManager.getInstance().execute(this::loadServersForCurrentTab)); } + } else if ("RESTUDIO_MARKER".equals(data)) { + fetchReStudioServers(); } loadServersForCurrentTab(); Main.setTitle(tab.getName() + " Host - Remotely Server Manager"); } + private void fetchReStudioServers() { + if (tabs().getActiveTab().getWidget() != null) tabs().getActiveTab().getWidget().setAccent(ThemeManager.getAccent("calm")); + + ReStudio.getInstance().getApi().getSftpPassword().thenCompose(sftpSecret -> + ReStudio.getInstance().getApi().getServers().thenApply(servers -> { + restudioInstances.clear(); + for (ServerModels.ClientServerView csv : servers) { + Map creds = new HashMap<>(); + creds.put("identifier", csv.identifier); + creds.put("host", csv.sftpIp); + creds.put("port", String.valueOf(csv.sftpPort)); + creds.put("user", csv.sftpUser); + creds.put("password", sftpSecret != null ? sftpSecret : ""); + + BackendConfig config = new BackendConfig("RESTUDIO", creds); + + Instance inst = new Instance(csv.name, "unknown", ""); + inst.setBackendConfig(config); + inst.setServer(true); + if (csv.isInstalling) { + inst.setState(InstanceState.INSTALLING); + } + if (csv.isSuspended) { + inst.setState(InstanceState.STOPPED); + } + + if (csv.loader != null) { + try { + inst.setModLoader(ModLoader.valueOf(csv.loader)); + } catch (IllegalArgumentException ignored) { + inst.setModLoader(ModLoader.VANILLA); + } + } + if (csv.version != null) { + inst.setVersionId(csv.version); + } + + restudioInstances.add(inst); + } + + return null; + }) + ).whenComplete((v, e) -> ScreenManager.getInstance().execute(() -> { + if (tabs().getActiveTab().getWidget() != null) tabs().getActiveTab().getWidget().setAccent(ThemeManager.getDefaultAccent()); + if (e != null) { + new Notification("Error fetching ReStudio servers", e.getMessage(), Notification.Type.ERROR); + if (tabs().getActiveTab().getWidget() != null) tabs().getActiveTab().getWidget().setAccent(ThemeManager.getAccent("danger")); + } else { + loadServersForCurrentTab(); + } + })); + } + private void onHostTabClosed(TabsManager.Tab tab) { - System.out.println("Requesting deletion of remote host"); if (tab != null && tab.getData() instanceof RemoteHost host) { ContextMenuWidget.Builder builder = new ContextMenuWidget.Builder(this).addIconItem("Confirm Deletion", "delete.png", () -> { instanceManager.removeRemoteHost(host); @@ -342,33 +421,41 @@ private void onDesktopIconClick(DesktopIconWidget widget, int button) { Instance inst = widget.getInstance(); RemoteHost rh = null; - if(inst.getBackendConfig() != null && !"LOCAL".equalsIgnoreCase(inst.getBackendConfig().type)) { - for(RemoteHost h : instanceManager.getRemoteHosts()) { - if(inst.getBackendConfig().credentials.getOrDefault("host", "").equals(h.getIp())) { - rh = h; - break; + boolean isRestudio = false; + + if (inst.getBackendConfig() != null) { + if ("RESTUDIO".equalsIgnoreCase(inst.getBackendConfig().type)) { + isRestudio = true; + } else if (!"LOCAL".equalsIgnoreCase(inst.getBackendConfig().type)) { + for(RemoteHost h : instanceManager.getRemoteHosts()) { + if(inst.getBackendConfig().credentials.getOrDefault("host", "").equals(h.getIp())) { + rh = h; + break; + } } } } RemoteHost finalRh = rh; - ContextMenuWidget.Builder builder = new ContextMenuWidget.Builder(this) - .addHeaderButton("edit.png", () -> client.setScreen(new ServerConfigurationScreen(this, widget.getInstance(), finalRh, remotelyClient)), "Edit Server's Settings") - .addHeaderButton("explorer.png", () -> client.setScreen(new FileExplorerScreen(this, widget.getInstance(), Path.of(widget.getInstance().getPath()), remotelyDir, false)), "Open Server's Folder"); + ContextMenuWidget.Builder builder = new ContextMenuWidget.Builder(this); - if (rh == null) { + builder.addHeaderButton("edit.png", () -> client.setScreen(new ServerConfigurationScreen(this, widget.getInstance(), finalRh, remotelyClient)), "Edit Server's Settings"); + builder.addHeaderButton("explorer.png", () -> client.setScreen(new FileExplorerScreen(this, widget.getInstance(), Path.of(widget.getInstance().getPath()), remotelyDir, false)), "Open Server's Folder"); + + if (rh == null && !isRestudio) { builder.addHeaderButton("map.png", () -> openWorldScreen(widget.getInstance()), "View World Map"); } - builder.addHeaderButton("copy.png", () -> duplicateInstance(inst), "Duplicate Server") - .addHeaderButton("delete.png", () -> { + if (!isRestudio) { + builder.addHeaderButton("copy.png", () -> duplicateInstance(inst), "Duplicate Server").addHeaderButton("delete.png", () -> { instanceForDeletion = widget.getInstance(); deleteServerPopup.setX((this.width - deleteServerPopup.getWidth())/2); deleteServerPopup.setY((this.height - deleteServerPopup.getHeight())/2); deleteServerPopup.show(); }, "Show Deletion Options", ThemeManager.getAccent("danger")); + } - if (rh == null) { + if (rh == null && !isRestudio) { builder.addIconItem("Customize Icon", "shades.png", () -> customizeIcon(inst), ""); } showContextMenu(widget.getX() + widget.getWidth() + 4, widget.getY() + 24, builder); @@ -423,11 +510,10 @@ private List getCurrentServers() { if (active == null) { return instanceManager.getLocalInstances(); } - int idx = tabs().getActiveTabIndex(); - if (idx <= 0) { - return instanceManager.getLocalInstances(); - } Object data = active.getData(); + if ("RESTUDIO_MARKER".equals(data)) { + return restudioInstances; + } if (!(data instanceof RemoteHost host)) { return instanceManager.getLocalInstances(); } @@ -572,8 +658,9 @@ private void connectRemoteHostAsync(RemoteHost hostInfo, Runnable onSuccess, Run private void openRemoteHostPopup(boolean isEditing) { int activeTabIndex = tabs().getActiveTabIndex(); - if (isEditing && activeTabIndex > 0 && tabs().getActiveTab() != null) { - RemoteHost host = (RemoteHost) tabs().getTabs().get(activeTabIndex).getData(); + Object data = (tabs().getActiveTab() != null) ? tabs().getActiveTab().getData() : null; + + if (isEditing && activeTabIndex > 0 && data instanceof RemoteHost host) { remoteHostConfirmButton.setMessage(("Save")); remoteHostDeleteButton.visible = true; @@ -646,8 +733,7 @@ private void onConfirmRemoteHost() { } private void onDeleteRemoteHost() { - if (tabs().getActiveTabIndex() > 0) { - RemoteHost hostToRemove = (RemoteHost) tabs().getActiveTab().getData(); + if (tabs().getActiveTabIndex() > 0 && tabs().getActiveTab().getData() instanceof RemoteHost hostToRemove) { instanceManager.removeRemoteHost(hostToRemove); tabs().removeTab(tabs().getActiveTabIndex()); tabs().setActiveTab(0); @@ -681,7 +767,7 @@ private void openImportFileExplorer() { } private void openModpackInstallation() { - RemoteHost currentHost = (tabs().getActiveTabIndex() > 0 && tabs().getActiveTab() != null) ? (RemoteHost) tabs().getActiveTab().getData() : null; + RemoteHost currentHost = (tabs().getActiveTabIndex() > 0 && tabs().getActiveTab() != null && tabs().getActiveTab().getData() instanceof RemoteHost) ? (RemoteHost) tabs().getActiveTab().getData() : null; client.setScreen(new ResourceBrowserScreen(this, null, ResourceType.MODPACK, true, currentHost)); } diff --git a/src/main/java/redxax/oxy/remotely/ui/server/ServerTerminal.java b/src/main/java/redxax/oxy/remotely/ui/server/ServerTerminal.java index a72c28d9..7b0d14a7 100644 --- a/src/main/java/redxax/oxy/remotely/ui/server/ServerTerminal.java +++ b/src/main/java/redxax/oxy/remotely/ui/server/ServerTerminal.java @@ -22,19 +22,31 @@ public class ServerTerminal extends TerminalWidget { private static final Pattern PROGRESS_TAG_PATTERN = Pattern.compile("\\[Progress:(\\d{1,3})]\\s*(.*)"); private boolean isReconnecting = false; + private volatile boolean explicitDisconnect = false; + private boolean forceStoppedView = false; private String reconnectReason = ""; private int reconnectCountdown = 3; private long lastTick = 0; + private final Consumer stateListener; + private final Consumer logListener; + public ServerTerminal(int x, int y, int width, int height, Instance instance, ExecutionProvider executionProvider) { super(x, y, width, height, instance, executionProvider); this.stoppedMessage = new IconMessage(0, 0, 64, 64, "Ready When You Are", "zz.png"); this.connectingMessage = new IconMessage(0, 0, 64, 64, "Connecting...", "reverse.png"); this.installingMessage = new IconMessage(0, 0, 64, 64, "Installing...\nPreparing", "remotely.png"); this.reconnectingMessage = new IconMessage(0, 0, 64, 64, "Connection Lost\nReconnecting...", "reverse.png"); - Consumer logListener = this::onLogLine; - if (getInstance() != null) getInstance().getLogger().addLogListener(logListener); + this.logListener = this::onLogLine; + this.stateListener = this::onStateChange; + + if (getInstance() != null) { + getInstance().getLogger().addLogListener(logListener); + getInstance().addStateListener(stateListener); + } + + this.addOutputListener(this::onTerminalOutput); this.setOnConnectionLost(this::handleConnectionLost); } @@ -61,6 +73,17 @@ private void handleConnectionLost(String reason) { } ScreenManager.getInstance().execute(() -> { + if (explicitDisconnect) { + explicitDisconnect = false; + isReconnecting = false; + forceStoppedView = true; + if (getInstance() != null) { + getInstance().setState(InstanceState.STOPPED); + } + stopProcess(); + return; + } + if (!isReconnecting) { isReconnecting = true; reconnectReason = reason != null ? reason : "Unknown error"; @@ -70,6 +93,18 @@ private void handleConnectionLost(String reason) { }); } + private void onStateChange(InstanceState newState) { + if (newState == InstanceState.STARTING || newState == InstanceState.RUNNING) { + ScreenManager.getInstance().execute(() -> { + forceStoppedView = false; + explicitDisconnect = false; + if (!isTerminalReady() && !isReconnecting) { + startServerProcess(); + } + }); + } + } + @Override public void tick() { super.tick(); @@ -97,7 +132,7 @@ protected void drawContent(IDrawContext ctx, int mouseX, int mouseY) { } else if (isReconnecting) { reconnectingMessage.setPosition(getX() + (getWidth() - reconnectingMessage.getWidth()) / 2, getY() + (getHeight() - reconnectingMessage.getHeight()) / 2); reconnectingMessage.render(ctx, mouseX, mouseY, Config.deltaTime); - } else if (!isTerminalReady() && !(getInstance() != null && getInstance().getBackend() instanceof LocalBackend)) { + } else if (!isTerminalReady() && !explicitDisconnect && !forceStoppedView && !(getInstance() != null && getInstance().getBackend() instanceof LocalBackend)) { connectingMessage.setPosition(getX() + (getWidth() - connectingMessage.getWidth()) / 2, getY() + (getHeight() - connectingMessage.getHeight()) / 2); connectingMessage.render(ctx, mouseX, mouseY, Config.deltaTime); } else { @@ -109,7 +144,7 @@ protected void drawContent(IDrawContext ctx, int mouseX, int mouseY) { boolean hasContent = getHistoryLinesCount() > 0 || getCursorY() > 4; - if (isStopped && !hasContent) { + if (isStopped && (!hasContent || forceStoppedView)) { stoppedMessage.setPosition(getX() + (getWidth() - stoppedMessage.getWidth()) / 2, getY() + (getHeight() - stoppedMessage.getHeight()) / 2); stoppedMessage.render(ctx, mouseX, mouseY, Config.deltaTime); } else { @@ -118,7 +153,15 @@ protected void drawContent(IDrawContext ctx, int mouseX, int mouseY) { } } + private void onTerminalOutput(String msg) { + if (msg == null) return; + if (msg.contains("Server is offline.")) { + explicitDisconnect = true; + } + } + private void onLogLine(String msg) { + if (msg == null) return; Matcher m = PROGRESS_TAG_PATTERN.matcher(msg); if (m.find()) { try { diff --git a/src/main/java/redxax/oxy/remotely/ui/server/containers/PlayersContainer.java b/src/main/java/redxax/oxy/remotely/ui/server/containers/PlayersContainer.java index a9bf9d9f..da4d6c09 100644 --- a/src/main/java/redxax/oxy/remotely/ui/server/containers/PlayersContainer.java +++ b/src/main/java/redxax/oxy/remotely/ui/server/containers/PlayersContainer.java @@ -1,8 +1,9 @@ package redxax.oxy.remotely.ui.server.containers; import org.lwjgl.glfw.GLFW; -import redxax.oxy.remotely.data.managed.ManagedPlayer; import redxax.oxy.remotely.data.managed.PlayerAction; +import redxax.oxy.remotely.data.player.model.UnifiedPlayer; +import redxax.oxy.remotely.ui.widgets.management.BanPlayerPopup; import redxax.oxy.remotely.ui.widgets.management.PlayerEntryWidget; import redxax.oxy.remotely.ui.widgets.management.PlayerManagerController; import restudio.rebase.instance.Instance; @@ -13,6 +14,11 @@ import restudio.rescreen.ui.rescreen.layout.ManagedLayout; import restudio.rescreen.ui.widgets.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + public class PlayersContainer extends Container { private final ReScreen host; private Instance instance; @@ -35,14 +41,54 @@ public void setInstance(Instance newInstance) { controller.setUiBindings(this, terminalWidget); } - public void rebuildPlayerWidgets() { - if (controller != null) controller.rebuildPlayerWidgets(); - } - public void fullRefresh() { if (controller != null) controller.fullRefresh(); } + public void syncUi(List snapshot) { + if (snapshot == null) return; + List processingList = new ArrayList<>(snapshot); + List toRemove = new ArrayList<>(); + + for (AnimatedWidget w : getWidgets()) { + if (w instanceof PlayerEntryWidget pew) { + UnifiedPlayer existing = pew.getPlayer(); + UnifiedPlayer match = processingList.stream() + .filter(p -> p.getUuid().equals(existing.getUuid())) + .findFirst().orElse(null); + + if (match != null) { + pew.update(match); + processingList.remove(match); + } else { + toRemove.add(pew); + } + } else { + if (!snapshot.isEmpty()) toRemove.add(w); + } + } + + for (AnimatedWidget w : toRemove) { + this.removeWidget(w); + } + + for (UnifiedPlayer p : processingList) { + this.addWidget(new PlayerEntryWidget(p, controller)); + } + + this.sortWidgets((w1, w2) -> { + if (w1 instanceof PlayerEntryWidget p1 && w2 instanceof PlayerEntryWidget p2) { + UnifiedPlayer u1 = p1.getPlayer(); + UnifiedPlayer u2 = p2.getPlayer(); + if (u1.isOnline() != u2.isOnline()) return u1.isOnline() ? -1 : 1; + return u1.getName().compareToIgnoreCase(u2.getName()); + } + return 0; + }); + + this.updateWidgetPositions(); + } + @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { if (button == GLFW.GLFW_MOUSE_BUTTON_RIGHT) { @@ -80,7 +126,7 @@ private void showGeneralContextMenu(double mouseX, double mouseY) { private void showPlayersContextMenu(double mouseX, double mouseY) { java.util.List selected = getSelectedWidgets(); if (selected.isEmpty()) return; - java.util.List players = new java.util.ArrayList<>(); + List players = new ArrayList<>(); for (AnimatedWidget w : selected) { if (w instanceof PlayerEntryWidget pew) { players.add(pew.getPlayer()); @@ -95,7 +141,7 @@ private void showPlayersContextMenu(double mouseX, double mouseY) { builder.addItem("LuckPerms Dashboard", controller::openLuckPermsDashboard, "Open full editor"); - java.util.List actions = controller.getPlayerActions(); + List actions = controller.getPlayerActions(); if (actions != null) { for (PlayerAction action : actions) { builder.addHeaderButton(action.icon, () -> showCustomActionMultiPopup(action, players), action.name); @@ -104,20 +150,20 @@ private void showPlayersContextMenu(double mouseX, double mouseY) { host.showContextMenu((int) mouseX, (int) mouseY, builder); } - private void kickSelected(java.util.List players) { - for (ManagedPlayer p : players) controller.kickPlayer(p, "Kicked by operator"); + private void kickSelected(List players) { + for (UnifiedPlayer p : players) controller.kickPlayer(p, "Kicked by operator"); } - private void unbanSelected(java.util.List players) { - for (ManagedPlayer p : players) controller.unbanPlayer(p); + private void unbanSelected(List players) { + for (UnifiedPlayer p : players) controller.unbanPlayer(p); } - private void toggleOpSelected(java.util.List players) { - for (ManagedPlayer p : players) controller.toggleOp(p); + private void toggleOpSelected(List players) { + for (UnifiedPlayer p : players) controller.toggleOp(p); } - private java.util.List findCustomVariables(String command) { - java.util.List variables = new java.util.ArrayList<>(); + private List findCustomVariables(String command) { + List variables = new ArrayList<>(); java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\$([a-zA-Z0-9_]+)"); java.util.regex.Matcher matcher = pattern.matcher(command); while (matcher.find()) { @@ -126,13 +172,13 @@ private java.util.List findCustomVariables(String command) { variables.add(var); } } - return new java.util.ArrayList<>(variables); + return new ArrayList<>(variables); } - private void showCustomActionMultiPopup(PlayerAction action, java.util.List players) { - java.util.List variables = findCustomVariables(action.command); + private void showCustomActionMultiPopup(PlayerAction action, List players) { + List variables = findCustomVariables(action.command); if (variables.isEmpty()) { - for (ManagedPlayer p : players) controller.runCustomCommand(p, action.command); + for (UnifiedPlayer p : players) controller.runCustomCommand(p, action.command); return; } PopupWidget.Builder builder = new PopupWidget.Builder("Execute: " + action.name) @@ -145,7 +191,7 @@ private void showCustomActionMultiPopup(PlayerAction action, java.util.List players) { + private void showBanMultiPopup(List players) { PopupWidget.Builder builder = new PopupWidget.Builder("Ban Selected") .size(320, 120).setAntiOutOfBound(true).setResizable(true); TextInputWidget reason = new TextInputWidget.Builder().placeholder("Reason").size(280, 18).build(); @@ -178,7 +224,7 @@ private void showBanMultiPopup(java.util.List players) { builder.addTitleButton(() -> { String r = reason.getText().trim(); boolean ip = ipBan.getValue(); - for (ManagedPlayer p : players) controller.banPlayer(p, r.isEmpty() ? "Banned by operator" : r, ip); + for (UnifiedPlayer p : players) controller.banPlayer(p, r.isEmpty() ? "Banned by operator" : r, ip); builder.getWidget().setVisible(false); }, "Ban", ThemeManager.getAccent("danger")); PopupWidget popup = builder.build(); diff --git a/src/main/java/redxax/oxy/remotely/ui/settings/controllers/ServerBackupSettingsController.java b/src/main/java/redxax/oxy/remotely/ui/settings/controllers/ServerBackupSettingsController.java index 21435b8c..265905e4 100644 --- a/src/main/java/redxax/oxy/remotely/ui/settings/controllers/ServerBackupSettingsController.java +++ b/src/main/java/redxax/oxy/remotely/ui/settings/controllers/ServerBackupSettingsController.java @@ -1,7 +1,5 @@ package redxax.oxy.remotely.ui.settings.controllers; -import redxax.oxy.remotely.backup.ServerBackupInfo; -import redxax.oxy.remotely.backup.ServerBackupType; import restudio.rebase.Rebase; import restudio.rebase.backup.BackupInfo; import restudio.rebase.instance.Instance; @@ -22,7 +20,6 @@ import java.nio.file.Paths; import java.time.Duration; import java.util.*; -import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; diff --git a/src/main/java/redxax/oxy/remotely/ui/settings/controllers/ServerEggSettingsController.java b/src/main/java/redxax/oxy/remotely/ui/settings/controllers/ServerEggSettingsController.java new file mode 100644 index 00000000..d212a032 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/ui/settings/controllers/ServerEggSettingsController.java @@ -0,0 +1,91 @@ +package redxax.oxy.remotely.ui.settings.controllers; + +import restudio.rescreen.ui.settings.Setting; +import restudio.rescreen.ui.settings.options.ConfigOption; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class ServerEggSettingsController { + + private final Map remoteVariables; + private final boolean isRemote; + + public ServerEggSettingsController(Map remoteVariables, boolean isRemote) { + this.remoteVariables = remoteVariables; + this.isRemote = isRemote; + } + + public List getSettings() { + if (!isRemote) return List.of(); + + Setting.Builder installer = new Setting.Builder("Installer & Modpacks"); + + List sources = Arrays.asList("None", "curseforge", "modrinth"); + installer.addOption(ConfigOption.builder("Modpack Source") + .description("Source for modpack installation (e.g. CurseForge, Modrinth).") + .options(sources) + .bind(() -> { + String val = remoteVariables.getOrDefault("MODPACK_SOURCE", ""); + return val.isEmpty() ? "None" : val; + }, val -> remoteVariables.put("MODPACK_SOURCE", "None".equals(val) ? "" : val)) + .defaultValue("None") + .build()); + + installer.addOption(ConfigOption.builder("Download URL") + .description("Direct URL to modpack zip or file to download.") + .bind(() -> remoteVariables.getOrDefault("DOWNLOAD_URL", ""), + val -> remoteVariables.put("DOWNLOAD_URL", val)) + .defaultValue("") + .build()); + + installer.addOption(ConfigOption.builder("CurseForge API Key") + .description("Optional API key for CurseForge downloads.") + .bind(() -> remoteVariables.getOrDefault("CURSEFORGE_API_KEY", ""), + val -> remoteVariables.put("CURSEFORGE_API_KEY", val)) + .defaultValue("") + .build()); + + installer.addOption(ConfigOption.builder("Modrinth API Key") + .description("Optional API key for Modrinth downloads.") + .bind(() -> remoteVariables.getOrDefault("MODRINTH_API_KEY", ""), + val -> remoteVariables.put("MODRINTH_API_KEY", val)) + .defaultValue("") + .build()); + + Setting.Builder advanced = new Setting.Builder("Advanced Startup"); + + List flags = Arrays.asList("None", "Aikar's Flags", "Velocity Flags"); + advanced.addOption(ConfigOption.builder("Additional Flags") + .description("Startup flags optimization.") + .options(flags) + .bind(() -> remoteVariables.getOrDefault("ADDITIONAL_FLAGS", "None"), + val -> remoteVariables.put("ADDITIONAL_FLAGS", val)) + .defaultValue("None") + .build()); + + advanced.addOption(ConfigOption.builder("Override Startup") + .description("Override startup command to support variables.") + .bind(() -> "1".equals(remoteVariables.getOrDefault("OVERRIDE_STARTUP", "1")), + val -> remoteVariables.put("OVERRIDE_STARTUP", val ? "1" : "0")) + .defaultValue(true) + .build()); + + advanced.addOption(ConfigOption.builder("Auto Update") + .description("Automatically update server software on restart.") + .bind(() -> "1".equals(remoteVariables.getOrDefault("AUTOMATIC_UPDATING", "0")), + val -> remoteVariables.put("AUTOMATIC_UPDATING", val ? "1" : "0")) + .defaultValue(false) + .build()); + + advanced.addOption(ConfigOption.builder("SIMD Operations") + .description("Enable SIMD (Vector API) for Java 16-21.") + .bind(() -> "1".equals(remoteVariables.getOrDefault("SIMD_OPERATIONS", "0")), + val -> remoteVariables.put("SIMD_OPERATIONS", val ? "1" : "0")) + .defaultValue(false) + .build()); + + return List.of(installer.build(), advanced.build()); + } +} diff --git a/src/main/java/redxax/oxy/remotely/ui/settings/controllers/ServerJvmSettingsController.java b/src/main/java/redxax/oxy/remotely/ui/settings/controllers/ServerJvmSettingsController.java index 859bc895..d44642ae 100644 --- a/src/main/java/redxax/oxy/remotely/ui/settings/controllers/ServerJvmSettingsController.java +++ b/src/main/java/redxax/oxy/remotely/ui/settings/controllers/ServerJvmSettingsController.java @@ -1,7 +1,6 @@ package redxax.oxy.remotely.ui.settings.controllers; import com.sun.management.OperatingSystemMXBean; -import redxax.oxy.remotely.config.RemotelyConfigManager; import restudio.rebase.Rebase; import restudio.rebase.backend.ServerBackend; import restudio.rebase.java.JavaManager; @@ -15,6 +14,7 @@ import java.lang.management.ManagementFactory; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; @@ -22,15 +22,14 @@ public class ServerJvmSettingsController { private final restudio.rebase.instance.Instance instance; - private final RemotelyConfigManager configManager; private final JavaManager javaManager; private static final int MIN_RAM_MB = 512; private final int maxSystemRamMb; private final boolean isRemote; + private Map remoteVariables; - public ServerJvmSettingsController(restudio.rebase.instance.Instance instance, RemotelyConfigManager configManager) { + public ServerJvmSettingsController(restudio.rebase.instance.Instance instance) { this.instance = instance; - this.configManager = configManager; this.javaManager = Rebase.get().getJavaManager(); ServerBackend backend = instance.getBackend(); @@ -40,86 +39,107 @@ public ServerJvmSettingsController(restudio.rebase.instance.Instance instance, R this.maxSystemRamMb = (int) (osBean.getTotalMemorySize() / 1024 / 1024); } + public void bindToRemoteVariables(Map vars) { + this.remoteVariables = vars; + } + public List getSettings() { Setting.Builder builder = new Setting.Builder("Java Configuration"); - String currentJvmArgs = getJvmArgs(); - String currentJavaPath = parseJavaPath(currentJvmArgs); - String remoteJavaPath = parseRemoteJavaPath(currentJvmArgs); - - if (isRemote) { - TextInputWidget remoteJavaInput = new TextInputWidget.Builder() - .text(remoteJavaPath != null ? remoteJavaPath : "java") - .placeholder("Remote Java Path (e.g., /usr/lib/jvm/java-21-openjdk/bin/java)") - .onChange(text -> updateJvmArgs(null, parseRam(getJvmArgs()), parseAdditionalArgs(getJvmArgs()), text)) + if (remoteVariables != null) { + TextInputWidget jarFile = new TextInputWidget.Builder() + .text(remoteVariables.getOrDefault("SERVER_JARFILE", "server.jar")) + .onChange(t -> remoteVariables.put("SERVER_JARFILE", t)) .size(500, 20) .build(); - builder.addRow("Remote Java Path", true, 20, remoteJavaInput); + builder.addRow("Server Jar File", true, 20, jarFile); + + TextInputWidget maxRam = new TextInputWidget.Builder() + .text(remoteVariables.getOrDefault("MAXIMUM_RAM", "90")) + .onChange(t -> remoteVariables.put("MAXIMUM_RAM", t)) + .size(60, 20) + .build(); + builder.addRow("Max RAM (%)", true, 20, maxRam); + } else { - List runtimes = new ArrayList<>(); - JavaRuntime defaultRuntime = new JavaRuntime("Auto-detect (Default)", null, false); - runtimes.add(defaultRuntime); - runtimes.addAll(javaManager.getRuntimes()); - - JavaRuntime selectedRuntime = runtimes.stream() - .filter(r -> Objects.equals(r.getPath(), currentJavaPath)) - .findFirst() - .orElse(defaultRuntime); - - DropDownWidget javaDropdown = new DropDownWidget.Builder<>(runtimes) - .displayFunction(JavaRuntime::getName) - .selectedItem(selectedRuntime) - .onSelectionChanged(runtime -> { - updateJvmArgs(runtime.getPath(), parseRam(getJvmArgs()), parseAdditionalArgs(getJvmArgs()), null); - checkCompatibility(runtime); + String currentJvmArgs = getJvmArgs(); + String currentJavaPath = parseJavaPath(currentJvmArgs); + String remoteJavaPath = parseRemoteJavaPath(currentJvmArgs); + + if (isRemote) { + TextInputWidget remoteJavaInput = new TextInputWidget.Builder() + .text(remoteJavaPath != null ? remoteJavaPath : "java") + .placeholder("Remote Java Path (e.g., /usr/lib/jvm/java-21-openjdk/bin/java)") + .onChange(text -> updateJvmArgs(null, parseRam(getJvmArgs()), parseAdditionalArgs(getJvmArgs()), text)) + .size(500, 20) + .build(); + builder.addRow("Remote Java Path", true, 20, remoteJavaInput); + } else { + List runtimes = new ArrayList<>(); + JavaRuntime defaultRuntime = new JavaRuntime("Auto-detect (Default)", null, false); + runtimes.add(defaultRuntime); + runtimes.addAll(javaManager.getRuntimes()); + + JavaRuntime selectedRuntime = runtimes.stream() + .filter(r -> Objects.equals(r.getPath(), currentJavaPath)) + .findFirst() + .orElse(defaultRuntime); + + DropDownWidget javaDropdown = new DropDownWidget.Builder<>(runtimes) + .displayFunction(JavaRuntime::getName) + .selectedItem(selectedRuntime) + .onSelectionChanged(runtime -> { + updateJvmArgs(runtime.getPath(), parseRam(getJvmArgs()), parseAdditionalArgs(getJvmArgs()), null); + checkCompatibility(runtime); + }) + .size(300, 20) + .build(); + builder.addRow("Java Runtime", true, 20, javaDropdown); + } + + int currentRamMb = parseRam(currentJvmArgs); + String additionalArgs = parseAdditionalArgs(currentJvmArgs); + + AtomicReference ramSliderRef = new AtomicReference<>(); + AtomicReference ramInputRef = new AtomicReference<>(); + + double sliderValue = Math.max(0, (double) (currentRamMb - MIN_RAM_MB) / (maxSystemRamMb - MIN_RAM_MB)); + + DoubleSliderWidget ramSlider = new DoubleSliderWidget.Builder() + .value(sliderValue) + .onChange(() -> { + int newRam = MIN_RAM_MB + (int) (ramSliderRef.get().getValue() * (maxSystemRamMb - MIN_RAM_MB)); + newRam = (newRam / 256) * 256; + ramInputRef.get().setText(String.valueOf(newRam)); + updateJvmArgs(isRemote ? null : parseJavaPath(getJvmArgs()), newRam, parseAdditionalArgs(getJvmArgs()), isRemote ? parseRemoteJavaPath(getJvmArgs()) : null); }) - .size(300, 20) + .size(230, 20) .build(); - builder.addRow("Java Runtime", true, 20, javaDropdown); - } + ramSliderRef.set(ramSlider); + + TextInputWidget ramInput = new TextInputWidget.Builder() + .text(String.valueOf(currentRamMb)) + .size(60, 20) + .onChange(text -> { + try { + int newRam = Integer.parseInt(text); + if (newRam >= MIN_RAM_MB && newRam <= maxSystemRamMb) { + ramSliderRef.get().setValue((double) (newRam - MIN_RAM_MB) / (maxSystemRamMb - MIN_RAM_MB)); + updateJvmArgs(isRemote ? null : parseJavaPath(getJvmArgs()), newRam, parseAdditionalArgs(getJvmArgs()), isRemote ? parseRemoteJavaPath(getJvmArgs()) : null); + } + } catch (NumberFormatException ignored) {} + }) + .build(); + ramInputRef.set(ramInput); + + builder.addRow("Memory (MB)", true, 20, ramSlider, ramInput); - int currentRamMb = parseRam(currentJvmArgs); - String additionalArgs = parseAdditionalArgs(currentJvmArgs); - - AtomicReference ramSliderRef = new AtomicReference<>(); - AtomicReference ramInputRef = new AtomicReference<>(); - - double sliderValue = Math.max(0, (double) (currentRamMb - MIN_RAM_MB) / (maxSystemRamMb - MIN_RAM_MB)); - - DoubleSliderWidget ramSlider = new DoubleSliderWidget.Builder() - .value(sliderValue) - .onChange(() -> { - int newRam = MIN_RAM_MB + (int) (ramSliderRef.get().getValue() * (maxSystemRamMb - MIN_RAM_MB)); - newRam = (newRam / 256) * 256; - ramInputRef.get().setText(String.valueOf(newRam)); - updateJvmArgs(isRemote ? null : parseJavaPath(getJvmArgs()), newRam, parseAdditionalArgs(getJvmArgs()), isRemote ? parseRemoteJavaPath(getJvmArgs()) : null); - }) - .size(230, 20) - .build(); - ramSliderRef.set(ramSlider); - - TextInputWidget ramInput = new TextInputWidget.Builder() - .text(String.valueOf(currentRamMb)) - .size(60, 20) - .onChange(text -> { - try { - int newRam = Integer.parseInt(text); - if (newRam >= MIN_RAM_MB && newRam <= maxSystemRamMb) { - ramSliderRef.get().setValue((double) (newRam - MIN_RAM_MB) / (maxSystemRamMb - MIN_RAM_MB)); - updateJvmArgs(isRemote ? null : parseJavaPath(getJvmArgs()), newRam, parseAdditionalArgs(getJvmArgs()), isRemote ? parseRemoteJavaPath(getJvmArgs()) : null); - } - } catch (NumberFormatException ignored) {} - }) - .build(); - ramInputRef.set(ramInput); - - builder.addRow("Memory (MB)", true, 20, ramSlider, ramInput); - - TextInputWidget jvmArgsInput = new TextInputWidget.Builder() - .text(additionalArgs) - .onChange(text -> updateJvmArgs(isRemote ? null : parseJavaPath(getJvmArgs()), parseRam(getJvmArgs()), text, isRemote ? parseRemoteJavaPath(getJvmArgs()) : null)) - .build(); - builder.addRow("Additional JVM Arguments", true, 20, jvmArgsInput); + TextInputWidget jvmArgsInput = new TextInputWidget.Builder() + .text(additionalArgs) + .onChange(text -> updateJvmArgs(isRemote ? null : parseJavaPath(getJvmArgs()), parseRam(getJvmArgs()), text, isRemote ? parseRemoteJavaPath(getJvmArgs()) : null)) + .build(); + builder.addRow("Additional JVM Arguments", true, 20, jvmArgsInput); + } return List.of(builder.build()); } diff --git a/src/main/java/redxax/oxy/remotely/ui/widgets/management/BackendPlayerDataProvider.java b/src/main/java/redxax/oxy/remotely/ui/widgets/management/BackendPlayerDataProvider.java deleted file mode 100644 index b00b0070..00000000 --- a/src/main/java/redxax/oxy/remotely/ui/widgets/management/BackendPlayerDataProvider.java +++ /dev/null @@ -1,100 +0,0 @@ -package redxax.oxy.remotely.ui.widgets.management; - -import redxax.oxy.remotely.data.managed.ManagedPlayer; -import redxax.oxy.remotely.data.managed.PlayerAction; -import redxax.oxy.remotely.data.player.IPlayerActionProvider; -import redxax.oxy.remotely.data.player.IPlayerDataProvider; -import restudio.rebase.backend.feature.PlayerManagementFeature; -import restudio.rescreen.ui.core.ScreenManager; - -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; - -public class BackendPlayerDataProvider implements IPlayerDataProvider, IPlayerActionProvider { - private final PlayerManagementFeature feature; - private final Map cache = new ConcurrentHashMap<>(); - private final List>> listeners = new CopyOnWriteArrayList<>(); - - public BackendPlayerDataProvider(PlayerManagementFeature feature) { - this.feature = feature; - } - - @Override - public void initialize() { - fullRefresh(); - } - - @Override - public void shutdown() { - listeners.clear(); - cache.clear(); - } - - @Override - public CompletableFuture fullRefresh() { - return feature.getOnlinePlayers().thenAccept(simplePlayers -> { - cache.clear(); - for (PlayerManagementFeature.SimplePlayer sp : simplePlayers) { - ManagedPlayer mp = new ManagedPlayer(sp.uuid(), sp.name()); - mp.isOnline = true; - cache.put(sp.uuid(), mp); - } - notifyListeners(); - }); - } - - @Override - public Map getCachedPlayers() { - return cache; - } - - @Override - public void addUpdateListener(Consumer> listener) { - listeners.add(listener); - } - - @Override - public void removeUpdateListener(Consumer> listener) { - listeners.remove(listener); - } - - private void notifyListeners() { - List list = new ArrayList<>(cache.values()); - ScreenManager.getInstance().execute(() -> { - for (Consumer> l : listeners) l.accept(list); - }); - } - - @Override - public CompletableFuture kickPlayer(ManagedPlayer player, String reason) { - return feature.kick(player.uuid, reason); - } - - @Override - public CompletableFuture banPlayer(ManagedPlayer player, String reason, boolean ipBan) { - return feature.ban(player.uuid, reason, ipBan); - } - - @Override - public CompletableFuture unbanPlayer(ManagedPlayer player) { - return feature.unban(player.uuid); - } - - @Override - public CompletableFuture toggleOp(ManagedPlayer player) { - return feature.setOp(player.uuid, !player.isOp); - } - - @Override - public CompletableFuture runCustomCommand(ManagedPlayer player, String commandTemplate) { - return CompletableFuture.failedFuture(new UnsupportedOperationException("Backend does not support arbitrary command execution.")); - } - - @Override - public CompletableFuture> getCustomActions() { - return CompletableFuture.completedFuture(Collections.emptyList()); - } -} diff --git a/src/main/java/redxax/oxy/remotely/ui/widgets/management/BanPlayerPopup.java b/src/main/java/redxax/oxy/remotely/ui/widgets/management/BanPlayerPopup.java index f8ab0cd2..9e0d2209 100644 --- a/src/main/java/redxax/oxy/remotely/ui/widgets/management/BanPlayerPopup.java +++ b/src/main/java/redxax/oxy/remotely/ui/widgets/management/BanPlayerPopup.java @@ -1,6 +1,6 @@ package redxax.oxy.remotely.ui.widgets.management; -import redxax.oxy.remotely.data.managed.ManagedPlayer; +import redxax.oxy.remotely.data.player.model.UnifiedPlayer; import restudio.rescreen.theme.ThemeManager; import restudio.rescreen.ui.core.Screen; import restudio.rescreen.ui.widgets.PopupWidget; @@ -10,11 +10,11 @@ public class BanPlayerPopup extends PopupWidget { - public BanPlayerPopup(Screen parent, ManagedPlayer player, PlayerManagerController controller) { - super(0, 0, 350, 200, "Ban " + player.name); + public BanPlayerPopup(Screen parent, UnifiedPlayer player, PlayerManagerController controller) { + super(0, 0, 350, 200, "Ban " + player.getName()); setLayer(500); - Builder builder = new Builder("Ban " + player.name).size(350, 160).setResizable(true); + Builder builder = new Builder("Ban " + player.getName()).size(350, 160).setResizable(true); var ref = new Object() { TextInputWidget reasonInput = new TextInputWidget.Builder().placeholder("Reason for ban").build(); @@ -24,13 +24,13 @@ public BanPlayerPopup(Screen parent, ManagedPlayer player, PlayerManagerControll String reason = ref.reasonInput.getText(); boolean ipBan = ipBanToggle.getValue(); controller.banPlayer(player, reason, ipBan); - new Notification("player.name + \" has been banned.", "Click Here To Unban", Notification.Type.SUCCESS, () -> controller.unbanPlayer(player)); + new Notification(player.getName() + " has been banned.", "Click Here To Unban", Notification.Type.SUCCESS, () -> controller.unbanPlayer(player)); hide(); }; ref.reasonInput = new TextInputWidget.Builder().placeholder("Reason for ban").onEnter(banAction).build(); builder.addRow("Reason", true, 20, ref.reasonInput); - if (player.isOnline && player.address != null && !player.address.isEmpty()) { + if (player.isOnline() && player.getIp().getValue() != null && !player.getIp().getValue().isEmpty()) { builder.addRow("IP Ban", false, 20, ipBanToggle); } diff --git a/src/main/java/redxax/oxy/remotely/ui/widgets/management/PlayerEntryWidget.java b/src/main/java/redxax/oxy/remotely/ui/widgets/management/PlayerEntryWidget.java index f12bbcf7..1e6cdc31 100644 --- a/src/main/java/redxax/oxy/remotely/ui/widgets/management/PlayerEntryWidget.java +++ b/src/main/java/redxax/oxy/remotely/ui/widgets/management/PlayerEntryWidget.java @@ -1,8 +1,8 @@ package redxax.oxy.remotely.ui.widgets.management; import redxax.oxy.remotely.data.integrations.luckperms.LuckPermsService; -import redxax.oxy.remotely.data.managed.ManagedPlayer; import redxax.oxy.remotely.data.managed.PlayerAction; +import redxax.oxy.remotely.data.player.model.UnifiedPlayer; import restudio.rebase.account.Account; import restudio.rescreen.platform.IDrawContext; import restudio.rescreen.theme.ThemeColor; @@ -24,7 +24,7 @@ public class PlayerEntryWidget extends MountableButtonWidget { - private final ManagedPlayer player; + private UnifiedPlayer player; private final PlayerManagerController controller; private final Identifier opIcon = Identifier.icon("op.png"); private final Identifier deopIcon = Identifier.icon("deop.png"); @@ -35,41 +35,104 @@ public class PlayerEntryWidget extends MountableButtonWidget { private String cachedGroup = ""; private boolean lpDataRequested = false; - public PlayerEntryWidget(ManagedPlayer player, PlayerManagerController controller) { - super(player.name, "", "", new CopyOnWriteArrayList<>(), null); + private SquareButtonWidget kickButton; + private SquareButtonWidget banButton; + private SquareButtonWidget opButton; + + public PlayerEntryWidget(UnifiedPlayer player, PlayerManagerController controller) { + super(player.getName(), "", "", new CopyOnWriteArrayList<>(), null); this.player = player; this.controller = controller; this.ClickableWhenInactive = true; + setupButtons(); + } + + private void setupButtons() { + mountedWidgets.clear(); boolean serverRunning = controller.isServerRunning(); - SquareButtonWidget kickButton = new SquareButtonWidget.Builder() + kickButton = new SquareButtonWidget.Builder() .imagePath("delete.png").size(18, 18).hint("Kick Player").accentType(ThemeManager.getAccent("danger")) .onClick(() -> controller.kickPlayer(player, "Kicked by operator")).build(); - kickButton.active = player.isOnline && serverRunning; - String banHint = player.isBanned ? "Unban Player" : "Ban Player"; - SquareButtonWidget banButton = new SquareButtonWidget.Builder().imagePath(player.isBanned ? "heart.png" : "close.png").size(18, 18).hint(banHint) - .accentType(player.isBanned ? ThemeManager.getAccent("nice") : ThemeManager.getAccent("danger")).onClick(() -> { - if (player.isBanned) { - controller.unbanPlayer(player); - } else { - new BanPlayerPopup(ScreenManager.getInstance().getCurrentScreen(), player, controller); - } - }).build(); + updateButtonsState(serverRunning); + + List buttons = new CopyOnWriteArrayList<>(); + List actions = controller.getPlayerActions(); + if (actions != null) { + for (PlayerAction action : actions) { + SquareButtonWidget actionButton = new SquareButtonWidget.Builder().identifier(Identifier.icon(action.icon)).size(18, 18).hint(action.name).onClick(() -> { + List variables = findCustomVariables(action.command); + if (variables.isEmpty()) { + controller.runCustomCommand(player, action.command); + } else { + showVariableInputPopup(player, action, variables); + } + }).build(); + actionButton.active = serverRunning; + buttons.add(actionButton); + } + } + + buttons.add(banButton); + buttons.add(kickButton); + buttons.add(opButton); + mountedWidgets.addAll(buttons); + } + + public void update(UnifiedPlayer newPlayerState) { + this.player = newPlayerState; + this.name = player.getName(); + boolean serverRunning = controller.isServerRunning(); + updateButtonsState(serverRunning); + } + + private void updateButtonsState(boolean serverRunning) { + kickButton.active = player.isOnline() && serverRunning; + + boolean isBanned = player.getBan().getValue() != null; + String banHint = isBanned ? "Unban Player" : "Ban Player"; + if (banButton == null) { + banButton = new SquareButtonWidget.Builder().imagePath(isBanned ? "heart.png" : "close.png").size(18, 18).hint(banHint) + .accentType(isBanned ? ThemeManager.getAccent("nice") : ThemeManager.getAccent("danger")).onClick(() -> { + if (player.getBan().getValue() != null) { + controller.unbanPlayer(player); + } else { + new BanPlayerPopup(ScreenManager.getInstance().getCurrentScreen(), player, controller); + } + }).build(); + } else { + banButton = new SquareButtonWidget.Builder().imagePath(isBanned ? "heart.png" : "close.png").size(18, 18).hint(banHint) + .accentType(isBanned ? ThemeManager.getAccent("nice") : ThemeManager.getAccent("danger")).onClick(() -> { + if (player.getBan().getValue() != null) { + controller.unbanPlayer(player); + } else { + new BanPlayerPopup(ScreenManager.getInstance().getCurrentScreen(), player, controller); + } + }).build(); + } banButton.active = serverRunning; - String opHint = player.isOp ? "De-Op Player" : "Op Player"; - SquareButtonWidget opButton = new SquareButtonWidget.Builder() - .identifier(player.isOp ? deopIcon : opIcon).size(18, 18).hint(opHint) - .accentType(ThemeManager.getAccent("calm")) - .onClick(() -> controller.toggleOp(player)).build(); + boolean isOp = player.isOp(); + String opHint = isOp ? "De-Op Player" : "Op Player"; + if (opButton == null) { + opButton = new SquareButtonWidget.Builder() + .identifier(isOp ? deopIcon : opIcon).size(18, 18).hint(opHint) + .accentType(ThemeManager.getAccent("calm")) + .onClick(() -> controller.toggleOp(player)).build(); + } else { + opButton = new SquareButtonWidget.Builder() + .identifier(isOp ? deopIcon : opIcon).size(18, 18).hint(opHint) + .accentType(ThemeManager.getAccent("calm")) + .onClick(() -> controller.toggleOp(player)).build(); + } opButton.active = serverRunning; List buttons = new CopyOnWriteArrayList<>(); List actions = controller.getPlayerActions(); if (actions != null) { - for (PlayerAction action : actions) { + for (PlayerAction action : actions) { SquareButtonWidget actionButton = new SquareButtonWidget.Builder().identifier(Identifier.icon(action.icon)).size(18, 18).hint(action.name).onClick(() -> { List variables = findCustomVariables(action.command); if (variables.isEmpty()) { @@ -82,14 +145,15 @@ public PlayerEntryWidget(ManagedPlayer player, PlayerManagerController controlle buttons.add(actionButton); } } - buttons.add(banButton); buttons.add(kickButton); buttons.add(opButton); + + mountedWidgets.clear(); mountedWidgets.addAll(buttons); } - public ManagedPlayer getPlayer() { + public UnifiedPlayer getPlayer() { return player; } @@ -99,7 +163,7 @@ public void tick() { if (icon == null && !faceRequested) { faceRequested = true; CompletableFuture.runAsync(() -> { - Account tempAccount = new Account(player.name, player.uuid.toString(), null, 0); + Account tempAccount = new Account(player.getName(), player.getUuid().toString(), null, 0); BufferedImage fetchedFace = tempAccount.getFace(); if (fetchedFace != null) { this.icon = fetchedFace; @@ -112,7 +176,7 @@ public void tick() { LuckPermsService lp = controller.getLuckPermsService(); if (lp != null && lp.isEnabled()) { lpDataRequested = true; - lp.getUserMetadata(player.uuid).thenAccept(meta -> { + lp.getUserMetadata(player.getUuid()).thenAccept(meta -> { if (meta != null) { this.cachedPrefix = meta.prefix != null ? meta.prefix : ""; this.cachedSuffix = meta.suffix != null ? meta.suffix : ""; @@ -127,32 +191,37 @@ public void tick() { protected void drawContent(IDrawContext ctx, int mouseX, int mouseY) { StringBuilder displayName = new StringBuilder(); if (!cachedPrefix.isEmpty()) displayName.append(cachedPrefix); - displayName.append(player.name); + displayName.append(player.getName()); if (!cachedSuffix.isEmpty()) displayName.append(cachedSuffix); name = displayName.toString().replace("&", "ยง"); hiddenText = cachedGroup; - if (player.isBanned || player.isIpBanned) { - String reason = (player.banInfo != null) ? player.banInfo.reason : ((player.ipBanInfo != null) ? player.ipBanInfo.reason : "Unknown"); - String expires = (player.banInfo != null) ? player.banInfo.expires : ((player.ipBanInfo != null) ? player.ipBanInfo.expires : "Unknown"); - description = "Banned For " + reason + " | Expires: " + expires + " | Type: " + (player.isIpBanned ? "IP Ban" : "Account Ban"); + boolean isBanned = player.getBan().getValue() != null; + if (isBanned) { + String reason = player.getBan().getValue().reason(); + String expires = player.getBan().getValue().expires(); + description = "Banned For " + reason + " | Expires: " + expires; accentType = ThemeManager.getAccent("danger"); - } else if (player.isOnline) { + } else if (player.isOnline()) { description = "Online"; - if (player.isOp) { - description += " | Operator (Level " + player.opLevel + ")"; + if (player.isOp()) { + description += " | Operator"; accentType = ThemeManager.getAccent("calm"); } else accentType = ThemeManager.getDefaultAccent(); + active = true; } else { description = "Offline"; - if (player.isOp) description += " | Operator"; - description += " | Last seen: " + (player.lastSeen > 0 ? TimeUtils.timeSense(player.lastSeen) : "never"); + if (player.isOp()) description += " | Operator"; + long lastSeen = player.getLastSeenValue(); + if (lastSeen > 0) { + description += " | Last seen: " + TimeUtils.timeSense(lastSeen); + } accentType = ThemeManager.getDefaultAccent(); active = false; } - titleColor = player.isOnline ? ThemeManager.getColor(ThemeColor.text) : ThemeManager.getColor(ThemeColor.textDark); + titleColor = player.isOnline() ? ThemeManager.getColor(ThemeColor.text) : ThemeManager.getColor(ThemeColor.textDark); super.drawContent(ctx, mouseX, mouseY); } @@ -170,7 +239,7 @@ private List findCustomVariables(String command) { return new ArrayList<>(variables); } - private void showVariableInputPopup(ManagedPlayer player, PlayerAction action, List variables) { + private void showVariableInputPopup(UnifiedPlayer player, PlayerAction action, List variables) { PopupWidget.Builder builder = new PopupWidget.Builder("Execute: " + action.name) .size(300, 60 + variables.size() * 30).setAntiOutOfBound(true).setResizable(true); diff --git a/src/main/java/redxax/oxy/remotely/ui/widgets/management/PlayerManagerController.java b/src/main/java/redxax/oxy/remotely/ui/widgets/management/PlayerManagerController.java index 73959dcf..2f2ee7c7 100644 --- a/src/main/java/redxax/oxy/remotely/ui/widgets/management/PlayerManagerController.java +++ b/src/main/java/redxax/oxy/remotely/ui/widgets/management/PlayerManagerController.java @@ -1,56 +1,53 @@ package redxax.oxy.remotely.ui.widgets.management; import redxax.oxy.remotely.data.integrations.luckperms.LuckPermsService; -import redxax.oxy.remotely.data.managed.ManagedPlayer; import redxax.oxy.remotely.data.managed.PlayerAction; import redxax.oxy.remotely.data.managed.PlayerSession; -import redxax.oxy.remotely.data.managed.SessionEventType; -import redxax.oxy.remotely.data.player.IPlayerActionProvider; -import redxax.oxy.remotely.data.player.IPlayerDataProvider; -import redxax.oxy.remotely.data.player.IPlayerHistoryCollector; import redxax.oxy.remotely.data.player.IPlayerHistoryProvider; -import redxax.oxy.remotely.data.player.msmp.MsmpPlayerProvider; -import redxax.oxy.remotely.data.player.standard.StandardPlayerActionProvider; -import redxax.oxy.remotely.data.player.standard.StandardPlayerDataProvider; +import redxax.oxy.remotely.data.player.PlayerService; +import redxax.oxy.remotely.data.player.action.BackendActionExecutor; +import redxax.oxy.remotely.data.player.action.MsmpActionExecutor; +import redxax.oxy.remotely.data.player.action.StandardActionExecutor; +import redxax.oxy.remotely.data.player.model.UnifiedPlayer; +import redxax.oxy.remotely.data.player.source.BackendPlayerSource; +import redxax.oxy.remotely.data.player.source.MsmpPlayerSource; +import redxax.oxy.remotely.data.player.source.StandardFileSource; +import redxax.oxy.remotely.data.player.source.StandardLogSource; import redxax.oxy.remotely.data.player.standard.StandardPlayerHistoryProvider; import redxax.oxy.remotely.ui.integrations.luckperms.LuckPermsDashboardScreen; -import restudio.rebase.api.RebaseApiFactory; +import redxax.oxy.remotely.ui.server.containers.PlayersContainer; import restudio.rebase.api.RebaseAPI; +import restudio.rebase.api.RebaseApiFactory; import restudio.rebase.backend.feature.PlayerManagementFeature; import restudio.rebase.instance.Instance; import restudio.rebase.instance.InstanceState; import restudio.rebase.ui.widgets.TerminalWidget; -import restudio.rescreen.debug.DebugManager; import restudio.rescreen.ui.core.ScreenManager; -import restudio.rescreen.ui.rescreen.Container; -import restudio.rescreen.ui.widgets.AnimatedButton; import restudio.rescreen.util.Notification; import java.nio.file.Path; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.Optional; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; public class PlayerManagerController { private static final Map registry = new HashMap<>(); private final Instance instance; - private Container container; - - private final CompositePlayerDataProvider compositeDataProvider = new CompositePlayerDataProvider(); + private PlayersContainer container; + private PlayerService playerService; - private List actionProviderChain; private IPlayerHistoryProvider historyProvider; - private IPlayerHistoryCollector historyCollector; + private LuckPermsService luckPermsService; private TerminalWidget terminalWidget; private boolean isInitialized = false; - private StandardPlayerDataProvider standardProviderRef; + private StandardFileSource standardFileSource; + private List playerActions = new ArrayList<>(); + private final Gson gson = new Gson(); private PlayerManagerController(Instance instance) { this.instance = instance; @@ -68,95 +65,72 @@ public static PlayerManagerController getOrCreate(Instance instance) { } public void reloadProviders() { - if (actionProviderChain != null) for (IPlayerActionProvider p : actionProviderChain) p.shutdown(); - if (historyProvider != null) historyProvider.shutdown(); - compositeDataProvider.shutdown(); - - initializeProviders(); - + initializeService(); if (container != null) { - compositeDataProvider.addUpdateListener(players -> ScreenManager.getInstance().execute(this::rebuildPlayerWidgets)); - compositeDataProvider.fullRefresh(); + playerService.addListener(snapshot -> ScreenManager.getInstance().execute(() -> { + if (container != null) container.syncUi(snapshot); + })); + container.syncUi(playerService.getRegistry().getAll()); } } - private void initializeProviders() { + private void initializeService() { + this.playerService = new PlayerService(); TerminalWidget tw = this.terminalWidget; RebaseAPI api = RebaseApiFactory.get(instance); Properties settings = instance.getSettings(); - MsmpPlayerProvider msmpProvider = null; + StandardPlayerHistoryProvider standardHistory = new StandardPlayerHistoryProvider(instance, api, tw, Path.of(instance.getPath()), name -> playerService.getRegistry().getAll().stream() + .filter(p -> p.getName().equalsIgnoreCase(name)) + .map(UnifiedPlayer::getUuid) + .findFirst().orElse(null)); + this.historyProvider = standardHistory; + boolean msmpEnabled = Boolean.parseBoolean(settings.getProperty("provider.msmp.enabled", "true")); if (msmpEnabled) { - msmpProvider = new MsmpPlayerProvider(instance.getMSMPManager()); + playerService.registerSource(new MsmpPlayerSource(instance.getMSMPManager())); + playerService.registerExecutor(new MsmpActionExecutor(instance.getMSMPManager())); } - BackendPlayerDataProvider backendProvider = null; boolean backendEnabled = Boolean.parseBoolean(settings.getProperty("provider.backend.enabled", "true")); if (backendEnabled && instance.getBackend() != null) { Optional feat = instance.getBackend().getFeature(PlayerManagementFeature.class); - if (feat.isPresent()) backendProvider = new BackendPlayerDataProvider(feat.get()); + if (feat.isPresent()) { + BackendPlayerSource backendPlayerSource = new BackendPlayerSource(feat.get()); + playerService.registerSource(backendPlayerSource); + playerService.registerExecutor(new BackendActionExecutor(feat.get())); + } } boolean standardEnabled = Boolean.parseBoolean(settings.getProperty("provider.standard.enabled", "true")); - StandardPlayerDataProvider standardProvider = null; - StandardPlayerActionProvider standardAction = null; - StandardPlayerHistoryProvider standardHistory = null; - if (standardEnabled) { - standardHistory = new StandardPlayerHistoryProvider(instance, api, tw, Path.of(instance.getPath()), name -> resolveUuidFromCache(instance, name)); - standardProvider = new StandardPlayerDataProvider(instance, api, tw, standardHistory); - standardAction = new StandardPlayerActionProvider(instance, api, tw); - this.standardProviderRef = standardProvider; - } else { - this.standardProviderRef = null; + standardFileSource = new StandardFileSource(instance, api); + playerService.registerSource(standardFileSource); + playerService.registerSource(new StandardLogSource(instance, standardHistory)); + playerService.registerExecutor(new StandardActionExecutor(tw)); } - compositeDataProvider.setBaseProvider(standardProvider); - List overlays = new ArrayList<>(); - if (msmpProvider != null) overlays.add(msmpProvider); - if (backendProvider != null) overlays.add(backendProvider); - compositeDataProvider.setOverlayProviders(overlays); - - List chain = new ArrayList<>(); - String priorityString = settings.getProperty("provider.priority.players", "msmp,backend,standard"); - String[] priorities = priorityString.split(","); - - for (String p : priorities) { - String key = p.trim().toLowerCase(Locale.ROOT); - if ("msmp".equals(key) && msmpProvider != null) chain.add(msmpProvider); - else if ("backend".equals(key) && backendProvider != null) chain.add(backendProvider); - else if ("standard".equals(key) && standardAction != null) chain.add(standardAction); - } - - if (standardAction != null && !chain.contains(standardAction)) { - chain.add(standardAction); - } - this.actionProviderChain = chain; - - this.historyProvider = standardHistory; - this.historyCollector = standardHistory; this.luckPermsService = new LuckPermsService(api, Path.of(instance.getPath())); - - compositeDataProvider.initialize(); - for (IPlayerActionProvider p : this.actionProviderChain) p.initialize(); - if (this.historyProvider != null) this.historyProvider.initialize(); this.luckPermsService.initialize(); - - if (msmpProvider != null) { - instance.getMSMPManager().addStatusListener(status -> compositeDataProvider.fullRefresh()); - } - } - - private static UUID resolveUuidFromCache(Instance instance, String name) { - PlayerManagerController c = registry.get(instance.getInstanceId()); - if (c != null) { - return c.getDataProvider().getCachedPlayers().values().stream().filter(p -> p.name.equalsIgnoreCase(name)).map(p -> p.uuid).findFirst().orElse(null); - } - return null; + if (this.historyProvider != null) this.historyProvider.initialize(); + loadActions(); + } + + private void loadActions() { + Path actionsPath = Path.of(instance.getPath(), "Remotely", "player-actions.json"); + RebaseApiFactory.get(instance).readFile(actionsPath).thenAccept(content -> { + if (content != null && !content.isEmpty()) { + try { + List loaded = gson.fromJson(content, new TypeToken>(){}.getType()); + this.playerActions = loaded != null ? loaded : new ArrayList<>(); + } catch (Exception e) { + this.playerActions = new ArrayList<>(); + } + } + }); } - public void setUiBindings(Container container, TerminalWidget terminalWidget) { + public void setUiBindings(PlayersContainer container, TerminalWidget terminalWidget) { this.container = container; boolean terminalChanged = this.terminalWidget != terminalWidget; this.terminalWidget = terminalWidget; @@ -165,118 +139,91 @@ public void setUiBindings(Container container, TerminalWidget terminalWidget) { reloadProviders(); isInitialized = true; } else { - this.compositeDataProvider.addUpdateListener(players -> ScreenManager.getInstance().execute(this::rebuildPlayerWidgets)); - ScreenManager.getInstance().execute(this::rebuildPlayerWidgets); + playerService.addListener(snapshot -> { + ScreenManager.getInstance().execute(() -> { + if (this.container != null) this.container.syncUi(snapshot); + }); + }); + if (container != null) container.syncUi(playerService.getRegistry().getAll()); } } public void handleFileUpdate(String fileName, String content) { - if (standardProviderRef != null) { - DebugManager.getInstance().log("PlayerManager", "Delegating file update for " + fileName + " to StandardPlayerDataProvider"); - standardProviderRef.updateFromContent(fileName, content); - } else { - DebugManager.getInstance().log("PlayerManager", "Received file update for " + fileName + " but no Standard Provider active"); + if (standardFileSource != null) { + standardFileSource.updateFromContent(fileName, content); } } - public CompletableFuture fullRefresh() { + public void fullRefresh() { if (Boolean.parseBoolean(instance.getSettings().getProperty("provider.msmp.enabled", "true"))) { if (!instance.getMSMPManager().isConnected) { instance.getMSMPManager().connect(); } } - return compositeDataProvider.fullRefresh(); + playerService.refreshSources(); } - public IPlayerDataProvider getDataProvider() { return compositeDataProvider; } public LuckPermsService getLuckPermsService() { return luckPermsService; } public List getPlayerActions() { - List all = new ArrayList<>(); - if (actionProviderChain == null) return all; - List> futures = new ArrayList<>(); - for (IPlayerActionProvider p : actionProviderChain) { - futures.add(p.getCustomActions().thenAccept(all::addAll).exceptionally(e -> null)); - } - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - return all; + return new ArrayList<>(playerActions); } public IPlayerHistoryProvider getHistoryProvider() { return historyProvider; } - public void rebuildPlayerWidgets() { - if (container == null) return; - container.clearWidgets(); - Map players = compositeDataProvider.getCachedPlayers(); - if (players.isEmpty()) { - container.addWidget(new AnimatedButton.Builder().label("No players found.").active(false).build()); - } else { - List sortedPlayers; - synchronized (players) { - sortedPlayers = players.values().stream().sorted(Comparator.comparing((ManagedPlayer p) -> !p.isOnline).thenComparing(p -> p.name.toLowerCase(Locale.ROOT))).toList(); - } - for (ManagedPlayer p : sortedPlayers) { - container.addWidget(new PlayerEntryWidget(p, this)); - } - } - container.updateWidgetPositions(); - } - - private CompletableFuture performAction(Function> action) { - if (actionProviderChain == null || actionProviderChain.isEmpty()) { - return CompletableFuture.failedFuture(new IllegalStateException("No valid player action provider available.")); - } - CompletableFuture future = new CompletableFuture<>(); - tryActionChain(actionProviderChain.iterator(), action, future); - return future; - } - - private void tryActionChain(Iterator iterator, Function> action, CompletableFuture future) { - if (!iterator.hasNext()) { - future.completeExceptionally(new IllegalStateException("All providers failed to execute action.")); - return; - } - IPlayerActionProvider provider = iterator.next(); - action.apply(provider).whenComplete((v, e) -> { - if (e == null) { - future.complete(null); + public void kickPlayer(UnifiedPlayer player, String reason) { + playerService.executeAction(player, "kick", reason).whenComplete((v, e) -> { + if (e != null) { + ScreenManager.getInstance().execute(() -> new Notification("Error", e.getMessage(), Notification.Type.ERROR)); } else { - tryActionChain(iterator, action, future); + playerService.refreshSources(); } }); } - public void kickPlayer(ManagedPlayer player, String reason) { - performAction(p -> p.kickPlayer(player, reason)).whenComplete((v, e) -> { - if (e != null) new Notification("Error", e.getMessage(), Notification.Type.ERROR); - else if (historyCollector != null) historyCollector.recordAccessChange(player.uuid, player.name, SessionEventType.KICK, reason, System.currentTimeMillis()); + public void banPlayer(UnifiedPlayer player, String reason, boolean ipBan) { + playerService.executeAction(player, "ban", reason, ipBan).whenComplete((v, e) -> { + if (e != null) { + ScreenManager.getInstance().execute(() -> new Notification("Error", e.getMessage(), Notification.Type.ERROR)); + } else { + playerService.refreshSources(); + } }); } - public void banPlayer(ManagedPlayer player, String reason, boolean ipBan) { - performAction(p -> p.banPlayer(player, reason, ipBan)).whenComplete((v, e) -> { - if (e != null) new Notification("Error", e.getMessage(), Notification.Type.ERROR); - else if (historyCollector != null) historyCollector.recordAccessChange(player.uuid, player.name, SessionEventType.BAN, (ipBan ? "ip=true;" : "ip=false;") + reason, System.currentTimeMillis()); + public void unbanPlayer(UnifiedPlayer player) { + playerService.executeAction(player, "unban").whenComplete((v, e) -> { + if (e != null) { + ScreenManager.getInstance().execute(() -> new Notification("Error", e.getMessage(), Notification.Type.ERROR)); + } else { + playerService.refreshSources(); + } }); } - public void unbanPlayer(ManagedPlayer player) { - performAction(p -> p.unbanPlayer(player)).whenComplete((v, e) -> { - if (e != null) new Notification("Error", e.getMessage(), Notification.Type.ERROR); - else if (historyCollector != null) historyCollector.recordAccessChange(player.uuid, player.name, SessionEventType.UNBAN, "", System.currentTimeMillis()); + public void toggleOp(UnifiedPlayer player) { + String action = player.isOp() ? "deop" : "op"; + playerService.executeAction(player, action).whenComplete((v, e) -> { + if (e != null) { + ScreenManager.getInstance().execute(() -> new Notification("Error", e.getMessage(), Notification.Type.ERROR)); + } else { + playerService.refreshSources(); + } }); } - public void toggleOp(ManagedPlayer player) { - performAction(p -> p.toggleOp(player)).whenComplete((v, e) -> { - if (e != null) new Notification("Error", e.getMessage(), Notification.Type.ERROR); - else if (historyCollector != null) historyCollector.recordAccessChange(player.uuid, player.name, SessionEventType.OP_CHANGE, player.isOp ? "op=false" : "op=true", System.currentTimeMillis()); - }); - } + public void runCustomCommand(UnifiedPlayer player, String commandTemplate) { + if (commandTemplate == null || commandTemplate.isEmpty()) return; + String command = commandTemplate + .replace("$name", player.getName()) + .replace("$uuid", player.getUuid().toString()); - public void runCustomCommand(ManagedPlayer player, String commandTemplate) { - performAction(p -> p.runCustomCommand(player, commandTemplate)).whenComplete((v, e) -> { - if (e != null) new Notification("Error", "Failed to execute command: " + e.getMessage(), Notification.Type.ERROR); + playerService.executeAction(player, "command", command).whenComplete((v, e) -> { + if (e != null) { + ScreenManager.getInstance().execute(() -> new Notification("Error", e.getMessage(), Notification.Type.ERROR)); + } else { + playerService.refreshSources(); + } }); } @@ -300,136 +247,4 @@ public void openLuckPermsDashboard() { new Notification("Error", "LuckPerms integration is disabled or not available.", Notification.Type.ERROR); } } - - public String getDebugChainInfo() { - if (actionProviderChain == null) return "None"; - return actionProviderChain.stream().map(p -> p.getClass().getSimpleName()).collect(Collectors.joining(" -> ")); - } - - private static class CompositePlayerDataProvider implements IPlayerDataProvider { - private IPlayerDataProvider baseProvider; - private List overlayProviders = new ArrayList<>(); - private final List>> listeners = new CopyOnWriteArrayList<>(); - - private final Map mergedCache = new ConcurrentHashMap<>(); - - void setBaseProvider(IPlayerDataProvider base) { - this.baseProvider = base; - } - - void setOverlayProviders(List overlays) { - this.overlayProviders = overlays; - } - - @Override - public void initialize() { - if (baseProvider != null) { - baseProvider.initialize(); - baseProvider.addUpdateListener(l -> mergeAndNotify()); - } - for (IPlayerDataProvider p : overlayProviders) { - p.initialize(); - p.addUpdateListener(l -> mergeAndNotify()); - } - mergeAndNotify(); - } - - @Override - public void shutdown() { - if (baseProvider != null) baseProvider.shutdown(); - for (IPlayerDataProvider p : overlayProviders) p.shutdown(); - listeners.clear(); - mergedCache.clear(); - } - - @Override - public CompletableFuture fullRefresh() { - List> futures = new ArrayList<>(); - if (baseProvider != null) futures.add(baseProvider.fullRefresh()); - for (IPlayerDataProvider p : overlayProviders) futures.add(p.fullRefresh()); - return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); - } - - @Override - public Map getCachedPlayers() { - return mergedCache; - } - - @Override - public void addUpdateListener(Consumer> listener) { - listeners.add(listener); - } - - @Override - public void removeUpdateListener(Consumer> listener) { - listeners.remove(listener); - } - - private synchronized void mergeAndNotify() { - mergedCache.clear(); - - if (baseProvider != null) { - for (ManagedPlayer mp : baseProvider.getCachedPlayers().values()) { - ManagedPlayer clone = copyPlayer(mp); - clone.isOnline = false; - mergedCache.put(clone.uuid, clone); - } - } - - boolean onlineSourceFound = false; - for (IPlayerDataProvider overlay : overlayProviders) { - Map overlayPlayers = overlay.getCachedPlayers(); - if (!overlayPlayers.isEmpty()) onlineSourceFound = true; - - for (ManagedPlayer onlineMp : overlayPlayers.values()) { - if (onlineMp.isOnline || onlineMp.isBanned || onlineMp.isOp) { - ManagedPlayer existing = mergedCache.get(onlineMp.uuid); - if (existing != null) { - if (onlineMp.isOnline) existing.isOnline = true; - if (onlineMp.isBanned) { - existing.isBanned = true; - existing.banInfo = onlineMp.banInfo; - } - if (onlineMp.isOp) { - existing.isOp = true; - existing.opLevel = onlineMp.opLevel; - } - if (onlineMp.ping >= 0) existing.ping = onlineMp.ping; - } else { - mergedCache.put(onlineMp.uuid, copyPlayer(onlineMp)); - } - } - } - } - - if (!onlineSourceFound && baseProvider != null) { - for (ManagedPlayer mp : baseProvider.getCachedPlayers().values()) { - if (mp.isOnline) { - ManagedPlayer target = mergedCache.get(mp.uuid); - if (target != null) target.isOnline = true; - } - } - } - - List list = new ArrayList<>(mergedCache.values()); - for (Consumer> l : listeners) { - l.accept(list); - } - } - - private ManagedPlayer copyPlayer(ManagedPlayer original) { - ManagedPlayer p = new ManagedPlayer(original.uuid, original.name); - p.isOnline = original.isOnline; - p.ping = original.ping; - p.address = original.address; - p.lastSeen = original.lastSeen; - p.isOp = original.isOp; - p.opLevel = original.opLevel; - p.isBanned = original.isBanned; - p.banInfo = original.banInfo; - p.isIpBanned = original.isIpBanned; - p.ipBanInfo = original.ipBanInfo; - return p; - } - } }