diff --git a/api/build.gradle.kts b/api/build.gradle.kts index e69de29..02bef1a 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -0,0 +1,15 @@ +description = "Api functions for valiobungee" + +java { + withJavadocJar() + withSourcesJar() +} + +dependencies { + testImplementation(libs.testing.juipter) +} + +tasks.test { + useJUnitPlatform() +} + diff --git a/api/src/main/java/net/limework/valiobungee/api/NetworkPlayer.java b/api/src/main/java/net/limework/valiobungee/api/NetworkPlayer.java new file mode 100644 index 0000000..dec28cd --- /dev/null +++ b/api/src/main/java/net/limework/valiobungee/api/NetworkPlayer.java @@ -0,0 +1,10 @@ +/* +* Copyright (c) 2026-present ValioBungee contributors +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Apache License Version 2.0 +* which accompanies this distribution, and is available at +* https://www.apache.org/licenses/LICENSE-2.0.txt +*/ +package net.limework.valiobungee.api; + +public interface NetworkPlayer {} diff --git a/api/src/main/java/net/limework/valiobungee/api/NetworkProxy.java b/api/src/main/java/net/limework/valiobungee/api/NetworkProxy.java new file mode 100644 index 0000000..52aabe8 --- /dev/null +++ b/api/src/main/java/net/limework/valiobungee/api/NetworkProxy.java @@ -0,0 +1,31 @@ +/* +* Copyright (c) 2026-present ValioBungee contributors +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Apache License Version 2.0 +* which accompanies this distribution, and is available at +* https://www.apache.org/licenses/LICENSE-2.0.txt +*/ +package net.limework.valiobungee.api; + +/** + * Proxy is an object for online proxy in a network + * + * @author Ham1255 + * @since 1.0.0 + */ +public interface NetworkProxy { + /** + * @return return the proxy id of this proxy + */ + String proxyId(); + + /** + * @return online players number in this proxy + */ + int onlinePlayers(); + + /** + * @return returns true if this object is proxy on it + */ + boolean isMe(); +} diff --git a/build.gradle.kts b/build.gradle.kts index 01d7c1e..d7db9cd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { subprojects { apply { plugin("com.diffplug.spotless") } - apply { plugin("java-library")} + apply { plugin("java-library") } java { toolchain { @@ -33,6 +33,9 @@ subprojects { } else { licenseHeaderFile(rootProject.file("copyright_header.txt")) } + if (project.name == "valiobungee-core") { + targetExclude("**/net/limework/valiobungee/core/proto/**") + } } } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 7fe5a3c..94dfb78 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.blossom) alias(libs.plugins.indragit) + alias(libs.plugins.protobuf) } description = "Core functions for valiobungee" @@ -20,3 +21,24 @@ java { withJavadocJar() withSourcesJar() } + +dependencies { + api(project(":valiobungee-api")) + api(libs.protobuf) + api(libs.caffeine) + api(libs.slf4j) + + testImplementation(libs.testing.juipter) + testImplementation(libs.testing.slf4j.simple) +} + +tasks.test { + useJUnitPlatform() +} + +protobuf { + protoc { + artifact = libs.protoc.get().toString() + } +} + diff --git a/core/src/main/java/net/limework/valiobungee/core/ProxyManager.java b/core/src/main/java/net/limework/valiobungee/core/ProxyManager.java new file mode 100644 index 0000000..f0949d1 --- /dev/null +++ b/core/src/main/java/net/limework/valiobungee/core/ProxyManager.java @@ -0,0 +1,103 @@ +/* +* Copyright (c) 2026-present ValioBungee contributors +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the GNU GENERAL PUBLIC LICENSE Version 3 +* which accompanies this distribution, and is available at +* https://www.gnu.org/licenses/gpl-3.0.txt +*/ +package net.limework.valiobungee.core; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import com.github.benmanes.caffeine.cache.RemovalListener; +import java.time.Duration; +import java.util.UUID; +import net.limework.valiobungee.core.proto.messages.*; +import net.limework.valiobungee.core.util.logging.LogProviderFactory; +import org.slf4j.Logger; + +/** + * This abstract class is responsible for proxy discovery and count online players as its cached + * locally Why abstract? Because it allows us to develop alternative implementation to use other + * software than valkey or redis + */ +public abstract class ProxyManager { + + protected final UUID proxyManagerId = UUID.randomUUID(); + protected final ValioBungeePlatform platform; + + public ProxyManager(ValioBungeePlatform platform) { + this.platform = platform; + } + + protected final Logger log = LogProviderFactory.get(); + + // proxy info class here stores the ProxyManager ID hence we use another cache for online players + // reason we use caffeine cache it allows us to auto remove entries without need for scheduled + // tasks in the proxies when a proxy disappears without sending death message + private final Cache heartbeats = + Caffeine.newBuilder() + .expireAfterWrite(Duration.ofSeconds(30)) + .removalListener( + (RemovalListener) + (key, value, cause) -> { + if (cause == RemovalCause.EXPIRED) { + assert value != null; + log.warn("proxy {} has disconnected but did not send death message", value); + } + }) + .build(); + + protected void handleProxyHeartBeat(Heartbeat payload) { + if (!this.heartbeats.asMap().containsKey(payload.getSender().getProxyId())) + log.info("Proxy {} has connected!", payload.getSender().getProxyId()); + this.heartbeats.put(payload.getSender().getProxyId(), payload); + } + + protected void handleProxyDeath(Death payload) { + this.heartbeats.invalidate(payload.getSender().getProxyId()); + log.info("Proxy {} has disconnected", payload.getSender().getProxyId()); + } + + public int onlinePlayersCount() { + // reason we + local online players because our proxy is not inside it own heartbeat cache + return heartbeats.asMap().values().stream().mapToInt(Heartbeat::getOnlinePlayersCount).sum() + + platform.localOnlinePlayers(); + } + + public int onlinePlayersCount(String proxyId) { + if (proxyId.equals(this.platform.proxyId())) { + return platform.localOnlinePlayers(); + } else { + if (!heartbeats.asMap().containsKey(proxyId)) return 0; // don't error? + return heartbeats.asMap().get(proxyId).getOnlinePlayersCount(); + } + } + + protected ProxyInfo createProxyInfo() { + return ProxyInfo.newBuilder() + .setProxyId(platform.proxyId()) + .setProxyManagerId(this.proxyManagerId.toString()) + .build(); + } + + protected Death createDeathPayload() { + return Death.newBuilder().setSender(createProxyInfo()).setReason(DeathReason.SHUTDOWN).build(); + } + + protected Heartbeat createHeartbeatPayload() { + return Heartbeat.newBuilder() + .setSender(createProxyInfo()) + .setOnlinePlayersCount(platform.localOnlinePlayers()) + .build(); + } + + protected abstract void publishDeathPayload(); + + protected abstract void publishHeartbeatPayload(); + + public abstract void init(); + + public abstract void close(); +} diff --git a/core/src/main/java/net/limework/valiobungee/core/ValioBungeePlatform.java b/core/src/main/java/net/limework/valiobungee/core/ValioBungeePlatform.java new file mode 100644 index 0000000..46e7327 --- /dev/null +++ b/core/src/main/java/net/limework/valiobungee/core/ValioBungeePlatform.java @@ -0,0 +1,21 @@ +/* +* Copyright (c) 2026-present ValioBungee contributors +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the GNU GENERAL PUBLIC LICENSE Version 3 +* which accompanies this distribution, and is available at +* https://www.gnu.org/licenses/gpl-3.0.txt +*/ +package net.limework.valiobungee.core; + +public interface ValioBungeePlatform { + + int localOnlinePlayers(); + + String platformProxyVendor(); + + String networkId(); + + String proxyId(); + + ProxyManager proxyManager(); +} diff --git a/core/src/main/java/net/limework/valiobungee/core/api/impl/ImplNetworkProxy.java b/core/src/main/java/net/limework/valiobungee/core/api/impl/ImplNetworkProxy.java new file mode 100644 index 0000000..02e59cb --- /dev/null +++ b/core/src/main/java/net/limework/valiobungee/core/api/impl/ImplNetworkProxy.java @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2026-present ValioBungee contributors +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the GNU GENERAL PUBLIC LICENSE Version 3 +* which accompanies this distribution, and is available at +* https://www.gnu.org/licenses/gpl-3.0.txt +*/ +package net.limework.valiobungee.core.api.impl; + +import net.limework.valiobungee.api.NetworkProxy; +import net.limework.valiobungee.core.ValioBungeePlatform; + +public class ImplNetworkProxy implements NetworkProxy { + + private final ValioBungeePlatform platform; + + private final String proxyId; + + public ImplNetworkProxy(ValioBungeePlatform platform, String proxyId) { + this.platform = platform; + this.proxyId = proxyId; + } + + @Override + public String proxyId() { + return this.proxyId; + } + + @Override + public int onlinePlayers() { + return this.platform.proxyManager().onlinePlayersCount(this.proxyId); + } + + @Override + public boolean isMe() { + return this.platform.proxyId().equals(proxyId); + } +} diff --git a/core/src/main/java/net/limework/valiobungee/core/util/logging/LogProviderFactory.java b/core/src/main/java/net/limework/valiobungee/core/util/logging/LogProviderFactory.java new file mode 100644 index 0000000..506b28d --- /dev/null +++ b/core/src/main/java/net/limework/valiobungee/core/util/logging/LogProviderFactory.java @@ -0,0 +1,24 @@ +/* +* Copyright (c) 2026-present ValioBungee contributors +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the GNU GENERAL PUBLIC LICENSE Version 3 +* which accompanies this distribution, and is available at +* https://www.gnu.org/licenses/gpl-3.0.txt +*/ +package net.limework.valiobungee.core.util.logging; + +import org.slf4j.Logger; + +public class LogProviderFactory { + private static Logger instance; + + public static void register(Logger logger) { + if (instance != null) throw new IllegalStateException("Logger already registered"); + instance = logger; + } + + public static Logger get() { + if (instance == null) throw new IllegalStateException("No logger registered"); + return instance; + } +} diff --git a/core/src/main/proto/message-protocol.proto b/core/src/main/proto/message-protocol.proto new file mode 100644 index 0000000..291f76f --- /dev/null +++ b/core/src/main/proto/message-protocol.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package net.limework.valiobungee.core.proto.messages; +option java_multiple_files = true; + +message ProxyInfo { + string proxy_id = 1; // UNIQUE ID + string proxy_manager_id = 2; // ProxyManager id. changes every reboot, used to detect duplicate proxy names +} + +// heartbeat ._.? + +message Heartbeat { + ProxyInfo sender = 1; + int32 online_players_count = 2; +} + +// death +enum DeathReason { + UNSPECIFIED = 0; // TESTING ONLY + RELOAD = 1; // plugin reload + SHUTDOWN = 2; // proxy shutdown +} +message Death { + ProxyInfo sender = 1; // proxy that died + DeathReason reason = 2; +} + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1419039..e32a413 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,24 @@ [versions] - +protobuf-plugin = "0.9.5" +protobuf = "3.25.8" # needed for reference to be used for protoc +slf4j = "2.0.17" [plugins] blossom = "net.kyori.blossom:2.2.0" indragit = "net.kyori.indra.git:4.0.0" shadow = "com.gradleup.shadow:9.3.1" spotless = "com.diffplug.spotless:8.2.0" +protobuf = { id = "com.google.protobuf", version.ref = "protobuf-plugin" } [libraries] +redisson = "org.redisson:redisson:4.3.0" +protobuf = { group = "com.google.protobuf", name = "protobuf-java", version.ref = "protobuf" } +protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } +caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.3" + +# logging +slf4j = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } +# testing +testing-juipter = "org.junit.jupiter:junit-jupiter:6.0.3" +testing-slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } +