diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c65bcf4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,115 @@
+# User-specific stuff
+.idea/
+
+*.iml
+*.ipr
+*.iws
+
+# IntelliJ
+out/
+
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+target/
+
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+.flattened-pom.xml
+
+# Common working directory
+run/
+
+.junie
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..7c6b218
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,3 @@
+wrapperVersion=3.3.4
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
diff --git a/api/pom.xml b/api/pom.xml
new file mode 100644
index 0000000..df3ad23
--- /dev/null
+++ b/api/pom.xml
@@ -0,0 +1,124 @@
+
+
+ 4.0.0
+
+ org.btuk
+ Proxy
+ 1.13.0-SNAPSHOT
+
+
+ org.btuk.proxy
+ api
+ Proxy API
+
+
+ UTF-8
+ 7.4.0
+ 3.6.0
+ 3.1.0
+ 3.1.5
+ 4.0.2
+
+
+
+
+ org.btuk.proxy
+ database
+
+
+
+ org.btuk.proxy
+ core
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+ ${jakarta-ws-rs-api.version}
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+ 2.1.1
+ provided
+
+
+
+
+ org.glassfish.jersey.containers
+ jersey-container-grizzly2-http
+ ${jersey.version}
+
+
+ org.glassfish.jersey.inject
+ jersey-hk2
+ ${jersey.version}
+
+
+ org.glassfish.jersey.media
+ jersey-media-json-jackson
+ ${jersey.version}
+
+
+
+
+
+
+ org.openapitools
+ openapi-generator-maven-plugin
+ ${openapi-generator.version}
+
+
+
+ generate
+
+
+ ${project.basedir}/src/main/resources/openapi.yaml
+ jaxrs-spec
+ org.btuk.proxy.api
+ org.btuk.proxy.api.model
+
+ true
+ true
+ true
+ true
+ false
+ false
+ false
+ false
+ false
+
+ false
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ ${build-helper-maven-plugin.version}
+
+
+ add-source
+ generate-sources
+
+ add-source
+
+
+
+ ${project.build.directory}/generated-sources/openapi/src/gen/java
+
+
+
+
+
+
+
+
diff --git a/api/src/main/java/org/btuk/proxy/api/impl/BuildingsApiImpl.java b/api/src/main/java/org/btuk/proxy/api/impl/BuildingsApiImpl.java
new file mode 100644
index 0000000..362f123
--- /dev/null
+++ b/api/src/main/java/org/btuk/proxy/api/impl/BuildingsApiImpl.java
@@ -0,0 +1,106 @@
+package org.btuk.proxy.api.impl;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Response;
+import org.btuk.proxy.api.BuildingsApi;
+import org.btuk.proxy.api.model.Building;
+import org.btuk.proxy.api.model.BuildingCount;
+import org.btuk.proxy.api.model.BuildingGridResponse;
+import org.btuk.proxy.api.model.GridCell;
+import org.btuk.proxy.database.dto.BuildingDTO;
+import org.btuk.proxy.database.dto.GridCellDTO;
+import org.btuk.proxy.database.sql.GlobalSQL;
+
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Path("/buildings")
+public class BuildingsApiImpl implements BuildingsApi {
+
+ private final GlobalSQL globalSQL;
+
+ @Inject
+ public BuildingsApiImpl(GlobalSQL globalSQL) {
+ this.globalSQL = globalSQL;
+ }
+
+ @Override
+ public Response getBuildingsByArea(Double minLat, Double maxLat, Double minLon, Double maxLon, UUID playerUuid) {
+ String playerUuidStr = playerUuid != null ? playerUuid.toString() : null;
+ List buildingDTOs = globalSQL.getBuildingsByArea(minLat, maxLat, minLon, maxLon, playerUuidStr);
+
+ List buildings = buildingDTOs.stream()
+ .map(this::mapBuilding)
+ .collect(Collectors.toList());
+ return Response.ok(buildings).build();
+ }
+
+ @Override
+ public Response getBuildingCount(List playerUuid, Double minLat, Double maxLat, Double minLon, Double maxLon, Boolean isPublic, Boolean playerBuilt) {
+ List playerUuidStrs = null;
+ if (playerUuid != null && !playerUuid.isEmpty()) {
+ playerUuidStrs = playerUuid.stream()
+ .map(UUID::toString)
+ .collect(Collectors.toList());
+ }
+
+ int count = globalSQL.getBuildingCount(playerUuidStrs, minLat, maxLat, minLon, maxLon, isPublic, playerBuilt);
+
+ BuildingCount buildingCount = new BuildingCount();
+ buildingCount.setCount(count);
+
+ return Response.ok(buildingCount).build();
+ }
+
+ @Override
+ public Response getBuildingGridCount(Double minLat, Double maxLat, Double minLon, Double maxLon, Double stepLat, Double stepLon, UUID playerUuid) {
+ if (stepLat == null || stepLat <= 0 || stepLon == null || stepLon <= 0) {
+ return Response.status(Response.Status.BAD_REQUEST).entity("stepLat and stepLon must be greater than 0").build();
+ }
+
+ String playerUuidStr = (playerUuid != null) ? playerUuid.toString() : null;
+
+ List dbGridCounts = globalSQL.getBuildingGridCounts(minLat, maxLat, minLon, maxLon, stepLat, stepLon, playerUuidStr);
+
+ List cells = new ArrayList<>();
+
+ for (GridCellDTO dbCell : dbGridCounts) {
+ GridCell cell = new GridCell();
+ cell.setLat(dbCell.lat());
+ cell.setLon(dbCell.lon());
+ cell.setMinLat(dbCell.minLat());
+ cell.setMaxLat(dbCell.maxLat());
+ cell.setMinLon(dbCell.minLon());
+ cell.setMaxLon(dbCell.maxLon());
+ cell.setRow(dbCell.row());
+ cell.setCol(dbCell.col());
+ cell.setCount(dbCell.count());
+ cells.add(cell);
+ }
+
+ BuildingGridResponse response = new BuildingGridResponse();
+ response.setCells(cells);
+
+ return Response.ok(response).build();
+ }
+
+ private Building mapBuilding(BuildingDTO dto) {
+ Building building = new Building();
+ building.setBuildingId(dto.buildingId());
+ building.setPlayerName(dto.playerName());
+ building.setPlayerId(dto.playerId() != null ? UUID.fromString(dto.playerId()) : null);
+ building.setIsPublic(dto.isPublic());
+ building.setPlayerBuilt(dto.playerBuilt());
+ if (dto.timeAdded() != null) {
+ building.setTimeAdded(Date.from(dto.timeAdded().toInstant(ZoneOffset.UTC)));
+ }
+ building.setLat(dto.lat());
+ building.setLon(dto.lon());
+ return building;
+ }
+}
diff --git a/api/src/main/java/org/btuk/proxy/api/impl/PlayerApiImpl.java b/api/src/main/java/org/btuk/proxy/api/impl/PlayerApiImpl.java
new file mode 100644
index 0000000..dcf0e2e
--- /dev/null
+++ b/api/src/main/java/org/btuk/proxy/api/impl/PlayerApiImpl.java
@@ -0,0 +1,81 @@
+package org.btuk.proxy.api.impl;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Response;
+import org.btuk.network.lib.dto.DirectMessage;
+import org.btuk.network.lib.utils.ChatUtils;
+import org.btuk.proxy.api.PlayerApi;
+import org.btuk.proxy.api.model.Message;
+import org.btuk.proxy.api.model.Player;
+import org.btuk.proxy.core.chat.ChatManager;
+import org.btuk.proxy.database.dto.PlayerDTO;
+import org.btuk.proxy.database.sql.GlobalSQL;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.btuk.network.lib.enums.ChatChannels.GLOBAL;
+import static org.btuk.proxy.core.utils.Constants.SERVER_SENDER;
+@Path("/player")
+public class PlayerApiImpl implements PlayerApi {
+
+ private final GlobalSQL globalSQL;
+ private final ChatManager chatManager;
+
+ @Inject
+ public PlayerApiImpl(GlobalSQL globalSQL, ChatManager chatManager) {
+ this.globalSQL = globalSQL;this.chatManager = chatManager;
+ }
+
+ @Override
+ public Response getOnlinePlayers() {
+ List onlinePlayers = globalSQL.getOnlinePlayers();
+ List players = onlinePlayers.stream()
+ .map(dto -> {
+ Player p = new Player();
+ p.setUuid(UUID.fromString(dto.uuid()));
+ p.setName(dto.name());
+ return p;
+ })
+ .collect(Collectors.toList());
+ return Response.ok(players).build();
+ }
+
+ @Override
+ public Response getPlayerUuid(String name) {
+ String uuid = globalSQL.getPlayerUuidByName(name);
+ if (uuid == null) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+ return Response.ok(UUID.fromString(uuid)).build();
+ }
+
+ @Override
+ public Response getPlayerUsername(String uuid) {
+ String username = globalSQL.getPlayerUsernameByUuid(uuid);
+ if (uuid == null) {
+ return Response.status(Response.Status.NOT_FOUND).build();
+ }
+ return Response.ok(username).build();
+ }
+
+ @Override
+ public Response sendPlayerMessage(String playerID, Message message){
+
+ try {
+ String messagePlainText = message.getMessage();
+ DirectMessage m = new DirectMessage(GLOBAL.getChannelName(), playerID, SERVER_SENDER, ChatUtils.success(messagePlainText), true);
+ chatManager.sendDirectMessage(m);
+ return Response.status(Response.Status.CREATED).build();
+ }catch (Exception e)
+ {
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity("Failed to deliver message to player.")
+ .build();
+ }
+
+
+ }
+}
diff --git a/api/src/main/java/org/btuk/proxy/api/impl/StatusApiImpl.java b/api/src/main/java/org/btuk/proxy/api/impl/StatusApiImpl.java
new file mode 100644
index 0000000..6326ba6
--- /dev/null
+++ b/api/src/main/java/org/btuk/proxy/api/impl/StatusApiImpl.java
@@ -0,0 +1,17 @@
+package org.btuk.proxy.api.impl;
+
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Response;
+import org.btuk.proxy.api.StatusApi;
+import org.btuk.proxy.api.model.Status;
+
+@Path("/status")
+public class StatusApiImpl implements StatusApi {
+
+ @Override
+ public Response getStatus() {
+ Status status = new Status();
+ status.setStatus("UP");
+ return Response.ok(status).build();
+ }
+}
diff --git a/api/src/main/java/org/btuk/proxy/api/server/ProxyApi.java b/api/src/main/java/org/btuk/proxy/api/server/ProxyApi.java
new file mode 100644
index 0000000..5723b78
--- /dev/null
+++ b/api/src/main/java/org/btuk/proxy/api/server/ProxyApi.java
@@ -0,0 +1,90 @@
+package org.btuk.proxy.api.server;
+
+import lombok.extern.java.Log;
+import org.btuk.proxy.api.impl.BuildingsApiImpl;
+import org.btuk.proxy.api.impl.PlayerApiImpl;
+import org.btuk.proxy.api.impl.StatusApiImpl;
+import org.btuk.proxy.core.chat.ChatManager;
+import org.btuk.proxy.database.sql.GlobalSQL;
+
+import org.glassfish.grizzly.http.server.HttpServer;
+import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
+import org.glassfish.jersey.internal.inject.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+
+import java.net.URI;
+
+@Log
+public class ProxyApi {
+
+ private HttpServer server;
+ private final boolean enabled;
+ private final int port;
+ private final GlobalSQL globalSQL;
+ private final ChatManager chatManager;
+
+
+ public ProxyApi(boolean enabled, int port, GlobalSQL globalSQL, ChatManager chatManager) {
+ this.enabled = enabled;
+ this.port = port;
+ this.globalSQL = globalSQL;
+ this.chatManager = chatManager;
+ }
+
+ public void start() {
+ if (!enabled) {
+ return;
+ }
+
+ int apiPort = port;
+ if (apiPort == 0) {
+ log.warning("API port is not set or 0, defaulting to 8080");
+ apiPort = 8080;
+ }
+
+ String baseUri = "http://0.0.0.0:" + apiPort + "/api/";
+
+
+ ResourceConfig rc = new ResourceConfig();
+
+ // 1. Disable WADL warning
+ rc.property("jersey.config.server.wadl.disableWadl", true);
+
+ // 2. Bind SQL and ChatManager dependencies for injection
+ rc.register(new AbstractBinder() {
+ @Override
+ protected void configure() {
+ bind(globalSQL).to(GlobalSQL.class);
+ bind(chatManager).to(ChatManager.class);
+ }
+ });
+
+ // 3. Register the implementation CLASSES (or scan the package)
+ rc.register(StatusApiImpl.class);
+ rc.register(PlayerApiImpl.class);
+ rc.register(BuildingsApiImpl.class);
+
+// ResourceConfig rc = new ResourceConfig()
+// .property("jersey.config.server.wadl.disableWadl", true)
+// .packages("org.btuk.proxy.api.impl");
+
+// ResourceConfig rc = new ResourceConfig()
+// .register(new StatusApiImpl())
+// .register(new PlayerApiImpl(globalSQL,chatManager))
+// .register(new BuildingsApiImpl(globalSQL));
+
+ try {
+ server = GrizzlyHttpServerFactory.createHttpServer(URI.create(baseUri), rc);
+ log.info("API server started at " + baseUri);
+ } catch (Exception e) {
+ log.severe("Failed to start API server: " + e.getMessage());
+ }
+ }
+
+ public void stop() {
+ if (server != null && server.isStarted()) {
+ server.shutdownNow();
+ log.info("API server stopped");
+ }
+ }
+}
diff --git a/api/src/main/resources/openapi.yaml b/api/src/main/resources/openapi.yaml
new file mode 100644
index 0000000..3eb16fe
--- /dev/null
+++ b/api/src/main/resources/openapi.yaml
@@ -0,0 +1,370 @@
+openapi: 3.0.3
+info:
+ title: Proxy API
+ description: Lightweight API for Proxy internal communication.
+ version: 1.0.0
+servers:
+ - url: http://172.18.0.1:51101/api
+paths:
+ /status:
+ get:
+ tags:
+ - Status
+ summary: Get API status
+ operationId: getStatus
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Status'
+ /player/uuid/{name}:
+ get:
+ tags:
+ - Player
+ summary: Get player uuid by name
+ operationId: getPlayerUuid
+ parameters:
+ - name: name
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: string
+ format: uuid
+ '404':
+ description: Player not found
+ /player/username/{uuid}:
+ get:
+ tags:
+ - Player
+ summary: Get player username by uuid
+ operationId: getPlayerUsername
+ parameters:
+ - name: uuid
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: string
+ '404':
+ description: Player not found
+ /player/message/{uuid}:
+ post:
+ tags:
+ - Player
+ summary: Send a message to the player on the server
+ operationId: sendPlayerMessage
+ parameters:
+ - name: uuid
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ description: The message to send and the player to send to
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Message'
+ responses:
+ "201":
+ description: Message Added
+ /player/online:
+ get:
+ tags:
+ - Player
+ summary: Get online players
+ operationId: getOnlinePlayers
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Player'
+ /buildings/area:
+ get:
+ tags:
+ - Buildings
+ summary: Get buildings within an area with privacy filtering
+ description: Returns full building objects within a bounding box. Excludes private buildings unless they belong to the specified `playerUuid`.
+ operationId: getBuildingsByArea
+ parameters:
+ - name: minLat
+ in: query
+ required: true
+ schema:
+ type: number
+ format: double
+ - name: maxLat
+ in: query
+ required: true
+ schema:
+ type: number
+ format: double
+ - name: minLon
+ in: query
+ required: true
+ schema:
+ type: number
+ format: double
+ - name: maxLon
+ in: query
+ required: true
+ schema:
+ type: number
+ format: double
+ - name: playerUuid
+ in: query
+ required: false
+ description: Context player UUID. If provided, includes private buildings owned by this player.
+ schema:
+ type: string
+ format: uuid
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Building'
+
+ /buildings/count:
+ get:
+ tags:
+ - Buildings
+ summary: Get building counts with filters
+ description: Returns the total count of buildings matching optional area, player, visibility, and source filters.
+ operationId: getBuildingCount
+ parameters:
+ - name: playerUuid
+ in: query
+ required: false
+ description: Filter by one or multiple player UUIDs. Repeat parameter for multiple (`?playerUuid=...&playerUuid=...`).
+ style: form
+ explode: true
+ schema:
+ type: array
+ items:
+ type: string
+ format: uuid
+ - name: minLat
+ in: query
+ required: false
+ schema:
+ type: number
+ format: double
+ - name: maxLat
+ in: query
+ required: false
+ schema:
+ type: number
+ format: double
+ - name: minLon
+ in: query
+ required: false
+ schema:
+ type: number
+ format: double
+ - name: maxLon
+ in: query
+ required: false
+ schema:
+ type: number
+ format: double
+ - name: isPublic
+ in: query
+ required: false
+ description: Filter specifically for public or private buildings.
+ schema:
+ type: boolean
+ - name: playerBuilt
+ in: query
+ required: false
+ description: Filter by player-built status.
+ schema:
+ type: boolean
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BuildingCount'
+
+ /buildings/grid-count:
+ get:
+ tags:
+ - Buildings
+ summary: Get aggregated building counts mapped over a grid
+ description: Divides a bounding box into a grid of cells and returns building counts per cell. Automatically filters out private buildings unless owned by the requesting player.
+ operationId: getBuildingGridCount
+ parameters:
+ - name: minLat
+ in: query
+ required: true
+ schema:
+ type: number
+ format: double
+ - name: maxLat
+ in: query
+ required: true
+ schema:
+ type: number
+ format: double
+ - name: minLon
+ in: query
+ required: true
+ schema:
+ type: number
+ format: double
+ - name: maxLon
+ in: query
+ required: true
+ schema:
+ type: number
+ format: double
+ - name: stepLat
+ in: query
+ required: true
+ description: Number of grid divisions along latitude.
+ schema:
+ type: number
+ format: double
+ - name: stepLon
+ in: query
+ required: true
+ description: Number of grid divisions along longitude.
+ schema:
+ type: number
+ format: double
+ - name: playerUuid
+ in: query
+ required: false
+ description: Context player UUID to include their own private buildings in the cell counts.
+ schema:
+ type: string
+ format: uuid
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BuildingGridResponse'
+
+components:
+ schemas:
+ Status:
+ type: object
+ properties:
+ status:
+ type: string
+ example: UP
+ Player:
+ type: object
+ properties:
+ uuid:
+ type: string
+ format: uuid
+ name:
+ type: string
+ Message:
+ type: object
+ properties:
+ message:
+ type: string
+ Building:
+ type: object
+ properties:
+ buildingId:
+ type: integer
+ playerName:
+ type: string
+ playerId:
+ type: string
+ format: uuid
+ isPublic:
+ type: boolean
+ playerBuilt:
+ type: boolean
+ timeAdded:
+ type: string
+ format: date-time
+ lat:
+ type: number
+ format: double
+ lon:
+ type: number
+ format: double
+ BuildingCount:
+ type: object
+ properties:
+ count:
+ type: integer
+ example: 42
+
+ GridCell:
+ type: object
+ properties:
+ lat:
+ type: number
+ format: double
+ lon:
+ type: number
+ format: double
+ maxLat:
+ type: number
+ format: double
+ maxLon:
+ type: number
+ format: double
+ minLat:
+ type: number
+ format: double
+ minLon:
+ type: number
+ format: double
+ row:
+ type: integer
+ col:
+ type: integer
+ count:
+ type: integer
+ example: 12
+
+ BuildingGridResponse:
+ type: object
+ properties:
+ rows:
+ type: integer
+ example: 10
+ cols:
+ type: integer
+ example: 10
+ totalCount:
+ type: integer
+ example: 142
+ cells:
+ type: array
+ items:
+ $ref: '#/components/schemas/GridCell'
\ No newline at end of file
diff --git a/app/pom.xml b/app/pom.xml
new file mode 100644
index 0000000..db0e890
--- /dev/null
+++ b/app/pom.xml
@@ -0,0 +1,87 @@
+
+
+ 4.0.0
+
+ org.btuk
+ Proxy
+ 1.13.0-SNAPSHOT
+
+
+ org.btuk.proxy
+ app
+ App
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+ org.btuk.proxy
+ api
+
+
+ org.btuk.proxy
+ core
+
+
+ org.btuk.proxy
+ database
+
+
+
+ com.github.BuildtheUK
+ NetworkLib
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+ org.apache.logging.log4j
+ log4j-core
+ provided
+
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+ provided
+
+
+ org.slf4j
+ jul-to-slf4j
+
+
+
+ net.dv8tion
+ JDA
+
+
+
+ net.kyori
+ adventure-text-serializer-plain
+ 4.16.0
+
+
+ net.kyori
+ adventure-text-minimessage
+ 4.16.0
+
+
+
+ org.yaml
+ snakeyaml
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
\ No newline at end of file
diff --git a/core/src/main/java/org/btuk/proxy/core/ProxyController.java b/app/src/main/java/org.btuk.proxy.app/ProxyController.java
similarity index 90%
rename from core/src/main/java/org/btuk/proxy/core/ProxyController.java
rename to app/src/main/java/org.btuk.proxy.app/ProxyController.java
index 3440eb4..b7e9aab 100644
--- a/core/src/main/java/org/btuk/proxy/core/ProxyController.java
+++ b/app/src/main/java/org.btuk.proxy.app/ProxyController.java
@@ -1,28 +1,12 @@
-package org.btuk.proxy.core;
+package org.btuk.proxy.app;
import lombok.Getter;
import lombok.extern.java.Log;
-import net.bteuk.network.lib.dto.OnlineUserRemove;
-
-import org.btuk.proxy.core.chat.automod.AutoMod;
-import org.btuk.proxy.database.DatabaseInit;
-import org.btuk.proxy.database.sql.GlobalSQL;
-import org.btuk.proxy.database.sql.PlotSQL;
-import org.btuk.proxy.database.sql.RegionSQL;
-
-import org.slf4j.bridge.SLF4JBridgeHandler;
-
-import javax.sql.DataSource;
-import java.io.File;
-import java.io.IOException;
-import java.sql.SQLException;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
-
+import org.btuk.network.lib.dto.OnlineUserRemove;
+import org.btuk.proxy.api.server.ProxyApi;
import org.btuk.proxy.core.chat.ChatHandler;
import org.btuk.proxy.core.chat.ChatManager;
+import org.btuk.proxy.core.chat.automod.AutoMod;
import org.btuk.proxy.core.config.Config;
import org.btuk.proxy.core.discord.Discord;
import org.btuk.proxy.core.discord.ReviewStatus;
@@ -37,9 +21,22 @@
import org.btuk.proxy.core.utils.Analytics;
import org.btuk.proxy.core.utils.Constants;
import org.btuk.proxy.core.utils.Moderation;
+import org.btuk.proxy.database.DatabaseInit;
+import org.btuk.proxy.database.sql.GlobalSQL;
+import org.btuk.proxy.database.sql.PlotSQL;
+import org.btuk.proxy.database.sql.RegionSQL;
+import org.slf4j.bridge.SLF4JBridgeHandler;
+
+import javax.sql.DataSource;
+import java.io.File;
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
import static java.awt.Color.RED;
-import static org.btuk.proxy.core.utils.Constants.LEAVE_MESSAGE;
/**
* Controller of the core proxy functionality; can be enabled by the proxy or external plugins that want to use the proxy functions.
@@ -74,6 +71,8 @@ public class ProxyController {
private UserManager userManager;
+ private ProxyApi proxyApi;
+
private static final String PROXY_CONFIG_NAME = "proxy-config.yml";
private static final String AUTOMOD_CONFIG_NAME = "automod.yml";
@@ -99,8 +98,8 @@ public ProxyController(File dataFolder) {
this.enabled = true;
}
-
- public void start(ChatHandler chatHandler, Scheduler scheduler, CoreServerManager coreServerManager, PlayerManager playerManager, TabManager tabManager, Consumer socketInitializer) throws IOException {
+ public void start(ChatHandler chatHandler, Scheduler scheduler, CoreServerManager coreServerManager, PlayerManager playerManager, TabManager tabManager,
+ Consumer socketInitializer) throws IOException {
if (!enabled) {
log.severe("Proxy is not enabled, see previous logs for errors.");
@@ -116,9 +115,10 @@ public void start(ChatHandler chatHandler, Scheduler scheduler, CoreServerManage
Moderation moderation = new Moderation(globalSQL);
AutoMod automod = new AutoMod(coreUserManager, new Config(dataFolder, AUTOMOD_CONFIG_NAME), moderation, discord, chatHandler, tabManager);
- ChatManager chatManager = new ChatManager(chatHandler, coreUserManager, analytics, globalSQL, moderation, automod);
+ ChatManager chatManager = new ChatManager(chatHandler, coreUserManager, analytics, globalSQL, moderation, automod, discord);
- this.userManager = new UserManager(coreUserManager, chatHandler, tabManager, globalSQL, plotSQL, regionSQL, coreServerManager, scheduler, chatManager, playerManager, analytics, discord, automod);
+ this.userManager = new UserManager(coreUserManager, chatHandler, tabManager, globalSQL, plotSQL, regionSQL, coreServerManager, scheduler, chatManager, playerManager,
+ analytics, discord, automod);
ServerManager serverManager = new ServerManager(coreServerManager, scheduler, globalSQL, chatHandler, tabManager, coreUserManager, userManager);
@@ -126,15 +126,20 @@ public void start(ChatHandler chatHandler, Scheduler scheduler, CoreServerManage
new ReviewStatus(config, globalSQL, plotSQL, regionSQL, discord, scheduler);
this.discord.addJDAEventListeners(chatManager, coreUserManager, tabManager, plotSQL);
-
+ this.proxyApi = new ProxyApi(config.getBoolean("api.enabled"), config.getInt("api.port"), globalSQL, chatManager);
serverManager.initOnlineServers();
- socketInitializer.accept(new ProxySocketHandler(chatManager, discord, userManager, serverManager, tabManager));
+ socketInitializer.accept(new ProxySocketHandler(chatManager, discord, userManager, serverManager));
+
+ proxyApi.start();
started = true;
}
public void stop() {
+ if (proxyApi != null) {
+ proxyApi.stop();
+ }
if (started) {
// Show the disconnect message for all players in discord.
if (discord != null) {
@@ -143,7 +148,7 @@ public void stop() {
coreUserManager.runForEachOnline(user -> {
if (user.isOnline()) {
- discord.sendConnectEmbed(LEAVE_MESSAGE, user.getName(), user.getUuid(), user.getPlayerSkin(), RED, (reply) -> {
+ discord.sendConnectEmbed(Constants.LEAVE_MESSAGE, user.getName(), user.getUuid(), user.getPlayerSkin(), RED, (reply) -> {
users.decrementAndGet();
disconnectLatch.countDown();
});
@@ -164,7 +169,7 @@ public void stop() {
// Clear JDA listeners
if (discord.getJda() != null) {
- //Unregister listeners.
+ // Unregister listeners.
discord.getJda().getEventManager().getRegisteredListeners().forEach(listener -> discord.getJda().getEventManager().unregister(listener));
}
diff --git a/core/pom.xml b/core/pom.xml
index 785ae1f..76da412 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -6,7 +6,7 @@
org.btuk
Proxy
- 1.11.1
+ 1.13.0-SNAPSHOT
org.btuk.proxy
@@ -26,7 +26,7 @@
- com.github.BTEUK
+ com.github.BuildtheUK
NetworkLib
@@ -75,6 +75,11 @@
org.apache.commons
commons-lang3
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
@@ -82,16 +87,6 @@
org.apache.maven.plugins
maven-compiler-plugin
- ${maven-compiler-plugin.version}
-
-
-
- org.projectlombok
- lombok
- ${lombok.version}
-
-
-
diff --git a/core/src/main/java/org/btuk/proxy/core/chat/ChatHandler.java b/core/src/main/java/org/btuk/proxy/core/chat/ChatHandler.java
index 8280fdf..434d042 100644
--- a/core/src/main/java/org/btuk/proxy/core/chat/ChatHandler.java
+++ b/core/src/main/java/org/btuk/proxy/core/chat/ChatHandler.java
@@ -1,6 +1,6 @@
package org.btuk.proxy.core.chat;
-import net.bteuk.network.lib.dto.AbstractTransferObject;
+import org.btuk.network.lib.dto.AbstractTransferObject;
import org.btuk.proxy.core.exceptions.ServerNotFoundException;
diff --git a/core/src/main/java/org/btuk/proxy/core/chat/ChatManager.java b/core/src/main/java/org/btuk/proxy/core/chat/ChatManager.java
index 8e99beb..fa64c5f 100644
--- a/core/src/main/java/org/btuk/proxy/core/chat/ChatManager.java
+++ b/core/src/main/java/org/btuk/proxy/core/chat/ChatManager.java
@@ -1,26 +1,25 @@
package org.btuk.proxy.core.chat;
-import net.bteuk.network.lib.dto.ChatMessage;
-import net.bteuk.network.lib.dto.DirectMessage;
-import net.bteuk.network.lib.dto.PrivateMessage;
-import net.bteuk.network.lib.dto.ReplyMessage;
-import net.bteuk.network.lib.utils.ChatUtils;
-
-import org.btuk.proxy.core.chat.automod.AutoMod;
-import org.btuk.proxy.database.sql.GlobalSQL;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
-
-import java.util.List;
-
+import org.btuk.network.lib.dto.ChatMessage;
+import org.btuk.network.lib.dto.DirectMessage;
+import org.btuk.network.lib.dto.PrivateMessage;
+import org.btuk.network.lib.dto.ReplyMessage;
+import org.btuk.network.lib.utils.ChatUtils;
+import org.btuk.proxy.core.chat.automod.AutoMod;
+import org.btuk.proxy.core.discord.Discord;
import org.btuk.proxy.core.user.CoreUserManager;
import org.btuk.proxy.core.user.User;
import org.btuk.proxy.core.utils.Analytics;
import org.btuk.proxy.core.utils.Moderation;
import org.btuk.proxy.core.utils.Time;
+import org.btuk.proxy.database.sql.GlobalSQL;
+
+import java.util.List;
-import static net.bteuk.network.lib.enums.ChatChannels.GLOBAL;
+import static org.btuk.network.lib.enums.ChatChannels.GLOBAL;
import static org.btuk.proxy.core.utils.Constants.DISCORD_SENDER;
import static org.btuk.proxy.core.utils.Constants.SERVER_SENDER;
@@ -41,17 +40,20 @@ public class ChatManager {
private final AutoMod autoMod;
+ private final Discord discord;
+
private static final List SERVER_USERS = List.of(new String[]{SERVER_SENDER, DISCORD_SENDER});
private static final String FOCUS_ENABLED_PRESET = "%s is in focus mode, unable to send message.";
- public ChatManager(ChatHandler chatHandler, CoreUserManager userManager, Analytics analytics, GlobalSQL globalSQL, Moderation moderation, AutoMod autoMod) {
+ public ChatManager(ChatHandler chatHandler, CoreUserManager userManager, Analytics analytics, GlobalSQL globalSQL, Moderation moderation, AutoMod autoMod, Discord discord) {
this.chatHandler = chatHandler;
this.userManager = userManager;
this.analytics = analytics;
this.globalSQL = globalSQL;
this.moderation = moderation;
this.autoMod = autoMod;
+ this.discord = discord;
}
/**
@@ -70,6 +72,10 @@ public void handle(ChatMessage chatMessage) {
if (player) {
analytics.addMessage(chatMessage.getSender(), Time.getDate(Time.currentTime()));
}
+ // Send the message to discord.
+ if (player) {
+ discord.handle(chatMessage);
+ }
}
/**
diff --git a/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoMod.java b/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoMod.java
index 00c9d28..2cbd378 100644
--- a/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoMod.java
+++ b/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoMod.java
@@ -1,16 +1,10 @@
package org.btuk.proxy.core.chat.automod;
import lombok.extern.java.Log;
-import net.bteuk.network.lib.dto.DirectMessage;
-import net.bteuk.network.lib.utils.ChatUtils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
-
-import java.time.Duration;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
+import org.btuk.network.lib.dto.DirectMessage;
+import org.btuk.network.lib.utils.ChatUtils;
import org.btuk.proxy.core.chat.ChatHandler;
import org.btuk.proxy.core.config.Config;
import org.btuk.proxy.core.discord.Discord;
@@ -20,7 +14,11 @@
import org.btuk.proxy.core.utils.Moderation;
import org.btuk.proxy.core.utils.Time;
-import static net.bteuk.network.lib.enums.ChatChannels.GLOBAL;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+
+import static org.btuk.network.lib.enums.ChatChannels.GLOBAL;
import static org.btuk.proxy.core.utils.Constants.SERVER_SENDER;
/**
@@ -93,7 +91,7 @@ public boolean moderate(String sender, Component messageComponent) {
* @return true if the message should be blocked
*/
private boolean checkMessage(User user, String message) {
- Map candidateWords = AutoModRule.getCandidateWords(message);
+ List candidateWords = AutoModRule.getCandidateWords(message);
boolean blockMessage = false;
for (AutoModRule rule : autoModConfig.getRules()) {
blockMessage |= checkRule(rule, candidateWords, user, message);
@@ -123,7 +121,7 @@ private void checkUser(User user) {
}
}
- private boolean checkRule(AutoModRule rule, Map candidateWords, User user, String message) {
+ private boolean checkRule(AutoModRule rule, List candidateWords, User user, String message) {
List matches = rule.getMatches(candidateWords);
if (matches.isEmpty()) {
return false;
diff --git a/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoModRule.java b/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoModRule.java
index a69fcd5..c7af2a2 100644
--- a/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoModRule.java
+++ b/core/src/main/java/org/btuk/proxy/core/chat/automod/AutoModRule.java
@@ -4,13 +4,13 @@
import java.text.Normalizer;
import java.time.Duration;
-import java.util.LinkedHashMap;
+import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
-import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
public abstract class AutoModRule {
@@ -19,6 +19,8 @@ public abstract class AutoModRule {
private static final Pattern NON_WHITESPACE_PATTERN = Pattern.compile("\\S+");
private static final Pattern NON_ALPHANUMERIC = Pattern.compile("[^\\p{L}\\p{N}]");
+ private static final char SPACE = ' ';
+
private final Set flaggedWords;
@Getter
private final String id;
@@ -26,13 +28,29 @@ public abstract class AutoModRule {
@Getter
private final Duration duration;
+ private final int maxLength;
+
public AutoModRule(String id, List flaggedWords, Duration duration) {
+ int maxLength = 0;
this.id = id;
- this.flaggedWords = flaggedWords.stream()
- .map(AutoModRule::normalize)
- .filter(word -> !word.isBlank())
- .collect(java.util.stream.Collectors.toUnmodifiableSet());
+ java.util.Set normalizedSet = new java.util.HashSet<>();
+ for (String word : flaggedWords) {
+ String normalized = normalize(word);
+ List tokens = new ArrayList<>();
+ Matcher matcher = TOKEN_PATTERN.matcher(normalized);
+ while (matcher.find()) {
+ tokens.add(matcher.group());
+ }
+ if (tokens.isEmpty()) continue;
+
+ normalizedSet.add(String.join(" ", tokens));
+ String joined = String.join("", tokens);
+ maxLength = Math.max(maxLength, joined.length());
+ normalizedSet.add(joined);
+ }
+ this.flaggedWords = java.util.Collections.unmodifiableSet(normalizedSet);
this.duration = duration;
+ this.maxLength = maxLength;
}
public abstract boolean blockMessage();
@@ -40,58 +58,112 @@ public AutoModRule(String id, List flaggedWords, Duration duration) {
/**
* Matches words against flagged words.
*
- * @param candidateWords map of candidate words keyed by normalized value
+ * @param candidateWords list of candidate words in order
* @return list of matches
*/
- public List getMatches(Map candidateWords) {
- return candidateWords.values().stream()
- .filter(candidateWord -> flaggedWords.contains(candidateWord.normalized()))
- .flatMap(candidateWord -> candidateWord.originals().stream()
- .map(original -> new AutoModMatch(original, candidateWord.normalized())))
- .toList();
+ public List getMatches(List candidateWords) {
+ List matches = new ArrayList<>();
+ int totalCandidates = candidateWords.size();
+ for (int i = 0; i < totalCandidates; i++) {
+ StringBuilder sbNormalizedWithSpaces = new StringBuilder();
+ StringBuilder sbNormalizedNoSpaces = new StringBuilder();
+
+ for (int j = i; j < totalCandidates; j++) {
+ CandidateWord cw = candidateWords.get(j);
+ String normalized = cw.normalized();
+
+ sbNormalizedNoSpaces.append(normalized);
+ if (sbNormalizedNoSpaces.length() > maxLength) {
+ break;
+ }
+
+ if (j > i) {
+ sbNormalizedWithSpaces.append(SPACE);
+ }
+ sbNormalizedWithSpaces.append(normalized);
+
+ String phraseWithSpaces = sbNormalizedWithSpaces.toString();
+ if (flaggedWords.contains(phraseWithSpaces)) {
+ StringBuilder sbOriginal = new StringBuilder();
+ for (int k = i; k <= j; k++) {
+ if (k > i) {
+ sbOriginal.append(SPACE);
+ }
+ sbOriginal.append(candidateWords.get(k).original());
+ }
+ matches.add(new AutoModMatch(sbOriginal.toString(), phraseWithSpaces));
+ } else if (j > i) {
+ // Try joining without spaces to catch things like "b a d" if "bad" is flagged
+ String phraseNoSpaces = sbNormalizedNoSpaces.toString();
+ if (flaggedWords.contains(phraseNoSpaces)) {
+ StringBuilder sbOriginal = new StringBuilder();
+ for (int k = i; k <= j; k++) {
+ sbOriginal.append(candidateWords.get(k).original());
+ }
+ matches.add(new AutoModMatch(sbOriginal.toString(), phraseNoSpaces));
+ }
+ }
+ }
+ }
+ return matches;
}
/**
- * Gets a map of candidate words based on a message.
+ * Gets a list of candidate words based on a message.
*
* @param message the message to get candidates for
- * @return map of candidate words keyed by normalized value
+ * @return list of candidate words in order
*/
- public static LinkedHashMap getCandidateWords(String message) {
- LinkedHashMap candidates = new LinkedHashMap<>();
-
- Matcher tokenMatcher = TOKEN_PATTERN.matcher(message);
- while (tokenMatcher.find()) {
- String originalToken = tokenMatcher.group();
- String normalizedToken = normalize(originalToken);
-
- addCandidate(candidates, normalizedToken, originalToken);
- }
+ public static List getCandidateWords(String message) {
+ List candidates = new ArrayList<>();
Matcher chunkMatcher = NON_WHITESPACE_PATTERN.matcher(message);
while (chunkMatcher.find()) {
String originalChunk = chunkMatcher.group();
-
- // Plain alphanumeric words are already handled by TOKEN_PATTERN,
- // so only process chunks that contain punctuation/symbols.
- if (originalChunk.chars().allMatch(Character::isLetterOrDigit)) {
- continue;
- }
-
String normalizedChunk = NON_ALPHANUMERIC.matcher(normalize(originalChunk)).replaceAll("");
- addCandidate(candidates, normalizedChunk, originalChunk);
- }
+ if (normalizedChunk.isBlank()) continue;
- return candidates;
- }
+ // Try to see if this chunk contains multiple words
+ Matcher tokenMatcher = TOKEN_PATTERN.matcher(originalChunk);
+ List subTokens = new ArrayList<>();
+ while (tokenMatcher.find()) {
+ subTokens.add(tokenMatcher.group());
+ }
- private static void addCandidate(LinkedHashMap candidates, String normalized, String original) {
- if (normalized.isBlank() || original.isBlank()) {
- return;
+ if (subTokens.size() > 1) {
+ // If it contains multi-character tokens, it's likely multiple words (e.g., "bad-word")
+ boolean hasMultiCharToken = false;
+ for (String token : subTokens) {
+ if (token.length() > 1) {
+ hasMultiCharToken = true;
+ break;
+ }
+ }
+
+ if (hasMultiCharToken) {
+ StringBuilder joinedSubBuilder = new StringBuilder();
+ for (String sub : subTokens) {
+ String normalizedSub = normalize(sub);
+ candidates.add(new CandidateWord(normalizedSub, sub));
+ joinedSubBuilder.append(normalizedSub);
+ }
+ // Also add the combined version if it's different from simple concatenation
+ String joinedSub = joinedSubBuilder.toString();
+ if (!normalizedChunk.equals(joinedSub)) {
+ candidates.add(new CandidateWord(normalizedChunk, originalChunk));
+ }
+ } else {
+ // All single letters, likely a single word broken up (e.g., "b.a.d")
+ candidates.add(new CandidateWord(normalizedChunk, originalChunk));
+ }
+ } else {
+ // Single token or no sub-tokens (punctuation only)
+ candidates.add(new CandidateWord(normalizedChunk, originalChunk));
+ }
}
- candidates.computeIfAbsent(normalized, CandidateWord::new).addOriginal(original);
+ return candidates;
}
private static String normalize(String input) {
diff --git a/core/src/main/java/org/btuk/proxy/core/chat/automod/CandidateWord.java b/core/src/main/java/org/btuk/proxy/core/chat/automod/CandidateWord.java
index b57d434..8bb43e3 100644
--- a/core/src/main/java/org/btuk/proxy/core/chat/automod/CandidateWord.java
+++ b/core/src/main/java/org/btuk/proxy/core/chat/automod/CandidateWord.java
@@ -1,17 +1,4 @@
package org.btuk.proxy.core.chat.automod;
-import java.util.ArrayList;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-
-public record CandidateWord(String normalized, List originals) {
-
- public CandidateWord(String normalized) {
- this(normalized, new ArrayList<>());
- }
-
- public void addOriginal(String original) {
- originals.add(original);
- }
+public record CandidateWord(String normalized, String original) {
}
\ No newline at end of file
diff --git a/core/src/main/java/org/btuk/proxy/core/discord/BotChatListener.java b/core/src/main/java/org/btuk/proxy/core/discord/BotChatListener.java
index 87945af..03bbc68 100644
--- a/core/src/main/java/org/btuk/proxy/core/discord/BotChatListener.java
+++ b/core/src/main/java/org/btuk/proxy/core/discord/BotChatListener.java
@@ -1,16 +1,15 @@
package org.btuk.proxy.core.discord;
import lombok.extern.java.Log;
-import net.bteuk.network.lib.dto.DiscordLinking;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.apache.commons.lang3.StringUtils;
+import org.btuk.network.lib.dto.DiscordLinking;
+import org.btuk.proxy.core.chat.ChatHandler;
import java.util.List;
-import org.btuk.proxy.core.chat.ChatHandler;
-
@Log
public class BotChatListener extends ListenerAdapter {
diff --git a/core/src/main/java/org/btuk/proxy/core/discord/Discord.java b/core/src/main/java/org/btuk/proxy/core/discord/Discord.java
index a435367..afcda52 100644
--- a/core/src/main/java/org/btuk/proxy/core/discord/Discord.java
+++ b/core/src/main/java/org/btuk/proxy/core/discord/Discord.java
@@ -3,20 +3,15 @@
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
-import net.bteuk.network.lib.dto.ChatMessage;
-import net.bteuk.network.lib.dto.DiscordDirectMessage;
-import net.bteuk.network.lib.dto.DiscordEmbed;
-import net.bteuk.network.lib.dto.DiscordLinking;
-import net.bteuk.network.lib.dto.DiscordRole;
-
-import org.btuk.proxy.core.chat.automod.AutoModMatch;
-import org.btuk.proxy.core.user.User;
-import org.btuk.proxy.database.sql.GlobalSQL;
-import org.btuk.proxy.database.sql.PlotSQL;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
-import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.entities.Activity;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.MessageEmbed;
+import net.dv8tion.jda.api.entities.Role;
+import net.dv8tion.jda.api.entities.UserSnowflake;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.utils.ChunkingFilter;
@@ -28,6 +23,22 @@
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.apache.commons.lang3.time.DurationFormatUtils;
+import org.btuk.network.lib.dto.ChatMessage;
+import org.btuk.network.lib.dto.DiscordDirectMessage;
+import org.btuk.network.lib.dto.DiscordEmbed;
+import org.btuk.network.lib.dto.DiscordLinking;
+import org.btuk.network.lib.dto.DiscordRole;
+import org.btuk.proxy.core.chat.ChatHandler;
+import org.btuk.proxy.core.chat.ChatManager;
+import org.btuk.proxy.core.chat.automod.AutoModMatch;
+import org.btuk.proxy.core.config.Config;
+import org.btuk.proxy.core.discord.command.CommandManager;
+import org.btuk.proxy.core.scheduler.Scheduler;
+import org.btuk.proxy.core.tab.TabManager;
+import org.btuk.proxy.core.user.CoreUserManager;
+import org.btuk.proxy.core.user.User;
+import org.btuk.proxy.database.sql.GlobalSQL;
+import org.btuk.proxy.database.sql.PlotSQL;
import java.awt.Color;
import java.nio.charset.StandardCharsets;
@@ -39,14 +50,6 @@
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
-import org.btuk.proxy.core.chat.ChatHandler;
-import org.btuk.proxy.core.chat.ChatManager;
-import org.btuk.proxy.core.config.Config;
-import org.btuk.proxy.core.discord.command.CommandManager;
-import org.btuk.proxy.core.scheduler.Scheduler;
-import org.btuk.proxy.core.tab.TabManager;
-import org.btuk.proxy.core.user.CoreUserManager;
-
@Log
public class Discord {
@@ -116,7 +119,7 @@ public Discord(Config config, GlobalSQL globalSQL, ChatHandler chatHandler, Sche
this.moderatorChat = jda.getTextChannelById(moderatorChannel);
//Load all members into cache.
- chat.getGuild().loadMembers().onSuccess(members -> {
+ chat.getGuild().loadMembers().onSuccess(_ -> {
log.info("Loaded all discord members into cache");
//Enable role syncing.
@@ -132,7 +135,7 @@ public void addJDAEventListeners(ChatManager chatManager, CoreUserManager coreUs
jda.addEventListener(new DiscordChatListener(this, chatManager, chatChannelId, staffChannelId));
jda.addEventListener(new BotChatListener(chatHandler, linking));
- CommandManager commandManager = new CommandManager(coreUserManager, tabManager, globalSQL, plotSQL);
+ CommandManager commandManager = new CommandManager(coreUserManager, tabManager, globalSQL, plotSQL, config);
jda.addEventListener(commandManager);
jda.getGuilds().forEach(commandManager::registerCommands);
}
@@ -338,7 +341,7 @@ public void addRole(long userId, long role_id, boolean sync) {
if (!member.getRoles().contains(role)) {
// If successful, resync if enabled.
chat.getGuild().addRoleToMember(member, role).queue(
- (user) -> {
+ _ -> {
if (sync && hasRoles != null && giveRoles != null) {
syncRoles();
}
@@ -368,7 +371,7 @@ public void removeRole(long userId, long role_id, boolean sync) {
// If the member does not have the role, add it.
if (member.getRoles().contains(role)) {
chat.getGuild().removeRoleFromMember(member, role).queue(
- (user) -> {
+ _ -> {
if (sync && hasRoles != null && giveRoles != null) {
syncRoles();
}
diff --git a/core/src/main/java/org/btuk/proxy/core/discord/DiscordChatListener.java b/core/src/main/java/org/btuk/proxy/core/discord/DiscordChatListener.java
index 8e075e1..c3673d2 100644
--- a/core/src/main/java/org/btuk/proxy/core/discord/DiscordChatListener.java
+++ b/core/src/main/java/org/btuk/proxy/core/discord/DiscordChatListener.java
@@ -1,9 +1,6 @@
package org.btuk.proxy.core.discord;
import lombok.extern.java.Log;
-import net.bteuk.network.lib.dto.ChatMessage;
-import net.bteuk.network.lib.enums.ChatChannels;
-import net.bteuk.network.lib.utils.ChatUtils;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.kyori.adventure.text.Component;
@@ -11,7 +8,9 @@
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.apache.commons.lang3.StringUtils;
-
+import org.btuk.network.lib.dto.ChatMessage;
+import org.btuk.network.lib.enums.ChatChannels;
+import org.btuk.network.lib.utils.ChatUtils;
import org.btuk.proxy.core.chat.ChatManager;
import static org.btuk.proxy.core.utils.Constants.DISCORD_SENDER;
diff --git a/core/src/main/java/org/btuk/proxy/core/discord/command/CommandManager.java b/core/src/main/java/org/btuk/proxy/core/discord/command/CommandManager.java
index 54c1fb5..fccf6c1 100644
--- a/core/src/main/java/org/btuk/proxy/core/discord/command/CommandManager.java
+++ b/core/src/main/java/org/btuk/proxy/core/discord/command/CommandManager.java
@@ -1,8 +1,5 @@
package org.btuk.proxy.core.discord.command;
-import org.btuk.proxy.database.sql.GlobalSQL;
-import org.btuk.proxy.database.sql.PlotSQL;
-
import lombok.extern.java.Log;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.events.guild.GuildJoinEvent;
@@ -14,14 +11,16 @@
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
+import org.btuk.proxy.core.config.Config;
+import org.btuk.proxy.core.tab.TabManager;
+import org.btuk.proxy.core.user.CoreUserManager;
+import org.btuk.proxy.database.sql.GlobalSQL;
+import org.btuk.proxy.database.sql.PlotSQL;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
-import org.btuk.proxy.core.tab.TabManager;
-import org.btuk.proxy.core.user.CoreUserManager;
-
/**
* Manages all Discord commands.
* Each command is stored in a map so when a command interaction is run it can be routed accordingly.
@@ -39,11 +38,14 @@ public class CommandManager extends ListenerAdapter {
private final PlotSQL plotSQL;
- public CommandManager(CoreUserManager userManager, TabManager tabManager, GlobalSQL globalSQL, PlotSQL plotSQL) {
+ private final Config config;
+
+ public CommandManager(CoreUserManager userManager, TabManager tabManager, GlobalSQL globalSQL, PlotSQL plotSQL, Config config) {
this.userManager = userManager;
this.tabManager = tabManager;
this.globalSQL = globalSQL;
this.plotSQL = plotSQL;
+ this.config = config;
commands = new ArrayList<>();
}
@@ -97,7 +99,17 @@ public void registerCommands(Guild guild) {
//Create commands.
commands.add(new Playerlist(userManager, tabManager, "playerlist", "List all online players on the Minecraft server."));
- //commands.add(new Map("map", "Sends a link to the UK progress map.")); // The progress map is no longer available.
+ String progressMap = config.getString("progress_map");
+ if (progressMap != null && !progressMap.isBlank()) {
+ commands.add(new Map("map", "Sends a link to the UK progress map.", progressMap));
+ commands.add(new Map("progress", "Sends a link to the UK progress map.", progressMap));
+ commands.add(new Map("progressmap", "Sends a link to the UK progress map.", progressMap));
+ }
+
+ String websiteLink = config.getString("website");
+ if (websiteLink != null && !websiteLink.isBlank()) {
+ commands.add(new Website("website", "Sends a link of the website", websiteLink));
+ }
commands.add(new ClaimedPlots(globalSQL, plotSQL, "claimedplots", "List all plots that are currently claimed.", playerOption));
commands.add(new SubmittedPlots(globalSQL, plotSQL, "submittedplots", "List all plots that are currently submitted.", playerOption));
diff --git a/core/src/main/java/org/btuk/proxy/core/discord/command/Map.java b/core/src/main/java/org/btuk/proxy/core/discord/command/Map.java
index 9d23928..7c0d58a 100644
--- a/core/src/main/java/org/btuk/proxy/core/discord/command/Map.java
+++ b/core/src/main/java/org/btuk/proxy/core/discord/command/Map.java
@@ -1,29 +1,27 @@
-//package org.btuk.proxy.core.discord.command;
-//
-//import org.btuk.proxy.Proxy;
-//import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
-//import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction;
-//
-//public class Map extends AbstractCommand {
-//
-// /**
-// * Constructor, saved the name and description of the command.
-// * Also registers the command in Discord.
-// * @param name Name of the command
-// * @param description Description of the command
-// */
-// public Map(String name, String description) {
-// super(name, description);
-// }
-//
-// @Override
-// public void onCommand(SlashCommandInteractionEvent event) {
-//
-// String playerListMessage = Proxy.getInstance().getConfig().getString("progress_map");
-//
-// ReplyCallbackAction reply = event.reply(playerListMessage);
-// reply = reply.setEphemeral(true);
-// reply.queue();
-//
-// }
-//}
\ No newline at end of file
+package org.btuk.proxy.core.discord.command;
+
+import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
+import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction;
+
+public class Map extends AbstractCommand {
+
+ private final String link;
+
+ /**
+ * Constructor, saved the name and description of the command.
+ * Also registers the command in Discord.
+ * @param name Name of the command
+ * @param description Description of the command
+ */
+ public Map(String name, String description, String link) {
+ super(name, description);
+ this.link = link;
+ }
+
+ @Override
+ public void onCommand(SlashCommandInteractionEvent event) {
+ ReplyCallbackAction reply = event.reply(link);
+ reply = reply.setEphemeral(true);
+ reply.queue();
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/btuk/proxy/core/discord/command/Playerlist.java b/core/src/main/java/org/btuk/proxy/core/discord/command/Playerlist.java
index e1eef47..2004a8f 100644
--- a/core/src/main/java/org/btuk/proxy/core/discord/command/Playerlist.java
+++ b/core/src/main/java/org/btuk/proxy/core/discord/command/Playerlist.java
@@ -1,20 +1,19 @@
package org.btuk.proxy.core.discord.command;
-import net.bteuk.network.lib.dto.OnlineUser;
-import net.bteuk.network.lib.dto.TabPlayer;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import org.btuk.network.lib.dto.OnlineUser;
+import org.btuk.network.lib.dto.TabPlayer;
+import org.btuk.proxy.core.tab.TabManager;
+import org.btuk.proxy.core.user.CoreUserManager;
+import org.btuk.proxy.core.user.User;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Optional;
import java.util.Set;
-import org.btuk.proxy.core.tab.TabManager;
-import org.btuk.proxy.core.user.CoreUserManager;
-import org.btuk.proxy.core.user.User;
-
public class Playerlist extends AbstractCommand {
private final CoreUserManager userManager;
diff --git a/core/src/main/java/org/btuk/proxy/core/discord/command/Website.java b/core/src/main/java/org/btuk/proxy/core/discord/command/Website.java
new file mode 100644
index 0000000..098552f
--- /dev/null
+++ b/core/src/main/java/org/btuk/proxy/core/discord/command/Website.java
@@ -0,0 +1,27 @@
+package org.btuk.proxy.core.discord.command;
+
+import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
+import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction;
+
+public class Website extends AbstractCommand {
+
+ private final String link;
+
+ /**
+ * Constructor, saved the name and description of the command.
+ * Also registers the command in Discord.
+ * @param name Name of the command
+ * @param description Description of the command
+ */
+ public Website(String name, String description, String link) {
+ super(name, description);
+ this.link = link;
+ }
+
+ @Override
+ public void onCommand(SlashCommandInteractionEvent event) {
+ ReplyCallbackAction reply = event.reply(link);
+ reply = reply.setEphemeral(true);
+ reply.queue();
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/btuk/proxy/core/server/ServerManager.java b/core/src/main/java/org/btuk/proxy/core/server/ServerManager.java
index e072d55..00986e6 100644
--- a/core/src/main/java/org/btuk/proxy/core/server/ServerManager.java
+++ b/core/src/main/java/org/btuk/proxy/core/server/ServerManager.java
@@ -1,18 +1,11 @@
package org.btuk.proxy.core.server;
import lombok.extern.java.Log;
-import net.bteuk.network.lib.dto.OnlineUserRemove;
-import net.bteuk.network.lib.dto.OnlineUsersReply;
-import net.bteuk.network.lib.dto.ServerShutdown;
-import net.bteuk.network.lib.dto.ServerStartup;
-import org.btuk.proxy.database.sql.GlobalSQL;
-
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
+import org.btuk.network.lib.dto.OnlineUserRemove;
+import org.btuk.network.lib.dto.OnlineUsersReply;
+import org.btuk.network.lib.dto.ServerShutdown;
+import org.btuk.network.lib.dto.ServerStartup;
import org.btuk.proxy.core.chat.ChatHandler;
import org.btuk.proxy.core.scheduler.Scheduler;
import org.btuk.proxy.core.tab.TabManager;
@@ -20,12 +13,18 @@
import org.btuk.proxy.core.user.User;
import org.btuk.proxy.core.user.UserManager;
import org.btuk.proxy.core.utils.Time;
+import org.btuk.proxy.database.sql.GlobalSQL;
+
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
@Log
public class ServerManager {
private final CoreServerManager coreServerManager;
-
+ private final Scheduler scheduler;
private final GlobalSQL globalSQL;
private final ChatHandler chatHandler;
@@ -40,6 +39,7 @@ public class ServerManager {
public ServerManager(CoreServerManager coreServerManager, Scheduler scheduler, GlobalSQL globalSQL, ChatHandler chatHandler, TabManager tabManager, CoreUserManager coreUserManager, UserManager userManager) {
this.coreServerManager = coreServerManager;
+ this.scheduler = scheduler;
this.globalSQL = globalSQL;
this.chatHandler = chatHandler;
this.tabManager = tabManager;
@@ -58,30 +58,30 @@ public void initOnlineServers() {
}
public void addServer(ServerStartup serverStartup) {
- // It is possible the server is already set to online, this probably means it crashed,
- // first clear all players that are 'connected' to this server and remove them.
- Optional optionalServer = coreServerManager.getServers().stream().filter(server -> server.getName().equals(serverStartup.getServerName())).findFirst();
- optionalServer.ifPresent(this::removeServerDueToTimeout);
+ String serverName = serverStartup.getServerName();
+ // It is possible the server was already set to online, this probably means it crashed.
+ // Even if the server is not currently tracked, there might be 'ghost' players.
+ cleanupServer(serverName);
try {
- Server server = coreServerManager.createServer(serverStartup.getServerName());
+ Server server = coreServerManager.createServer(serverName);
threadExecutor.submit(() -> addServerIfOnline(server));
} catch (RuntimeException e) {
- log.warning("Unable to add server " + serverStartup.getServerName() + ", it can not be found.");
+ log.warning("Unable to add server " + serverName + ", it can not be found.");
}
}
public void removeServer(ServerShutdown serverShutdown) {
- Optional optionalServer = coreServerManager.getServers().stream().filter(server -> server.getName().equals(serverShutdown.getServerName())).findFirst();
- optionalServer.ifPresent(coreServerManager::removeServer);
-
- // Set the server offline in the database.
- globalSQL.update("UPDATE server_data SET online=0 WHERE name='" + serverShutdown.getServerName() + "';");
+ cleanupServer(serverShutdown.getServerName());
}
private void addServerIfOnline(Server server) {
+ addServerIfOnline(server, 0);
+ }
+
+ private void addServerIfOnline(Server server, int attempt) {
// Skip if the server is already added.
- if (coreServerManager.getServers().stream().anyMatch(server::equals)) {
+ if (coreServerManager.getServers().stream().anyMatch(s -> s.getName().equals(server.getName()))) {
return;
}
if (server.canPing()) {
@@ -95,34 +95,48 @@ private void addServerIfOnline(Server server) {
tabManager.sendAddTeam();
} else {
// The server is not online.
- log.warning(String.format("Server " + server.getName() + " is not online."));
+ if (attempt < 30) {
+ scheduler.createDelayedTask(() -> threadExecutor.submit(() -> addServerIfOnline(server, attempt + 1)), 1, TimeUnit.SECONDS);
+ } else {
+ log.warning(String.format("Server " + server.getName() + " is not online."));
+ }
}
}
private void pingServers() {
coreServerManager.getServers().forEach(server -> threadExecutor.submit(() -> updatePing(server)));
- // If any server has a ping of more than 120 seconds, set the server to offline and remove all online players that were connected to the server.
+ // If any server has a ping of more than 60 seconds, set the server to offline and remove all online players that were connected to the server.
// This probably means the server crashed.
- List offlineServers = coreServerManager.getServers().stream().filter(server -> server.getLastPing() < Time.currentTime() - 1000 * 120).toList();
+ List offlineServers = coreServerManager.getServers().stream().filter(server -> server.getLastPing() < Time.currentTime() - 1000 * 60).toList();
offlineServers.forEach(this::removeServerDueToTimeout);
}
- private void removeServerDueToTimeout(Server server) {
+ private void cleanupServer(String serverName) {
// Set the server offline in the database.
- globalSQL.update("UPDATE server_data SET online=0 WHERE name='" + server.getName() + "';");
+ globalSQL.update("UPDATE server_data SET online=0 WHERE name='" + serverName + "';");
- // Remove all users connected to this server,
+ // Remove all users connected to this server
// and also send a message to all other online servers to remove these users from their list.
- List offlineServerUsers = coreUserManager.getUsersOnServer(server.getName());
- offlineServerUsers.forEach(user -> {
- OnlineUserRemove onlineUserRemove = new OnlineUserRemove(user.getUuid());
- chatHandler.handle(onlineUserRemove);
- userManager.disconnectUser(user);
- }
- );
+ // Delay this by a second so the switch server events have time to be processed.
+ scheduler.createDelayedTask(() -> {
+ List offlineServerUsers = coreUserManager.getUsersOnServer(serverName);
+ offlineServerUsers.forEach(user -> {
+ if (user.getSwitchServer() != null && user.getSwitchServer().getFromServer().equals(serverName)) {
+ log.info("User " + user.getName() + " is switching servers on shutdown.");
+ } else {
+ OnlineUserRemove onlineUserRemove = new OnlineUserRemove(user.getUuid());
+ chatHandler.handle(onlineUserRemove);
+ userManager.disconnectUser(user);
+ }
+ });
+ }, 1, TimeUnit.SECONDS);
+
+ // Remove server from the list if it exists.
+ coreServerManager.getServer(serverName).ifPresent(coreServerManager::removeServer);
+ }
- // Remove server from the list.
- coreServerManager.removeServer(server);
+ private void removeServerDueToTimeout(Server server) {
+ cleanupServer(server.getName());
}
private void updatePing(Server server) {
diff --git a/core/src/main/java/org/btuk/proxy/core/socket/ProxySocketHandler.java b/core/src/main/java/org/btuk/proxy/core/socket/ProxySocketHandler.java
index c59fe66..0324333 100644
--- a/core/src/main/java/org/btuk/proxy/core/socket/ProxySocketHandler.java
+++ b/core/src/main/java/org/btuk/proxy/core/socket/ProxySocketHandler.java
@@ -1,13 +1,30 @@
package org.btuk.proxy.core.socket;
import lombok.extern.java.Log;
-import net.bteuk.network.lib.dto.*;
-import net.bteuk.network.lib.socket.SocketHandler;
-
+import org.btuk.network.lib.dto.AbstractTransferObject;
+import org.btuk.network.lib.dto.ChatMessage;
+import org.btuk.network.lib.dto.DirectMessage;
+import org.btuk.network.lib.dto.DiscordDirectMessage;
+import org.btuk.network.lib.dto.DiscordEmbed;
+import org.btuk.network.lib.dto.DiscordLinking;
+import org.btuk.network.lib.dto.DiscordRole;
+import org.btuk.network.lib.dto.FocusEvent;
+import org.btuk.network.lib.dto.ModerationEvent;
+import org.btuk.network.lib.dto.MuteEvent;
+import org.btuk.network.lib.dto.PlotMessage;
+import org.btuk.network.lib.dto.PrivateMessage;
+import org.btuk.network.lib.dto.ReplyMessage;
+import org.btuk.network.lib.dto.ServerShutdown;
+import org.btuk.network.lib.dto.ServerStartup;
+import org.btuk.network.lib.dto.SwitchServerEvent;
+import org.btuk.network.lib.dto.TeleportEvent;
+import org.btuk.network.lib.dto.UserConnectRequest;
+import org.btuk.network.lib.dto.UserDisconnect;
+import org.btuk.network.lib.dto.UserUpdate;
+import org.btuk.network.lib.socket.SocketHandler;
import org.btuk.proxy.core.chat.ChatManager;
import org.btuk.proxy.core.discord.Discord;
import org.btuk.proxy.core.server.ServerManager;
-import org.btuk.proxy.core.tab.TabManager;
import org.btuk.proxy.core.user.UserManager;
@Log
@@ -17,14 +34,12 @@ public class ProxySocketHandler implements SocketHandler {
private final Discord discord;
private final UserManager userManager;
private final ServerManager serverManager;
- private final TabManager tabManager;
- public ProxySocketHandler(ChatManager chatManager, Discord discord, UserManager userManager, ServerManager serverManager, TabManager tabManager) {
+ public ProxySocketHandler(ChatManager chatManager, Discord discord, UserManager userManager, ServerManager serverManager) {
this.chatManager = chatManager;
this.discord = discord;
this.userManager = userManager;
this.serverManager = serverManager;
- this.tabManager = tabManager;
}
@Override
@@ -33,7 +48,6 @@ public synchronized AbstractTransferObject handle(AbstractTransferObject abstrac
switch (abstractTransferObject) {
case ChatMessage chatMessage -> {
chatManager.handle(chatMessage);
- discord.handle(chatMessage);
}
case DirectMessage directMessage -> chatManager.handle(directMessage);
case PrivateMessage privateMessage -> chatManager.handle(privateMessage);
diff --git a/core/src/main/java/org/btuk/proxy/core/tab/AbstractTabManager.java b/core/src/main/java/org/btuk/proxy/core/tab/AbstractTabManager.java
index 125ff8e..d1ab730 100644
--- a/core/src/main/java/org/btuk/proxy/core/tab/AbstractTabManager.java
+++ b/core/src/main/java/org/btuk/proxy/core/tab/AbstractTabManager.java
@@ -1,14 +1,20 @@
package org.btuk.proxy.core.tab;
import lombok.Getter;
-import net.bteuk.network.lib.dto.AddTeamEvent;
-import net.bteuk.network.lib.dto.TabPlayer;
-import net.bteuk.network.lib.utils.ChatUtils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.minimessage.MiniMessage;
+import org.btuk.network.lib.dto.AddTeamEvent;
+import org.btuk.network.lib.dto.TabPlayer;
+import org.btuk.network.lib.utils.ChatUtils;
+import org.btuk.proxy.core.chat.ChatHandler;
+import org.btuk.proxy.core.config.Config;
+import org.btuk.proxy.core.player.Player;
+import org.btuk.proxy.core.scheduler.Scheduler;
+import org.btuk.proxy.core.user.CoreUserManager;
+import org.btuk.proxy.core.user.User;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
@@ -17,13 +23,6 @@
import java.util.Set;
import java.util.concurrent.TimeUnit;
-import org.btuk.proxy.core.chat.ChatHandler;
-import org.btuk.proxy.core.config.Config;
-import org.btuk.proxy.core.player.Player;
-import org.btuk.proxy.core.scheduler.Scheduler;
-import org.btuk.proxy.core.user.CoreUserManager;
-import org.btuk.proxy.core.user.User;
-
public abstract class AbstractTabManager implements TabManager {
private final Config config;
diff --git a/core/src/main/java/org/btuk/proxy/core/tab/TabManager.java b/core/src/main/java/org/btuk/proxy/core/tab/TabManager.java
index c50ef3a..b552bb6 100644
--- a/core/src/main/java/org/btuk/proxy/core/tab/TabManager.java
+++ b/core/src/main/java/org/btuk/proxy/core/tab/TabManager.java
@@ -1,11 +1,10 @@
package org.btuk.proxy.core.tab;
-import net.bteuk.network.lib.dto.TabPlayer;
+import org.btuk.network.lib.dto.TabPlayer;
+import org.btuk.proxy.core.user.User;
import java.util.Optional;
-import org.btuk.proxy.core.user.User;
-
public interface TabManager {
void updatePlayerInTablistOfPlayer(User user, User userToUpdate);
diff --git a/core/src/main/java/org/btuk/proxy/core/user/CoreUserManager.java b/core/src/main/java/org/btuk/proxy/core/user/CoreUserManager.java
index 1145525..b8b13c2 100644
--- a/core/src/main/java/org/btuk/proxy/core/user/CoreUserManager.java
+++ b/core/src/main/java/org/btuk/proxy/core/user/CoreUserManager.java
@@ -1,6 +1,6 @@
package org.btuk.proxy.core.user;
-import net.bteuk.network.lib.dto.OnlineUser;
+import org.btuk.network.lib.dto.OnlineUser;
import java.util.ArrayList;
import java.util.Collections;
diff --git a/core/src/main/java/org/btuk/proxy/core/user/User.java b/core/src/main/java/org/btuk/proxy/core/user/User.java
index e4dec96..673a388 100644
--- a/core/src/main/java/org/btuk/proxy/core/user/User.java
+++ b/core/src/main/java/org/btuk/proxy/core/user/User.java
@@ -5,30 +5,37 @@
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
-import net.bteuk.network.lib.dto.DirectMessage;
-import net.bteuk.network.lib.dto.TeleportEvent;
-import net.bteuk.network.lib.dto.UserConnectReply;
-import net.bteuk.network.lib.dto.UserConnectRequest;
-import net.bteuk.network.lib.enums.ChatChannels;
-import net.bteuk.network.lib.enums.TeleportRequestType;
-import net.bteuk.network.lib.utils.ChatUtils;
-
-import org.btuk.proxy.core.exceptions.ServerNotFoundException;
-import org.btuk.proxy.core.utils.TeleportRequest;
-
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.Style;
+import net.kyori.adventure.text.format.TextDecoration;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import org.btuk.network.lib.dto.DirectMessage;
+import org.btuk.network.lib.dto.TeleportEvent;
+import org.btuk.network.lib.dto.UserConnectReply;
+import org.btuk.network.lib.dto.UserConnectRequest;
+import org.btuk.network.lib.enums.ChatChannels;
+import org.btuk.network.lib.enums.TeleportRequestType;
+import org.btuk.network.lib.utils.ChatUtils;
+import org.btuk.proxy.core.chat.ChatHandler;
import org.btuk.proxy.core.chat.automod.AutoMod;
import org.btuk.proxy.core.chat.automod.AutoModFlag;
import org.btuk.proxy.core.chat.automod.AutoModFlagRule;
import org.btuk.proxy.core.chat.automod.AutoModMatch;
import org.btuk.proxy.core.chat.automod.AutoModRule;
+import org.btuk.proxy.core.exceptions.ServerNotFoundException;
+import org.btuk.proxy.core.player.Player;
+import org.btuk.proxy.core.scheduler.ScheduledTask;
+import org.btuk.proxy.core.scheduler.Scheduler;
+import org.btuk.proxy.core.scheduler.TaskStatus;
+import org.btuk.proxy.core.tab.TabManager;
+import org.btuk.proxy.core.utils.Analytics;
+import org.btuk.proxy.core.utils.SwitchServer;
+import org.btuk.proxy.core.utils.TeleportRequest;
+import org.btuk.proxy.core.utils.Time;
import org.btuk.proxy.database.dto.AutoModFlagDTO;
import org.btuk.proxy.database.sql.GlobalSQL;
-import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.format.NamedTextColor;
-import net.kyori.adventure.text.format.Style;
-import net.kyori.adventure.text.format.TextDecoration;
-import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import java.io.IOException;
import java.net.HttpURLConnection;
@@ -47,16 +54,6 @@
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
-import org.btuk.proxy.core.chat.ChatHandler;
-import org.btuk.proxy.core.player.Player;
-import org.btuk.proxy.core.scheduler.ScheduledTask;
-import org.btuk.proxy.core.scheduler.Scheduler;
-import org.btuk.proxy.core.scheduler.TaskStatus;
-import org.btuk.proxy.core.tab.TabManager;
-import org.btuk.proxy.core.utils.Analytics;
-import org.btuk.proxy.core.utils.SwitchServer;
-import org.btuk.proxy.core.utils.Time;
-
import static org.btuk.proxy.core.utils.Constants.SERVER_SENDER;
/**
@@ -135,10 +132,6 @@ public class User {
@Getter
private boolean focusEnabled;
- @Getter
- @Setter
- private boolean blockNextDisconnect = false;
-
@Getter
@Setter
private int previousPlotSubmissionCount = 0;
@@ -229,6 +222,8 @@ public void disconnect(Runnable runnable) {
analytics.save(this, Time.getDate(time), time);
online = false;
+ // Ensure no existing disconnect exists.
+ cancelDisconnectTask();
// Run a delayed task to remove the user.
disconnectTask = scheduler.createDelayedTask(runnable, 5L, TimeUnit.MINUTES);
}
@@ -243,6 +238,10 @@ public void reconnect() {
online = true;
// Can't be afk on reconnect.
afk = false;
+ cancelDisconnectTask();
+ }
+
+ public void cancelDisconnectTask() {
if (disconnectTask != null && disconnectTask.getStatus() == TaskStatus.SCHEDULED) {
disconnectTask.cancel();
}
@@ -253,10 +252,7 @@ public void reconnect() {
* Delete the user instance.
*/
public void delete() {
- // If the disconnectTask is running cancel.
- if (disconnectTask != null && disconnectTask.getStatus() == TaskStatus.SCHEDULED) {
- disconnectTask.cancel();
- }
+ cancelDisconnectTask();
}
public void mute(User user) {
@@ -295,7 +291,12 @@ public boolean isMuted() {
public UserConnectReply createUserConnectReply() {
// Create database object if not exists.
- if (newUser && globalSQL.createUser(uuid, name, playerSkin)) {
+ if (newUser) {
+ if (!globalSQL.createUser(uuid, name, playerSkin)) {
+ // We don't want to send a reply to the server since this could cause issues.
+ // The user won't be able to do anything, so this is not a perfect solution.
+ throw new RuntimeException("Failed to create user " + uuid + " in database.");
+ }
newUser = false;
}
@@ -550,6 +551,12 @@ public void saveAutoModFlags() {
globalSQL.saveAutoModFlags(uuid, flags);
}
+ public void updatePlayerSkin() {
+ if (playerSkin != null) {
+ globalSQL.update("UPDATE player_data SET player_skin='" + playerSkin + "' WHERE uuid='" + uuid + "';");
+ }
+ }
+
private static JsonNode getJsonNodeFromUrl(URL url) throws IOException {
StringBuilder inline = new StringBuilder();
Scanner scanner = new Scanner(url.openStream());
diff --git a/core/src/main/java/org/btuk/proxy/core/user/UserManager.java b/core/src/main/java/org/btuk/proxy/core/user/UserManager.java
index dc680fb..a2ac078 100644
--- a/core/src/main/java/org/btuk/proxy/core/user/UserManager.java
+++ b/core/src/main/java/org/btuk/proxy/core/user/UserManager.java
@@ -2,37 +2,55 @@
import lombok.Getter;
import lombok.extern.java.Log;
-import net.bteuk.network.lib.dto.*;
-import net.bteuk.network.lib.enums.ChatChannels;
-import net.bteuk.network.lib.enums.ModerationAction;
-import net.bteuk.network.lib.enums.TeleportRequestType;
-import net.bteuk.network.lib.utils.ChatUtils;
-import org.btuk.proxy.database.sql.GlobalSQL;
-import org.btuk.proxy.database.sql.PlotSQL;
-import org.btuk.proxy.database.sql.RegionSQL;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
-
-import java.awt.Color;
-import java.util.*;
-import java.util.concurrent.TimeUnit;
-
-import org.btuk.proxy.core.chat.automod.AutoMod;
-import org.btuk.proxy.core.discord.Discord;
-import org.btuk.proxy.core.tab.TabManager;
+import org.btuk.network.lib.dto.ChatMessage;
+import org.btuk.network.lib.dto.DirectMessage;
+import org.btuk.network.lib.dto.FocusEvent;
+import org.btuk.network.lib.dto.ModerationEvent;
+import org.btuk.network.lib.dto.MuteEvent;
+import org.btuk.network.lib.dto.OnlineUser;
+import org.btuk.network.lib.dto.OnlineUserAdd;
+import org.btuk.network.lib.dto.OnlineUserRemove;
+import org.btuk.network.lib.dto.PlotMessage;
+import org.btuk.network.lib.dto.SwitchServerEvent;
+import org.btuk.network.lib.dto.TeleportEvent;
+import org.btuk.network.lib.dto.UserConnectReply;
+import org.btuk.network.lib.dto.UserConnectRequest;
+import org.btuk.network.lib.dto.UserDisconnect;
+import org.btuk.network.lib.dto.UserRemove;
+import org.btuk.network.lib.dto.UserUpdate;
+import org.btuk.network.lib.enums.ChatChannels;
+import org.btuk.network.lib.enums.ModerationAction;
+import org.btuk.network.lib.enums.TeleportRequestType;
+import org.btuk.network.lib.utils.ChatUtils;
import org.btuk.proxy.core.chat.ChatHandler;
import org.btuk.proxy.core.chat.ChatManager;
+import org.btuk.proxy.core.chat.automod.AutoMod;
+import org.btuk.proxy.core.discord.Discord;
import org.btuk.proxy.core.exceptions.ErrorMessage;
import org.btuk.proxy.core.exceptions.ServerNotFoundException;
import org.btuk.proxy.core.player.PlayerManager;
import org.btuk.proxy.core.scheduler.Scheduler;
import org.btuk.proxy.core.server.CoreServerManager;
+import org.btuk.proxy.core.tab.TabManager;
import org.btuk.proxy.core.utils.Analytics;
import org.btuk.proxy.core.utils.SwitchServer;
import org.btuk.proxy.core.utils.Time;
+import org.btuk.proxy.database.sql.GlobalSQL;
+import org.btuk.proxy.database.sql.PlotSQL;
+import org.btuk.proxy.database.sql.RegionSQL;
-import static net.bteuk.network.lib.enums.ChatChannels.GLOBAL;
-import static org.btuk.proxy.core.utils.Constants.*;
+import java.awt.Color;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import static org.btuk.network.lib.enums.ChatChannels.GLOBAL;
+import static org.btuk.proxy.core.utils.Constants.JOIN_MESSAGE;
+import static org.btuk.proxy.core.utils.Constants.LEAVE_MESSAGE;
+import static org.btuk.proxy.core.utils.Constants.RECONNECT_MESSAGE;
+import static org.btuk.proxy.core.utils.Constants.SERVER_SENDER;
+import static org.btuk.proxy.core.utils.Constants.WELCOME_MESSAGE;
/**
* Class to manage the users on the network.
@@ -113,9 +131,9 @@ public void handleUserDisconnect(UserDisconnect disconnect) {
return;
}
- if (user.isBlockNextDisconnect()) {
- log.warning("User has already reconnected, cancelling disconnect.");
- user.setBlockNextDisconnect(false);
+ // If the player is not on the server they disconnected from, it implies they switched server.
+ if (!user.getServer().equals(disconnect.getServer())) {
+ log.warning("User disconnect received but they are on a different server, cancelling disconnect.");
return;
}
@@ -271,11 +289,8 @@ public User addUser(UserConnectRequest request) {
switchServer.cancelTimeout();
user.setSwitchServer(null);
+ user.cancelDisconnectTask();
} else {
- // If the user is still online, quickly cancel the disconnect event.
- if (user.isOnline()) {
- user.setBlockNextDisconnect(true);
- }
// Cancel disconnect task.
user.reconnect();
@@ -298,6 +313,9 @@ public User addUser(UserConnectRequest request) {
} else {
// Send the connect message.
joinMessage = JOIN_MESSAGE;
+
+ // Update the player skin in the database.
+ user.updatePlayerSkin();
}
}
diff --git a/core/src/main/java/org/btuk/proxy/core/utils/Moderation.java b/core/src/main/java/org/btuk/proxy/core/utils/Moderation.java
index b7860c5..f2d4174 100644
--- a/core/src/main/java/org/btuk/proxy/core/utils/Moderation.java
+++ b/core/src/main/java/org/btuk/proxy/core/utils/Moderation.java
@@ -2,6 +2,8 @@
import org.btuk.proxy.database.sql.GlobalSQL;
+import java.util.UUID;
+
public class Moderation {
private final GlobalSQL globalSQL;
diff --git a/core/src/main/java/org/btuk/proxy/core/utils/SwitchServer.java b/core/src/main/java/org/btuk/proxy/core/utils/SwitchServer.java
index 20049d3..176ba56 100644
--- a/core/src/main/java/org/btuk/proxy/core/utils/SwitchServer.java
+++ b/core/src/main/java/org/btuk/proxy/core/utils/SwitchServer.java
@@ -29,6 +29,7 @@ public class SwitchServer {
private final User user;
+ @Getter
private final String fromServer;
@Getter
diff --git a/core/src/main/resources/proxy-config.yml b/core/src/main/resources/proxy-config.yml
index d7b73fb..0350500 100644
--- a/core/src/main/resources/proxy-config.yml
+++ b/core/src/main/resources/proxy-config.yml
@@ -1,5 +1,5 @@
#Config version (do not update)
-version: "1.11.1"
+version: "1.13.0-SNAPSHOT"
#Bot token
token: "insert token here"
@@ -63,6 +63,11 @@ role_syncing:
give: [ ]
+#API Configuration
+api:
+ enabled: true
+ port: 61101
+
#Database login
host: localhost
port: 3306
@@ -81,4 +86,7 @@ tab:
footer: "\nServer Info: /help\nMore Info: /discord"
#Discord user activity playing status (your server name or IP)
-DiscordPlaying: "server.net"
\ No newline at end of file
+DiscordPlaying: "server.net"
+
+progress_map: "https://progress.buildtheuk.org"
+website: "https://btuk.org"
\ No newline at end of file
diff --git a/core/src/test/java/org/btuk/proxy/core/chat/automod/AutoModRuleTest.java b/core/src/test/java/org/btuk/proxy/core/chat/automod/AutoModRuleTest.java
new file mode 100644
index 0000000..beaa868
--- /dev/null
+++ b/core/src/test/java/org/btuk/proxy/core/chat/automod/AutoModRuleTest.java
@@ -0,0 +1,59 @@
+package org.btuk.proxy.core.chat.automod;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class AutoModRuleTest {
+
+ @Test
+ void testTooBadMatch() {
+ AutoModRule rule = new AutoModFlagRule("test", List.of("too bad"), 1, Duration.ZERO, true);
+ List candidates = AutoModRule.getCandidateWords("this is too bad");
+ List matches = rule.getMatches(candidates);
+ assertFalse(matches.isEmpty(), "Should match 'too bad'");
+ assertEquals("too bad", matches.getFirst().flaggedWord());
+ assertEquals("too bad", matches.getFirst().messageWord());
+ }
+
+ @Test
+ void testTooBadLeetspeakMatch() {
+ AutoModRule rule = new AutoModFlagRule("test", List.of("too bad"), 1, Duration.ZERO, true);
+ List candidates = AutoModRule.getCandidateWords("this is t00 b.a.d");
+ List matches = rule.getMatches(candidates);
+ assertFalse(matches.isEmpty(), "Should match 't00 b.a.d' against 'too bad'");
+ assertEquals("too bad", matches.getFirst().flaggedWord());
+ }
+
+ @Test
+ void testPunctuationInFlaggedWord() {
+ // Test case where flagged word has punctuation
+ AutoModRule rule = new AutoModFlagRule("test", List.of("bad-word"), 1, Duration.ZERO, true);
+ List candidates = AutoModRule.getCandidateWords("you are a bad-word");
+ List matches = rule.getMatches(candidates);
+
+ assertFalse(matches.isEmpty(), "Should match 'bad-word' even if flagged as 'bad-word'");
+ assertTrue(matches.stream().anyMatch(m -> m.flaggedWord().equals("badword") || m.flaggedWord().equals("bad word")));
+ }
+
+ @Test
+ void testDoubleSpaceInMessage() {
+ AutoModRule rule = new AutoModFlagRule("test", List.of("too bad"), 1, Duration.ZERO, true);
+ List candidates = AutoModRule.getCandidateWords("this is too bad");
+ List matches = rule.getMatches(candidates);
+ assertFalse(matches.isEmpty(), "Should match 'too bad' (double space) against 'too bad'");
+ }
+
+ @Test
+ void testMixedPunctuationAndLeetspeak() {
+ AutoModRule rule = new AutoModFlagRule("test", List.of("bad word"), 1, Duration.ZERO, true);
+ List candidates = AutoModRule.getCandidateWords("you are b.a.d-w.0.r.d");
+ List matches = rule.getMatches(candidates);
+ assertFalse(matches.isEmpty(), "Should match 'b.a.d-w.0.r.d' against 'bad word'");
+ }
+}
diff --git a/database/pom.xml b/database/pom.xml
index a7b7a8e..194960b 100644
--- a/database/pom.xml
+++ b/database/pom.xml
@@ -4,7 +4,7 @@
org.btuk
Proxy
- 1.11.1
+ 1.13.0-SNAPSHOT
org.btuk.proxy
@@ -13,7 +13,7 @@
- com.github.BTEUK
+ com.github.BuildtheUK
NetworkLib
@@ -36,23 +36,4 @@
2.0.0-1.21.4
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- ${maven-compiler-plugin.version}
-
-
-
- org.projectlombok
- lombok
- ${lombok.version}
-
-
-
-
-
-
\ No newline at end of file
diff --git a/database/src/main/java/org/btuk/proxy/database/DatabaseUpdates.java b/database/src/main/java/org/btuk/proxy/database/DatabaseUpdates.java
index e1c9039..1f69302 100644
--- a/database/src/main/java/org/btuk/proxy/database/DatabaseUpdates.java
+++ b/database/src/main/java/org/btuk/proxy/database/DatabaseUpdates.java
@@ -18,7 +18,7 @@
public class DatabaseUpdates {
// Version of the database that this build expects.
- private static final Version EXPECTED_VERSION = Version.of(1, 10, 0);
+ private static final Version EXPECTED_VERSION = Version.of(1, 12, 0);
private final GlobalSQL globalSQL;
@@ -78,10 +78,19 @@ private List migrationSteps() {
new MigrationStep(Version.of(1, 7, 3), this::update1_7_3),
new MigrationStep(Version.of(1, 9, 4), this::update1_9_4),
new MigrationStep(Version.of(1, 9, 5), this::update1_9_5),
- new MigrationStep(Version.of(1, 11, 0), () -> {})
+ new MigrationStep(Version.of(1, 12, 0), this::update1_12_0)
);
}
+ private void update1_12_0() {
+ // Add indexes for performance optimization.
+ globalSQL.update("CREATE INDEX idx_player_data_1 ON player_data(name);");
+ globalSQL.update("CREATE INDEX idx_buildings_1 ON buildings(player_id);");
+ globalSQL.update("CREATE INDEX idx_buildings_2 ON buildings(lat, lon);");
+ globalSQL.update("CREATE INDEX idx_messages_1 ON messages(recipient);");
+ globalSQL.update("CREATE INDEX idx_moderation_1 ON moderation(uuid, end_time, type);");
+ }
+
private void update1_9_5() {
//add the new fields isPublic, playerBuilt and timeAdded to the buildings database
globalSQL.update("ALTER TABLE buildings ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT TRUE, ADD COLUMN player_built BOOLEAN NOT NULL DEFAULT TRUE, ADD COLUMN time_added DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP;");
@@ -292,10 +301,14 @@ private MigrationStep(Version targetVersion, Runnable migration) {
private Version migrate(Version currentVersion) {
log.info("Migrating database from version " + currentVersion + " to version " + targetVersion);
- migration.run();
-
- globalSQL.update("UPDATE unique_data SET data_value='" + targetVersion + "' WHERE data_key='version';");
- return targetVersion;
+ try {
+ migration.run();
+ globalSQL.update("UPDATE unique_data SET data_value='" + targetVersion + "' WHERE data_key='version';");
+ return targetVersion;
+ } catch (Exception e) {
+ log.severe("Failed to migrate database to version " + targetVersion + ": " + e.getMessage());
+ throw e;
+ }
}
}
diff --git a/database/src/main/java/org/btuk/proxy/database/dto/BuildingDTO.java b/database/src/main/java/org/btuk/proxy/database/dto/BuildingDTO.java
new file mode 100644
index 0000000..d797948
--- /dev/null
+++ b/database/src/main/java/org/btuk/proxy/database/dto/BuildingDTO.java
@@ -0,0 +1,15 @@
+package org.btuk.proxy.database.dto;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+public record BuildingDTO(
+ int buildingId,
+ String playerId,
+ String playerName, // Added field
+ boolean isPublic,
+ boolean playerBuilt,
+ LocalDateTime timeAdded,
+ double lat,
+ double lon
+) {}
diff --git a/database/src/main/java/org/btuk/proxy/database/dto/GridCellDTO.java b/database/src/main/java/org/btuk/proxy/database/dto/GridCellDTO.java
new file mode 100644
index 0000000..1c7c4a3
--- /dev/null
+++ b/database/src/main/java/org/btuk/proxy/database/dto/GridCellDTO.java
@@ -0,0 +1,13 @@
+package org.btuk.proxy.database.dto;
+
+public record GridCellDTO(
+ double lat,
+ double lon,
+ double minLat,
+ double maxLat,
+ double minLon,
+ double maxLon,
+ int row,
+ int col,
+ int count
+) {}
diff --git a/database/src/main/java/org/btuk/proxy/database/dto/PlayerDTO.java b/database/src/main/java/org/btuk/proxy/database/dto/PlayerDTO.java
new file mode 100644
index 0000000..b63661f
--- /dev/null
+++ b/database/src/main/java/org/btuk/proxy/database/dto/PlayerDTO.java
@@ -0,0 +1,6 @@
+package org.btuk.proxy.database.dto;
+
+import java.util.UUID;
+
+public record PlayerDTO(String uuid, String name) {
+}
diff --git a/database/src/main/java/org/btuk/proxy/database/sql/GlobalSQL.java b/database/src/main/java/org/btuk/proxy/database/sql/GlobalSQL.java
index 2c23c1a..9ee7513 100644
--- a/database/src/main/java/org/btuk/proxy/database/sql/GlobalSQL.java
+++ b/database/src/main/java/org/btuk/proxy/database/sql/GlobalSQL.java
@@ -2,16 +2,20 @@
import lombok.extern.java.Log;
+import org.btuk.proxy.database.dto.AutoModFlagDTO;
+import org.btuk.proxy.database.dto.BuildingDTO;
+import org.btuk.proxy.database.dto.GridCellDTO;
+import org.btuk.proxy.database.dto.PlayerDTO;
+
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
-import org.btuk.proxy.database.dto.AutoModFlagDTO;
-
@Log
public class GlobalSQL extends AbstractSQL {
public GlobalSQL(DataSource datasource) {
@@ -19,11 +23,15 @@ public GlobalSQL(DataSource datasource) {
}
public boolean createUser(String uuid, String name, String playerSkin) {
- if (uuid == null || name == null || playerSkin == null) {
- log.warning("createUser called with null argument(s)");
+ if (uuid == null || name == null) {
+ log.severe("createUser called with null argument(s)");
return false;
}
+ if (playerSkin == null) {
+ log.warning("createUser called without a player skin.");
+ }
+
final String sql = """
INSERT INTO player_data(uuid, name, last_online, last_submit, player_skin)
VALUES(?, ?, ?, ?, ?)
@@ -201,4 +209,231 @@ INSERT INTO moderation(uuid, start_time, end_time, reason, type)
log.severe("Failed to insert moderation record for " + uuid + ": " + e.getMessage());
}
}
+
+ public String getPlayerUuidByName(String name) {
+ if (name == null) {
+ return null;
+ }
+
+ try (Connection conn = conn(); PreparedStatement statement = conn.prepareStatement("SELECT uuid FROM player_data WHERE name=?;")) {
+ statement.setString(1, name);
+ try (ResultSet results = statement.executeQuery()) {
+ if (results.next()) {
+ return results.getString(1);
+ }
+ }
+ } catch (SQLException e) {
+ log.severe("Failed to get player uuid for " + name + ": " + e.getMessage());
+ }
+ return null;
+ }
+
+ public String getPlayerUsernameByUuid(String uuid) {
+ if (uuid == null) {
+ return null;
+ }
+
+ try (Connection conn = conn(); PreparedStatement statement = conn.prepareStatement("SELECT name FROM player_data WHERE uuid=?;")) {
+ statement.setString(1, uuid);
+ try (ResultSet results = statement.executeQuery()) {
+ if (results.next()) {
+ return results.getString(1);
+ }
+ }
+ } catch (SQLException e) {
+ log.severe("Failed to get player uuid for " + uuid + ": " + e.getMessage());
+ }
+ return null;
+ }
+
+ public List getOnlinePlayers() {
+ List players = new ArrayList<>();
+ try (Connection conn = conn(); PreparedStatement statement = conn.prepareStatement("SELECT uuid, name FROM player_data WHERE uuid IN (SELECT uuid FROM online_users);");
+ ResultSet results = statement.executeQuery()) {
+ while (results.next()) {
+ players.add(new PlayerDTO(results.getString(1), results.getString(2)));
+ }
+ } catch (SQLException e) {
+ log.severe("Failed to get online players: " + e.getMessage());
+ }
+ return players;
+ }
+
+ private BuildingDTO mapBuilding(ResultSet results) throws SQLException {
+ Timestamp timestamp = results.getTimestamp("time_added");
+ return new BuildingDTO(
+ results.getInt("building_id"),
+ results.getString("player_id"),
+ results.getString("player_name"),
+ results.getBoolean("is_public"),
+ results.getBoolean("player_built"),
+ timestamp != null ? timestamp.toLocalDateTime() : null,
+ results.getDouble("lat"),
+ results.getDouble("lon")
+ );
+ }
+
+ // Dynamic flexible building count with optional spatial, player, and status filters
+ public int getBuildingCount(List playerUuids, Double minLat, Double maxLat, Double minLon, Double maxLon, Boolean isPublic, Boolean playerBuilt) {
+ StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM buildings WHERE 1=1");
+ List