diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..6d2eaaa4
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+.github/
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
index f7fa1f5e..5064e201 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,5 @@
+/gradlew text eol=lf
+
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b3c24f1e..855bb0c7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -16,11 +16,11 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3.3.0
+ - uses: actions/checkout@v4.1.4
- name: Checkout submodules
run: git submodule update --init --recursive
- name: Set up JDK 8
- uses: actions/setup-java@v3.6.0
+ uses: actions/setup-java@v4.2.1
with:
java-version: '8'
distribution: 'adopt'
@@ -29,8 +29,8 @@ jobs:
- name: Build with Gradle
run: ./gradlew dist
- name: Upload artifact
- uses: actions/upload-artifact@v3.1.1
+ uses: actions/upload-artifact@v4.3.3
with:
name: brainwine
- path: build/libs/brainwine.jar
+ path: build/dist/brainwine.jar
retention-days: 7
diff --git a/.gitignore b/.gitignore
index 3e0742f1..80177931 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
# Gradle
.gradle
build
+!**/src/**/build
# Eclipse
*.launch
@@ -11,4 +12,4 @@ build
bin
# Misc
-run
\ No newline at end of file
+run
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..7df02ee2
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,27 @@
+#? Builder
+FROM --platform=$BUILDPLATFORM debian:bookworm-slim AS builder
+RUN apt update -y && apt upgrade -y && apt autoremove -y
+RUN apt install -y git gradle
+WORKDIR /src
+COPY . /src
+RUN git submodule init && git submodule update
+RUN chmod +x gradlew && ./gradlew dist
+
+#? Runner
+FROM --platform=$BUILDPLATFORM amazoncorretto:22-alpine-jdk AS runner
+RUN apk update && apk upgrade
+VOLUME ["/data"]
+WORKDIR /data
+COPY --from=builder /src/build/dist /app
+ARG GATEWAY_PORT=5001
+ARG SERVER_PORT=5002
+ARG PORTAL_PORT=5003
+EXPOSE $GATEWAY_PORT $SERVER_PORT $PORTAL_PORT
+CMD ["sh", "-c", "java -jar /app/brainwine.jar disablegui"]
+
+LABEL org.opencontainers.image.title="Brainwine"
+LABEL org.opencontainers.image.description=" A portable private server for Deepworld."
+LABEL org.opencontainers.image.source="https://github.com/kuroppoi/brainwine"
+LABEL org.opencontainers.image.licenses="MIT"
+LABEL org.opencontainers.image.documentation="https://github.com/kuroppoi/brainwine"
+LABEL org.opencontainers.image.vendor=""
diff --git a/README.md b/README.md
index b57da347..d715894a 100644
--- a/README.md
+++ b/README.md
@@ -1,53 +1,71 @@
-# Brainwine
-[](https://github.com/kuroppoi/brainwine/actions)
-
-Brainwine is a Deepworld private server written in Java, made with user-friendliness and portability in mind.
-Due to the time it will take for this project to be complete (and my inconsistent working on it), brainwine has been prematurely open-sourced
-and is free for all to use.\
-Keep in mind, though, that this server is not finished yet. Expect to encounter bad code, bugs and missing features!\
-Brainwine is currently compatible with the following versions of Deepworld:
-- Steam: `v3.13.1`
+
Brainwine
+
+
+
+
+
+Brainwine is a Deepworld private server written in Java, designed to be portable and easy to use.\
+It's still a work in progress, so keep in mind that it's not yet feature-complete. (A to-do list can be found [here](https://github.com/kuroppoi/brainwine/projects/1).)\
+Brainwine currently supports the following versions of Deepworld:
+
+- Windows: `v3.13.1`
- iOS: `v2.11.0.1`
- MacOS: `v2.11.1`
-## Features
-A list of all planned, in-progress and finished features can be found [here.](https://github.com/kuroppoi/brainwine/projects/1)
+## Quick Local Setup
+
+- Install [Java 8](https://adoptium.net/temurin/releases/?package=jdk&version=8).
+- Download the [latest Brainwine release](https://github.com/kuroppoi/brainwine/releases/latest).
+- Start Brainwine by running the jar file.
+- In the window that appears, press "Start Server" to start the server.
+- Press "Start Deepworld" to launch the game.
+ - If you want to play on iOS, download a patching kit for it [here](https://github.com/kuroppoi/brainwine/releases/tag/patching-kits-1.0).
+- Register a new account and play the game.
+
+## Building
+
+### Prerequisites
-## Setup
+- Java 8 Development Kit
-### Setting up the client
+Run the following to build the program:
-Before you can connect to a server, a few modifications need to be made to the Deepworld game client.\
-The exact process of this differs per platform.\
-You may download an installation package for your desired platform [here.](https://github.com/kuroppoi/brainwine/releases/tag/patching-kits-1.0)
+```sh
+git clone --recurse-submodules https://github.com/kuroppoi/brainwine.git
+cd brainwine
+./gradlew dist
+```
-### Setting up the server
+The output executable jar `brainwine.jar` will be located in the `/build/dist` directory.\
+To start the server without a user interface, run the following:
-#### Prerequisites
+```sh
+# This behavior is the default on platforms that do not support Java's Desktop API.
+java -jar brainwine.jar disablegui
+```
-- Java 8 or newer
+## Docker
-You can download the latest release [here.](https://github.com/kuroppoi/brainwine/releases/latest)\
-Alternatively, if you wish to build from source, clone this repository with the `--recurse-submodules` flag\
-and run `gradlew dist` in the root directory of the repository.\
-After the build has finished, the output jar will be located in `build/libs`.\
-You may then start the server through the gui, or start it directly by running the jar with the `disablegui` flag.
+Run the following to build the image:
-#### Configurations
+```sh
+git clone https://github.com/kuroppoi/brainwine
+cd brainwine
+docker buildx build -t brainwine:latest .
+```
-On first-time startup, configuration files will be generated which you may modify however you like:
-- `api.json` Configuration file for news & API connectivity information.
-- `loottables.json` Configuration file for which loot may be obtained from containers.
-- `spawning.json` Configuration file for entity spawns per biome.
-- `generators` Folder containing configuration files for zone generators.
+To then run the image in a container, run the following:
-## Contributions
+```sh
+# Replace ${PWD} with %cd% if you're using a Windows Command Prompt.
+docker run -p 5001-5003:5001-5003 --volume ${PWD}/run:/data brainwine:latest
+```
-Disagree with how I did something? Found a potential error? See some room for improvement? Or just want to add a feature?
-Glad to hear it! Feel free to make a pull request anytime. Just make sure you follow the code style!
-And, apologies in advance for the lack of documentation. Haven't gotten around to do it yet. Sorry!
+Or alternatively, if you wish to use docker compose:
-## Issues
+```sh
+docker compose up
+```
-Found a bug? Before posting an issue, make sure your build is up-to-date and your issue has not already been posted before.
-Provide a detailed explanation of the issue, and how to reproduce it. I'll get to it ASAP!
+The server files will be stored in a docker volume and can be accessed from `/data` in the container.\
+Feel free to play around with the configuration by editing `docker-compose.yml`.
diff --git a/api/build.gradle b/api/build.gradle
index f441adea..84dcbf45 100644
--- a/api/build.gradle
+++ b/api/build.gradle
@@ -8,9 +8,5 @@ repositories {
dependencies {
implementation 'io.javalin:javalin:4.6.8'
- implementation project(':shared')
-}
-
-jar {
- archiveBaseName = 'brainwine-api'
+ implementation project(':brainwine-shared')
}
diff --git a/api/src/main/java/brainwine/api/DataFetcher.java b/api/src/main/java/brainwine/api/DataFetcher.java
index a72d6ad9..a5f2737a 100644
--- a/api/src/main/java/brainwine/api/DataFetcher.java
+++ b/api/src/main/java/brainwine/api/DataFetcher.java
@@ -9,7 +9,11 @@ public interface DataFetcher {
public boolean isPlayerNameTaken(String name);
public String registerPlayer(String name);
public String login(String name, String password);
+ public String fetchPlayerName(String name);
+ public String fetchPlayerId(String apiToken);
public boolean verifyAuthToken(String name, String token);
- public boolean verifyApiToken(String apiToken);
+ public ZoneInfo getZoneInfo(String nameOrId);
public Collection fetchZoneInfo();
+ public Collection fetchRecentZoneInfo(String apiToken);
+ public Collection fetchBookmarkedZoneInfo(String apiToken);
}
diff --git a/api/src/main/java/brainwine/api/DefaultDataFetcher.java b/api/src/main/java/brainwine/api/DefaultDataFetcher.java
index 9c6b42c3..27d224cf 100644
--- a/api/src/main/java/brainwine/api/DefaultDataFetcher.java
+++ b/api/src/main/java/brainwine/api/DefaultDataFetcher.java
@@ -23,13 +23,23 @@ public String login(String name, String password) {
throw exception;
}
+ @Override
+ public String fetchPlayerName(String name) {
+ throw exception;
+ }
+
+ @Override
+ public String fetchPlayerId(String apiToken) {
+ throw exception;
+ }
+
@Override
public boolean verifyAuthToken(String name, String token) {
throw exception;
}
-
+
@Override
- public boolean verifyApiToken(String apiToken) {
+ public ZoneInfo getZoneInfo(String nameOrId) {
throw exception;
}
@@ -37,4 +47,14 @@ public boolean verifyApiToken(String apiToken) {
public Collection fetchZoneInfo() {
throw exception;
}
+
+ @Override
+ public Collection fetchRecentZoneInfo(String apiToken) {
+ throw exception;
+ }
+
+ @Override
+ public Collection fetchBookmarkedZoneInfo(String apiToken) {
+ throw exception;
+ }
}
diff --git a/api/src/main/java/brainwine/api/GatewayService.java b/api/src/main/java/brainwine/api/GatewayService.java
index 2ff42eb7..c2927ea8 100644
--- a/api/src/main/java/brainwine/api/GatewayService.java
+++ b/api/src/main/java/brainwine/api/GatewayService.java
@@ -111,7 +111,7 @@ private void handlePlayerLogin(Context ctx) {
return;
}
- ctx.json(new ServerConnectInfo(api.getGameServerHost(), name, token));
+ ctx.json(new ServerConnectInfo(api.getGameServerHost(), dataFetcher.fetchPlayerName(name), token));
}
/**
diff --git a/api/src/main/java/brainwine/api/MapRenderer.java b/api/src/main/java/brainwine/api/MapRenderer.java
new file mode 100644
index 00000000..1115e21e
--- /dev/null
+++ b/api/src/main/java/brainwine/api/MapRenderer.java
@@ -0,0 +1,118 @@
+package brainwine.api;
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.imageio.ImageIO;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import brainwine.api.models.ZoneInfo;
+import brainwine.api.util.ImageUtils;
+
+/**
+ * Functions for rendering zone maps.
+ */
+public class MapRenderer {
+
+ private static final Logger logger = LogManager.getLogger();
+ private static final double[] depths = { 0.03, 0.05, 0.08, 0.12, 0.17, 0.26, 0.3 };
+ private static final Map colorMap = new HashMap<>();
+ private static final BufferedImage mapCrossImage;
+
+ static {
+ // Set color map data
+ colorMap.put("plain", new int[] { 0xFFFFFF, 0x5DB830, 0x417431, 0x414E1C, 0x4E441C, 0x2A240C, 0x18150B });
+ colorMap.put("arctic", new int[] { 0xFFFFFF, 0x75A49E, 0x456B74, 0x33535F, 0x2D4F5D, 0x142E3F, 0x0B1A25 });
+ colorMap.put("hell", new int[] { 0xFFFFFF, 0xBE8D6F, 0x905548, 0x7F3C32, 0x6F3932, 0x5F1814, 0x380E0D });
+ colorMap.put("brain", new int[] { 0xFFFFFF, 0xA19599, 0x705C6E, 0x5D4257, 0x4E3E55, 0x3B1C36, 0x2A0B28 });
+ colorMap.put("desert", new int[] { 0xECDE93, 0xB18E58, 0x7B5822, 0x614312, 0x4E350C, 0x322209, 0x1E1506 });
+ colorMap.put("space", new int[] { 0xFFFFFF, 0xEEEEEE, 0xDDDDDD, 0xCCCCCC, 0xBBBBBB, 0xAAAAAA, 0x999999 });
+
+ // Load image resources
+ mapCrossImage = loadImageResource("/map/crossMark.png");
+ }
+
+ private static BufferedImage loadImageResource(String name) {
+ try(InputStream inputStream = MapRenderer.class.getResourceAsStream(name)) {
+ return ImageIO.read(inputStream);
+ } catch(Exception e) {
+ logger.error("Failed to load image resource '{}'", name, e);
+ }
+
+ return null;
+ }
+
+ public static BufferedImage drawSurfaceMap(ZoneInfo zone) {
+ return drawSurfaceMap(zone.getWidth(), zone.getHeight(), zone.getBiome(), zone.getSurface());
+ }
+
+ /**
+ * Creates a surface map render that is visually almost identical to V2 client map renders.
+ */
+ public static BufferedImage drawSurfaceMap(int width, int height, String biome, int[] surfaceArray) {
+ BufferedImage image = ImageUtils.createImage(300000, width, height);
+ Graphics2D g2d = image.createGraphics();
+ int[] colors = colorMap.getOrDefault(biome.toLowerCase(), colorMap.get("plain"));
+ int[] raster = ((DataBufferInt)image.getRaster().getDataBuffer()).getData();
+ int surfaceMin = height;
+ int surfaceMax = 0;
+
+ // Find highest & lowest surface points
+ for(int surface : surfaceArray) {
+ surfaceMin = Math.min(surfaceMin, surface);
+ surfaceMax = Math.max(surfaceMax, surface);
+ }
+
+ int surfaceCenter = (int)Math.floor((surfaceMax - surfaceMin) * 0.5 + surfaceMin);
+ double scaleX = (double)image.getWidth() / width;
+ double scaleY = (double)image.getHeight() / height;
+
+ // Fill raster with background color fast
+ int length = (int)(surfaceMax * scaleY) * image.getWidth();
+ raster[0] = 0x80808080;
+
+ for(int i = 1; i < length; i += i) {
+ System.arraycopy(raster, 0, raster, i, length - i < i ? length - i : i);
+ }
+
+ // Draw zone surface
+ // TODO potentially slow (largely because of Java2D) and can probably be optimized further
+ for(int i = 0; i < image.getWidth(); i++) {
+ int surface = surfaceArray[(int)(i / scaleX)];
+ int distanceToPeak = surface - surfaceMin;
+ double y = (height - surface) * scaleY;
+ double layerHeight = 1.0;
+ int start = image.getHeight() - (int)(y * layerHeight);
+
+ for(int j = 0; j < depths.length; j++) {
+ double scale = (double)distanceToPeak / height / depths.length * (j < 2 ? 2.0 : 0.5);
+ layerHeight -= (depths[j] - scale);
+ int end = j + 1 >= depths.length ? image.getHeight() : image.getHeight() - (int)(y * layerHeight);
+ g2d.setColor(new Color(colors[j > 0 || surface < surfaceCenter ? j : 1])); // Only use snow color if surface is above surface center
+ g2d.fillRect(i, start, 1, end - start);
+ start = end;
+ }
+ }
+
+ g2d.dispose();
+ return image;
+ }
+
+ /**
+ * Draws a red cross mark on an image at the given zone coordinates.
+ */
+ public static void drawCrossMark(ZoneInfo zone, int x, int y, BufferedImage mapImage) {
+ double scaleX = (double)mapImage.getWidth() / zone.getWidth();
+ double scaleY = (double)mapImage.getHeight() / zone.getHeight();
+ Graphics2D g2d = mapImage.createGraphics();
+ g2d.drawImage(mapCrossImage, (int)(x * scaleX) - 24, (int)(y * scaleY) - 25, 50, 60, null);
+ g2d.dispose();
+ }
+}
diff --git a/api/src/main/java/brainwine/api/PortalService.java b/api/src/main/java/brainwine/api/PortalService.java
index 46d0d00c..feb015fa 100644
--- a/api/src/main/java/brainwine/api/PortalService.java
+++ b/api/src/main/java/brainwine/api/PortalService.java
@@ -4,14 +4,23 @@
import static brainwine.api.util.ContextUtils.handleQueryParam;
import static brainwine.shared.LogMarkers.SERVER_MARKER;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+
+import javax.imageio.ImageIO;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import brainwine.api.models.ZoneInfo;
+import brainwine.api.util.ImageUtils;
import brainwine.shared.JsonHelper;
import io.javalin.Javalin;
+import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.plugin.json.JavalinJackson;
@@ -22,6 +31,7 @@ public class PortalService {
private static final int zoneSearchPageSize = 6;
private static final Logger logger = LogManager.getLogger();
+ private final Map surfaceMapCache = new HashMap<>();
private final DataFetcher dataFetcher;
private final Javalin portal;
@@ -30,6 +40,7 @@ public PortalService(Api api, int port) {
logger.info(SERVER_MARKER, "Starting PortalService @ port {} ...", port);
portal = Javalin.create(config -> config.jsonMapper(new JavalinJackson(JsonHelper.MAPPER)))
.exception(Exception.class, this::handleException)
+ .get("/v1/map/{zone}", this::handleMapRequest)
.get("/v1/worlds", this::handleZoneSearch)
.start(port);
}
@@ -42,19 +53,69 @@ private void handleException(Exception exception, Context ctx) {
error(ctx, "%s", exception);
}
+ /**
+ * Handler function for map render requests.
+ * TODO add throttle & ownership privacy
+ */
+ private void handleMapRequest(Context ctx) throws IOException {
+ ZoneInfo zone = dataFetcher.getZoneInfo(ctx.pathParam("zone"));
+
+ if(zone == null) {
+ error(ctx, "Zone not found.");
+ return;
+ }
+
+ // TODO proper cache management
+ if(surfaceMapCache.size() > 50) {
+ surfaceMapCache.clear();
+ }
+
+ BufferedImage image = surfaceMapCache.computeIfAbsent(zone.getName(), x -> MapRenderer.drawSurfaceMap(zone));
+ String position = ctx.queryParam("pos");
+
+ if(position != null) {
+ String[] segments = position.split(",", 2);
+
+ if(segments.length != 2) {
+ error(ctx, "Position must be formatted as x,y");
+ return;
+ }
+
+ try {
+ int x = Integer.parseInt(segments[0]);
+ int y = Integer.parseInt(segments[1]);
+ image = ImageUtils.copyImage(image); // Copying is important: we do not want to draw to cached images!
+ MapRenderer.drawCrossMark(zone, x, y, image);
+ } catch(NumberFormatException e) {
+ error(ctx, "Coordinates must be valid numbers.");
+ return;
+ }
+ }
+
+ ctx.contentType(ContentType.IMAGE_PNG);
+ ImageIO.write(image, "png", ctx.res.getOutputStream());
+ }
+
/**
* Handler function for zone search requests.
* TODO could use some work.
*/
private void handleZoneSearch(Context ctx) {
- final List zones = (List)dataFetcher.fetchZoneInfo();
String apiToken = ctx.queryParam("api_token");
+ String playerId = apiToken != null ? dataFetcher.fetchPlayerId(apiToken) : null;
- if(apiToken == null || !dataFetcher.verifyApiToken(apiToken)) {
- error(ctx, "A valid api token is required for this request.");
+ if(playerId == null && (ctx.queryParam("account") != null || ctx.queryParam("residency") != null)) {
+ error(ctx, "Request contains one or more parameters that require a valid API token.");
return;
}
+ // TODO filtering is a bit convoluted, see if we can make it more efficient in the future.
+ String account = ctx.queryParam("account");
+ final List zones = account == null ? (List)dataFetcher.fetchZoneInfo()
+ : account.equals("recent") ? (List)dataFetcher.fetchRecentZoneInfo(apiToken)
+ : account.equals("bookmarked") ? (List)dataFetcher.fetchBookmarkedZoneInfo(apiToken) : new ArrayList<>();
+ zones.removeIf(zone -> zone.isPrivate() && (playerId == null || (!playerId.equals(zone.getOwner()) && !zone.getMembers().contains(playerId))));
+
handleQueryParam(ctx, "name", String.class, name -> {
zones.removeIf(zone -> !zone.getName().toLowerCase().contains(name.toLowerCase()));
});
@@ -71,25 +132,32 @@ private void handleZoneSearch(Context ctx) {
zones.removeIf(zone -> zone.isPvp() != pvp);
});
- handleQueryParam(ctx, "protected", boolean.class, locked -> {
- zones.removeIf(zone -> zone.isLocked() != locked);
+ handleQueryParam(ctx, "protected", boolean.class, value -> {
+ zones.removeIf(zone -> zone.isProtected() != value);
});
handleQueryParam(ctx, "residency", String.class, residency -> {
- zones.clear(); // not supported yet
- });
-
- handleQueryParam(ctx, "account", String.class, account -> {
- zones.clear(); // not supported yet
+ switch(residency) {
+ case "owned":
+ zones.removeIf(zone -> !playerId.equals(zone.getOwner()));
+ break;
+ case "member":
+ zones.removeIf(zone -> !zone.getMembers().contains(playerId));
+ break;
+ default:
+ zones.clear();
+ break;
+ }
});
handleQueryParam(ctx, "sort", String.class, sort -> {
switch(sort) {
- case "popularity":
- zones.removeIf(zone -> zone.getPlayerCount() == 0);
+ case "popularity": // Sort by most players first
+ //zones.removeIf(zone -> zone.getPlayerCount() == 0);
zones.sort((a, b) -> Integer.compare(b.getPlayerCount(), a.getPlayerCount()));
break;
- case "created":
+ case "created": // Sort by newest first
+ zones.sort((a, b) -> b.getCreationDate().compareTo(a.getCreationDate()));
break;
}
});
diff --git a/api/src/main/java/brainwine/api/models/ZoneInfo.java b/api/src/main/java/brainwine/api/models/ZoneInfo.java
index 9c6e2cb0..41982c71 100644
--- a/api/src/main/java/brainwine/api/models/ZoneInfo.java
+++ b/api/src/main/java/brainwine/api/models/ZoneInfo.java
@@ -1,9 +1,15 @@
package brainwine.api.models;
import java.time.OffsetDateTime;
+import java.util.Collections;
+import java.util.List;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
+/**
+ * TODO split model in two: one for internal use & one for {@code /v1/worlds} serialization.
+ */
public class ZoneInfo {
private final String name;
@@ -11,22 +17,34 @@ public class ZoneInfo {
private final String activity;
private final boolean pvp;
private final boolean premium;
- private final boolean locked;
+ private final boolean isPrivate;
+ private final boolean isProtected;
private final int playerCount;
+ private final int width;
+ private final int height;
+ private final int[] surface;
private final double explorationProgress;
private final OffsetDateTime creationDate;
+ private final String owner;
+ private final List members;
- public ZoneInfo(String name, String biome, String activity, boolean pvp, boolean premium, boolean locked,
- int playerCount, double explorationProgress, OffsetDateTime creationDate) {
+ public ZoneInfo(String name, String biome, String activity, boolean pvp, boolean premium, boolean isPrivate, boolean isProtected,
+ int playerCount, int width, int height, int[] surface, double explorationProgress, OffsetDateTime creationDate, String owner, List members) {
this.name = name;
this.biome = biome;
this.activity = activity;
this.pvp = pvp;
this.premium = premium;
- this.locked = locked;
+ this.isPrivate = isPrivate;
+ this.isProtected = isProtected;
this.playerCount = playerCount;
+ this.width = width;
+ this.height = height;
+ this.surface = surface;
this.explorationProgress = explorationProgress;
this.creationDate = creationDate;
+ this.owner = owner;
+ this.members = members;
}
public String getName() {
@@ -49,9 +67,12 @@ public boolean isPremium() {
return premium;
}
- @JsonProperty("protected")
- public boolean isLocked() {
- return locked;
+ public boolean isPrivate() {
+ return isPrivate;
+ }
+
+ public boolean isProtected() {
+ return !isPrivate && isProtected; // Only display protection lock if world is public
}
@JsonProperty("players")
@@ -59,6 +80,21 @@ public int getPlayerCount() {
return playerCount;
}
+ @JsonIgnore
+ public int getWidth() {
+ return width;
+ }
+
+ @JsonIgnore
+ public int getHeight() {
+ return height;
+ }
+
+ @JsonIgnore
+ public int[] getSurface() {
+ return surface;
+ }
+
@JsonProperty("explored")
public double getExplorationProgress() {
return explorationProgress;
@@ -68,4 +104,14 @@ public double getExplorationProgress() {
public OffsetDateTime getCreationDate() {
return creationDate;
}
+
+ @JsonIgnore
+ public String getOwner() {
+ return owner;
+ }
+
+ @JsonIgnore
+ public List getMembers() {
+ return Collections.unmodifiableList(members);
+ }
}
diff --git a/api/src/main/java/brainwine/api/util/ImageUtils.java b/api/src/main/java/brainwine/api/util/ImageUtils.java
new file mode 100644
index 00000000..fe18f9b5
--- /dev/null
+++ b/api/src/main/java/brainwine/api/util/ImageUtils.java
@@ -0,0 +1,33 @@
+package brainwine.api.util;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+
+public class ImageUtils {
+
+ /**
+ * Creates an image with a pixel count as close to the desired pixel count as possible
+ * while also retaining the same aspect ratio.
+ */
+ public static BufferedImage createImage(int pixelCount, double scaleX, double scaleY) {
+ double factor = Math.sqrt(scaleX * scaleY / pixelCount);
+ int width = (int)Math.round(scaleX / factor);
+ int height = (int)Math.round(scaleY / factor);
+ return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ }
+
+ /**
+ * Fast image copying function.
+ */
+ public static BufferedImage copyImage(BufferedImage image) {
+ if(image.getType() != BufferedImage.TYPE_INT_ARGB) {
+ throw new IllegalArgumentException("Image type must be TYPE_INT_ARGB");
+ }
+
+ BufferedImage copy = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
+ int[] src = ((DataBufferInt)image.getRaster().getDataBuffer()).getData();
+ int[] dst = ((DataBufferInt)copy.getRaster().getDataBuffer()).getData();
+ System.arraycopy(src, 0, dst, 0, dst.length);
+ return copy;
+ }
+}
diff --git a/api/src/main/resources/map/crossMark.png b/api/src/main/resources/map/crossMark.png
new file mode 100644
index 00000000..52f4633d
Binary files /dev/null and b/api/src/main/resources/map/crossMark.png differ
diff --git a/build-logic/build.gradle b/build-logic/build.gradle
new file mode 100644
index 00000000..513b583f
--- /dev/null
+++ b/build-logic/build.gradle
@@ -0,0 +1,28 @@
+plugins {
+ id 'java-gradle-plugin'
+}
+
+sourceSets {
+ boot {
+
+ }
+ main {
+ compileClasspath += sourceSets.boot.output
+ runtimeClasspath += sourceSets.boot.output
+ }
+}
+
+gradlePlugin {
+ plugins {
+ distributionPlugin {
+ id = "brainwine.distribution"
+ implementationClass = "brainwine.build.DistributionPlugin"
+ }
+ }
+}
+
+jar {
+ from sourceSets.boot.output
+}
+
+version = '1.0.0-SNAPSHOT'
diff --git a/build-logic/src/boot/java/brainwine/bootstrap/Bootstrap.java b/build-logic/src/boot/java/brainwine/bootstrap/Bootstrap.java
new file mode 100644
index 00000000..f621e188
--- /dev/null
+++ b/build-logic/src/boot/java/brainwine/bootstrap/Bootstrap.java
@@ -0,0 +1,68 @@
+package brainwine.bootstrap;
+
+import static brainwine.bootstrap.Constants.BOOT_CLASS_KEY;
+import static brainwine.bootstrap.Constants.JAR_LIBRARY_PATH;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Enumeration;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+public class Bootstrap {
+
+ public static void main(String[] args) {
+ new Bootstrap().run(args);
+ }
+
+ private void run(String[] args) {
+ String mainClassName = null;
+
+ try {
+ Enumeration resources = getClass().getClassLoader().getResources(JarFile.MANIFEST_NAME);
+
+ while(resources.hasMoreElements()) {
+ try(InputStream inputStream = resources.nextElement().openStream()) {
+ Manifest manifest = new Manifest(inputStream);
+
+ if(getClass().getName().equals(manifest.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS))) {
+ mainClassName = manifest.getMainAttributes().getValue(BOOT_CLASS_KEY);
+ break;
+ }
+ }
+ }
+ } catch(IOException e) {
+ System.err.println("Could not get main class name");
+ e.printStackTrace();
+ System.exit(-1);
+ }
+
+ URL[] libraryUrls = null;
+
+ try {
+ libraryUrls = DirectoryIndex.extractDirectory(JAR_LIBRARY_PATH, new File("libraries"));
+ } catch(Exception e) {
+ System.err.println("Could not extract library JARs");
+ e.printStackTrace();
+ System.exit(-1);
+ }
+
+ URLClassLoader classLoader = new URLClassLoader(libraryUrls, getClass().getClassLoader().getParent());
+ Thread.currentThread().setContextClassLoader(classLoader);
+
+ try {
+ Class> mainClass = Class.forName(mainClassName, true, classLoader);
+ Method method = mainClass.getMethod("main", String[].class);
+ method.invoke(null, (Object)args);
+ } catch(ReflectiveOperationException e) {
+ System.err.println("Could not invoke entry point");
+ e.printStackTrace();
+ System.exit(-1);
+ }
+ }
+}
diff --git a/build-logic/src/boot/java/brainwine/bootstrap/Constants.java b/build-logic/src/boot/java/brainwine/bootstrap/Constants.java
new file mode 100644
index 00000000..88d76f5c
--- /dev/null
+++ b/build-logic/src/boot/java/brainwine/bootstrap/Constants.java
@@ -0,0 +1,12 @@
+package brainwine.bootstrap;
+
+import java.util.jar.Attributes;
+
+public class Constants {
+
+ public static final Attributes.Name BOOT_CLASS_KEY = new Attributes.Name("Boot-Class");
+ public static final String DIRECTORY_INDEX_FILE = "index";
+ public static final String JAR_LICENSE_PATH = "META-INF/LICENSE";
+ public static final String JAR_LIBRARY_PATH = "META-INF/libraries";
+ public static final Class> MAIN_CLASS = Bootstrap.class;
+}
diff --git a/build-logic/src/boot/java/brainwine/bootstrap/DirectoryIndex.java b/build-logic/src/boot/java/brainwine/bootstrap/DirectoryIndex.java
new file mode 100644
index 00000000..5bb08c8b
--- /dev/null
+++ b/build-logic/src/boot/java/brainwine/bootstrap/DirectoryIndex.java
@@ -0,0 +1,114 @@
+package brainwine.bootstrap;
+
+import static brainwine.bootstrap.Constants.DIRECTORY_INDEX_FILE;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+public class DirectoryIndex {
+
+ private final Map map = new HashMap<>();
+
+ @Override
+ public int hashCode() {
+ return map.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ return map.equals(object);
+ }
+
+ public static URL[] extractDirectory(String path, File outputDirectory) throws IOException {
+ DirectoryIndex index = new DirectoryIndex();
+ outputDirectory.mkdirs();
+
+ // Read index file
+ try(InputStream inputStream = DirectoryIndex.class.getResourceAsStream(String.format("/%s/%s", path, DIRECTORY_INDEX_FILE))) {
+ index.read(inputStream);
+ }
+
+ URL[] urls = new URL[index.size()];
+ int loopIndex = 0;
+
+ // Extract files
+ for(String name : index.getNames()) {
+ try(InputStream inputStream = DirectoryIndex.class.getResourceAsStream(String.format("/%s/%s", path, name))) {
+ File outputFile = new File(outputDirectory, name);
+ urls[loopIndex++] = outputFile.toURI().toURL();
+
+ // Skip file if it already exists and the hashes match
+ if(outputFile.exists() && index.getHash(name).equals(SHA256.hash(outputFile))) {
+ continue;
+ }
+
+ Files.copy(inputStream, outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
+ return urls;
+ }
+
+ public void read(InputStream inputStream) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+ reader.lines().forEach(line -> {
+ String[] segments = line.split("\t");
+
+ if(segments.length == 2) {
+ map.put(segments[0], segments[1]);
+ }
+ });
+ }
+
+ public void write(OutputStream outputStream) throws IOException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
+
+ for(String name : map.keySet()) {
+ writer.write(String.format("%s\t%s\n", name, map.get(name)));
+ }
+
+ writer.flush();
+ }
+
+ public void put(String name, String hash) {
+ map.put(name, hash);
+ }
+
+ public String remove(String name) {
+ return map.remove(name);
+ }
+
+ public String getHash(String name) {
+ return map.get(name);
+ }
+
+ public Set getNames() {
+ return map.keySet();
+ }
+
+ public Collection getHashes() {
+ return map.values();
+ }
+
+ public Set> getEntries() {
+ return map.entrySet();
+ }
+
+ public int size() {
+ return map.size();
+ }
+}
diff --git a/build-logic/src/boot/java/brainwine/bootstrap/SHA256.java b/build-logic/src/boot/java/brainwine/bootstrap/SHA256.java
new file mode 100644
index 00000000..1e1df74c
--- /dev/null
+++ b/build-logic/src/boot/java/brainwine/bootstrap/SHA256.java
@@ -0,0 +1,28 @@
+package brainwine.bootstrap;
+
+import java.io.File;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.file.Files;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class SHA256 {
+
+ public static String hash(File file) throws IOException {
+ return hash(Files.readAllBytes(file.toPath()));
+ }
+
+ public static String hash(byte[] bytes) {
+ MessageDigest digest;
+
+ try {
+ digest = MessageDigest.getInstance("SHA-256");
+ } catch(NoSuchAlgorithmException e) {
+ throw new RuntimeException(e); // This should never happen
+ }
+
+ byte[] hash = digest.digest(bytes);
+ return new BigInteger(1, hash).toString(16).toUpperCase();
+ }
+}
diff --git a/build-logic/src/main/java/brainwine/build/DistributionPlugin.java b/build-logic/src/main/java/brainwine/build/DistributionPlugin.java
new file mode 100644
index 00000000..c1580d92
--- /dev/null
+++ b/build-logic/src/main/java/brainwine/build/DistributionPlugin.java
@@ -0,0 +1,14 @@
+package brainwine.build;
+
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.plugins.JavaPlugin;
+
+public class DistributionPlugin implements Plugin {
+
+ @Override
+ public void apply(Project project) {
+ project.getPlugins().apply(JavaPlugin.class);
+ project.getTasks().register("dist", DistributionTask.class, task -> task.dependsOn("build"));
+ }
+}
diff --git a/build-logic/src/main/java/brainwine/build/DistributionTask.java b/build-logic/src/main/java/brainwine/build/DistributionTask.java
new file mode 100644
index 00000000..f53f0f66
--- /dev/null
+++ b/build-logic/src/main/java/brainwine/build/DistributionTask.java
@@ -0,0 +1,107 @@
+package brainwine.build;
+
+import static brainwine.bootstrap.Constants.BOOT_CLASS_KEY;
+import static brainwine.bootstrap.Constants.JAR_LIBRARY_PATH;
+import static brainwine.bootstrap.Constants.JAR_LICENSE_PATH;
+import static brainwine.bootstrap.Constants.MAIN_CLASS;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import javax.inject.Inject;
+
+import org.gradle.api.DefaultTask;
+import org.gradle.api.GradleException;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.file.FileTree;
+import org.gradle.api.initialization.IncludedBuild;
+import org.gradle.api.invocation.Gradle;
+import org.gradle.api.provider.Property;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.Optional;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.TaskAction;
+import org.gradle.api.tasks.bundling.Jar;
+
+public abstract class DistributionTask extends DefaultTask {
+
+ private final FileTree bootCodeTree;
+ private File outputDirectory;
+
+ @Input
+ public abstract Property getMainClass();
+
+ @Input
+ @Optional
+ public abstract Property getArchiveFileName();
+
+ @InputFile
+ @Optional
+ public abstract Property getLicenseFile();
+
+ @Inject
+ public DistributionTask(Gradle gradle) {
+ IncludedBuild build = gradle.getIncludedBuilds().stream().filter(x -> x.getName().equals("build-logic")).findFirst().get();
+ bootCodeTree = getProject().fileTree(new File(build.getProjectDir(), "build/classes/java/boot"));
+ outputDirectory = new File(getProject().getBuildDir(), "dist");
+ }
+
+ @TaskAction
+ public void createDistributionArchive() throws IOException {
+ Configuration config = getProject().getConfigurations().getByName("runtimeClasspath");
+ Jar jarTask = (Jar)getProject().getTasks().getByName("jar");
+ String archiveFileName = getArchiveFileName().getOrElse(jarTask.getArchiveFileName().get());
+ File outputDirectory = new File(getProject().getBuildDir(), "dist");
+ outputDirectory.mkdirs();
+ File outputFile = new File(outputDirectory, archiveFileName);
+
+ // Fetch libraries
+ List libraryFiles = new ArrayList<>();
+ config.getResolvedConfiguration().getResolvedArtifacts().forEach(artifact -> libraryFiles.add(artifact.getFile()));
+ jarTask.getOutputs().getFiles().forEach(libraryFiles::add);
+
+ // Create jar manifest
+ Manifest manifest = new Manifest();
+ manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+ manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, MAIN_CLASS.getName());
+ manifest.getMainAttributes().put(BOOT_CLASS_KEY, getMainClass().get());
+ manifest.getMainAttributes().putValue("Multi-Release", "true");
+
+ // Create jar file
+ try(JarBundler bundler = new JarBundler(new FileOutputStream(outputFile), manifest)) {
+ // Add boot code
+ bootCodeTree.visit(details -> {
+ if(!details.isDirectory()) {
+ try {
+ bundler.addFile(details.getFile(), details.getPath());
+ } catch(IOException e) {
+ throw new GradleException(e.getMessage(), e);
+ }
+ }
+ });
+
+ // Add libraries
+ bundler.embedDirectory(libraryFiles, JAR_LIBRARY_PATH);
+
+ // Add license
+ if(getLicenseFile().isPresent()) {
+ bundler.addFile(getLicenseFile().get(), JAR_LICENSE_PATH);
+ }
+ }
+ }
+
+ public void setOutputDirectory(File outputDirectory) {
+ this.outputDirectory = outputDirectory;
+ }
+
+ @OutputDirectory
+ public File getOutputDirectory() {
+ return outputDirectory;
+ }
+}
diff --git a/build-logic/src/main/java/brainwine/build/JarBundler.java b/build-logic/src/main/java/brainwine/build/JarBundler.java
new file mode 100644
index 00000000..f36b7eb8
--- /dev/null
+++ b/build-logic/src/main/java/brainwine/build/JarBundler.java
@@ -0,0 +1,60 @@
+package brainwine.build;
+
+import static brainwine.bootstrap.Constants.DIRECTORY_INDEX_FILE;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.Collection;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+import brainwine.bootstrap.DirectoryIndex;
+import brainwine.bootstrap.SHA256;
+
+public class JarBundler implements AutoCloseable {
+
+ private final JarOutputStream outputStream;
+
+ public JarBundler(OutputStream outputStream, Manifest manifest) throws IOException {
+ this.outputStream = new JarOutputStream(outputStream, manifest);
+ }
+
+ @Override
+ public void close() throws IOException {
+ outputStream.close();
+ }
+
+ public void embedDirectory(Collection files, String path) throws IOException {
+ DirectoryIndex index = new DirectoryIndex();
+
+ // Add files
+ for(File file : files) {
+ String name = file.getName();
+ byte[] bytes = Files.readAllBytes(file.toPath());
+ String hash = SHA256.hash(bytes);
+ addFile(bytes, String.format("%s/%s", path, file.getName()));
+ index.put(name, hash);
+ }
+
+ // Add index file containing file names and hashes
+ addEntry(String.format("%s/%s", path, DIRECTORY_INDEX_FILE), index::write);
+ }
+
+ public void addFile(File file, String name) throws IOException {
+ addFile(Files.readAllBytes(file.toPath()), name);
+ }
+
+ public void addFile(byte[] bytes, String name) throws IOException {
+ addEntry(name, outputStream -> outputStream.write(bytes));
+ }
+
+ public void addEntry(String name, OutputStreamWriter writer) throws IOException {
+ JarEntry entry = new JarEntry(name);
+ outputStream.putNextEntry(entry);
+ writer.write(outputStream);
+ outputStream.closeEntry();
+ }
+}
diff --git a/build-logic/src/main/java/brainwine/build/OutputStreamWriter.java b/build-logic/src/main/java/brainwine/build/OutputStreamWriter.java
new file mode 100644
index 00000000..b7f18e56
--- /dev/null
+++ b/build-logic/src/main/java/brainwine/build/OutputStreamWriter.java
@@ -0,0 +1,10 @@
+package brainwine.build;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+@FunctionalInterface
+public interface OutputStreamWriter {
+
+ public void write(OutputStream outputStream) throws IOException;
+}
diff --git a/build.gradle b/build.gradle
index fcf038c0..c47d5847 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,9 +1,9 @@
plugins {
- id 'java'
+ id 'brainwine.distribution'
}
ext {
- mainClass = 'brainwine.Bootstrap'
+ mainClass = 'brainwine.Main'
workingDirectory = 'run'
}
@@ -15,26 +15,14 @@ dependencies {
implementation 'com.formdev:flatlaf-intellij-themes:3.0'
implementation 'com.formdev:flatlaf-extras:3.0'
implementation 'com.formdev:flatlaf:3.0'
- implementation project(':api')
- implementation project(':gameserver')
- implementation project(':shared')
+ implementation project(':brainwine-api')
+ implementation project(':brainwine-gameserver')
+ implementation project(':brainwine-shared')
}
-task dist(type: Jar) {
- manifest {
- attributes 'Multi-Release': 'true',
- 'Main-Class': project.ext.mainClass
- }
-
- from {
- configurations.runtimeClasspath.collect {
- it.isDirectory() ? it : zipTree(it)
- }
- }
-
- duplicatesStrategy = DuplicatesStrategy.EXCLUDE
- dependsOn configurations.runtimeClasspath
- with jar
+dist {
+ mainClass = project.ext.mainClass
+ licenseFile = file("${project.rootDir}/LICENSE.md")
}
task run(type: JavaExec) {
diff --git a/deepworld-config b/deepworld-config
index ec2749d9..34e1ec6f 160000
--- a/deepworld-config
+++ b/deepworld-config
@@ -1 +1 @@
-Subproject commit ec2749d94b86dbbdefa52e0146ce278688e28370
+Subproject commit 34e1ec6fc0ecb3633b45b8868ae7c8fc3477696c
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..b07f071e
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,13 @@
+services:
+ brainwine:
+ # build: .
+ image: brainwine:latest
+ ports:
+ - "5001:5001" # gateway
+ - "5002:5002" # server
+ - "5003:5003" # portal
+ volumes:
+ - bw-data:/data
+
+volumes:
+ bw-data: {}
\ No newline at end of file
diff --git a/gameserver/build.gradle b/gameserver/build.gradle
index 847e487e..b707887c 100644
--- a/gameserver/build.gradle
+++ b/gameserver/build.gradle
@@ -24,11 +24,7 @@ dependencies {
implementation 'org.reflections:reflections:0.10.2'
implementation 'io.netty:netty-all:4.1.79.Final'
implementation 'org.mindrot:jbcrypt:0.4'
- implementation project(':shared')
-}
-
-jar {
- archiveBaseName = 'brainwine-gameserver'
+ implementation project(':brainwine-shared')
}
processResources.includeEmptyDirs = false
diff --git a/gameserver/src/main/java/brainwine/gameserver/GameConfiguration.java b/gameserver/src/main/java/brainwine/gameserver/GameConfiguration.java
index 60740dbb..a06e55e9 100644
--- a/gameserver/src/main/java/brainwine/gameserver/GameConfiguration.java
+++ b/gameserver/src/main/java/brainwine/gameserver/GameConfiguration.java
@@ -22,13 +22,12 @@
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
-import com.fasterxml.jackson.core.JsonProcessingException;
-
import brainwine.gameserver.command.CommandManager;
-import brainwine.gameserver.entity.player.Player;
-import brainwine.gameserver.entity.player.Skill;
import brainwine.gameserver.item.Item;
import brainwine.gameserver.item.ItemRegistry;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.player.Skill;
+import brainwine.gameserver.shop.ShopManager;
import brainwine.gameserver.util.MapHelper;
import brainwine.gameserver.util.VersionUtils;
import brainwine.shared.JsonHelper;
@@ -55,6 +54,7 @@ public static void init() {
loadConfigOverrides();
logger.info(SERVER_MARKER, "Configuring ...");
configure();
+ ShopManager.loadShopData();
logger.info(SERVER_MARKER, "Caching versioned configurations ...");
cacheVersionedConfigs();
logger.info(SERVER_MARKER, "Load complete! Took {} milliseconds", System.currentTimeMillis() - startTime);
@@ -79,6 +79,10 @@ private static void configure() {
MapHelper.put(baseConfig, "shop.currency", new HashMap<>());
Map items = MapHelper.getMap(baseConfig, "items");
+ // Clear shop data
+ MapHelper.put(baseConfig, "shop.sections", new ArrayList<>());
+ MapHelper.put(baseConfig, "shop.items", new ArrayList<>());
+
// Add custom commands to the client config
CommandManager.getCommandNames().forEach(command -> {
MapHelper.put(baseConfig, String.format("commands.%s", command), true);
@@ -130,15 +134,23 @@ private static void configure() {
}
}
- // Map skill bonuses
+ // Map stat bonuses
Map bonuses = MapHelper.getMap(config, "bonus");
if(bonuses != null) {
Map skillBonuses = new HashMap<>();
- bonuses.forEach((type, amount) -> {
- if(amount instanceof Integer && Skill.fromId(type) != null) {
- skillBonuses.put(type, (int)amount);
+ bonuses.forEach((type, value) -> {
+ if(!(value instanceof Number)) {
+ return;
+ }
+
+ Number amount = (Number)value;
+
+ if(Skill.fromId(type) != null) {
+ skillBonuses.put(type, amount.intValue());
+ } else if("regen".equals(type)) {
+ config.put("regen_bonus", amount.doubleValue());
}
});
@@ -162,9 +174,9 @@ private static void configure() {
try {
Item item = JsonHelper.readValue(config, Item.class);
ItemRegistry.registerItem(item);
- } catch (JsonProcessingException e) {
- logger.fatal(SERVER_MARKER, "Failed to register item {}", id, e);
- System.exit(0);
+ } catch (Exception e) {
+ logger.fatal(SERVER_MARKER, "Failed to register item '{}'", id, e);
+ throw new RuntimeException(e); // Server SHOULD NOT attempt to start if there is a problem with the item configuration
}
});
@@ -202,7 +214,7 @@ private static void loadConfigFiles() {
}
} catch(Exception e) {
logger.fatal(SERVER_MARKER, "Could not load configuration files", e);
- System.exit(-1);
+ throw new RuntimeException(e); // Server SHOULD NOT attempt to start if the game configuration can't be loaded
}
}
diff --git a/gameserver/src/main/java/brainwine/gameserver/GameServer.java b/gameserver/src/main/java/brainwine/gameserver/GameServer.java
index d7df4f1f..f31c8d94 100644
--- a/gameserver/src/main/java/brainwine/gameserver/GameServer.java
+++ b/gameserver/src/main/java/brainwine/gameserver/GameServer.java
@@ -8,23 +8,25 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import brainwine.gameserver.achievements.AchievementManager;
+import brainwine.gameserver.achievement.AchievementManager;
import brainwine.gameserver.command.CommandExecutor;
import brainwine.gameserver.command.CommandManager;
import brainwine.gameserver.entity.EntityRegistry;
-import brainwine.gameserver.entity.player.NotificationType;
-import brainwine.gameserver.entity.player.PlayerManager;
import brainwine.gameserver.loot.LootManager;
+import brainwine.gameserver.minigame.Pandora;
+import brainwine.gameserver.player.NotificationType;
+import brainwine.gameserver.player.PlayerManager;
import brainwine.gameserver.prefab.PrefabManager;
import brainwine.gameserver.server.NetworkRegistry;
import brainwine.gameserver.server.Server;
import brainwine.gameserver.zone.EntityManager;
+import brainwine.gameserver.zone.GrowthManager;
import brainwine.gameserver.zone.ZoneManager;
import brainwine.gameserver.zone.gen.ZoneGenerator;
public class GameServer implements CommandExecutor {
- public static final int GLOBAL_SAVE_INTERVAL = 30000; // 30 seconds
+ public static final int GLOBAL_SAVE_INTERVAL = 300000; // 5 minutes
private static final Logger logger = LogManager.getLogger();
private static GameServer instance;
private final Thread handlerThread;
@@ -48,6 +50,8 @@ public GameServer() {
AchievementManager.loadAchievements();
EntityRegistry.init();
EntityManager.loadEntitySpawns();
+ GrowthManager.loadGrowthData();
+ Pandora.loadConfig();
lootManager = new LootManager();
prefabManager = new PrefabManager();
ZoneGenerator.init();
@@ -78,10 +82,7 @@ public void tick() {
long now = System.currentTimeMillis();
float deltaTime = (now - lastTick) / 1000.0F; // in seconds
lastTick = now;
-
- while(!tasks.isEmpty()) {
- tasks.poll().run();
- }
+ pollTasks();
if(lastSave + GLOBAL_SAVE_INTERVAL < System.currentTimeMillis()) {
zoneManager.saveZones();
@@ -90,7 +91,6 @@ public void tick() {
}
zoneManager.tick(deltaTime);
- playerManager.tick();
}
/**
@@ -107,13 +107,24 @@ public void queueSynchronousTask(Runnable task) {
}
}
+ /**
+ * Polls and executes all currently queued tasks.
+ */
+ private void pollTasks() {
+ while(!tasks.isEmpty()) {
+ tasks.poll().run();
+ }
+ }
+
/**
* Called by the bootstrapper when the program closes.
*/
public void onShutdown() {
logger.info(SERVER_MARKER, "Shutting down GameServer ...");
+ playerManager.getOnlinePlayers().forEach(player -> player.kick("Server is shutting down."));
server.close();
ZoneGenerator.stopAsyncZoneGenerator(true);
+ pollTasks(); // Run any remaining tasks to ensure everything is cleaned up properly
logger.info(SERVER_MARKER, "Saving zone data ...");
zoneManager.onShutdown();
logger.info(SERVER_MARKER, "Saving player data ...");
diff --git a/gameserver/src/main/java/brainwine/gameserver/Naming.java b/gameserver/src/main/java/brainwine/gameserver/Naming.java
new file mode 100644
index 00000000..093eb64a
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/Naming.java
@@ -0,0 +1,118 @@
+package brainwine.gameserver;
+
+/**
+ * TODO all I'm doing here is moving the problem somewhere else.
+ *
+ * Entity names are sourced from: https://github.com/bytebin/deepworld-gameserver/blob/master/config/fake.yml
+ */
+@Deprecated
+public class Naming {
+
+ private static final String[] ZONE_FIRST_NAMES = {
+ "Malvern", "Tralee", "Horncastle", "Old", "Westwood",
+ "Citta", "Tadley", "Mossley", "West", "East",
+ "North", "South", "Wadpen", "Githam", "Soatnust",
+ "Highworth", "Creakynip", "Upper", "Lower", "Cannock",
+ "Dovercourt", "Limerick", "Pickering", "Glumshed", "Crusthack",
+ "Osyltyr", "Aberstaple", "New", "Stroud", "Crumclum",
+ "Crumsidle", "Bankswund", "Fiddletrast", "Bournpan", "St.",
+ "Funderbost", "Bexwoddly", "Pilkingheld", "Wittlepen", "Rabbitbleaker",
+ "Griffingumby", "Guilthead", "Bigglelund", "Bunnymold", "Rosesidle",
+ "Crushthorn", "Tanlyward", "Ahncrace", "Pilkingking", "Dingstrath",
+ "Axebury", "Ginglingtap", "Ballybibby", "Shadehoven"
+ };
+
+ private static final String[] ZONE_LAST_NAMES = {
+ "Falls", "Alloa", "Glen", "Way", "Dolente",
+ "Peak", "Heights", "Creek", "Banffshire", "Chagford",
+ "Gorge", "Valley", "Catacombs", "Depths", "Mines",
+ "Crickbridge", "Guildbost", "Pits", "Vaults", "Ruins",
+ "Dell", "Keep", "Chatterdin", "Scrimmance", "Gitwick",
+ "Ridge", "Alresford", "Place", "Bridge", "Glade",
+ "Mill", "Court", "Dooftory", "Hills", "Specklewint",
+ "Grove", "Aylesbury", "Wagwouth", "Russetcumby", "Point",
+ "Canyon", "Cranwarry", "Bluff", "Passage", "Crantippy",
+ "Kerbodome", "Dale", "Cemetery"
+ };
+
+ public static final String[] ENTITY_FIRST_NAMES = {
+ "Aaron", "Abby", "Abigale", "Abraham", "Ada", "Adella", "Agnes", "Alan",
+ "Albert", "Alexander", "Allie", "Almira", "Almyra", "Alonzo", "Alva", "Ambrose",
+ "Amelia", "Amon", "Amos", "Andrew", "Ann", "Annie", "Aquilla", "Archibald",
+ "Arnold", "Arrah", "Asa", "Augustus", "Barnabas", "Bartholomew", "Beatrice", "Becky",
+ "Benedict", "Benjamin", "Bennet", "Bernard", "Bernice", "Bertram", "Bess", "Bessie",
+ "Beth", "Betsy", "Buford", "Byron", "Calvin", "Charity", "Charles", "Charlotte",
+ "Chastity", "Christopher", "Claire", "Clarence", "Clement", "Clinton", "Cole", "Columbus",
+ "Commodore Perry", "Constance", "Cynthia", "Daniel", "David", "Dick", "Dorothy", "Edith",
+ "Edmund", "Edna", "Edward", "Edwin", "Edwina", "Eldon", "Eleanor", "Eli",
+ "Elijah", "Eliza", "Elizabeth", "Ella", "Ellie", "Elvira", "Emma", "Emmett",
+ "Enoch", "Esther", "Ethel", "Ettie", "Eudora", "Eva", "Ezekiel", "Ezra",
+ "Fanny", "Fidelia", "Flora", "Florence", "Frances", "Francis", "Franklin", "Frederick",
+ "Gabriel", "Garrett", "Geneve", "Genevieve", "George", "George", "Georgia", "Gertie",
+ "Gertrude", "Gideon", "Gilbert", "Ginny", "Gladys", "Grace", "Granville", "Hannah",
+ "Harland", "Harold", "Harrison", "Harvey", "Hattie", "Helen", "Helene", "Henrietta",
+ "Henry", "Hester", "Hettie", "Hiram", "Hope", "Horace", "Horatio", "Hortence",
+ "Hugh", "Isaac", "Isaac Newton", "Isabella", "Isaiah", "Israel", "Jacob", "James",
+ "Jane", "Jasper", "Jedediah", "Jefferson", "Jennie", "Jeptha", "Jessamine", "Jesse",
+ "Joel", "John Paul", "John Wesley", "Jonathan", "Joseph", "Josephine", "Josephus", "Joshua",
+ "Josiah", "Judith", "Julia", "Julian", "Juliet", "Julius", "Katherine", "Lafayette",
+ "Laura", "Lawrence", "Leah", "Leander", "Lenora", "Les", "Letitia", "Levi",
+ "Levi", "Lewis", "Lila", "Lilly", "Liza", "Lorena", "Lorraine", "Lottie",
+ "Louis", "Louisa", "Louise", "Lucas", "Lucas", "Lucian", "Lucian", "Lucius",
+ "Lucius", "Lucy", "Luke", "Luke", "Lulu", "Luther", "Luther", "Lydia",
+ "Mahulda", "Marcellus", "Margaret", "Mark", "Martha", "Martin", "Mary", "Mary Elizabeth",
+ "Mary Frances", "Masheck", "Matilda", "Matthew", "Maude", "Maurice", "Maxine", "Maxwell",
+ "Mercy", "Meriwether", "Meriwether Lewis", "Merrill", "Mildred", "Minerva", "Missouri", "Molly",
+ "Mordecai", "Morgan", "Morris", "Myrtle", "Nancy", "Natalie", "Nathaniel", "Ned",
+ "Nellie", "Nettie", "Newton", "Nicholas", "Nimrod", "Ninian", "Nora", "Obediah",
+ "Octavius", "Orpha", "Orville", "Oscar", "Owen", "Parthena", "Patrick", "Patrick Henry",
+ "Patsy", "Paul", "Paul", "Peggy", "Permelia", "Perry", "Peter", "Philomena",
+ "Phoebe", "Pleasant", "Polly", "Preshea", "Rachel", "Ralph", "Raymond", "Rebecca",
+ "Reuben", "Rhoda", "Richard", "Robert", "Robert Lee", "Roderick", "Rowena", "Rudolph",
+ "Rufina", "Rufus", "Ruth", "Sally", "Sam Houston", "Samantha", "Samuel", "Sarah",
+ "Sarah Ann", "Sarah Elizabeth", "Savannah", "Selina", "Seth", "Silas", "Simeon", "Simon",
+ "Sophronia", "Stanley", "Stella", "Stephen", "Thaddeus", "Theodore", "Theodosia", "Thomas",
+ "Timothy", "Ulysses", "Uriah", "Vertiline", "Victor", "Victoria", "Virginia", "Vivian",
+ "Walter", "Warren", "Washington", "Wilfred", "William", "Winnifred", "Zachariah", "Zebulon",
+ "Zedock", "Zona", "Zylphia"
+ };
+
+ public static final String[] ENTITY_LAST_NAMES = {
+ "Abraham", "Adams", "Alcorn", "Alderdice", "Angus", "Ashdown", "Ayre", "Backhaus",
+ "Baldwin", "Bamford", "Beaton", "Blackwood", "Blair", "Blewett", "Bornholdt", "Bowden",
+ "Burrows", "Cameron", "Carroll", "Clarke", "Claxton", "Collins", "Colson", "Connor",
+ "Conroy", "Cullen", "Cunningham", "Curd", "Curnow", "Cusack", "Dagon", "Dalton",
+ "Dawes", "Desmond", "Dewar", "Dickenson", "Donnell", "Drummond", "Dunstan", "English",
+ "Eveans", "Faraday", "Faulkner", "Fitzgerald", "Fitzpatrick", "Fletcher", "Foster", "Franklin",
+ "Fulton", "Gallagher", "Gibbons", "Gilmore", "Glover", "Goodfellow", "Goodwin", "Griffiths",
+ "Gullifer", "Hadley", "Haeffner", "Hanlon", "Harding", "Harris", "Holloway", "Hughes",
+ "Jarvis", "Jefferies", "Johnstone", "Kaylock", "Keane", "Kemp", "Kernaghan", "Kirby",
+ "Kirkland", "Knight", "LaFontaine", "Lawford", "Lawrence", "Lennox", "Longley", "Lonsdale",
+ "Luckett", "Lyons", "Macklin", "Madill", "Marsden", "Marshall", "Martin", "Mather",
+ "Mathieson", "Maunder", "McColl", "McDermott", "McGillicuddy", "McKenzie", "McLachlan", "McNeil",
+ "Meaklim", "Meighan", "Mellor", "Meyers", "Milsom", "Mitchell", "Mitchelson", "Moore",
+ "Morgan", "Morrison", "Mortimer", "Moulsdale", "Murphy", "Nelson", "Nolan", "Noonan",
+ "O'Keefe", "O'Sullivan", "Palmer", "Parnell", "Pattison", "Pettit", "Phillips", "Pinner",
+ "Porter", "Prosser", "Ramseyer", "Renton", "Rickard", "Riddington", "Roche", "Rowe",
+ "Russell", "Salisbury", "Saunders", "Sawyer", "Scanlan", "Scarborough", "Schwarer", "Sheary",
+ "Sheedy", "Shelton", "Shields", "Shinnick", "Skinner", "Sommer", "Spencer", "Stanbury",
+ "Stanton", "Storey", "Swaisbrick", "Thorley", "Thumpston", "Tichborne", "Tinning", "Tobin",
+ "Todd", "Trimble", "Twomey", "Upton", "Urwin", "Vandenburg", "Vinge", "Wakefield",
+ "Wakenshaw", "Walden", "Wallace", "Walton", "Warner", "Webb", "Whitehill", "Wickes",
+ "Wilberforce", "Wilkinson", "Wolstenholme", "Wright"
+ };
+
+ public static String getRandomZoneName() {
+ return getRandomName(ZONE_FIRST_NAMES, ZONE_LAST_NAMES);
+ }
+
+ public static String getRandomEntityName() {
+ return getRandomName(ENTITY_FIRST_NAMES, ENTITY_LAST_NAMES);
+ }
+
+ private static String getRandomName(String[] firstNames, String[] lastNames) {
+ String firstName = firstNames[(int)(Math.random() * firstNames.length)];
+ String lastName = lastNames[(int)(Math.random() * lastNames.length)];
+ return firstName + " " + lastName;
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/StringGenerator.java b/gameserver/src/main/java/brainwine/gameserver/StringGenerator.java
new file mode 100644
index 00000000..d817a301
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/StringGenerator.java
@@ -0,0 +1,76 @@
+package brainwine.gameserver;
+
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.Function;
+
+public class StringGenerator {
+
+ private static final String[] ZONE_PREFIXES = {
+ "East", "Fort", "Lower", "Mount", "New", "North", "Old", "South", "St.", "Upper", "West"
+ };
+
+ public static final String[] ZONE_SUFFIXES = {
+ "Bluff", "Bridge", "Canyon", "Court", "Cove", "Crossing", "Dale", "Dell", "Falls", "Field",
+ "Glade", "Glen", "Gorge", "Grove", "Heights", "Hills", "Hollow", "Keep", "Mill", "Passage",
+ "Peak", "Place", "Point", "Ridge", "Shore", "Springs", "Vale", "Valley", "Village", "Way",
+ "Woods"
+ };
+
+ private static final String[] ZONE_NAMES = {
+ "Abroath", "Acton", "Ashington", "Awktill", "Ayrshire", "Bakewell", "Ballysud", "Bextrast", "Bifflesud", "Biggledrip",
+ "Bigsbyshire", "Bigsbythrow", "Birkenhead", "Bixnay", "Blackcastle", "Blantyre", "Brambleclum", "Bredlyclum", "Bredlystaple", "Bunnyworth",
+ "Burford", "Butterminster", "Butterward", "Buxenbridge", "Caithness", "Callington", "Canterhersh", "Canterswin", "Casterbib", "Chatterchurch",
+ "Chesterwint", "Chickerell", "Chippinggumby", "Chippingtad", "Chumshire", "Cindermead", "Coleford", "Combbridge", "Crackwardine", "Cratham",
+ "Crickmouster", "Crouchfold", "Croydon", "Crumbibby", "Cuddlekenne", "Cuddleton", "Dallyswaine", "Danderkale", "Dandylang", "Dantyfret",
+ "Dantyswade", "Darlington", "Desborough", "Didlythwaite", "Dingstrath", "Dodgefell", "Dorchester", "Dorfenhersh", "Dorfentory", "Drogheda",
+ "Dropsud", "Dundalk", "Dungarvan", "Edenbridge", "Fidgetbridge", "Flitwick", "Funderbridge", "Ginglingwent", "Gitbridge", "Godalming",
+ "Gravesend", "Hailsham", "Heathfield", "Heathshire", "Hertford", "Holmcombe", "Holmfit", "Holmtoft", "Hufflegander", "Hunlygin",
+ "Kilbigsby", "Killarney", "Knickermere", "Larkhall", "Larkshed", "Launceston", "Lechlade", "Leftkenne", "Littleblighter", "London",
+ "Martport", "Marvotippy", "Middleham", "Mildenhall", "Mosshersh", "Mumblecoddle", "Mumblehersh", "Mumslybost", "Natherthwaite", "Neekenne",
+ "Nerdlydin", "Newbury", "Nibcastle", "Orkney", "Padendigby", "Pegglekeld", "Petabinge", "Pettipane", "Pickering", "Pillway",
+ "Pillwint", "Potton", "Princeglum", "Puttermouth", "Putterwint", "Pyllchurch", "Richmond", "Rosesidle", "Rosewouth", "Rotherham",
+ "Russetcumby", "Saltbost", "Saltbourne", "Scrimpstring", "Scrimwan", "Scrumpbourne", "Sedbergh", "Shimshot", "Shingletip", "Skipton",
+ "Slughersh", "Slugwoddly", "Snortsdin", "Southam", "Southsea", "Southwick", "Southwold", "Specklewint", "Spilsby", "Stockport",
+ "Stockton", "Stompcoddle", "Stumpchurch", "Stumpclum", "Stumpswade", "Tanlyward", "Thumbsham", "Wadfret", "Wagfield", "Watton",
+ "Wetherby", "Willowtap", "Winchelsea", "Wittlewoddly", "Wixleybost", "Wixleywicket", "Wolsingham", "Woodbridge", "Xandercott", "Yarmouth",
+ "Yateley", "Yetterbury", "Yorkhang", "Yostercumby"
+ };
+
+ public static String getRandomZoneName() {
+ Random random = ThreadLocalRandom.current();
+ int format = random.nextInt(3);
+
+ switch(format) {
+ case 0: return ZONE_NAMES[random.nextInt(ZONE_NAMES.length)];
+ case 1: return getRandomName(ZONE_PREFIXES, ZONE_NAMES);
+ case 2: return getRandomName(ZONE_NAMES, ZONE_SUFFIXES);
+ }
+
+ return "Mystery Zone"; // Should not happen
+ }
+
+ public static String getRandomZoneName(Function dupeCheck, int maxRetries) {
+ String name = getRandomZoneName();
+ int retries = 0;
+
+ if(dupeCheck == null) {
+ return name;
+ }
+
+ while(dupeCheck.apply(name)) {
+ if(retries >= maxRetries) {
+ return null;
+ }
+
+ name = getRandomZoneName();
+ retries++;
+ }
+
+ return name;
+ }
+
+ private static String getRandomName(String[] first, String[] second) {
+ return String.format("%s %s", first[(int)(Math.random() * first.length)], second[(int)(Math.random() * second.length)]);
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/Timer.java b/gameserver/src/main/java/brainwine/gameserver/Timer.java
new file mode 100644
index 00000000..7c817fc5
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/Timer.java
@@ -0,0 +1,42 @@
+package brainwine.gameserver;
+
+/**
+ * Model for synchronous timers.
+ */
+public class Timer {
+
+ private T key;
+ private long time;
+ private Runnable action;
+
+ public Timer(T key, long delay, Runnable action) {
+ this.key = key;
+ this.time = System.currentTimeMillis() + delay;
+ this.action = action;
+ }
+
+ public boolean process() {
+ return process(false);
+ }
+
+ public boolean process(boolean force) {
+ if(force || System.currentTimeMillis() >= time) {
+ action.run();
+ return true;
+ }
+
+ return false;
+ }
+
+ public T getKey() {
+ return key;
+ }
+
+ public long getTime() {
+ return time;
+ }
+
+ public Runnable getAction() {
+ return action;
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/Achievement.java
similarity index 87%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/Achievement.java
index c74fb030..36abee7d 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/Achievement.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/Achievement.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonCreator.Mode;
@@ -12,7 +12,7 @@
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.player.Player;
import brainwine.gameserver.serialization.AchievementSerializer;
import brainwine.gameserver.util.MathUtils;
@@ -28,7 +28,13 @@
@Type(name = "ScavengingAchievement", value = ScavengingAchievement.class),
@Type(name = "DiscoveryAchievement", value = DiscoveryAchievement.class),
@Type(name = "SpawnerStoppageAchievement", value = SpawnerStoppageAchievement.class),
- @Type(name = "Journeyman", value = JourneymanAchievement.class)
+ @Type(name = "UndertakerAchievement", value = UndertakerAchievement.class),
+ @Type(name = "DeliveranceAchievement", value = DeliveranceAchievement.class),
+ @Type(name = "TrappingAchievement", value = TrappingAchievement.class),
+ @Type(name = "Journeyman", value = JourneymanAchievement.class),
+ @Type(name = "ArchitectAchievement", value = ArchitectAchievement.class),
+ @Type(name = "VotingAchievement", value = VotingAchievement.class),
+ @Type(name = "PositionAchievement", value = PositionAchievement.class),
})
@JsonSerialize(using = AchievementSerializer.class)
@JsonIgnoreProperties(ignoreUnknown = true)
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/AchievementManager.java b/gameserver/src/main/java/brainwine/gameserver/achievement/AchievementManager.java
similarity index 98%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/AchievementManager.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/AchievementManager.java
index ede955f9..4515fc1d 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/AchievementManager.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/AchievementManager.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import static brainwine.shared.LogMarkers.SERVER_MARKER;
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievement/ArchitectAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/ArchitectAchievement.java
new file mode 100644
index 00000000..78c24be5
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/ArchitectAchievement.java
@@ -0,0 +1,15 @@
+package brainwine.gameserver.achievement;
+
+import brainwine.gameserver.player.Player;
+import com.fasterxml.jackson.annotation.JacksonInject;
+
+public class ArchitectAchievement extends Achievement {
+ public ArchitectAchievement(@JacksonInject("title") String title) {
+ super(title);
+ }
+
+ @Override
+ public int getProgress(Player player) {
+ return player.getStatistics().getLandmarkVotesReceived();
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/CraftingAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/CraftingAchievement.java
similarity index 88%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/CraftingAchievement.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/CraftingAchievement.java
index 450dbd32..ecf6ba8a 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/CraftingAchievement.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/CraftingAchievement.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import java.util.List;
@@ -6,9 +6,9 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-import brainwine.gameserver.entity.player.Player;
-import brainwine.gameserver.entity.player.PlayerStatistics;
import brainwine.gameserver.item.Item;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.player.PlayerStatistics;
public class CraftingAchievement extends Achievement {
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievement/DeliveranceAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/DeliveranceAchievement.java
new file mode 100644
index 00000000..0f01df73
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/DeliveranceAchievement.java
@@ -0,0 +1,19 @@
+package brainwine.gameserver.achievement;
+
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+import brainwine.gameserver.player.Player;
+
+public class DeliveranceAchievement extends Achievement {
+
+ @JsonCreator
+ public DeliveranceAchievement(@JacksonInject("title") String title) {
+ super(title);
+ }
+
+ @Override
+ public int getProgress(Player player) {
+ return player.getStatistics().getDeliverances();
+ }
+}
\ No newline at end of file
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/DiscoveryAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/DiscoveryAchievement.java
similarity index 88%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/DiscoveryAchievement.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/DiscoveryAchievement.java
index ddccda34..d1e9efde 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/DiscoveryAchievement.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/DiscoveryAchievement.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import java.util.Map.Entry;
@@ -6,10 +6,10 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-import brainwine.gameserver.entity.player.Player;
-import brainwine.gameserver.entity.player.PlayerStatistics;
import brainwine.gameserver.item.Item;
import brainwine.gameserver.item.ItemGroup;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.player.PlayerStatistics;
public class DiscoveryAchievement extends Achievement {
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/ExploringAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/ExploringAchievement.java
similarity index 81%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/ExploringAchievement.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/ExploringAchievement.java
index 995d7b34..57f97018 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/ExploringAchievement.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/ExploringAchievement.java
@@ -1,9 +1,9 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.player.Player;
public class ExploringAchievement extends Achievement {
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/HuntingAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/HuntingAchievement.java
similarity index 89%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/HuntingAchievement.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/HuntingAchievement.java
index 04d26f06..c2b0faeb 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/HuntingAchievement.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/HuntingAchievement.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import java.util.Map.Entry;
@@ -7,7 +7,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import brainwine.gameserver.entity.EntityGroup;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.player.Player;
public class HuntingAchievement extends Achievement {
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/JourneymanAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/JourneymanAchievement.java
similarity index 80%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/JourneymanAchievement.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/JourneymanAchievement.java
index f3a8b3d0..c0f48aeb 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/JourneymanAchievement.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/JourneymanAchievement.java
@@ -1,9 +1,9 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.player.Player;
public class JourneymanAchievement extends Achievement {
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/LazyAchievementGetter.java b/gameserver/src/main/java/brainwine/gameserver/achievement/LazyAchievementGetter.java
similarity index 87%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/LazyAchievementGetter.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/LazyAchievementGetter.java
index 685413a8..8a12c843 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/LazyAchievementGetter.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/LazyAchievementGetter.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import brainwine.gameserver.util.LazyGetter;
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/LooterAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/LooterAchievement.java
similarity index 81%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/LooterAchievement.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/LooterAchievement.java
index e08c6ade..06e9732d 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/LooterAchievement.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/LooterAchievement.java
@@ -1,9 +1,9 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.player.Player;
public class LooterAchievement extends Achievement {
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/MiningAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/MiningAchievement.java
similarity index 89%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/MiningAchievement.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/MiningAchievement.java
index 8ea7b139..b5443189 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/MiningAchievement.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/MiningAchievement.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import java.util.Map.Entry;
@@ -6,8 +6,8 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-import brainwine.gameserver.entity.player.Player;
import brainwine.gameserver.item.ItemGroup;
+import brainwine.gameserver.player.Player;
public class MiningAchievement extends Achievement {
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievement/PositionAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/PositionAchievement.java
new file mode 100644
index 00000000..3172915f
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/PositionAchievement.java
@@ -0,0 +1,36 @@
+package brainwine.gameserver.achievement;
+
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.Zone;
+
+public class PositionAchievement extends Achievement {
+
+ @JsonProperty("top")
+ protected int top = -1;
+
+ @JsonProperty("bottom")
+ protected int bottom = -1;
+
+ @JsonProperty("left")
+ protected int left = -1;
+
+ @JsonProperty("right")
+ protected int right = -1;
+
+ @JsonCreator
+ public PositionAchievement(@JacksonInject("title") String title) {
+ super(title);
+ }
+
+ @Override
+ public boolean isCompleted(Player player) {
+ Zone zone = player.getZone();
+ int x = left >= 0 ? left : right >= 0 ? zone.getWidth() - right : -1;
+ int y = top >= 0 ? top : bottom >= 0 ? zone.getHeight() - bottom : -1;
+ return (x < 0 || Math.abs(player.getBlockX() - x) <= 1) && (y < 0 || Math.abs(player.getBlockY() - y) <= 1);
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/RaiderAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/RaiderAchievement.java
similarity index 81%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/RaiderAchievement.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/RaiderAchievement.java
index 37581cc6..07777e60 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/RaiderAchievement.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/RaiderAchievement.java
@@ -1,9 +1,9 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.player.Player;
public class RaiderAchievement extends Achievement {
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/ScavengingAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/ScavengingAchievement.java
similarity index 76%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/ScavengingAchievement.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/ScavengingAchievement.java
index e7b7d984..e7331d50 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/ScavengingAchievement.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/ScavengingAchievement.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import java.util.List;
@@ -6,9 +6,9 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-import brainwine.gameserver.entity.player.Player;
-import brainwine.gameserver.entity.player.PlayerStatistics;
import brainwine.gameserver.item.Item;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.player.PlayerStatistics;
public class ScavengingAchievement extends Achievement {
@@ -25,9 +25,9 @@ public int getProgress(Player player) {
PlayerStatistics statistics = player.getStatistics();
if(items == null) {
- return statistics.getUniqueItemsMined();
+ return statistics.getUniqueItemsScavenged();
} else {
- return (int)(statistics.getItemsMined().entrySet().stream()
+ return (int)(statistics.getItemsScavenged().entrySet().stream()
.filter(entry -> entry.getValue() > 0 && items.contains(entry.getKey()))
.count());
}
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/SidekickAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/SidekickAchievement.java
similarity index 81%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/SidekickAchievement.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/SidekickAchievement.java
index f5442475..3ffbec4c 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/SidekickAchievement.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/SidekickAchievement.java
@@ -1,9 +1,9 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.player.Player;
public class SidekickAchievement extends Achievement {
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievements/SpawnerStoppageAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/SpawnerStoppageAchievement.java
similarity index 82%
rename from gameserver/src/main/java/brainwine/gameserver/achievements/SpawnerStoppageAchievement.java
rename to gameserver/src/main/java/brainwine/gameserver/achievement/SpawnerStoppageAchievement.java
index ac3b3b0a..dc358259 100644
--- a/gameserver/src/main/java/brainwine/gameserver/achievements/SpawnerStoppageAchievement.java
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/SpawnerStoppageAchievement.java
@@ -1,9 +1,9 @@
-package brainwine.gameserver.achievements;
+package brainwine.gameserver.achievement;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.player.Player;
public class SpawnerStoppageAchievement extends Achievement {
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievement/TrappingAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/TrappingAchievement.java
new file mode 100644
index 00000000..7dec1553
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/TrappingAchievement.java
@@ -0,0 +1,19 @@
+package brainwine.gameserver.achievement;
+
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+import brainwine.gameserver.player.Player;
+
+public class TrappingAchievement extends Achievement {
+
+ @JsonCreator
+ public TrappingAchievement(@JacksonInject("title") String title) {
+ super(title);
+ }
+
+ @Override
+ public int getProgress(Player player) {
+ return player.getStatistics().getTotalTrappings();
+ }
+}
\ No newline at end of file
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievement/UndertakerAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/UndertakerAchievement.java
new file mode 100644
index 00000000..c86856db
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/UndertakerAchievement.java
@@ -0,0 +1,19 @@
+package brainwine.gameserver.achievement;
+
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+import brainwine.gameserver.player.Player;
+
+public class UndertakerAchievement extends Achievement {
+
+ @JsonCreator
+ public UndertakerAchievement(@JacksonInject("title") String title) {
+ super(title);
+ }
+
+ @Override
+ public int getProgress(Player player) {
+ return player.getStatistics().getUndertakings();
+ }
+}
\ No newline at end of file
diff --git a/gameserver/src/main/java/brainwine/gameserver/achievement/VotingAchievement.java b/gameserver/src/main/java/brainwine/gameserver/achievement/VotingAchievement.java
new file mode 100644
index 00000000..f2491d5a
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/achievement/VotingAchievement.java
@@ -0,0 +1,17 @@
+package brainwine.gameserver.achievement;
+
+import brainwine.gameserver.player.Player;
+import com.fasterxml.jackson.annotation.JacksonInject;
+
+public class VotingAchievement extends Achievement {
+
+ public VotingAchievement(@JacksonInject("title") String title) {
+ super(title);
+ }
+
+ @Override
+ public int getProgress(Player player) {
+ return player.getStatistics().getLandmarksUpvoted();
+ }
+
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/ApiCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/ApiCommand.java
new file mode 100644
index 00000000..6f4635f0
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/ApiCommand.java
@@ -0,0 +1,41 @@
+package brainwine.gameserver.command;
+
+import brainwine.gameserver.GameServer;
+import brainwine.gameserver.dialog.Dialog;
+import brainwine.gameserver.dialog.DialogSection;
+import brainwine.gameserver.player.Player;
+
+@CommandInfo(name = "api", description = "Lets you configure your API settings.")
+public class ApiCommand extends Command {
+
+ @Override
+ public void execute(CommandExecutor executor, String[] args) {
+ Player player = ((Player)executor);
+
+ // Create & show settings dialog
+ Dialog dialog = new Dialog();
+ dialog.setTitle("API Settings");
+ dialog.addSection(new DialogSection().setTitle("Your API token:").setText(String.format(player.isV3() ? "%s" : "%s", player.getApiToken())).setTextColor("ffd95f"));
+ dialog.addSection(new DialogSection().setText("Generate new API token").setChoice("reissue"));
+ player.showDialog(dialog, input -> {
+ // Handle cancellation
+ if(input.length == 0 || (input.length == 1 && "cancel".equals(input[0]))) {
+ return;
+ }
+
+ if("reissue".equals(input[0]) && GameServer.getInstance().getPlayerManager().issueApiToken(player)) {
+ player.kick("API token changed.", true);
+ }
+ });
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/api";
+ }
+
+ @Override
+ public boolean canExecute(CommandExecutor executor) {
+ return executor instanceof Player;
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/Command.java b/gameserver/src/main/java/brainwine/gameserver/command/Command.java
index 3c42d26b..1e1b1661 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/Command.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/Command.java
@@ -1,24 +1,38 @@
package brainwine.gameserver.command;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
+
public abstract class Command {
public abstract void execute(CommandExecutor executor, String[] args);
+ public abstract String getUsage(CommandExecutor executor);
- public abstract String getName();
-
- public String[] getAliases() {
- return null;
- }
-
- public String getDescription() {
- return "No description for this command";
+ public boolean canExecute(CommandExecutor executor) {
+ return true;
}
- public String getUsage(CommandExecutor executor) {
- return "/" + getName();
+ protected final boolean checkArgumentCount(CommandExecutor executor, String[] args, int... counts) {
+ int highestCount = 0;
+
+ for(int count : counts) {
+ if(count > highestCount) {
+ highestCount = count;
+ }
+
+ if(args.length == count) {
+ return true;
+ }
+ }
+
+ if(args.length > highestCount) {
+ return true;
+ }
+
+ sendUsageMessage(executor);
+ return false;
}
- public boolean canExecute(CommandExecutor executor) {
- return true;
+ protected final void sendUsageMessage(CommandExecutor executor) {
+ executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
}
}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/CommandExecutor.java b/gameserver/src/main/java/brainwine/gameserver/command/CommandExecutor.java
index 9929a1b1..77b05758 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/CommandExecutor.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/CommandExecutor.java
@@ -1,6 +1,6 @@
package brainwine.gameserver.command;
-import brainwine.gameserver.entity.player.NotificationType;
+import brainwine.gameserver.player.NotificationType;
public interface CommandExecutor {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/CommandInfo.java b/gameserver/src/main/java/brainwine/gameserver/command/CommandInfo.java
new file mode 100644
index 00000000..f723865c
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/CommandInfo.java
@@ -0,0 +1,15 @@
+package brainwine.gameserver.command;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface CommandInfo {
+
+ public String name();
+ public String description();
+ public String[] aliases() default {};
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/CommandManager.java b/gameserver/src/main/java/brainwine/gameserver/command/CommandManager.java
index 2a731f29..b11de02d 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/CommandManager.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/CommandManager.java
@@ -1,52 +1,25 @@
package brainwine.gameserver.command;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import static brainwine.shared.LogMarkers.SERVER_MARKER;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.reflections.Reflections;
-import brainwine.gameserver.command.commands.AcidityCommand;
-import brainwine.gameserver.command.commands.AdminCommand;
-import brainwine.gameserver.command.commands.BanCommand;
-import brainwine.gameserver.command.commands.BroadcastCommand;
-import brainwine.gameserver.command.commands.EntityCommand;
-import brainwine.gameserver.command.commands.ExperienceCommand;
-import brainwine.gameserver.command.commands.ExportCommand;
-import brainwine.gameserver.command.commands.GenerateZoneCommand;
-import brainwine.gameserver.command.commands.GiveCommand;
-import brainwine.gameserver.command.commands.HealthCommand;
-import brainwine.gameserver.command.commands.HelpCommand;
-import brainwine.gameserver.command.commands.ImportCommand;
-import brainwine.gameserver.command.commands.KickCommand;
-import brainwine.gameserver.command.commands.LevelCommand;
-import brainwine.gameserver.command.commands.MuteCommand;
-import brainwine.gameserver.command.commands.PlayerIdCommand;
-import brainwine.gameserver.command.commands.PositionCommand;
-import brainwine.gameserver.command.commands.PrefabListCommand;
-import brainwine.gameserver.command.commands.RegisterCommand;
-import brainwine.gameserver.command.commands.RickrollCommand;
-import brainwine.gameserver.command.commands.SayCommand;
-import brainwine.gameserver.command.commands.SeedCommand;
-import brainwine.gameserver.command.commands.SettleLiquidsCommand;
-import brainwine.gameserver.command.commands.SkillPointsCommand;
-import brainwine.gameserver.command.commands.StopCommand;
-import brainwine.gameserver.command.commands.TeleportCommand;
-import brainwine.gameserver.command.commands.ThinkCommand;
-import brainwine.gameserver.command.commands.TimeCommand;
-import brainwine.gameserver.command.commands.UnbanCommand;
-import brainwine.gameserver.command.commands.UnmuteCommand;
-import brainwine.gameserver.command.commands.WeatherCommand;
-import brainwine.gameserver.command.commands.ZoneIdCommand;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.player.Player;
+@SuppressWarnings("unchecked")
public class CommandManager {
public static final String CUSTOM_COMMAND_PREFIX = "!"; // TODO configurable
@@ -67,38 +40,17 @@ public static void init() {
private static void registerCommands() {
logger.info(SERVER_MARKER, "Registering commands ...");
- registerCommand(new StopCommand());
- registerCommand(new RegisterCommand());
- registerCommand(new TeleportCommand());
- registerCommand(new KickCommand());
- registerCommand(new MuteCommand());
- registerCommand(new UnmuteCommand());
- registerCommand(new BanCommand());
- registerCommand(new UnbanCommand());
- registerCommand(new SayCommand());
- registerCommand(new ThinkCommand());
- registerCommand(new BroadcastCommand());
- registerCommand(new PlayerIdCommand());
- registerCommand(new ZoneIdCommand());
- registerCommand(new AdminCommand());
- registerCommand(new HelpCommand());
- registerCommand(new GiveCommand());
- registerCommand(new GenerateZoneCommand());
- registerCommand(new SeedCommand());
- registerCommand(new PrefabListCommand());
- registerCommand(new ExportCommand());
- registerCommand(new ImportCommand());
- registerCommand(new PositionCommand());
- registerCommand(new RickrollCommand());
- registerCommand(new EntityCommand());
- registerCommand(new HealthCommand());
- registerCommand(new ExperienceCommand());
- registerCommand(new LevelCommand());
- registerCommand(new SkillPointsCommand());
- registerCommand(new SettleLiquidsCommand());
- registerCommand(new WeatherCommand());
- registerCommand(new AcidityCommand());
- registerCommand(new TimeCommand());
+ Reflections reflections = new Reflections("brainwine.gameserver.command");
+ Set> classes = reflections.getTypesAnnotatedWith(CommandInfo.class);
+
+ for(Class> clazz : classes) {
+ if(!Command.class.isAssignableFrom(clazz)) {
+ logger.warn(SERVER_MARKER, "Attempted to register non-command class {}", clazz.getSimpleName());
+ continue;
+ }
+
+ registerCommand((Class extends Command>)clazz);
+ }
}
public static void executeCommand(CommandExecutor executor, String commandLine) {
@@ -138,27 +90,44 @@ public static void executeCommand(CommandExecutor executor, String commandName,
command.execute(executor, args);
}
- public static void registerCommand(Command command) {
- String name = command.getName();
-
- if(commands.containsKey(name)) {
- logger.warn(SERVER_MARKER, "Attempted to register duplicate command {} with name {}", command.getClass(), name);
- return;
- }
-
- commands.put(name, command);
- String[] aliases = command.getAliases();
-
- if(aliases != null) {
- for(String alias : aliases) {
- if(commands.containsKey(alias) || CommandManager.aliases.containsKey(alias)) {
- logger.warn(SERVER_MARKER, "Duplicate alias {} for command {}", alias, command.getClass());
- continue;
- }
-
- CommandManager.aliases.put(alias, command);
- }
- }
+ public static void registerCommand(Class extends Command> type) {
+ CommandInfo info = type.getAnnotation(CommandInfo.class);
+
+ if(info == null) {
+ logger.warn(SERVER_MARKER, "Cannot register command '{}' because it does not have the CommandInfo annotation", type.getSimpleName());
+ return;
+ }
+
+ String name = info.name().toLowerCase();
+
+ if(commands.containsKey(name)) {
+ logger.warn(SERVER_MARKER, "Attempted to register duplicate command '{}' with name '{}'", type.getSimpleName(), name);
+ return;
+ }
+
+ Command command = null;
+
+ try {
+ command = type.getConstructor().newInstance();
+ } catch(ReflectiveOperationException e) {
+ logger.error(SERVER_MARKER, "Failed to instantiate command '{}'", type.getSimpleName(), e);
+ return;
+ }
+
+ commands.put(name, command);
+
+ if(info.aliases() != null) {
+ List aliases = Stream.of(info.aliases()).map(String::toLowerCase).collect(Collectors.toList());
+
+ for(String alias : aliases) {
+ if(commands.containsKey(alias) || CommandManager.aliases.containsKey(alias)) {
+ logger.warn(SERVER_MARKER, "Duplicate alias {} for command {}", alias, command.getClass());
+ continue;
+ }
+
+ CommandManager.aliases.put(alias, command);
+ }
+ }
}
public static Set getCommandNames() {
@@ -173,7 +142,7 @@ public static Command getCommand(String name) {
}
public static Command getCommand(String name, boolean allowAlias) {
- return commands.getOrDefault(name, allowAlias ? aliases.get(name) : null);
+ return commands.getOrDefault(name.toLowerCase(), allowAlias ? aliases.get(name.toLowerCase()) : null);
}
public static Collection getCommands() {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/HelpCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/HelpCommand.java
similarity index 71%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/HelpCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/HelpCommand.java
index 2516d580..325ee39e 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/HelpCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/HelpCommand.java
@@ -1,6 +1,6 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import java.util.ArrayList;
import java.util.Arrays;
@@ -8,16 +8,18 @@
import org.apache.commons.lang3.math.NumberUtils;
-import brainwine.gameserver.command.Command;
-import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.command.CommandManager;
-
+@CommandInfo(name = "help", description = "Displays a list of commands.")
public class HelpCommand extends Command {
@Override
public void execute(CommandExecutor executor, String[] args) {
List commands = new ArrayList<>(CommandManager.getCommands());
commands.removeIf(command -> !command.canExecute(executor));
+ commands.sort((a, b) -> {
+ CommandInfo info1 = a.getClass().getAnnotation(CommandInfo.class);
+ CommandInfo info2 = b.getClass().getAnnotation(CommandInfo.class);
+ return info1.name().compareTo(info2.name());
+ });
int pageSize = 8;
int pageCount = (int)Math.ceil(commands.size() / (double)pageSize);
int page = 1;
@@ -41,11 +43,12 @@ public void execute(CommandExecutor executor, String[] args) {
return;
}
- executor.notify(String.format("========== Information about '/%s' ==========", command.getName()), SYSTEM);
- executor.notify(String.format("Description: %s", command.getDescription()), SYSTEM);
+ CommandInfo info = command.getClass().getAnnotation(CommandInfo.class);
+ executor.notify(String.format("========== Information about '/%s' ==========", info.name()), SYSTEM);
+ executor.notify(String.format("Description: %s", info.description()), SYSTEM);
executor.notify(String.format("Usage: %s", command.getUsage(executor)), SYSTEM);
- executor.notify(String.format("Aliases: %s", command.getAliases() == null ? "None :("
- : Arrays.toString(command.getAliases())), SYSTEM);
+ executor.notify(String.format("Aliases: %s", info.aliases() == null ? "None :("
+ : Arrays.toString(info.aliases())), SYSTEM);
return;
}
}
@@ -56,19 +59,10 @@ public void execute(CommandExecutor executor, String[] args) {
executor.notify(String.format("========== Command List (Page %s of %s) ==========", page, pageCount), SYSTEM);
for(Command command : commandsToDisplay) {
- executor.notify(String.format("%s - %s", command.getUsage(executor), command.getDescription()), SYSTEM);
+ CommandInfo info = command.getClass().getAnnotation(CommandInfo.class);
+ executor.notify(String.format("%s - %s", command.getUsage(executor), info.description()), SYSTEM);
}
}
-
- @Override
- public String getName() {
- return "help";
- }
-
- @Override
- public String getDescription() {
- return "Displays command information.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/RegisterCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/RegisterCommand.java
similarity index 80%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/RegisterCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/RegisterCommand.java
index f8ff79d1..5c1f8679 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/RegisterCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/RegisterCommand.java
@@ -1,16 +1,15 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command;
+
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import org.apache.commons.validator.routines.EmailValidator;
import org.mindrot.jbcrypt.BCrypt;
import brainwine.gameserver.GameServer;
-import brainwine.gameserver.command.Command;
-import brainwine.gameserver.command.CommandExecutor;
import brainwine.gameserver.dialog.DialogHelper;
-import brainwine.gameserver.entity.player.Player;
-
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "register", description = "Shows a prompt with which you can register your account.")
public class RegisterCommand extends Command {
@Override
@@ -55,13 +54,8 @@ public void execute(CommandExecutor executor, String[] args) {
}
@Override
- public String getName() {
- return "register";
- }
-
- @Override
- public String getDescription() {
- return "Shows a prompt with which you can register your account.";
+ public String getUsage(CommandExecutor executor) {
+ return "/register";
}
@Override
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/SayCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/SayCommand.java
similarity index 62%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/SayCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/SayCommand.java
index e93375b1..c41fec0e 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/SayCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/SayCommand.java
@@ -1,12 +1,11 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
-import brainwine.gameserver.command.Command;
-import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.ChatType;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.player.ChatType;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "say", description = "Shows a speech bubble to nearby players.")
public class SayCommand extends Command {
@Override
@@ -27,16 +26,6 @@ public void execute(CommandExecutor executor, String[] args) {
player.getZone().sendChatMessage(player, text, ChatType.SPEECH);
}
- @Override
- public String getName() {
- return "say";
- }
-
- @Override
- public String getDescription() {
- return "Shows a speech bubble to nearby players.";
- }
-
@Override
public String getUsage(CommandExecutor executor) {
return "/say ";
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/ThinkCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/ThinkCommand.java
similarity index 62%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/ThinkCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/ThinkCommand.java
index e4b52401..2c5f9e01 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/ThinkCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/ThinkCommand.java
@@ -1,12 +1,11 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
-import brainwine.gameserver.command.Command;
-import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.ChatType;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.player.ChatType;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "think", description = "Shows a thought bubble to nearby players.")
public class ThinkCommand extends Command {
@Override
@@ -27,16 +26,6 @@ public void execute(CommandExecutor executor, String[] args) {
player.getZone().sendChatMessage(player, text, ChatType.THOUGHT);
}
- @Override
- public String getName() {
- return "think";
- }
-
- @Override
- public String getDescription() {
- return "Shows a thought bubble to nearby players.";
- }
-
@Override
public String getUsage(CommandExecutor executor) {
return "/think ";
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/AcidityCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/AcidityCommand.java
similarity index 78%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/AcidityCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/AcidityCommand.java
index 77aa5689..e66bea1a 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/AcidityCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/AcidityCommand.java
@@ -1,12 +1,14 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
import brainwine.gameserver.zone.Zone;
+@CommandInfo(name = "acidity", description = "Displays or changes the acidity in the current zone.")
public class AcidityCommand extends Command {
@Override
@@ -35,16 +37,6 @@ public void execute(CommandExecutor executor, String[] args) {
zone.setAcidity(value);
executor.notify(String.format("Acidity has been set to %s in %s.", value, zone.getName()), SYSTEM);
}
-
- @Override
- public String getName() {
- return "acidity";
- }
-
- @Override
- public String getDescription() {
- return "Displays or changes the acidity in the current zone.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/AdminCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/AdminCommand.java
similarity index 81%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/AdminCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/AdminCommand.java
index cfce46ce..e93f6f9e 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/AdminCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/AdminCommand.java
@@ -1,12 +1,14 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "admin", description = "Grants or revokes administrator rights.")
public class AdminCommand extends Command {
@Override
@@ -38,16 +40,6 @@ public void execute(CommandExecutor executor, String[] args) {
executor.notify(String.format("Changed administrator status of player %s to %s", target.getName(), admin), SYSTEM);
}
- @Override
- public String getName() {
- return "admin";
- }
-
- @Override
- public String getDescription() {
- return "Allows you to grant or revoke administrator rights.";
- }
-
@Override
public String getUsage(CommandExecutor executor) {
return "/admin [true|false]";
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/BanCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/BanCommand.java
similarity index 83%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/BanCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/BanCommand.java
index 2296cd8d..ebdc8fd1 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/BanCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/BanCommand.java
@@ -1,6 +1,6 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
@@ -9,9 +9,11 @@
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
import brainwine.gameserver.util.DateTimeUtils;
+@CommandInfo(name = "ban", description = "Bans a player from the server.")
public class BanCommand extends Command {
@Override
@@ -58,21 +60,6 @@ public void execute(CommandExecutor executor, String[] args) {
target.getName(), endDate.format(DateTimeFormatter.RFC_1123_DATE_TIME), reason), SYSTEM);
}
- @Override
- public String getName() {
- return "ban";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] { "banish" };
- }
-
- @Override
- public String getDescription() {
- return "Bans a player from the server.";
- }
-
@Override
public String getUsage(CommandExecutor executor) {
return "/ban [reason]";
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/BroadcastCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/BroadcastCommand.java
similarity index 63%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/BroadcastCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/BroadcastCommand.java
index 08c6f8a9..3f8d7e5a 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/BroadcastCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/BroadcastCommand.java
@@ -1,13 +1,15 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.POPUP;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.POPUP;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "broadcast", description = "Broadcasts a message to all online players.", aliases = "bc")
public class BroadcastCommand extends Command {
@Override
@@ -26,21 +28,6 @@ public void execute(CommandExecutor executor, String[] args) {
executor.notify("Your message has been broadcasted.", POPUP);
}
- @Override
- public String getName() {
- return "broadcast";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] { "bc" };
- }
-
- @Override
- public String getDescription() {
- return "Broadcasts a message to all online players.";
- }
-
@Override
public String getUsage(CommandExecutor executor) {
return "/broadcast ";
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/admin/CrownsCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/CrownsCommand.java
new file mode 100644
index 00000000..45f2d873
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/CrownsCommand.java
@@ -0,0 +1,89 @@
+package brainwine.gameserver.command.admin;
+
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
+
+import brainwine.gameserver.GameServer;
+import brainwine.gameserver.command.Command;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+
+@CommandInfo(name = "crowns", description = "Display or update a player's crown balance.")
+public class CrownsCommand extends Command {
+
+ @Override
+ public void execute(CommandExecutor executor, String[] args) {
+ if(args.length == 0 || args.length == 2) {
+ executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
+ return;
+ }
+
+ Player target = GameServer.getInstance().getPlayerManager().getPlayer(args[0]);
+
+ // Check if player exists
+ if(target == null) {
+ executor.notify("This player does not exist.", SYSTEM);
+ return;
+ }
+
+ // Show number of crowns if no further arguments are given
+ if(args.length == 1) {
+ executor.notify(String.format("%s has %s crowns.", target.getName(), target.getCrowns()), SYSTEM);
+ return;
+ }
+
+ int amount = 0;
+ int currentAmount = target.getCrowns();
+
+ try {
+ amount = Integer.parseInt(args[2]);
+ } catch(NumberFormatException e) {
+ executor.notify("Amount must be a valid number.", SYSTEM);
+ return;
+ }
+
+ // Update target player's crown balance
+ switch(args[1]) {
+ case "set":
+ target.setCrowns(Math.max(0, amount));
+ break;
+ case "add":
+ target.setCrowns(Math.max(0, currentAmount + amount)); // Can overflow but realistically won't matter
+ break;
+ case "remove":
+ target.setCrowns(Math.max(0, currentAmount - amount));
+ break;
+ default:
+ executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
+ return;
+ }
+
+ // Calculate balance difference and send notifications
+ int difference = target.getCrowns() - currentAmount;
+ amount = Math.abs(difference);
+
+ if(difference > 0) {
+ executor.notify(String.format("Gave %s crown%s to %s. (New balance: %s, was: %s)", amount, amount == 1 ? "" : "s", target.getName(), target.getCrowns(), currentAmount), SYSTEM);
+ target.notify(String.format("You've received %s crown%s from an administrator.", amount, amount == 1 ? "" : "s"), SYSTEM);
+ return;
+ }
+
+ if(difference < 0) {
+ executor.notify(String.format("Took %s crown%s from %s. (New balance: %s, was: %s)", amount, amount == 1 ? "" : "s", target.getName(), target.getCrowns(), currentAmount), SYSTEM);
+ target.notify(String.format("%s crown%s %s been taken from your account by an administrator.", amount, amount == 1 ? "" : "s", amount == 1 ? "has" : "have"), SYSTEM);
+ return;
+ }
+
+ executor.notify(String.format("No changes were made to %s's crown balance.", target.getName()), SYSTEM);
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/crowns [ ]";
+ }
+
+ @Override
+ public boolean canExecute(CommandExecutor executor) {
+ return executor.isAdmin();
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/admin/EcoCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/EcoCommand.java
new file mode 100644
index 00000000..c1ca1c19
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/EcoCommand.java
@@ -0,0 +1,98 @@
+package brainwine.gameserver.command.admin;
+
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import brainwine.gameserver.command.Command;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.item.Item;
+import brainwine.gameserver.item.ItemRegistry;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.EcologicalMachine;
+import brainwine.gameserver.zone.Zone;
+
+@CommandInfo(name = "eco", description = "Manage ecological machine parts.")
+public class EcoCommand extends Command {
+
+ @Override
+ public void execute(CommandExecutor executor, String[] args) {
+ if(args.length != 1 && args.length != 3) {
+ executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
+ return;
+ }
+
+ Player player = (Player)executor;
+ Zone zone = player.getZone();
+ EcologicalMachine machine = EcologicalMachine.fromName(args[0]);
+
+ if(machine == null) {
+ player.notify(String.format("Machine type must be one of %s", Arrays.toString(EcologicalMachine.values()).toLowerCase()), SYSTEM);
+ return;
+ }
+
+ if(args.length == 3) {
+ String action = args[1];
+ Item part = null;
+
+ if(!args[2].equals("all")) {
+ part = ItemRegistry.getItem(args[2]);
+
+ if(!machine.isMachinePart(part)) {
+ player.notify(String.format("Machine component must be one of %s", machine.getParts()), SYSTEM);
+ return;
+ }
+ }
+
+ if(action.equals("add")) {
+ if(part == null) {
+ machine.getParts().forEach(zone::addMachinePart);
+ player.notify(String.format("Added all %s components.", machine.getId()), SYSTEM);
+ return;
+ }
+
+ if(zone.addMachinePart(part)) {
+ player.notify(String.format("Added %s component '%s'", machine.getId(), part.getId()), SYSTEM);
+ return;
+ }
+
+ player.notify(String.format("That %s component has already been discovered.", machine.getId()), SYSTEM);
+ return;
+ }
+
+ if(action.equals("remove")) {
+ if(part == null) {
+ machine.getParts().forEach(zone::removeMachinePart);
+ player.notify(String.format("Removed all %s components.", machine.getId()), SYSTEM);
+ return;
+ }
+
+ if(zone.removeMachinePart(part)) {
+ player.notify(String.format("Removed %s component '%s'", machine.getId(), part.getId()), SYSTEM);
+ return;
+ }
+
+ player.notify(String.format("That %s component has not been discovered.", machine.getId()), SYSTEM);
+ return;
+ }
+
+ executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
+ return;
+ }
+
+ Collection- parts = zone.getDiscoveredParts(machine);
+ player.notify(String.format("Discovered %s/%s %s components%s", parts.size(), machine.getPartCount(), machine.getId(), parts.isEmpty() ? "." : ": " + parts), SYSTEM);
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/eco [ ]";
+ }
+
+ @Override
+ public boolean canExecute(CommandExecutor executor) {
+ return executor.isAdmin() && executor instanceof Player;
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/admin/EntityCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/EntityCommand.java
new file mode 100644
index 00000000..b9281b0c
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/EntityCommand.java
@@ -0,0 +1,39 @@
+package brainwine.gameserver.command.admin;
+
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
+
+import brainwine.gameserver.command.Command;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.NotificationType;
+import brainwine.gameserver.player.Player;
+
+@CommandInfo(name = "entity", description = "Spawns an entity at your current location.")
+public class EntityCommand extends Command {
+
+ @Override
+ public void execute(CommandExecutor executor, String[] args) {
+ if(args.length == 0) {
+ executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
+ return;
+ }
+
+ Player player = (Player)executor;
+ String type = args[0];
+
+ if(player.getZone().spawnEntity(type, player.getBlockX(), player.getBlockY(), true) == null) {
+ executor.notify(String.format("Entity type '%s' does not exist.", type), NotificationType.SYSTEM);
+ return;
+ }
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/entity ";
+ }
+
+ @Override
+ public boolean canExecute(CommandExecutor executor) {
+ return executor.isAdmin() && executor instanceof Player;
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/ExperienceCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/ExperienceCommand.java
similarity index 80%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/ExperienceCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/ExperienceCommand.java
index 3706c4a9..c4329ed7 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/ExperienceCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/ExperienceCommand.java
@@ -1,12 +1,14 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "experience", description = "Sets the experience of the target player.", aliases = { "xp", "exp" })
public class ExperienceCommand extends Command {
@Override
@@ -53,21 +55,6 @@ public void execute(CommandExecutor executor, String[] args) {
target.setExperience(experience);
executor.notify(String.format("Successfully set %s's experience to %s.", target.getName(), experience), SYSTEM);
}
-
- @Override
- public String getName() {
- return "experience";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] { "xp", "exp" };
- }
-
- @Override
- public String getDescription() {
- return "Sets the experience of the target player.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/ExportCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/ExportCommand.java
similarity index 81%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/ExportCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/ExportCommand.java
index d4fdb669..21e4f6cf 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/ExportCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/ExportCommand.java
@@ -1,18 +1,19 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
-import java.util.Arrays;
import java.util.regex.Pattern;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
import brainwine.gameserver.prefab.Prefab;
import brainwine.gameserver.prefab.PrefabManager;
import brainwine.gameserver.zone.Zone;
+@CommandInfo(name = "export", description = "Exports a section of a zone to a prefab file.")
public class ExportCommand extends Command {
public static final Pattern PREFAB_NAME_PATTERN = Pattern.compile("\\w+(/\\w+)*");
@@ -25,12 +26,13 @@ public void execute(CommandExecutor executor, String[] args) {
return;
}
+ boolean overwrite = args.length >= 6 && args[5].equalsIgnoreCase("overwrite");
Zone zone = ((Player)executor).getZone();
PrefabManager prefabManager = GameServer.getInstance().getPrefabManager();
- String name = String.join(" ", Arrays.copyOfRange(args, 4, args.length)).toLowerCase();
+ String name = args[4].toLowerCase();
- if(prefabManager.getPrefab(name) != null) {
- executor.notify("A prefab with that name already exists.", SYSTEM);
+ if(!overwrite && prefabManager.prefabExists(name)) {
+ executor.notify("A prefab with that name already exists. Add 'overwrite' at the end of the command if you wish to overwrite it.", SYSTEM);
return;
} else if(!PREFAB_NAME_PATTERN.matcher(name).matches()) {
executor.notify("Please enter a valid prefab name. Example: dungeons/my_epic_dungeon (or just my_epic_dungeon)", SYSTEM);
@@ -73,26 +75,16 @@ public void execute(CommandExecutor executor, String[] args) {
executor.notify(String.format("Exporting your prefab as '%s' ...", name), SYSTEM);
try {
- prefabManager.addPrefab(prefab);
+ prefabManager.createPrefab(prefab, overwrite);
executor.notify(String.format("Your prefab '%s' was successfully exported!", name), SYSTEM);
} catch (Exception e) {
executor.notify(String.format("An error occured while exporting prefab '%s': %s", name, e.getMessage()), SYSTEM);
}
}
- @Override
- public String getName() {
- return "export";
- }
-
- @Override
- public String getDescription() {
- return "Exports a section of a zone to a prefab file.";
- }
-
@Override
public String getUsage(CommandExecutor executor) {
- return "/export ";
+ return "/export [overwrite]";
}
@Override
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/admin/FindCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/FindCommand.java
new file mode 100644
index 00000000..966f7e1c
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/FindCommand.java
@@ -0,0 +1,41 @@
+package brainwine.gameserver.command.admin;
+
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
+
+import brainwine.gameserver.command.Command;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.MetaBlock;
+
+@CommandInfo(name = "find", description = "Displays the location of a random meta block of the specified type.")
+public class FindCommand extends Command {
+
+ @Override
+ public void execute(CommandExecutor executor, String[] args) {
+ if(args.length == 0) {
+ executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
+ return;
+ }
+
+ Player player = (Player)executor;
+ MetaBlock metaBlock = player.getZone().getMetaBlocks().stream().filter(x -> x.getItem().hasId(args[0])).findAny().orElse(null);
+
+ if(metaBlock == null) {
+ player.notify(String.format("No meta block of type '%s' exists in this zone.", args[0]), SYSTEM);
+ return;
+ }
+
+ player.notify(String.format("Target found at X: %s, Y: %s", metaBlock.getX(), metaBlock.getY()), SYSTEM);
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/find ";
+ }
+
+ @Override
+ public boolean canExecute(CommandExecutor executor) {
+ return executor.isAdmin() && executor instanceof Player;
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/GenerateZoneCommand.java
similarity index 77%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/GenerateZoneCommand.java
index f2f66eea..e281dcdb 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/GenerateZoneCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/GenerateZoneCommand.java
@@ -1,37 +1,45 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
import brainwine.gameserver.zone.Biome;
import brainwine.gameserver.zone.Zone;
import brainwine.gameserver.zone.gen.ZoneGenerator;
+@CommandInfo(name = "genzone", description = "Asynchronously generates a new zone.", aliases = "generate")
public class GenerateZoneCommand extends Command {
public static final int MIN_WIDTH = 200;
public static final int MIN_HEIGHT = 200;
public static final int MAX_WIDTH = 4000;
public static final int MAX_HEIGHT = 1600;
+ private boolean generating;
@Override
public void execute(CommandExecutor executor, String[] args) {
- Biome biome = Biome.getRandomBiome();
- int width = 2000;
- int height = 600;
+ Biome biome = args.length > 0 ? Biome.fromName(args[0]) : Biome.getRandomBiome();
+ int width = biome == Biome.DEEP ? 1200 : 2000;
+ int height = biome == Biome.DEEP ? 1000 : 600;
int seed = (int)(Math.random() * Integer.MAX_VALUE);
- if(args.length > 0 && args.length < 2) {
+ if(args.length > 1 && args.length < 3) {
executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
return;
}
- if(args.length >= 2) {
+ if(generating) {
+ executor.notify("Already generating a zone, please try again in a moment.", SYSTEM);
+ return;
+ }
+
+ if(args.length >= 3) {
try {
- width = Integer.parseInt(args[0]);
- height = Integer.parseInt(args[1]);
+ width = Integer.parseInt(args[1]);
+ height = Integer.parseInt(args[2]);
} catch(NumberFormatException e) {
executor.notify("Zone width and height must be valid numbers.", SYSTEM);
return;
@@ -47,10 +55,6 @@ public void execute(CommandExecutor executor, String[] args) {
}
}
- if(args.length >= 3) {
- biome = Biome.fromName(args[2]);
- }
-
ZoneGenerator generator = null;
if(args.length >= 4) {
@@ -78,6 +82,7 @@ public void execute(CommandExecutor executor, String[] args) {
}
}
+ generating = true;
executor.notify("Your zone is being generated. It should be ready soon!", SYSTEM);
generator.generateZoneAsync(biome, width, height, seed, zone -> {
if(zone == null) {
@@ -86,27 +91,14 @@ public void execute(CommandExecutor executor, String[] args) {
GameServer.getInstance().getZoneManager().addZone(zone);
executor.notify(String.format("Your zone '%s' is ready for exploration!", zone.getName()), SYSTEM);
}
+
+ generating = false;
});
}
-
- @Override
- public String getName() {
- return "genzone";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] { "generate" };
- }
-
- @Override
- public String getDescription() {
- return "Asynchronously generates a new zone.";
- }
@Override
public String getUsage(CommandExecutor executor) {
- return "/genzone [ ] [biome] [generator] [seed]";
+ return "/genzone [biome] [ ] [generator] [seed]";
}
@Override
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/GiveCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/GiveCommand.java
similarity index 87%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/GiveCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/GiveCommand.java
index 6102bc81..fc3c619c 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/GiveCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/GiveCommand.java
@@ -1,6 +1,6 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import java.util.ArrayList;
import java.util.List;
@@ -8,10 +8,12 @@
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
import brainwine.gameserver.item.Item;
import brainwine.gameserver.item.ItemRegistry;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "give", description = "Give or take items from players.")
public class GiveCommand extends Command {
@Override
@@ -35,7 +37,7 @@ public void execute(CommandExecutor executor, String[] args) {
title = "of every item";
for(Item item : ItemRegistry.getItems()) {
- if(!item.isClothing() && !item.isAir()) {
+ if(!item.isAir()) {
items.add(item);
}
}
@@ -84,16 +86,6 @@ public void execute(CommandExecutor executor, String[] args) {
executor.notify(String.format("Took %s %s from %s", -quantity, title, target.getName()), SYSTEM);
}
}
-
- @Override
- public String getName() {
- return "give";
- }
-
- @Override
- public String getDescription() {
- return "Adds items to a player's inventory.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/admin/GrowCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/GrowCommand.java
new file mode 100644
index 00000000..a216aa39
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/GrowCommand.java
@@ -0,0 +1,46 @@
+package brainwine.gameserver.command.admin;
+
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
+
+import brainwine.gameserver.command.Command;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.GrowthManager;
+import brainwine.gameserver.zone.Zone;
+
+@CommandInfo(name = "grow", description = "Simulate plant growth in all loaded chunks.")
+public class GrowCommand extends Command {
+
+ @Override
+ public void execute(CommandExecutor executor, String[] args) {
+ if(args.length == 0) {
+ executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
+ return;
+ }
+
+ int cycles = 0;
+
+ try {
+ cycles = Math.min(GrowthManager.MAX_RAIN_CYCLES, Integer.parseInt(args[0]));
+ } catch(NumberFormatException e) {
+ executor.notify("Rain cycles must be a valid number.", SYSTEM);
+ return;
+ }
+
+ Player player = (Player)executor;
+ Zone zone = player.getZone();
+ zone.updateGrowables(cycles);
+ player.notify(String.format("Simulated %s rain cycles.", cycles), SYSTEM);
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/grow ";
+ }
+
+ @Override
+ public boolean canExecute(CommandExecutor executor) {
+ return executor.isAdmin() && executor instanceof Player;
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/HealthCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/HealthCommand.java
similarity index 80%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/HealthCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/HealthCommand.java
index 5e84fa39..df42e037 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/HealthCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/HealthCommand.java
@@ -1,12 +1,14 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "health", description = "Sets the target player's health.", aliases = "hp")
public class HealthCommand extends Command {
@Override
@@ -47,21 +49,6 @@ public void execute(CommandExecutor executor, String[] args) {
executor.notify(String.format("Set %s's health to %s", target.getName(), target.getHealth()), SYSTEM);
}
}
-
- @Override
- public String getName() {
- return "health";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] { "hp" };
- }
-
- @Override
- public String getDescription() {
- return "Sets a player's health.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/ImportCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/ImportCommand.java
similarity index 82%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/ImportCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/ImportCommand.java
index 7178f867..54e01d42 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/ImportCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/ImportCommand.java
@@ -1,13 +1,15 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
import brainwine.gameserver.prefab.Prefab;
+@CommandInfo(name = "import", description = "Places a prefab at the specified location.")
public class ImportCommand extends Command {
@Override
@@ -46,16 +48,6 @@ public void execute(CommandExecutor executor, String[] args) {
player.notify(String.format("Successfully imported '%s' @ [x: %s, y: %s, width: %s, height: %s]",
name, x, y, prefab.getWidth(), prefab.getHeight()), SYSTEM);
}
-
- @Override
- public String getName() {
- return "import";
- }
-
- @Override
- public String getDescription() {
- return "Places a prefab at your or a specified location.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/KickCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/KickCommand.java
similarity index 79%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/KickCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/KickCommand.java
index 1ba646b8..417165d7 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/KickCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/KickCommand.java
@@ -1,14 +1,16 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import java.util.Arrays;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "kick", description = "Kicks a player from the server.")
public class KickCommand extends Command {
@Override
@@ -38,16 +40,6 @@ public void execute(CommandExecutor executor, String[] args) {
executor.notify("Kicked player " + player.getName() + " for '" + reason + "'", SYSTEM);
}
- @Override
- public String getName() {
- return "kick";
- }
-
- @Override
- public String getDescription() {
- return "Kicks a player from the server.";
- }
-
@Override
public String getUsage(CommandExecutor executor) {
return "/kick [reason]";
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/LevelCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/LevelCommand.java
similarity index 80%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/LevelCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/LevelCommand.java
index 0e1437a2..1c99a483 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/LevelCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/LevelCommand.java
@@ -1,12 +1,14 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "level", description = "Sets the level of the target player.", aliases = { "lvl", "lv" })
public class LevelCommand extends Command {
@Override
@@ -55,21 +57,6 @@ public void execute(CommandExecutor executor, String[] args) {
target.setLevel(level);
executor.notify(String.format("Successfully set %s's level to %s.", target.getName(), level), SYSTEM);
}
-
- @Override
- public String getName() {
- return "level";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] { "lvl", "lv" };
- }
-
- @Override
- public String getDescription() {
- return "Sets the level of the target player.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/admin/LootCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/LootCommand.java
new file mode 100644
index 00000000..d733af95
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/LootCommand.java
@@ -0,0 +1,80 @@
+package brainwine.gameserver.command.admin;
+
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
+
+import brainwine.gameserver.GameServer;
+import brainwine.gameserver.command.Command;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.loot.Loot;
+import brainwine.gameserver.loot.LootManager;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.player.Skill;
+
+@CommandInfo(name = "loot", description = "Awards loot to a player.")
+public class LootCommand extends Command {
+
+ @Override
+ public void execute(CommandExecutor executor, String[] args) {
+ if(args.length < 2) {
+ executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
+ return;
+ }
+
+ Player target = GameServer.getInstance().getPlayerManager().getPlayer(args[0]);
+
+ // Check if player exists
+ if(target == null) {
+ executor.notify("That player does not exist.", SYSTEM);
+ return;
+ }
+
+ // Check if player is online
+ if(!target.isOnline()) {
+ executor.notify(String.format("%s is not online.", target.getName()), SYSTEM);
+ return;
+ }
+
+ int luck = target.getTotalSkillLevel(Skill.LUCK);
+ int maxLuck = (int)(LootManager.MAX_BONUS_ROLLS * LootManager.LEVELS_PER_BONUS_ROLL);
+
+ if(args.length >= 3) {
+ try {
+ luck = Math.max(1, Math.min(maxLuck, Integer.parseInt(args[2])));
+ } catch(NumberFormatException e) {
+ executor.notify("Luck must be a valid number.", SYSTEM);
+ return;
+ }
+ }
+
+ String category = args[1];
+ LootManager lootManager = GameServer.getInstance().getLootManager();
+
+ // Check if loot table exists
+ if(lootManager.getLootTable(category) == null) {
+ executor.notify(String.format("Loot category must be one of: %s", lootManager.getLootCategories()), SYSTEM);
+ return;
+ }
+
+ Loot loot = lootManager.getRandomLoot(luck, target.getZone().getBiome(), target.getInventory().getWardrobe(), category);
+
+ // Check if eligible loot was found
+ if(loot == null) {
+ executor.notify(String.format("Could not find any eligible loot for category '%s'.", category), SYSTEM);
+ return;
+ }
+
+ target.awardLoot(loot);
+ executor.notify(String.format("Awarded level %s %s loot to %s.", luck, category, target.getName()), SYSTEM);
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/loot [luck]";
+ }
+
+ @Override
+ public boolean canExecute(CommandExecutor executor) {
+ return executor.isAdmin();
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/MuteCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/MuteCommand.java
similarity index 82%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/MuteCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/MuteCommand.java
index 9b0a9732..39cc651f 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/MuteCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/MuteCommand.java
@@ -1,6 +1,6 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
@@ -9,9 +9,11 @@
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
import brainwine.gameserver.util.DateTimeUtils;
+@CommandInfo(name = "mute", description = "Mutes a player, preventing them from chatting.", aliases = "silence")
public class MuteCommand extends Command {
@Override
@@ -58,21 +60,6 @@ public void execute(CommandExecutor executor, String[] args) {
target.getName(), endDate.format(DateTimeFormatter.RFC_1123_DATE_TIME), reason), SYSTEM);
}
- @Override
- public String getName() {
- return "mute";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] { "silence", };
- }
-
- @Override
- public String getDescription() {
- return "Mutes a player, preventing them from chatting.";
- }
-
@Override
public String getUsage(CommandExecutor executor) {
return "/mute [reason]";
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/PlayerIdCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/PlayerIdCommand.java
similarity index 77%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/PlayerIdCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/PlayerIdCommand.java
index 0b0f8def..978406d7 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/PlayerIdCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/PlayerIdCommand.java
@@ -1,12 +1,14 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "pid", description = "Displays the document id of a player.")
public class PlayerIdCommand extends Command {
@Override
@@ -34,16 +36,6 @@ public void execute(CommandExecutor executor, String[] args) {
executor.notify(target.getDocumentId(), SYSTEM);
}
- @Override
- public String getName() {
- return "pid";
- }
-
- @Override
- public String getDescription() {
- return "Displays the document id of a player.";
- }
-
@Override
public String getUsage(CommandExecutor executor) {
return String.format("/pid %s", executor instanceof Player ? "[player]" : "");
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/PositionCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/PositionCommand.java
similarity index 55%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/PositionCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/PositionCommand.java
index 1ddede5b..81ec1030 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/PositionCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/PositionCommand.java
@@ -1,11 +1,13 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "position", description = "Displays the coordinates of the block you are standing on.", aliases = { "pos", "feet", "coords", "location" })
public class PositionCommand extends Command {
@Override
@@ -13,21 +15,6 @@ public void execute(CommandExecutor executor, String[] args) {
Player player = (Player)executor;
player.notify(String.format("X: %s Y: %s", (int)player.getX(), (int)player.getY() + 1), SYSTEM);
}
-
- @Override
- public String getName() {
- return "pos";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] { "position", "feet", "coords", "location" };
- }
-
- @Override
- public String getDescription() {
- return "Displays the coordinates of the block you are standing on.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/PrefabListCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/PrefabListCommand.java
similarity index 78%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/PrefabListCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/PrefabListCommand.java
index 50ca8e3e..91988d34 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/PrefabListCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/PrefabListCommand.java
@@ -1,6 +1,6 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import java.util.ArrayList;
import java.util.List;
@@ -10,8 +10,10 @@
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
import brainwine.gameserver.prefab.Prefab;
+@CommandInfo(name = "prefabs", description = "Displays a list of all prefabs.")
public class PrefabListCommand extends Command {
@Override
@@ -34,21 +36,6 @@ public void execute(CommandExecutor executor, String[] args) {
executor.notify(prefab.getName(), SYSTEM);
}
}
-
- @Override
- public String getName() {
- return "prefabs";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] { "prefablist" };
- }
-
- @Override
- public String getDescription() {
- return "Displays a list of all prefabs.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/SeedCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/SeedCommand.java
similarity index 78%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/SeedCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/SeedCommand.java
index 5135e794..1e26ac1c 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/SeedCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/SeedCommand.java
@@ -1,13 +1,15 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
import brainwine.gameserver.zone.Zone;
+@CommandInfo(name = "seed", description = "Displays the seed of a zone.")
public class SeedCommand extends Command {
@Override
@@ -35,16 +37,6 @@ public void execute(CommandExecutor executor, String[] args) {
executor.notify("Seed: " + target.getSeed(), SYSTEM);
}
- @Override
- public String getName() {
- return "seed";
- }
-
- @Override
- public String getDescription() {
- return "Displays the seed of a zone.";
- }
-
@Override
public String getUsage(CommandExecutor executor) {
return String.format("/seed %s", executor instanceof Player ? "[zone]" : "");
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/SettleLiquidsCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/SettleLiquidsCommand.java
similarity index 61%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/SettleLiquidsCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/SettleLiquidsCommand.java
index b81d20a5..fd9d3873 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/SettleLiquidsCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/SettleLiquidsCommand.java
@@ -1,11 +1,13 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "settle", description = "Settles all liquids in all active chunks in the current zone. Warning - can cause lag!")
public class SettleLiquidsCommand extends Command {
@Override
@@ -19,18 +21,8 @@ public void execute(CommandExecutor executor, String[] args) {
}
@Override
- public String getName() {
- return "settleliquids";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] {"settle"};
- }
-
- @Override
- public String getDescription() {
- return "Settles all liquids in all active chunks in the current zone. Warning - can cause lag!";
+ public String getUsage(CommandExecutor executor) {
+ return "/settle";
}
@Override
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/SkillPointsCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/SkillPointsCommand.java
similarity index 80%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/SkillPointsCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/SkillPointsCommand.java
index f6abefc8..5430fe00 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/SkillPointsCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/SkillPointsCommand.java
@@ -1,12 +1,14 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "skillpoints", description = "Sets the skill points of the target player.", aliases = "points")
public class SkillPointsCommand extends Command {
@Override
@@ -54,21 +56,6 @@ public void execute(CommandExecutor executor, String[] args) {
target.notify(String.format("Your skill point count has been set to %s.", amount), SYSTEM);
executor.notify(String.format("Successfully set %s's skill point count to %s.", target.getName(), amount), SYSTEM);
}
-
- @Override
- public String getName() {
- return "skillpoints";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] { "points" };
- }
-
- @Override
- public String getDescription() {
- return "Sets the skill points of the target player.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/StopCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/StopCommand.java
similarity index 57%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/StopCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/StopCommand.java
index b7054adb..b0a3844b 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/StopCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/StopCommand.java
@@ -1,9 +1,11 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+@CommandInfo(name = "stop", description = "Gracefully shuts down the server.", aliases = { "exit", "close", "shutdown" })
public class StopCommand extends Command {
@Override
@@ -12,18 +14,8 @@ public void execute(CommandExecutor executor, String[] args) {
}
@Override
- public String getName() {
- return "stop";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] { "exit", "close", "shutdown" };
- }
-
- @Override
- public String getDescription() {
- return "Gracefully shuts down the server after the current tick.";
+ public String getUsage(CommandExecutor executor) {
+ return "/stop";
}
@Override
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/admin/TeleportCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/TeleportCommand.java
new file mode 100644
index 00000000..3f59df01
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/TeleportCommand.java
@@ -0,0 +1,127 @@
+package brainwine.gameserver.command.admin;
+
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
+
+import brainwine.gameserver.GameServer;
+import brainwine.gameserver.command.Command;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.player.PlayerManager;
+import brainwine.gameserver.zone.Zone;
+
+@CommandInfo(name = "teleport", description = "Teleports you or another player to the specified target position or player.", aliases = "tp")
+public class TeleportCommand extends Command {
+
+ @Override
+ public void execute(CommandExecutor executor, String[] args) {
+ if(args.length == 0 || args.length > 3) {
+ executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
+ return;
+ }
+
+ PlayerManager playerManager = GameServer.getInstance().getPlayerManager();
+ Player player = (Player)executor;
+ Player subject = player; // Player that is being teleported (executor by default)
+ Zone targetZone = subject.getZone();
+ int x = 0;
+ int y = 0;
+
+ if(args.length == 1) {
+ // Teleport executor to target player
+ Player target = playerManager.getPlayer(args[0]);
+
+ if(target == null) {
+ player.notify(String.format("Player '%s' not found.", args[0]));
+ return;
+ }
+
+ if(!target.isOnline()) {
+ player.notify(String.format("Player '%s' is not online.", target.getName()));
+ return;
+ }
+
+ if(subject == target) {
+ player.notify("You cannot teleport to yourself.");
+ return;
+ }
+
+ x = target.getBlockX();
+ y = target.getBlockY();
+ targetZone = target.getZone();
+ } else if(args.length == 2) {
+ // Teleport executor to target position OR teleport subject to player
+ try {
+ x = Integer.parseInt(args[0]);
+ y = Integer.parseInt(args[1]);
+ } catch(NumberFormatException e) {
+ // If first 2 params are not numbers then we are probably teleporting a player to another player
+ subject = playerManager.getPlayer(args[0]); // Do null check later
+ Player target = playerManager.getPlayer(args[1]);
+
+ if(target == null) {
+ player.notify(String.format("Player '%s' not found.", args[1]));
+ return;
+ }
+
+ if(!target.isOnline()) {
+ player.notify(String.format("Player '%s' is not online.", target.getName()));
+ return;
+ }
+
+ if(subject == target) {
+ player.notify("You cannot teleport a player to themselves.");
+ return;
+ }
+
+ x = target.getBlockX();
+ y = target.getBlockY();
+ targetZone = target.getZone();
+ }
+ } else if(args.length == 3) {
+ // Teleport subject to a position
+ subject = playerManager.getPlayer(args[0]); // Do null check later
+
+ try {
+ x = Integer.parseInt(args[1]);
+ y = Integer.parseInt(args[2]);
+ } catch(NumberFormatException e) {
+ player.notify("X and Y must be valid numbers.");
+ return;
+ }
+ }
+
+ // Check if subject is present
+ if(subject == null) {
+ player.notify(String.format("Player '%s' not found.", args[0])); // Subject is always first parameter so this should be fine
+ return;
+ }
+
+ if(!subject.isOnline()) {
+ player.notify(String.format("Player '%s' is not online.", subject.getName()));
+ return;
+ }
+
+ // Check if coordinates are in bounds
+ if(!targetZone.areCoordinatesInBounds(x, y)) {
+ player.notify("Cannot teleport out of bounds!", SYSTEM);
+ return;
+ }
+
+ if(targetZone == subject.getZone()) {
+ subject.teleport(x, y);
+ } else {
+ subject.changeZone(targetZone, x, y);
+ }
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/teleport [player] ";
+ }
+
+ @Override
+ public boolean canExecute(CommandExecutor executor) {
+ return executor instanceof Player && executor.isAdmin();
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/TimeCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/TimeCommand.java
similarity index 82%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/TimeCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/TimeCommand.java
index e61c24e1..6d949fda 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/TimeCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/TimeCommand.java
@@ -1,12 +1,14 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
import brainwine.gameserver.zone.Zone;
+@CommandInfo(name = "time", description = "Displays or changes the time in the current zone.")
public class TimeCommand extends Command {
@Override
@@ -45,16 +47,6 @@ public void execute(CommandExecutor executor, String[] args) {
zone.setTime(value);
executor.notify(String.format("Time has been set to %s in %s.", value, zone.getName()), SYSTEM);
}
-
- @Override
- public String getName() {
- return "time";
- }
-
- @Override
- public String getDescription() {
- return "Displays or changes the time in the current zone.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/UnbanCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/UnbanCommand.java
similarity index 73%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/UnbanCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/UnbanCommand.java
index eecb4ed1..8e2f675c 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/UnbanCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/UnbanCommand.java
@@ -1,12 +1,14 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "unban", description = "Unbans a player.", aliases = "pardon")
public class UnbanCommand extends Command {
@Override
@@ -31,21 +33,6 @@ public void execute(CommandExecutor executor, String[] args) {
target.unban(executor instanceof Player ? (Player)executor : null);
executor.notify(String.format("Player %s has been unbanned.", target.getName()), SYSTEM);
}
-
- @Override
- public String getName() {
- return "unban";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] { "pardon" };
- }
-
- @Override
- public String getDescription() {
- return "Unbans a player.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/UnmuteCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/UnmuteCommand.java
similarity index 78%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/UnmuteCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/UnmuteCommand.java
index 185a145c..f22e0984 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/UnmuteCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/UnmuteCommand.java
@@ -1,12 +1,14 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+@CommandInfo(name = "unmute", description = "Unmutes a player.")
public class UnmuteCommand extends Command {
@Override
@@ -31,16 +33,6 @@ public void execute(CommandExecutor executor, String[] args) {
target.unmute(executor instanceof Player ? (Player)executor : null);
executor.notify(String.format("Player %s has been unmuted.", target.getName()), SYSTEM);
}
-
- @Override
- public String getName() {
- return "unmute";
- }
-
- @Override
- public String getDescription() {
- return "Unmutes a player.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/WeatherCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/WeatherCommand.java
similarity index 80%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/WeatherCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/WeatherCommand.java
index e7027c79..618072b7 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/WeatherCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/WeatherCommand.java
@@ -1,14 +1,16 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
import brainwine.gameserver.zone.Biome;
import brainwine.gameserver.zone.WeatherManager;
import brainwine.gameserver.zone.Zone;
+@CommandInfo(name = "weather", description = "Displays or changes the weather in the current zone.")
public class WeatherCommand extends Command {
@Override
@@ -32,16 +34,6 @@ public void execute(CommandExecutor executor, String[] args) {
zone.getWeatherManager().createRandomRain(dry);
executor.notify(String.format("Weather has been %s in %s.", dry ? "cleared" : "made rainy", zone.getName()), SYSTEM);
}
-
- @Override
- public String getName() {
- return "weather";
- }
-
- @Override
- public String getDescription() {
- return "Displays or changes the weather in the current zone.";
- }
@Override
public String getUsage(CommandExecutor executor) {
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/ZoneIdCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/admin/ZoneIdCommand.java
similarity index 77%
rename from gameserver/src/main/java/brainwine/gameserver/command/commands/ZoneIdCommand.java
rename to gameserver/src/main/java/brainwine/gameserver/command/admin/ZoneIdCommand.java
index 11bf20a0..c4346441 100644
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/ZoneIdCommand.java
+++ b/gameserver/src/main/java/brainwine/gameserver/command/admin/ZoneIdCommand.java
@@ -1,13 +1,15 @@
-package brainwine.gameserver.command.commands;
+package brainwine.gameserver.command.admin;
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.command.Command;
import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
import brainwine.gameserver.zone.Zone;
+@CommandInfo(name = "zid", description = "Displays the document id of a zone.")
public class ZoneIdCommand extends Command {
@Override
@@ -35,16 +37,6 @@ public void execute(CommandExecutor executor, String[] args) {
executor.notify(target.getDocumentId(), SYSTEM);
}
- @Override
- public String getName() {
- return "zid";
- }
-
- @Override
- public String getDescription() {
- return "Displays the document id of a zone.";
- }
-
@Override
public String getUsage(CommandExecutor executor) {
return String.format("/zid %s", executor instanceof Player ? "[zone]" : "");
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/EntityCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/commands/EntityCommand.java
deleted file mode 100644
index f52ad8ee..00000000
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/EntityCommand.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package brainwine.gameserver.command.commands;
-
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
-
-import brainwine.gameserver.command.Command;
-import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.EntityConfig;
-import brainwine.gameserver.entity.EntityRegistry;
-import brainwine.gameserver.entity.npc.Npc;
-import brainwine.gameserver.entity.player.NotificationType;
-import brainwine.gameserver.entity.player.Player;
-import brainwine.gameserver.zone.Zone;
-
-public class EntityCommand extends Command {
-
- @Override
- public void execute(CommandExecutor executor, String[] args) {
- if(args.length == 0) {
- executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
- return;
- }
-
- Player player = (Player)executor;
- String name = args[0];
- EntityConfig config = EntityRegistry.getEntityConfig(name);
-
- if(config == null) {
- executor.notify(String.format("Entity with name '%s' does not exist.", name), NotificationType.SYSTEM);
- return;
- }
-
- Zone zone = player.getZone();
- zone.spawnEntity(new Npc(zone, config), (int)player.getX(), (int)player.getY(), true);
- }
-
- @Override
- public String getName() {
- return "entity";
- }
-
- @Override
- public String getDescription() {
- return "Spawns an entity at your current location.";
- }
-
- @Override
- public String getUsage(CommandExecutor executor) {
- return "/entity ";
- }
-
- @Override
- public boolean canExecute(CommandExecutor executor) {
- return executor.isAdmin() && executor instanceof Player;
- }
-}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/RickrollCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/commands/RickrollCommand.java
deleted file mode 100644
index 1113787e..00000000
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/RickrollCommand.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package brainwine.gameserver.command.commands;
-
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
-
-import brainwine.gameserver.GameServer;
-import brainwine.gameserver.command.Command;
-import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
-import brainwine.gameserver.server.messages.EventMessage;
-
-public class RickrollCommand extends Command {
-
- @Override
- public void execute(CommandExecutor executor, String[] args) {
- if(args.length < 1) {
- executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
- return;
- }
-
- Player player = GameServer.getInstance().getPlayerManager().getPlayer(args[0]);
-
- if(player == null) {
- executor.notify("This player does not exist.", SYSTEM);
- return;
- } else if(!player.isOnline()) {
- executor.notify("This player is offline.", SYSTEM);
- return;
- } else if(!player.isV3()) {
- executor.notify("Cannot open URLs on iOS clients.", SYSTEM);
- return;
- }
-
- player.sendMessage(new EventMessage("openUrl", "https://www.youtube.com/watch?v=dQw4w9WgXcQ"));
- executor.notify(String.format("Successfully rickrolled %s!", player.getName()), SYSTEM);
- }
-
- @Override
- public String getName() {
- return "rickroll";
- }
-
- @Override
- public String getDescription() {
- return "Makes a player hate you forever.";
- }
-
- @Override
- public String getUsage(CommandExecutor executor) {
- return "/rickroll ";
- }
-
- @Override
- public boolean canExecute(CommandExecutor executor) {
- return executor.isAdmin();
- }
-}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/TeleportCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/commands/TeleportCommand.java
deleted file mode 100644
index 387d0d67..00000000
--- a/gameserver/src/main/java/brainwine/gameserver/command/commands/TeleportCommand.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package brainwine.gameserver.command.commands;
-
-import static brainwine.gameserver.entity.player.NotificationType.SYSTEM;
-
-import brainwine.gameserver.command.Command;
-import brainwine.gameserver.command.CommandExecutor;
-import brainwine.gameserver.entity.player.Player;
-
-public class TeleportCommand extends Command {
-
- @Override
- public void execute(CommandExecutor executor, String[] args) {
- if(args.length != 2) {
- executor.notify(String.format("Usage: %s", getUsage(executor)), SYSTEM);
- return;
- }
-
- Player player = (Player)executor;
- int x = 0;
- int y = 0;
-
- try {
- x = Integer.parseInt(args[0]);
- y = Integer.parseInt(args[1]);
- } catch(NumberFormatException e) {
- player.notify("x and y must be numerical.", SYSTEM);
- return;
- }
-
- if(!player.getZone().areCoordinatesInBounds(x, y)) {
- player.notify("Cannot teleport out of bounds!", SYSTEM);
- return;
- }
-
- player.teleport(x, y);
- }
-
- @Override
- public String getName() {
- return "teleport";
- }
-
- @Override
- public String[] getAliases() {
- return new String[] { "tp" };
- }
-
- @Override
- public String getDescription() {
- return "Teleports you to the specified position.";
- }
-
- @Override
- public String getUsage(CommandExecutor executor) {
- return "/teleport ";
- }
-
- @Override
- public boolean canExecute(CommandExecutor executor) {
- return executor instanceof Player && executor.isAdmin();
- }
-}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/world/WorldAddCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldAddCommand.java
new file mode 100644
index 00000000..d3860a8d
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldAddCommand.java
@@ -0,0 +1,47 @@
+package brainwine.gameserver.command.world;
+
+import brainwine.gameserver.GameServer;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.Zone;
+
+@CommandInfo(name = "wadd", description = "Add a member to your private world.")
+public class WorldAddCommand extends WorldCommand {
+
+ @Override
+ public void execute(Zone zone, Player player, String[] args) {
+ if(!checkArgumentCount(player, args, 1)) {
+ return;
+ }
+
+ Player target = GameServer.getInstance().getPlayerManager().getPlayer(args[0]);
+
+ // Check if target exists
+ if(target == null) {
+ player.notify(String.format("Player '%s' not found.", args[0]));
+ return;
+ }
+
+ // World owners cannot add themselves as members
+ if(zone.isOwner(target)) {
+ player.notify("You own this world and cannot add yourself as a member.");
+ return;
+ }
+
+ // Check if target is already a member
+ if(zone.isMember(target)) {
+ player.notify(String.format("%s is already a member of this world.", target.getName()));
+ return;
+ }
+
+ // TODO send feedback to target
+ zone.addMember(target);
+ player.notify(String.format("%s has been added.", target.getName()));
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/wadd ";
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/world/WorldCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldCommand.java
new file mode 100644
index 00000000..7c46e0f6
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldCommand.java
@@ -0,0 +1,33 @@
+package brainwine.gameserver.command.world;
+
+import brainwine.gameserver.command.Command;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.Zone;
+
+/**
+ * Base class for commands regarding private world management.
+ */
+public abstract class WorldCommand extends Command {
+
+ public abstract void execute(Zone zone, Player player, String[] args);
+
+ @Override
+ public void execute(CommandExecutor executor, String[] args) {
+ Player player = (Player)executor;
+ Zone zone = player.getZone();
+
+ // Check if player owns world
+ if(!player.isGodMode() && !zone.isOwner(player)) {
+ player.notify("Sorry, you do not own this world.");
+ return;
+ }
+
+ execute(zone, player, args);
+ }
+
+ @Override
+ public boolean canExecute(CommandExecutor executor) {
+ return executor instanceof Player;
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/world/WorldEnterCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldEnterCommand.java
new file mode 100644
index 00000000..21ca9c40
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldEnterCommand.java
@@ -0,0 +1,57 @@
+package brainwine.gameserver.command.world;
+
+import brainwine.gameserver.GameServer;
+import brainwine.gameserver.command.Command;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.Zone;
+
+@CommandInfo(name = "wenter", description = "Enter a world with a specific entry code.")
+public class WorldEnterCommand extends Command {
+
+ @Override
+ public void execute(CommandExecutor executor, String[] args) {
+ if(!checkArgumentCount(executor, args, 1)) {
+ return;
+ }
+
+ Player player = (Player)executor;
+ String entryCode = args[0];
+ Zone zone = GameServer.getInstance().getZoneManager().getZoneByEntryCode(entryCode);
+
+ // Check if zone exists
+ if(zone == null) {
+ player.notify("Can't find a zone for that code.");
+ return;
+ }
+
+ // Check if player is already a member of the target zone
+ if(zone.isOwner(player) || zone.isMember(player)) {
+ player.notify(String.format("You're already a member of %s.\nFind yourself a teleporter.", zone.getName()));
+ return;
+ }
+
+ // Add player to zone
+ if(!zone.isOwned()) {
+ zone.setOwner(player);
+ } else {
+ zone.addMember(player);
+ }
+
+ // Send player to zone if they're not there already
+ if(zone != player.getZone()) {
+ player.changeZone(zone);
+ }
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/wenter
";
+ }
+
+ @Override
+ public boolean canExecute(CommandExecutor executor) {
+ return executor instanceof Player;
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/world/WorldHelpCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldHelpCommand.java
new file mode 100644
index 00000000..7a50173d
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldHelpCommand.java
@@ -0,0 +1,21 @@
+package brainwine.gameserver.command.world;
+
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.dialog.DialogHelper;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.Zone;
+
+@CommandInfo(name = "whelp", description = "Displays the world help menu.")
+public class WorldHelpCommand extends WorldCommand {
+
+ @Override
+ public void execute(Zone zone, Player player, String[] args) {
+ player.showDialog(DialogHelper.getDialog("world_help"));
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/whelp";
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/world/WorldInfoCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldInfoCommand.java
new file mode 100644
index 00000000..bc322cee
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldInfoCommand.java
@@ -0,0 +1,41 @@
+package brainwine.gameserver.command.world;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import brainwine.gameserver.GameServer;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.dialog.Dialog;
+import brainwine.gameserver.dialog.DialogSection;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.Zone;
+
+@CommandInfo(name = "winfo", description = "Displays private world information.")
+public class WorldInfoCommand extends WorldCommand {
+
+ @Override
+ public void execute(Zone zone, Player player, String[] args) {
+ // Fetch member names
+ List memberNames = zone.getMembers().stream()
+ .map(x -> GameServer.getInstance().getPlayerManager().getPlayerById(x).getName())
+ .collect(Collectors.toList());
+
+ // Create & show world info dialog
+ // TODO there's gotta be a cleaner way to do this...
+ Dialog dialog = new Dialog()
+ .addSection(new DialogSection().setTitle("World Info"))
+ .addSection(new DialogSection().setText(" "))
+ .addSection(new DialogSection().setText(player.isV3() ? "Entry Code" : "Entry Code").setTextColor("4d5b82"))
+ .addSection(new DialogSection().setText(zone.hasEntryCode() ? zone.getEntryCode() : "Use /wrecode to generate an entry code"))
+ .addSection(new DialogSection().setText(" "))
+ .addSection(new DialogSection().setText(player.isV3() ? "Members" : "Members").setTextColor("4d5b82"))
+ .addSection(new DialogSection().setText(memberNames.isEmpty() ? "None :(" : memberNames.toString().replaceAll("[\\[+\\]]", "")));
+ player.showDialog(dialog);
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/winfo";
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/world/WorldProtectedCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldProtectedCommand.java
new file mode 100644
index 00000000..4772aedb
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldProtectedCommand.java
@@ -0,0 +1,59 @@
+package brainwine.gameserver.command.world;
+
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.dialog.DialogHelper;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.Zone;
+
+@CommandInfo(name = "wprotected", description = "Turn off the protected status of a private world.")
+public class WorldProtectedCommand extends WorldCommand {
+
+ @Override
+ public void execute(Zone zone, Player player, String[] args) {
+ if(!checkArgumentCount(player, args, 1)) {
+ return;
+ }
+
+ if(!args[0].equalsIgnoreCase("on") && !args[0].equalsIgnoreCase("off")) {
+ sendUsageMessage(player);
+ return;
+ }
+
+ boolean value = args[0].equalsIgnoreCase("on");
+
+ if(value == zone.isProtected()) {
+ player.notify(String.format("Your world is already %s.", value ? "protected" : "unprotected"));
+ return;
+ }
+
+ if(!value) {
+ // Show confirmation dialog
+ player.showDialog(DialogHelper.messageDialog("Are you sure?",
+ "WARNING: You cannot revert back to protected status once your world is unprotected. Are you sure you want to make it unprotected?")
+ .setActions("yesno"), input -> {
+ // Check cancellation
+ if(input.length == 1 && "cancel".equals(input[0])) {
+ return;
+ }
+
+ // Disable world protection
+ zone.setProtected(false);
+ });
+ } else {
+ // Deny request if player is not in god mode
+ if(!player.isGodMode()) {
+ player.notify("Sorry, you cannot revert your world back to protected status.");
+ return;
+ }
+
+ // Enable world protection
+ zone.setProtected(true);
+ }
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return executor.isAdmin() ? "/wprotected " : "/wprotected off";
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/world/WorldPublicCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldPublicCommand.java
new file mode 100644
index 00000000..6a415375
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldPublicCommand.java
@@ -0,0 +1,47 @@
+package brainwine.gameserver.command.world;
+
+import java.time.temporal.ChronoUnit;
+
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.Zone;
+
+@CommandInfo(name = "wpublic", description = "Toggle world accessibility.")
+public class WorldPublicCommand extends WorldCommand {
+
+ public static final String ACTION_ID = "wpublic";
+
+ @Override
+ public void execute(Zone zone, Player player, String[] args) {
+ if(!checkArgumentCount(player, args, 1)) {
+ return;
+ }
+
+ // Check if command is on cooldown
+ if(!player.isGodMode() && zone.isActionOnCooldown(ACTION_ID, 1, ChronoUnit.HOURS)) {
+ player.notify("Sorry, you can toggle accessibility only once an hour.");
+ return;
+ }
+
+ if(!args[0].equalsIgnoreCase("on") && !args[0].equalsIgnoreCase("off")) {
+ sendUsageMessage(player);
+ return;
+ }
+
+ boolean value = args[0].equalsIgnoreCase("on");
+
+ if(value == zone.isPublic()) {
+ player.notify(String.format("Your world is already %s.", value ? "public" : "private"));
+ return;
+ }
+
+ zone.setPrivate(!value);
+ zone.recordActionTime(ACTION_ID);
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/wpublic ";
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/world/WorldPvpCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldPvpCommand.java
new file mode 100644
index 00000000..bca69332
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldPvpCommand.java
@@ -0,0 +1,47 @@
+package brainwine.gameserver.command.world;
+
+import java.time.temporal.ChronoUnit;
+
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.Zone;
+
+@CommandInfo(name = "wpvp", description = "Turn PvP on or off in a private world.")
+public class WorldPvpCommand extends WorldCommand {
+
+ public static final String ACTION_ID = "wpvp";
+
+ @Override
+ public void execute(Zone zone, Player player, String[] args) {
+ if(!checkArgumentCount(player, args, 1)) {
+ return;
+ }
+
+ // Check if command is on cooldown
+ if(!player.isGodMode() && zone.isActionOnCooldown(ACTION_ID, 1, ChronoUnit.HOURS)) {
+ player.notify("Sorry, you can toggle PvP only once an hour.");
+ return;
+ }
+
+ if(!args[0].equalsIgnoreCase("on") && !args[0].equalsIgnoreCase("off")) {
+ sendUsageMessage(player);
+ return;
+ }
+
+ boolean value = args[0].equalsIgnoreCase("on");
+
+ if(value == zone.isPvp()) {
+ player.notify(String.format("PvP is already %s.", value ? "enabled" : "disabled"));
+ return;
+ }
+
+ zone.setPvp(value);
+ zone.recordActionTime(ACTION_ID);
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/wpvp ";
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/world/WorldRecodeCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldRecodeCommand.java
new file mode 100644
index 00000000..402d8824
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldRecodeCommand.java
@@ -0,0 +1,27 @@
+package brainwine.gameserver.command.world;
+
+import brainwine.gameserver.GameServer;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.Zone;
+
+@CommandInfo(name = "wrecode", description = "Issues a new entry code for your private world.")
+public class WorldRecodeCommand extends WorldCommand {
+
+ @Override
+ public void execute(Zone zone, Player player, String[] args) {
+ // Try to generate a new entry code
+ if(!GameServer.getInstance().getZoneManager().issueEntryCode(zone)) {
+ player.notify("Unable to change the entry code, please try again.");
+ return;
+ }
+
+ player.notify(String.format("Your world entry code has been changed to %s.", zone.getEntryCode()));
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/wrecode";
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/world/WorldRemoveCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldRemoveCommand.java
new file mode 100644
index 00000000..92b2eacb
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldRemoveCommand.java
@@ -0,0 +1,40 @@
+package brainwine.gameserver.command.world;
+
+import brainwine.gameserver.GameServer;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.Zone;
+
+@CommandInfo(name = "wremove", description = "Remove a member from your private world.")
+public class WorldRemoveCommand extends WorldCommand {
+
+ @Override
+ public void execute(Zone zone, Player player, String[] args) {
+ if(!checkArgumentCount(player, args, 1)) {
+ return;
+ }
+
+ Player target = GameServer.getInstance().getPlayerManager().getPlayer(args[0]);
+
+ // Check if target exists
+ if(target == null) {
+ player.notify(String.format("Player '%s' not found.", args[0]));
+ return;
+ }
+
+ // Check if target is not a member
+ if(!zone.isMember(target)) {
+ player.notify(String.format("%s is not a member of this world.", target.getName()));
+ return;
+ }
+
+ zone.removeMember(target);
+ player.notify(String.format("%s has been removed.", target.getName()));
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/wremove ";
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/command/world/WorldRenameCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldRenameCommand.java
new file mode 100644
index 00000000..4bd23488
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/command/world/WorldRenameCommand.java
@@ -0,0 +1,67 @@
+package brainwine.gameserver.command.world;
+
+import static brainwine.gameserver.player.NotificationType.SYSTEM;
+
+import java.time.temporal.ChronoUnit;
+import java.util.regex.Pattern;
+
+import brainwine.gameserver.GameServer;
+import brainwine.gameserver.command.CommandExecutor;
+import brainwine.gameserver.command.CommandInfo;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.zone.Zone;
+import brainwine.gameserver.zone.ZoneManager;
+
+@CommandInfo(name = "wrename", description = "Rename your private world.")
+public class WorldRenameCommand extends WorldCommand {
+
+ public static final String ACTION_ID = "wrename";
+ private static final Pattern NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9 ]{5,20}$");
+
+ @Override
+ public void execute(Zone zone, Player player, String[] args) {
+ if(!checkArgumentCount(player, args, 1)) {
+ return;
+ }
+
+ // Check if command is on cooldown
+ if(!player.isGodMode() && zone.isActionOnCooldown(ACTION_ID, 1, ChronoUnit.DAYS)) {
+ player.notify("Sorry, you can rename your world only once a day.");
+ return;
+ }
+
+ ZoneManager zoneManager = GameServer.getInstance().getZoneManager();
+ String name = String.join(" ", args).trim().replaceAll(" +", " ");
+
+ // Verify name length
+ if(name.length() < 5 || name.length() > 20) {
+ player.notify("World name must be between 5 and 20 characters.");
+ return;
+ }
+
+ // Verify name pattern
+ if(!NAME_PATTERN.matcher(name).matches()) {
+ player.notify("World name can only contain letters and numbers.");
+ return;
+ }
+
+ // Check if name already exists
+ if(zoneManager.doesZoneExist(name)) {
+ player.notify(String.format("World name '%s' is already taken.", name));
+ return;
+ }
+
+ // Try rename zone (shouldn't fail)
+ if(!zoneManager.renameZone(zone, name)) {
+ player.notify("An unexpected problem occured while renaming your world.", SYSTEM);
+ return;
+ }
+
+ zone.recordActionTime(ACTION_ID);
+ }
+
+ @Override
+ public String getUsage(CommandExecutor executor) {
+ return "/wrename ";
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/dialog/Dialog.java b/gameserver/src/main/java/brainwine/gameserver/dialog/Dialog.java
index d931eed2..cd2690c0 100644
--- a/gameserver/src/main/java/brainwine/gameserver/dialog/Dialog.java
+++ b/gameserver/src/main/java/brainwine/gameserver/dialog/Dialog.java
@@ -1,12 +1,15 @@
package brainwine.gameserver.dialog;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonSetter;
@JsonInclude(Include.NON_DEFAULT)
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -15,6 +18,7 @@ public class Dialog {
private DialogType type = DialogType.STANDARD;
private DialogAlignment alignment = DialogAlignment.LEFT;
private List sections = new ArrayList<>();
+ private Object actions;
private String title;
private String target;
@@ -61,6 +65,29 @@ public List getSections() {
return sections;
}
+ @JsonSetter
+ private void setActions(Object actions) {
+ this.actions = actions;
+ }
+
+ public Dialog setActions(String actions) {
+ this.actions = actions;
+ return this;
+ }
+
+ public Dialog setActions(String... actions) {
+ return setActions(Arrays.asList(actions));
+ }
+
+ public Dialog setActions(Collection actions) {
+ this.actions = actions;
+ return this;
+ }
+
+ public Object getActions() {
+ return actions;
+ }
+
public Dialog setTitle(String title) {
this.title = title;
return this;
diff --git a/gameserver/src/main/java/brainwine/gameserver/dialog/DialogSection.java b/gameserver/src/main/java/brainwine/gameserver/dialog/DialogSection.java
index 9374d8a5..813795b3 100644
--- a/gameserver/src/main/java/brainwine/gameserver/dialog/DialogSection.java
+++ b/gameserver/src/main/java/brainwine/gameserver/dialog/DialogSection.java
@@ -3,13 +3,15 @@
import java.util.ArrayList;
import java.util.List;
+import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonFormat.Shape;
import brainwine.gameserver.dialog.input.DialogInput;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
+import brainwine.gameserver.util.Vector2i;
@JsonInclude(Include.NON_DEFAULT)
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -18,8 +20,10 @@ public class DialogSection {
private List items = new ArrayList<>();
private String title;
private String text;
+ private String choice;
private String textColor;
private double textScale;
+ private Vector2i location;
private DialogInput input;
public DialogSection addItem(DialogListItem item) {
@@ -50,6 +54,15 @@ public String getText() {
return text;
}
+ public DialogSection setChoice(String choice) {
+ this.choice = choice;
+ return this;
+ }
+
+ public String getChoice() {
+ return choice;
+ }
+
/**
* v2 clients only!
* For v3 clients, use {@link #setText} with HTML color tags.
@@ -75,6 +88,20 @@ public double getTextScale() {
return textScale;
}
+ /**
+ * v2 clients only!
+ */
+ public DialogSection setLocation(int x, int y) {
+ this.location = new Vector2i(x, y);
+ return this;
+ }
+
+ @JsonProperty("map")
+ @JsonFormat(shape = Shape.ARRAY)
+ public Vector2i getLocation() {
+ return location;
+ }
+
public DialogSection setInput(DialogInput input) {
this.input = input;
return this;
diff --git a/gameserver/src/main/java/brainwine/gameserver/dialog/input/DialogSelectInput.java b/gameserver/src/main/java/brainwine/gameserver/dialog/input/DialogSelectInput.java
index cffab2c4..39ce7672 100644
--- a/gameserver/src/main/java/brainwine/gameserver/dialog/input/DialogSelectInput.java
+++ b/gameserver/src/main/java/brainwine/gameserver/dialog/input/DialogSelectInput.java
@@ -3,9 +3,12 @@
import java.util.Arrays;
import java.util.Collection;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
public class DialogSelectInput extends DialogInput {
private Collection options;
+ private int maxColumns;
public DialogSelectInput setOptions(String... options) {
return setOptions(Arrays.asList(options));
@@ -19,4 +22,14 @@ public DialogSelectInput setOptions(Collection options) {
public Collection getOptions() {
return options;
}
+
+ public DialogSelectInput setMaxColumns(int maxColumns) {
+ this.maxColumns = maxColumns;
+ return this;
+ }
+
+ @JsonProperty("max columns")
+ public int getMaxColumns() {
+ return maxColumns;
+ }
}
diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java
index 202e395b..a4775d86 100644
--- a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java
@@ -1,16 +1,25 @@
package brainwine.gameserver.entity;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.item.DamageType;
+import brainwine.gameserver.item.Item;
+import brainwine.gameserver.item.ItemUseType;
+import brainwine.gameserver.item.Layer;
+import brainwine.gameserver.minigame.Minigame;
+import brainwine.gameserver.player.Player;
import brainwine.gameserver.server.Message;
+import brainwine.gameserver.server.messages.EffectMessage;
import brainwine.gameserver.server.messages.EntityChangeMessage;
import brainwine.gameserver.server.messages.EntityStatusMessage;
import brainwine.gameserver.util.MapHelper;
import brainwine.gameserver.util.MathUtils;
+import brainwine.gameserver.zone.Block;
+import brainwine.gameserver.zone.MetaBlock;
import brainwine.gameserver.zone.Zone;
public abstract class Entity {
@@ -18,8 +27,11 @@ public abstract class Entity {
public static final float DEFAULT_HEALTH = 5;
public static final float POSITION_MODIFIER = 100F;
public static final int VELOCITY_MODIFIER = (int)POSITION_MODIFIER;
+ public static final int ATTACK_RETENTION_TIME = 2000;
+ public static final int ATTACK_INVINCIBLE_TIME = 333;
protected final Map properties = new HashMap<>();
protected final List trackers = new ArrayList<>();
+ protected final List recentAttacks = new ArrayList<>();
protected int type;
protected String name;
protected float health = DEFAULT_HEALTH;
@@ -29,10 +41,19 @@ public abstract class Entity {
protected float y;
protected float velocityX;
protected float velocityY;
+ protected int blockX;
+ protected int blockY;
+ protected int lastBlockX;
+ protected int lastBlockY;
protected int targetX;
protected int targetY;
+ protected int sizeX = 1;
+ protected int sizeY = 1;
protected FacingDirection direction = FacingDirection.WEST;
protected int animation;
+ protected boolean invulnerable;
+ protected Minigame minigame;
+ protected EntityAttack lastAttack; // Used for tracking in entity deaths -- do not use this for anything else!
protected long lastDamagedAt;
public Entity(Zone zone) {
@@ -40,10 +61,20 @@ public Entity(Zone zone) {
}
public void tick(float deltaTime) {
+ long now = System.currentTimeMillis();
+
+ // Update block position
+ updateBlockPosition();
+
+ // Clear expired recent attacks
+ recentAttacks.removeIf(attack -> now >= attack.getTime() + ATTACK_RETENTION_TIME);
+ }
+
+ public void die(EntityAttack cause) {
// Override
}
- public void die(Player killer) {
+ public void attacked(EntityAttack attack, float damage) {
// Override
}
@@ -53,18 +84,93 @@ public void heal(float amount) {
}
}
- public void damage(float amount) {
- damage(amount, null);
+ public void attack(Entity attacker, Item weapon, float baseDamage, DamageType damageType) {
+ attack(attacker, weapon, baseDamage, damageType, false);
}
- public void damage(float amount, Player attacker) {
- setHealth(health - amount);
+ public void attack(Entity attacker, Item weapon, float baseDamage, DamageType damageType, boolean trueDamage) {
+ // Ignore attack if entity is dead or invulnerable
+ if(isDead() || isInvulnerable()) {
+ return;
+ }
- if(health <= 0) {
- die(attacker);
+ // Ignore attack if there is no damage to deal
+ if(baseDamage <= 0 || damageType == null || damageType == DamageType.NONE) {
+ return;
}
+ EntityAttack attack = new EntityAttack(attacker, weapon, baseDamage, damageType);
+ recentAttacks.add(attack);
+ lastAttack = attack;
lastDamagedAt = System.currentTimeMillis();
+
+ // Kill entity if attacker is a player in god mode
+ if(attacker != null && attacker.isPlayer() && ((Player)attacker).isGodMode()) {
+ setHealth(0.0F);
+ return;
+ }
+
+ // Ignore multipliers if true damage should be dealt
+ if(trueDamage) {
+ setHealth(health - baseDamage);
+ return;
+ }
+
+ float attackMultiplier = attacker != null ? Math.max(0.0F, attacker.getAttackMultiplier(attack)) : 1.0F;
+ float defense = Math.max(0.0F, 1.0F - getDefense(attack));
+ float damage = baseDamage * attackMultiplier * defense;
+ setHealth(health - damage);
+ }
+
+ public float getAttackMultiplier(EntityAttack attack) {
+ return 1.0F; // Override
+ }
+
+ public float getDefense(EntityAttack attack) {
+ return 1.0F; // Override
+ }
+
+ public void spawnEffect(String type) {
+ spawnEffect(type, 1);
+ }
+
+ public void spawnEffect(String type, Object data) {
+ float effectX = x + sizeX / 2.0F;
+ float effectY = y + sizeY / 2.0F;
+ sendMessageToTrackers(new EffectMessage(effectX, effectY, type, data));
+ }
+
+ public void emote(String message) {
+ float effectX = x + sizeX / 2.0F;
+ float effectY = y - sizeY + 1;
+ sendMessageToTrackers(new EffectMessage(effectX, effectY, "emote", message));
+ }
+
+ public void updateBlockPosition() {
+ lastBlockX = blockX;
+ lastBlockY = blockY;
+ blockX = (int)x;
+ blockY = (int)y;
+
+ // Check if block position has changed
+ if(lastBlockX != blockX || lastBlockY != blockY) {
+ blockPositionChanged();
+ }
+ }
+
+ public void blockPositionChanged() {
+ // Check for touchplates
+ if(zone != null && zone.isChunkLoaded(blockX, blockY)) {
+ MetaBlock metaBlock = zone.getMetaBlock(blockX, blockY);
+ Block block = zone.getBlock(blockX, blockY);
+ Item item = block.getFrontItem();
+ int mod = block.getFrontMod();
+
+ // Trigger a switch interaction if the entity stepped on a touchplate
+ if(item.hasUse(ItemUseType.TRIGGER)) {
+ ItemUseType.SWITCH.getInteraction().interact(zone, this, blockX, blockY, Layer.FRONT, item, mod, metaBlock, null, null);
+ }
+ }
}
public boolean canSee(Entity other) {
@@ -80,7 +186,7 @@ public boolean inRange(Entity other, float range) {
return inRange(other.getX(), other.getY(), range);
}
- public boolean inRange(float x, float y, float range) {
+ public boolean inRange(float x, float y, double range) {
return MathUtils.inRange(this.x, this.y, x, y, range);
}
@@ -128,6 +234,18 @@ public List getTrackers() {
return trackers;
}
+ public boolean wasAttackedRecently(Entity entity, int delay) {
+ return recentAttacks.stream().filter(attack -> attack.getAttacker() == entity && System.currentTimeMillis() < attack.getTime() + delay).findFirst().isPresent();
+ }
+
+ public EntityAttack getMostRecentAttack() {
+ return recentAttacks.isEmpty() ? null : recentAttacks.get(recentAttacks.size() - 1);
+ }
+
+ public List getRecentAttacks() {
+ return Collections.unmodifiableList(recentAttacks);
+ }
+
public void setId(int id) {
this.id = id;
}
@@ -157,8 +275,18 @@ public boolean isDead() {
}
public void setHealth(float health) {
- float maxHealth = getMaxHealth();
- this.health = health < 0 ? 0 : health > maxHealth ? maxHealth : health;
+ float damage = this.health - Math.max(0.0F, health);
+ this.health = Math.max(0.0F, Math.min(getMaxHealth(), health));
+
+ if(lastAttack != null) {
+ attacked(lastAttack, damage);
+ }
+
+ if(this.health <= 0.0F) {
+ die(lastAttack);
+ }
+
+ lastAttack = null;
}
public float getHealth() {
@@ -168,6 +296,7 @@ public float getHealth() {
public void setPosition(float x, float y) {
this.x = x;
this.y = y;
+ updateBlockPosition();
}
public float getX() {
@@ -204,6 +333,26 @@ public int getTargetY() {
return targetY;
}
+ public int getBlockX() {
+ return blockX;
+ }
+
+ public int getBlockY() {
+ return blockY;
+ }
+
+ public int getSizeX() {
+ return sizeX;
+ }
+
+ public int getSizeY() {
+ return sizeY;
+ }
+
+ public void setDirection(int direction) {
+ setDirection(direction > 0 ? FacingDirection.EAST : direction < 0 ? FacingDirection.WEST : this.direction);
+ }
+
public void setDirection(FacingDirection direction) {
this.direction = direction;
}
@@ -220,6 +369,26 @@ public int getAnimation() {
return animation;
}
+ public void setInvulnerable(boolean invulnerable) {
+ this.invulnerable = invulnerable;
+ }
+
+ public boolean isInvulnerable() {
+ return invulnerable;
+ }
+
+ public void setMinigame(Minigame minigame) {
+ this.minigame = minigame;
+ }
+
+ public boolean hasActiveMinigame() {
+ return minigame != null && minigame.isActive();
+ }
+
+ public Minigame getMinigame() {
+ return minigame;
+ }
+
public void setZone(Zone zone) {
this.zone = zone;
}
@@ -228,6 +397,10 @@ public Zone getZone() {
return zone;
}
+ public final boolean isPlayer() {
+ return this instanceof Player; // Not very OOP
+ }
+
/**
* @return A {@link Map} containing all the data necessary for use in {@link EntityStatusMessage}.
*/
diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/EntityAttack.java b/gameserver/src/main/java/brainwine/gameserver/entity/EntityAttack.java
new file mode 100644
index 00000000..70ccebfe
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/EntityAttack.java
@@ -0,0 +1,41 @@
+package brainwine.gameserver.entity;
+
+import brainwine.gameserver.item.DamageType;
+import brainwine.gameserver.item.Item;
+
+public class EntityAttack {
+
+ private final Entity attacker;
+ private final Item weapon;
+ private final float baseDamage;
+ private final DamageType damageType;
+ private final long time;
+
+ public EntityAttack(Entity attacker, Item weapon, float baseDamage, DamageType damageType) {
+ this.attacker = attacker;
+ this.weapon = weapon;
+ this.baseDamage = baseDamage;
+ this.damageType = damageType;
+ this.time = System.currentTimeMillis();
+ }
+
+ public Entity getAttacker() {
+ return attacker;
+ }
+
+ public Item getWeapon() {
+ return weapon;
+ }
+
+ public float getBaseDamage() {
+ return baseDamage;
+ }
+
+ public DamageType getDamageType() {
+ return damageType;
+ }
+
+ public long getTime() {
+ return time;
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java b/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java
index 9fec3615..ba6c7fb4 100644
--- a/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java
@@ -26,9 +26,15 @@ public class EntityConfig {
private final String name;
private final int type;
+ private String title = "Unknown";
private int experienceYield;
private float maxHealth = Entity.DEFAULT_HEALTH;
private float baseSpeed = 3;
+ private boolean character;
+ private boolean human;
+ private boolean named;
+ private boolean trappable;
+ private Item trappablePetItem;
private Vector2i size = new Vector2i(1, 1);
private EntityGroup group = EntityGroup.NONE;
private WeightedMap loot = new WeightedMap<>();
@@ -63,7 +69,11 @@ public String getName() {
public int getType() {
return type;
}
-
+
+ public String getTitle() {
+ return title;
+ }
+
@JsonProperty("xp")
public int getExperienceYield() {
return experienceYield;
@@ -79,6 +89,30 @@ public float getBaseSpeed() {
return baseSpeed;
}
+ public boolean isCharacter() {
+ return character;
+ }
+
+ public boolean isHuman() {
+ return human;
+ }
+
+ public boolean isNamed() {
+ return named;
+ }
+
+ public boolean isTrappable() {
+ return trappable;
+ }
+
+ public boolean hasTrappablePetItem() {
+ return trappablePetItem != null && !trappablePetItem.isAir();
+ }
+
+ public Item getTrappablePetItem() {
+ return trappablePetItem;
+ }
+
@JsonSetter(nulls = Nulls.SKIP)
private void setSize(Vector2i size) {
this.size = size;
diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java
index 9953c74b..e60ed8c0 100644
--- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java
@@ -9,18 +9,21 @@
import java.util.Map.Entry;
import java.util.concurrent.ThreadLocalRandom;
-import brainwine.gameserver.behavior.SequenceBehavior;
+import brainwine.gameserver.Naming;
import brainwine.gameserver.entity.Entity;
+import brainwine.gameserver.entity.EntityAttack;
import brainwine.gameserver.entity.EntityConfig;
import brainwine.gameserver.entity.EntityLoot;
import brainwine.gameserver.entity.EntityRegistry;
import brainwine.gameserver.entity.FacingDirection;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.entity.npc.behavior.SequenceBehavior;
import brainwine.gameserver.item.DamageType;
import brainwine.gameserver.item.Item;
import brainwine.gameserver.item.Layer;
+import brainwine.gameserver.minigame.Minigame;
+import brainwine.gameserver.player.Appearance;
+import brainwine.gameserver.player.Player;
import brainwine.gameserver.util.MapHelper;
-import brainwine.gameserver.util.Pair;
import brainwine.gameserver.util.Vector2i;
import brainwine.gameserver.util.WeightedMap;
import brainwine.gameserver.zone.MetaBlock;
@@ -28,13 +31,11 @@
public class Npc extends Entity {
- public static final int ATTACK_RETENTION_TIME = 2000;
- public static final int ATTACK_INVINCIBLE_TIME = 333;
private final EntityConfig config;
private final String typeName;
private final float maxHealth;
private final float baseSpeed;
- private final Vector2i size;
+ private final boolean persist;
private final WeightedMap loot;
private final WeightedMap placedLoot;
private final Map- > lootByWeapon;
@@ -43,7 +44,6 @@ public class Npc extends Entity {
private final List animations;
private final SequenceBehavior behaviorTree;
private final Map activeDefenses = new HashMap<>();
- private final Map> recentAttacks = new HashMap<>();
private final List children = new ArrayList<>();
private float speed;
private int moveX;
@@ -52,6 +52,7 @@ public class Npc extends Entity {
private Vector2i mountBlock;
private Entity owner;
private Entity target;
+ private boolean artificial;
private long lastBehavedAt = System.currentTimeMillis();
private long lastTrackedAt = System.currentTimeMillis();
@@ -100,12 +101,24 @@ public Npc(Zone zone, EntityConfig config) {
properties.put("sl", slots);
}
+ // Generate random name
+ if(config.isNamed()) {
+ this.name = Naming.getRandomEntityName();
+ }
+
+ // Generate random appearance
+ if(config.isHuman()) {
+ properties.putAll(Appearance.getRandomAppearance());
+ }
+
this.config = config;
this.typeName = config.getName();
this.type = config.getType();
this.maxHealth = config.getMaxHealth();
this.baseSpeed = config.getBaseSpeed();
- this.size = config.getSize();
+ this.persist = config.isCharacter();
+ this.sizeX = config.getSize().getX();
+ this.sizeY = config.getSize().getY();
this.loot = config.getLoot();
this.placedLoot = config.getPlacedLoot();
this.lootByWeapon = config.getLootByWeapon();
@@ -120,11 +133,9 @@ public Npc(Zone zone, EntityConfig config) {
@Override
public void tick(float deltaTime) {
+ super.tick(deltaTime);
long now = System.currentTimeMillis();
- // Clear expired recent attacks
- recentAttacks.values().removeIf(attack -> now >= attack.getLast() + ATTACK_RETENTION_TIME);
-
// Tick behavior when it is ready
if(now >= lastBehavedAt + (int)(1000 / speed)) {
lastBehavedAt = now;
@@ -146,43 +157,69 @@ public void tick(float deltaTime) {
}
@Override
- public void die(Player killer) {
+ public void die(EntityAttack cause) {
+ // Remove itself from the guard block metadata if it was guarding one
+ if(isGuard()) {
+ MetaBlock metaBlock = zone.getMetaBlock(guardBlock.getX(), guardBlock.getY());
+
+ if(metaBlock != null) {
+ List guards = MapHelper.getList(metaBlock.getMetadata(), "!");
+
+ if(guards != null) {
+ guards.remove(typeName);
+ }
+ }
+ }
+
+ // Destroy mount block if it has one
+ if(isMounted()) {
+ zone.updateBlock(mountBlock.getX(), mountBlock.getY(), Layer.FRONT, 0);
+ }
+
+ // Do nothing else if cause data isn't present
+ if(cause == null) {
+ return;
+ }
+
+ Entity killer = cause.getAttacker();
+
// Grant loot & track kill
- if(killer != null) {
+ if(!artificial && killer != null && killer.isPlayer()) {
+ Player player = (Player)killer;
+
if(!isPlayerPlaced()) {
// Track assists
- for(Player attacker : recentAttacks.keySet()) {
- if(attacker != killer) {
- attacker.getStatistics().trackAssist(config);
- }
- }
+ recentAttacks.stream()
+ .filter(attack -> attack.getAttacker() != killer && attack.getAttacker() instanceof Player)
+ .map(attack -> (Player)attack.getAttacker())
+ .distinct() // TODO might be expensive
+ .forEach(attacker -> attacker.getStatistics().trackAssist(config));
- killer.getStatistics().trackKill(config);
+ // Track kill
+ player.getStatistics().trackKill(config);
}
- EntityLoot loot = getRandomLoot(killer);
+ EntityLoot loot = getRandomLoot(player, cause.getWeapon());
if(loot != null) {
Item item = loot.getItem();
if(!item.isAir()) {
- killer.getInventory().addItem(item, loot.getQuantity(), true);
+ player.getInventory().addItem(item, loot.getQuantity(), true);
}
}
}
- // Remove itself from the guard block metadata if it was guarding one
- if(isGuard()) {
- MetaBlock metaBlock = zone.getMetaBlock(guardBlock.getX(), guardBlock.getY());
-
- if(metaBlock != null) {
- MapHelper.getList(metaBlock.getMetadata(), "!", Collections.emptyList()).remove(typeName);
- }
+ // Minigame tracking
+ if(hasActiveMinigame()) {
+ minigame.entityKilled(this, cause);
}
-
- // Destroy mount block if it has one
- if(isMounted()) {
- zone.updateBlock(mountBlock.getX(), mountBlock.getY(), Layer.FRONT, 0);
+ }
+
+ @Override
+ public void attacked(EntityAttack attack, float damage) {
+ if(hasActiveMinigame()) {
+ minigame.entityAttacked(this, attack, damage);
}
}
@@ -191,6 +228,20 @@ public float getMaxHealth() {
return maxHealth;
}
+ @Override
+ public float getDefense(EntityAttack attack) {
+ Entity attacker = attack.getAttacker();
+ Player player = attacker != null && attacker.isPlayer() ? (Player)attacker : null;
+
+ // Full defense if block is mounted and is protected
+ if(isMounted() && zone.isBlockProtected(mountBlock.getX(), mountBlock.getY(), player)) {
+ return 1.0F;
+ }
+
+ // Otherwise, calculate defense
+ return getBaseDefense(attack.getDamageType()) + activeDefenses.getOrDefault(attack.getDamageType(), 0F);
+ }
+
@Override
public Map getStatusConfig() {
Map config = super.getStatusConfig();
@@ -215,11 +266,18 @@ public void move(int x, int y, String animation) {
}
public void move(int x, int y, float speed, String animation) {
+ move(x, y, speed, animation, true);
+ }
+
+ public void move(int x, int y, float speed, String animation, boolean changeDirection) {
this.speed = speed;
- direction = x > 0 ? FacingDirection.EAST : x < 0 ? FacingDirection.WEST : direction;
moveX = x;
moveY = y;
+ if(changeDirection) {
+ setDirection(x);
+ }
+
if(animation != null) {
setAnimation(animation);
}
@@ -229,37 +287,6 @@ public EntityConfig getConfig() {
return config;
}
- public Vector2i getSize() {
- return size;
- }
-
- public void attack(Player attacker, Item weapon) {
- // Prevent damage if this entity is mounted and its mount is protected
- if(!attacker.isGodMode() && isMounted() && zone.isBlockProtected(mountBlock.getX(), mountBlock.getY(), attacker)) {
- return;
- }
-
- Pair
- recentAttack = recentAttacks.get(attacker);
- long now = System.currentTimeMillis();
-
- // Reject the attack if the player already attacked this entity recently
- if(!attacker.isGodMode() && recentAttack != null && now < recentAttack.getLast() + ATTACK_INVINCIBLE_TIME) {
- return;
- }
-
- float damage = attacker.isGodMode() ? 9999 : calculateDamage(weapon.getDamage(), weapon.getDamageType());
- damage(damage, attacker);
- recentAttacks.put(attacker, new Pair<>(weapon, now));
- }
-
- public float calculateDamage(float baseDamage, DamageType type) {
- return baseDamage * (1 - getDefense(type));
- }
-
- public Collection> getRecentAttacks() {
- return Collections.unmodifiableCollection(recentAttacks.values());
- }
-
public void setDefense(DamageType type, float amount) {
if(amount == 0) {
activeDefenses.remove(type);
@@ -268,21 +295,11 @@ public void setDefense(DamageType type, float amount) {
}
}
- public float getDefense(DamageType type) {
- return getDefense(type, true);
- }
-
- public float getDefense(DamageType type, boolean includeBaseDefense) {
- return (includeBaseDefense ? getBaseDefense(type) : 0) + activeDefenses.getOrDefault(type, 0F);
- }
-
public boolean isTransient() {
- return !isGuard() && !isMounted();
+ return !isGuard() && !isMounted() && !persist;
}
- public EntityLoot getRandomLoot(Player awardee) {
- Item weapon = awardee.getHeldItem();
-
+ public EntityLoot getRandomLoot(Player awardee, Item weapon) {
if(isOwnedBy(awardee)) {
return placedLoot.next();
} else if(lootByWeapon.containsKey(weapon)) {
@@ -372,6 +389,18 @@ public Entity getTarget() {
return target;
}
+ public void setArtificial(boolean artificial) {
+ this.artificial = artificial;
+ }
+
+ public boolean isArtificial() {
+ return artificial;
+ }
+
+ public boolean isPersistent() {
+ return persist;
+ }
+
public void setSpeed(float speed) {
this.speed = speed;
}
@@ -403,15 +432,15 @@ public boolean isBlocked(int oX, int oY) {
int tY = y + oY;
boolean blocked = zone.isBlockSolid(tX, tY) || (oX != 0 && zone.isBlockSolid(tX, y)) || (oY != 0 && zone.isBlockSolid(x, tY));
- if(size.getX() > 1) {
- int additionalWidth = size.getX() - 1;
+ if(sizeX > 1) {
+ int additionalWidth = sizeX - 1;
blocked = blocked || zone.isBlockSolid(tX + additionalWidth, tY)
|| (oX != 0 && zone.isBlockSolid(tX + additionalWidth, y))
|| (oY != 0 && zone.isBlockSolid(x + additionalWidth, tY));
}
- if(size.getY() > 1) {
- int additionalHeight = size.getY() - 1;
+ if(sizeY > 1) {
+ int additionalHeight = sizeY - 1;
blocked = blocked || zone.isBlockSolid(tX, tY - additionalHeight)
|| (oX != 0 && zone.isBlockSolid(tX, y - additionalHeight))
|| (oY != 0 && zone.isBlockSolid(x, tY - additionalHeight));
diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/NpcData.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/NpcData.java
new file mode 100644
index 00000000..1d173303
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/NpcData.java
@@ -0,0 +1,47 @@
+package brainwine.gameserver.entity.npc;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import brainwine.gameserver.entity.EntityConfig;
+
+/**
+ * Storage data for persistent non-player characters.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class NpcData {
+
+ private EntityConfig type;
+ private String name;
+ private int x;
+ private int y;
+
+ @JsonCreator
+ public NpcData(@JsonProperty(value = "type", required = true) EntityConfig type) {
+ this.type = type;
+ }
+
+ public NpcData(Npc npc) {
+ this.type = npc.getConfig();
+ this.name = npc.getName();
+ this.x = npc.getBlockX();
+ this.y = npc.getBlockY();
+ }
+
+ public EntityConfig getType() {
+ return type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getX() {
+ return x;
+ }
+
+ public int getY() {
+ return y;
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/Behavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/Behavior.java
similarity index 59%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/Behavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/Behavior.java
index 610f801b..21756c16 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/Behavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/Behavior.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.behavior;
+package brainwine.gameserver.entity.npc.behavior;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
@@ -7,26 +7,27 @@
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
-import brainwine.gameserver.behavior.composed.CrawlerBehavior;
-import brainwine.gameserver.behavior.composed.DiggerBehavior;
-import brainwine.gameserver.behavior.composed.FlyerBehavior;
-import brainwine.gameserver.behavior.composed.WalkerBehavior;
-import brainwine.gameserver.behavior.parts.ClimbBehavior;
-import brainwine.gameserver.behavior.parts.DigBehavior;
-import brainwine.gameserver.behavior.parts.EruptionAttackBehavior;
-import brainwine.gameserver.behavior.parts.FallBehavior;
-import brainwine.gameserver.behavior.parts.FlyBehavior;
-import brainwine.gameserver.behavior.parts.FlyTowardBehavior;
-import brainwine.gameserver.behavior.parts.FollowBehavior;
-import brainwine.gameserver.behavior.parts.IdleBehavior;
-import brainwine.gameserver.behavior.parts.RandomlyTargetBehavior;
-import brainwine.gameserver.behavior.parts.ReporterBehavior;
-import brainwine.gameserver.behavior.parts.ShielderBehavior;
-import brainwine.gameserver.behavior.parts.SpawnAttackBehavior;
-import brainwine.gameserver.behavior.parts.TurnBehavior;
-import brainwine.gameserver.behavior.parts.UnblockBehavior;
-import brainwine.gameserver.behavior.parts.WalkBehavior;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.composed.CrawlerBehavior;
+import brainwine.gameserver.entity.npc.behavior.composed.DiggerBehavior;
+import brainwine.gameserver.entity.npc.behavior.composed.FlyerBehavior;
+import brainwine.gameserver.entity.npc.behavior.composed.WalkerBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.ClimbBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.ConveyorBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.DigBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.EruptionAttackBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.FallBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.FlyBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.FlyTowardBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.FollowBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.IdleBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.RandomlyTargetBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.ReporterBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.ShielderBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.SpawnAttackBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.TurnBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.UnblockBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.WalkBehavior;
/**
* Heavily based on Deepworld's original "rubyhave" (ha ha very punny) behavior system.
@@ -46,6 +47,7 @@
@Type(name = "walk", value = WalkBehavior.class),
@Type(name = "fall", value = FallBehavior.class),
@Type(name = "turn", value = TurnBehavior.class),
+ @Type(name = "conveyor", value = ConveyorBehavior.class),
@Type(name = "follow", value = FollowBehavior.class),
@Type(name = "climb", value = ClimbBehavior.class),
@Type(name = "dig", value = DigBehavior.class),
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/CompositeBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/CompositeBehavior.java
similarity index 97%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/CompositeBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/CompositeBehavior.java
index 2f569553..ef075088 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/CompositeBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/CompositeBehavior.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.behavior;
+package brainwine.gameserver.entity.npc.behavior;
import static brainwine.shared.LogMarkers.SERVER_MARKER;
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/SelectorBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/SelectorBehavior.java
similarity index 91%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/SelectorBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/SelectorBehavior.java
index bf1025ac..85bbbb64 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/SelectorBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/SelectorBehavior.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.behavior;
+package brainwine.gameserver.entity.npc.behavior;
import java.util.Map;
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/SequenceBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/SequenceBehavior.java
similarity index 97%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/SequenceBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/SequenceBehavior.java
index 9bc5890e..0999021a 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/SequenceBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/SequenceBehavior.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.behavior;
+package brainwine.gameserver.entity.npc.behavior;
import static brainwine.shared.LogMarkers.SERVER_MARKER;
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/CrawlerBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/CrawlerBehavior.java
similarity index 61%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/composed/CrawlerBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/CrawlerBehavior.java
index 53575bca..46f98e62 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/CrawlerBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/CrawlerBehavior.java
@@ -1,17 +1,18 @@
-package brainwine.gameserver.behavior.composed;
+package brainwine.gameserver.entity.npc.behavior.composed;
import java.util.Map;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.SelectorBehavior;
-import brainwine.gameserver.behavior.parts.ClimbBehavior;
-import brainwine.gameserver.behavior.parts.FallBehavior;
-import brainwine.gameserver.behavior.parts.IdleBehavior;
-import brainwine.gameserver.behavior.parts.TurnBehavior;
-import brainwine.gameserver.behavior.parts.WalkBehavior;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.SelectorBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.ClimbBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.ConveyorBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.FallBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.IdleBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.TurnBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.WalkBehavior;
import brainwine.gameserver.util.MapHelper;
public class CrawlerBehavior extends SelectorBehavior {
@@ -28,6 +29,8 @@ public CrawlerBehavior(Npc entity) {
@Override
public void addChildren(Map config) {
+ addChild(ConveyorBehavior.class, config);
+
if(config.containsKey("idle")) {
addChild(IdleBehavior.class, MapHelper.getMap(config, "idle"));
}
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/DiggerBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/DiggerBehavior.java
similarity index 60%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/composed/DiggerBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/DiggerBehavior.java
index d6fb9175..3cd50486 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/DiggerBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/DiggerBehavior.java
@@ -1,17 +1,18 @@
-package brainwine.gameserver.behavior.composed;
+package brainwine.gameserver.entity.npc.behavior.composed;
import java.util.Map;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.SelectorBehavior;
-import brainwine.gameserver.behavior.parts.DigBehavior;
-import brainwine.gameserver.behavior.parts.FallBehavior;
-import brainwine.gameserver.behavior.parts.IdleBehavior;
-import brainwine.gameserver.behavior.parts.TurnBehavior;
-import brainwine.gameserver.behavior.parts.WalkBehavior;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.SelectorBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.ConveyorBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.DigBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.FallBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.IdleBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.TurnBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.WalkBehavior;
import brainwine.gameserver.util.MapHelper;
public class DiggerBehavior extends SelectorBehavior {
@@ -28,6 +29,8 @@ public DiggerBehavior(Npc entity) {
@Override
public void addChildren(Map config) {
+ addChild(ConveyorBehavior.class, config);
+
if(config.containsKey("idle")) {
addChild(IdleBehavior.class, MapHelper.getMap(config, "idle"));
}
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/FlyerBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/FlyerBehavior.java
similarity index 71%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/composed/FlyerBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/FlyerBehavior.java
index d511c6f1..1f5b60d7 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/FlyerBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/FlyerBehavior.java
@@ -1,15 +1,15 @@
-package brainwine.gameserver.behavior.composed;
+package brainwine.gameserver.entity.npc.behavior.composed;
import java.util.Map;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.SelectorBehavior;
-import brainwine.gameserver.behavior.parts.FlyBehavior;
-import brainwine.gameserver.behavior.parts.FlyTowardBehavior;
-import brainwine.gameserver.behavior.parts.IdleBehavior;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.SelectorBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.FlyBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.FlyTowardBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.IdleBehavior;
import brainwine.gameserver.util.MapHelper;
public class FlyerBehavior extends SelectorBehavior {
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/WalkerBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/WalkerBehavior.java
similarity index 62%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/composed/WalkerBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/WalkerBehavior.java
index 4ad4a9a7..ddd9dcb1 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/WalkerBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/composed/WalkerBehavior.java
@@ -1,16 +1,17 @@
-package brainwine.gameserver.behavior.composed;
+package brainwine.gameserver.entity.npc.behavior.composed;
import java.util.Map;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.SelectorBehavior;
-import brainwine.gameserver.behavior.parts.FallBehavior;
-import brainwine.gameserver.behavior.parts.IdleBehavior;
-import brainwine.gameserver.behavior.parts.TurnBehavior;
-import brainwine.gameserver.behavior.parts.WalkBehavior;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.SelectorBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.ConveyorBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.FallBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.IdleBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.TurnBehavior;
+import brainwine.gameserver.entity.npc.behavior.parts.WalkBehavior;
import brainwine.gameserver.util.MapHelper;
public class WalkerBehavior extends SelectorBehavior {
@@ -27,6 +28,8 @@ public WalkerBehavior(Npc entity) {
@Override
protected void addChildren(Map config) {
+ addChild(ConveyorBehavior.class, config);
+
if(config.containsKey("idle")) {
addChild(IdleBehavior.class, MapHelper.getMap(config, "idle"));
}
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ClimbBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ClimbBehavior.java
similarity index 90%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/ClimbBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ClimbBehavior.java
index dc86bc90..9b3c7cb1 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ClimbBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ClimbBehavior.java
@@ -1,11 +1,11 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.Behavior;
import brainwine.gameserver.entity.FacingDirection;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
public class ClimbBehavior extends Behavior {
diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ConveyorBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ConveyorBehavior.java
new file mode 100644
index 00000000..6e82a51b
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ConveyorBehavior.java
@@ -0,0 +1,50 @@
+package brainwine.gameserver.entity.npc.behavior.parts;
+
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
+import brainwine.gameserver.item.ItemUseType;
+import brainwine.gameserver.zone.Block;
+
+public class ConveyorBehavior extends Behavior {
+
+ protected String animation = "idle";
+ protected Block conveyorBlock;
+
+ @JsonCreator
+ public ConveyorBehavior(@JacksonInject Npc entity) {
+ super(entity);
+ }
+
+ @Override
+ public boolean behave() {
+ int direction = conveyorBlock.getFrontMod() == 0 ? 1 : -1;
+ float movingSurfacePower = conveyorBlock.getFrontItem().getPower();
+
+ // Randomly change direction to match the conveyor belt's
+ if(Math.random() < 0.333) {
+ entity.setDirection(direction);
+ }
+
+ // Fail if entity is blocked to allow for other behavior like crawling to work
+ if(entity.isBlocked(direction, 0)) {
+ return false;
+ }
+
+ entity.move(direction, 0, movingSurfacePower, animation, false);
+ return true;
+ }
+
+ @Override
+ public boolean canBehave() {
+ // Check if entity is standing on a conveyor belt
+ conveyorBlock = entity.getZone().findBlock(entity.getBlockX(), entity.getBlockY() + 1, block -> block.getFrontItem().hasUse(ItemUseType.MOVE));
+ return conveyorBlock != null;
+ }
+
+ public void setAnimation(String animation) {
+ this.animation = animation;
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/DigBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/DigBehavior.java
similarity index 86%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/DigBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/DigBehavior.java
index ab133f94..109bf246 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/DigBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/DigBehavior.java
@@ -1,10 +1,10 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.Behavior;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
import brainwine.gameserver.zone.Zone;
public class DigBehavior extends Behavior {
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/EruptionAttackBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/EruptionAttackBehavior.java
similarity index 96%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/EruptionAttackBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/EruptionAttackBehavior.java
index a21ff180..fa396978 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/EruptionAttackBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/EruptionAttackBehavior.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import java.util.Map;
@@ -6,10 +6,10 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonSetter;
-import brainwine.gameserver.behavior.Behavior;
import brainwine.gameserver.entity.EntityConfig;
import brainwine.gameserver.entity.EntityStatus;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
import brainwine.gameserver.server.messages.EntityStatusMessage;
import brainwine.gameserver.util.MapHelper;
import brainwine.gameserver.util.Vector2i;
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FallBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FallBehavior.java
similarity index 87%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/FallBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FallBehavior.java
index 686f1e55..3e713eb6 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FallBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FallBehavior.java
@@ -1,11 +1,11 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.Behavior;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
public class FallBehavior extends Behavior {
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FlyBehavior.java
similarity index 94%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FlyBehavior.java
index 20676877..f8594f3e 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FlyBehavior.java
@@ -1,12 +1,12 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import java.util.concurrent.ThreadLocalRandom;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.Behavior;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
import brainwine.gameserver.util.Vector2i;
public class FlyBehavior extends Behavior {
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyTowardBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FlyTowardBehavior.java
similarity index 95%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyTowardBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FlyTowardBehavior.java
index baa0a9de..1f60e15b 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyTowardBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FlyTowardBehavior.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FollowBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FollowBehavior.java
similarity index 84%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/FollowBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FollowBehavior.java
index 087b004d..ecbee083 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FollowBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/FollowBehavior.java
@@ -1,11 +1,11 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.Behavior;
import brainwine.gameserver.entity.FacingDirection;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
public class FollowBehavior extends Behavior {
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/IdleBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/IdleBehavior.java
similarity index 96%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/IdleBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/IdleBehavior.java
index 68112f65..d9f95979 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/IdleBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/IdleBehavior.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
@@ -7,9 +7,9 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonSetter;
-import brainwine.gameserver.behavior.Behavior;
import brainwine.gameserver.entity.FacingDirection;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
import brainwine.gameserver.util.Vector2i;
public class IdleBehavior extends Behavior {
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/RandomlyTargetBehavior.java
similarity index 85%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/RandomlyTargetBehavior.java
index 3311393d..3f0aab1c 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/RandomlyTargetBehavior.java
@@ -1,11 +1,11 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.Behavior;
import brainwine.gameserver.entity.npc.Npc;
-import brainwine.gameserver.entity.player.Player;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
+import brainwine.gameserver.player.Player;
public class RandomlyTargetBehavior extends Behavior {
@@ -31,7 +31,7 @@ public boolean behave() {
if(!entity.hasTarget()) {
Player target = entity.getZone().getRandomPlayerInRange(entity.getX(), entity.getY(), range);
- if(target != null && !target.isGodMode() && !target.isDead() && !entity.isOwnedBy(target) && (!blockable || entity.canSee(target))) {
+ if(target != null && !target.isGodMode() && !target.isStealthy() && !target.isDead() && !entity.isOwnedBy(target) && (!blockable || entity.canSee(target))) {
entity.setTarget(target);
targetLockedAt = now;
}
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ReporterBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ReporterBehavior.java
similarity index 80%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/ReporterBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ReporterBehavior.java
index c77fc7e1..a1beaf8c 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ReporterBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ReporterBehavior.java
@@ -1,10 +1,10 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.Behavior;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
public class ReporterBehavior extends Behavior {
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ShielderBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java
similarity index 87%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/ShielderBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java
index 0a0de4ed..664fd2c5 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ShielderBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/ShielderBehavior.java
@@ -1,7 +1,6 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import java.util.Arrays;
-import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
@@ -9,11 +8,10 @@
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.Behavior;
+import brainwine.gameserver.entity.EntityAttack;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
import brainwine.gameserver.item.DamageType;
-import brainwine.gameserver.item.Item;
-import brainwine.gameserver.util.Pair;
public class ShielderBehavior extends Behavior {
@@ -32,11 +30,12 @@ public ShielderBehavior(@JacksonInject Npc entity) {
@Override
public boolean behave() {
long now = System.currentTimeMillis();
- Collection> recentAttacks = entity.getRecentAttacks();
+ EntityAttack attack = entity.getMostRecentAttack();
- if(!recentAttacks.isEmpty()) {
+ if(attack != null) {
lastAttackedAt = now;
- DamageType type = recentAttacks.stream().findFirst().get().getFirst().getDamageType();
+ DamageType type = attack.getDamageType();
+
if(currentShield == null && now >= shieldStart + (recharge * 1000)) {
if(defenses.contains(type)) {
setShield(type);
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/SpawnAttackBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/SpawnAttackBehavior.java
similarity index 90%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/SpawnAttackBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/SpawnAttackBehavior.java
index 6650737d..9871ad3c 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/SpawnAttackBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/SpawnAttackBehavior.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import java.util.Map;
@@ -6,13 +6,12 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonSetter;
-import brainwine.gameserver.behavior.Behavior;
import brainwine.gameserver.entity.EntityConfig;
import brainwine.gameserver.entity.EntityStatus;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
import brainwine.gameserver.server.messages.EntityStatusMessage;
import brainwine.gameserver.util.MapHelper;
-import brainwine.gameserver.util.Vector2i;
public class SpawnAttackBehavior extends Behavior {
@@ -41,9 +40,8 @@ public boolean behave() {
if(npc) {
// Spawn child at parent's location
- Vector2i size = entity.getSize();
- int spawnX = (int)(entity.getX() + (size.getX() / 2F));
- int spawnY = (int)(entity.getY() + (size.getY() / 2F));
+ int spawnX = (int)(entity.getX() + (entity.getSizeX() / 2.0F));
+ int spawnY = (int)(entity.getY() + (entity.getSizeX() / 2.0F));
Npc child = new Npc(entity.getZone(), entityConfig);
child.setOwner(entity);
entity.addChild(child);
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/TurnBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/TurnBehavior.java
similarity index 85%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/TurnBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/TurnBehavior.java
index 8ae0134c..9e2fcf05 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/TurnBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/TurnBehavior.java
@@ -1,11 +1,11 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.Behavior;
import brainwine.gameserver.entity.FacingDirection;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
public class TurnBehavior extends Behavior {
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/UnblockBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/UnblockBehavior.java
similarity index 74%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/UnblockBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/UnblockBehavior.java
index 929b0dc0..8c40083a 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/UnblockBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/UnblockBehavior.java
@@ -1,4 +1,4 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
@@ -6,9 +6,8 @@
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.Behavior;
import brainwine.gameserver.entity.npc.Npc;
-import brainwine.gameserver.util.Vector2i;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
import brainwine.gameserver.zone.Zone;
public class UnblockBehavior extends Behavior {
@@ -23,12 +22,11 @@ public UnblockBehavior(@JacksonInject Npc entity) {
@Override
public boolean behave() {
Zone zone = entity.getZone();
- Vector2i size = entity.getSize();
Random random = ThreadLocalRandom.current();
for(int i = 0; i < rate; i++) {
- int x = (int)entity.getX() + random.nextInt(size.getX());
- int y = (int)entity.getY() - random.nextInt(size.getY());
+ int x = (int)entity.getX() + random.nextInt(entity.getSizeX());
+ int y = (int)entity.getY() - random.nextInt(entity.getSizeY());
if(zone.isChunkLoaded(x, y) && zone.getBlock(x, y).getFrontItem().isDiggable()) {
zone.digBlock(x, y);
diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/WalkBehavior.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/WalkBehavior.java
similarity index 89%
rename from gameserver/src/main/java/brainwine/gameserver/behavior/parts/WalkBehavior.java
rename to gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/WalkBehavior.java
index 8e746eea..62e23dda 100644
--- a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/WalkBehavior.java
+++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/behavior/parts/WalkBehavior.java
@@ -1,12 +1,12 @@
-package brainwine.gameserver.behavior.parts;
+package brainwine.gameserver.entity.npc.behavior.parts;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
-import brainwine.gameserver.behavior.Behavior;
import brainwine.gameserver.entity.FacingDirection;
import brainwine.gameserver.entity.npc.Npc;
+import brainwine.gameserver.entity.npc.behavior.Behavior;
public class WalkBehavior extends Behavior {
diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/ClothingSlot.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/ClothingSlot.java
deleted file mode 100644
index f0ab70be..00000000
--- a/gameserver/src/main/java/brainwine/gameserver/entity/player/ClothingSlot.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package brainwine.gameserver.entity.player;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonValue;
-
-public enum ClothingSlot {
-
- HAIR("h"),
- FACIAL_HAIR("fh"),
- TOPS("t"),
- BOTTOMS("b"),
- FOOTWEAR("fw"),
- HEADGEAR("hg"),
- FACIAL_GEAR("fg"),
- SUIT("u"),
- TOPS_OVERLAY("to"),
- ARMS_OVERLAY("ao"),
- LEGS_OVERLAY("lo"),
- FOOTWEAR_OVERLAY("fo");
-
- private final String id;
-
- private ClothingSlot(String id) {
- this.id = id;
- }
-
- @JsonCreator
- public static ClothingSlot fromId(String id) {
- for(ClothingSlot value : values()) {
- if(value.getId().equals(id)) {
- return value;
- }
- }
-
- return null;
- }
-
- @JsonValue
- public String getId() {
- return id;
- }
-}
diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/ColorSlot.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/ColorSlot.java
deleted file mode 100644
index 8fe4caa7..00000000
--- a/gameserver/src/main/java/brainwine/gameserver/entity/player/ColorSlot.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package brainwine.gameserver.entity.player;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonValue;
-
-public enum ColorSlot {
-
- SKIN_COLOR("c*"),
- HAIR_COLOR("h*");
-
- private final String id;
-
- private ColorSlot(String id) {
- this.id = id;
- }
-
- @JsonCreator
- public static ColorSlot fromId(String id) {
- for(ColorSlot value : values()) {
- if(value.getId().equals(id)) {
- return value;
- }
- }
-
- return null;
- }
-
- @JsonValue
- public String getId() {
- return id;
- }
-}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/Action.java b/gameserver/src/main/java/brainwine/gameserver/item/Action.java
index 960392c1..543ecee4 100644
--- a/gameserver/src/main/java/brainwine/gameserver/item/Action.java
+++ b/gameserver/src/main/java/brainwine/gameserver/item/Action.java
@@ -1,13 +1,64 @@
package brainwine.gameserver.item;
+import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
+import brainwine.gameserver.item.consumables.Consumable;
+import brainwine.gameserver.item.consumables.ConvertConsumable;
+import brainwine.gameserver.item.consumables.HealConsumable;
+import brainwine.gameserver.item.consumables.NameChangeConsumable;
+import brainwine.gameserver.item.consumables.RefillConsumable;
+import brainwine.gameserver.item.consumables.SkillConsumable;
+import brainwine.gameserver.item.consumables.SkillResetConsumable;
+import brainwine.gameserver.item.consumables.StealthConsumable;
+import brainwine.gameserver.item.consumables.TeleportConsumable;
+
+/**
+ * Action types for items.
+ *
+ * All consumables depend on their action type, but not all items with actions are consumables.
+ * This creates a bit of an awkward situation in terms of implementation, but we're just gonna have to deal with that.
+ */
public enum Action {
+ CONVERT(new ConvertConsumable()),
DIG,
- HEAL,
- REFILL,
+ HEAL(new HealConsumable()),
+ NAME_CHANGE(new NameChangeConsumable()),
+ REFILL(new RefillConsumable()),
+ SKILL(new SkillConsumable()),
+ SKILL_RESET(new SkillResetConsumable()),
+ SMASH,
+ STEALTH(new StealthConsumable()),
+ TELEPORT(new TeleportConsumable()),
@JsonEnumDefaultValue
NONE;
+
+ private final Consumable consumable;
+
+ private Action(Consumable consumable) {
+ this.consumable = consumable;
+ }
+
+ private Action() {
+ this(null);
+ }
+
+ @JsonCreator
+ public static Action fromId(String id) {
+ String formatted = id.toUpperCase().replace(" ", "_");
+
+ for(Action value : values()) {
+ if(value.toString().equals(formatted)) {
+ return value;
+ }
+ }
+
+ return NONE;
+ }
+
+ public Consumable getConsumable() {
+ return consumable;
+ }
}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/DamageType.java b/gameserver/src/main/java/brainwine/gameserver/item/DamageType.java
index b4dee83a..1f3ab4d1 100644
--- a/gameserver/src/main/java/brainwine/gameserver/item/DamageType.java
+++ b/gameserver/src/main/java/brainwine/gameserver/item/DamageType.java
@@ -13,6 +13,7 @@ public enum DamageType {
FIRE,
PIERCING,
SLASHING,
+ SUFFOCATION,
@JsonEnumDefaultValue
NONE;
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/FieldDamage.java b/gameserver/src/main/java/brainwine/gameserver/item/FieldDamage.java
new file mode 100644
index 00000000..be3e9bc2
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/item/FieldDamage.java
@@ -0,0 +1,23 @@
+package brainwine.gameserver.item;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+@JsonFormat(shape = JsonFormat.Shape.ARRAY)
+public class FieldDamage {
+
+ private DamageType type;
+ private float maxDamage;
+ private float radius;
+
+ public DamageType getType() {
+ return type;
+ }
+
+ public float getMaxDamage() {
+ return maxDamage;
+ }
+
+ public float getRadius() {
+ return radius;
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/Item.java b/gameserver/src/main/java/brainwine/gameserver/item/Item.java
index fa2e36cc..a60c5d85 100644
--- a/gameserver/src/main/java/brainwine/gameserver/item/Item.java
+++ b/gameserver/src/main/java/brainwine/gameserver/item/Item.java
@@ -5,6 +5,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@@ -12,10 +13,12 @@
import com.fasterxml.jackson.annotation.JsonValue;
import brainwine.gameserver.dialog.DialogType;
-import brainwine.gameserver.entity.player.Skill;
+import brainwine.gameserver.player.Skill;
import brainwine.gameserver.util.Pair;
import brainwine.gameserver.util.Vector2i;
+import brainwine.gameserver.util.WeightedMap;
+// TODO I don't like some parts of this, maybe they can be reworked.
@JsonIgnoreProperties(ignoreUnknown = true)
public class Item {
@@ -27,6 +30,9 @@ public class Item {
@JsonProperty("code")
private int code;
+ @JsonProperty("category")
+ private String category;
+
@JsonProperty("title")
private String title;
@@ -36,6 +42,9 @@ public class Item {
@JsonProperty("fieldable")
private Fieldability fieldability = Fieldability.TRUE;
+ @JsonProperty("tradeable")
+ private Tradeability tradeability = Tradeability.TRUE;
+
@JsonProperty("loot_graphic")
private DialogType lootGraphic = DialogType.STANDARD;
@@ -72,9 +81,18 @@ public class Item {
@JsonProperty("guard")
private int guardLevel;
+ @JsonProperty("spacing")
+ private int spacing;
+
+ @JsonProperty("spawn_spacing")
+ private int spawnSpacing;
+
@JsonProperty("power")
private float power;
+ @JsonProperty("toughness")
+ private float toughness;
+
@JsonProperty("earthy")
private boolean earthy;
@@ -90,9 +108,15 @@ public class Item {
@JsonProperty("placeover")
private boolean placeover;
+ @JsonProperty("custom_mine")
+ private boolean customMine;
+
@JsonProperty("custom_place")
private boolean customPlace;
+ @JsonProperty("field_place")
+ private boolean fieldPlace;
+
@JsonProperty("base")
private boolean base;
@@ -111,18 +135,33 @@ public class Item {
@JsonProperty("entity")
private boolean entity;
+ @JsonProperty("steam")
+ private boolean steam;
+
+ @JsonProperty("ownership")
+ private boolean ownership;
+
+ @JsonProperty("membership")
+ private boolean membership;
+
@JsonProperty("inventory")
private LazyItemGetter inventoryItem;
@JsonProperty("decay inventory")
private LazyItemGetter decayInventoryItem;
+ @JsonProperty("mod_inventory")
+ private Pair modInventoryItem;
+
@JsonProperty("crafting quantity")
private int craftingQuantity = 1;
@JsonProperty("loot")
private String[] lootCategories = {};
+ @JsonProperty("regen_bonus")
+ private double regenBonus = 1.0;
+
@JsonProperty("tool_bonus")
private double toolBonus;
@@ -132,6 +171,9 @@ public class Item {
@JsonProperty("skill_bonuses")
private Map skillBonuses = new HashMap<>();
+ @JsonProperty("power_bonus")
+ private Pair powerBonus;
+
@JsonProperty("mining skill")
private Pair miningSkill;
@@ -144,6 +186,21 @@ public class Item {
@JsonProperty("damage")
private Pair damageInfo;
+ @JsonProperty("timer")
+ private Pair timer;
+
+ @JsonProperty("timer_delay")
+ private int timerDelay;
+
+ @JsonProperty("timer_mine")
+ private boolean processTimerOnBreak;
+
+ @JsonProperty("field_damage")
+ private FieldDamage fieldDamage;
+
+ @JsonProperty("spacing_items")
+ private List spacingItems = new ArrayList<>();
+
@JsonProperty("ingredients")
private List craftingIngredients = new ArrayList<>();
@@ -153,6 +210,12 @@ public class Item {
@JsonProperty("use")
private Map useConfigs = new HashMap<>();
+ @JsonProperty("convert")
+ private Map conversions = new HashMap<>();
+
+ @JsonProperty("spawn_entity")
+ private WeightedMap entitySpawns = new WeightedMap<>();
+
@JsonCreator
private Item(@JsonProperty(value = "id", required = true) String id,
@JsonProperty(value = "code", required = true) int code) {
@@ -207,6 +270,15 @@ public int getCode() {
return code;
}
+ public String getCategory() {
+ if(category != null) {
+ return category;
+ }
+
+ int index = id.indexOf('/');
+ return index > 1 ? id.substring(0, index) : null;
+ }
+
public String getTitle() {
return title;
}
@@ -223,6 +295,10 @@ public Fieldability getFieldability() {
return fieldability;
}
+ public Tradeability getTradeability() {
+ return tradeability;
+ }
+
public DialogType getLootGraphic() {
return lootGraphic;
}
@@ -303,10 +379,30 @@ public int getGuardLevel() {
return guardLevel;
}
+ public boolean hasSpacing() {
+ return spacing > 0;
+ }
+
+ public int getSpacing() {
+ return spacing;
+ }
+
+ public boolean hasSpawnSpacing() {
+ return spawnSpacing > 0;
+ }
+
+ public int getSpawnSpacing() {
+ return spawnSpacing;
+ }
+
public float getPower() {
return power;
}
+ public float getToughness() {
+ return toughness;
+ }
+
public boolean isEarthy() {
return earthy;
}
@@ -331,10 +427,18 @@ public boolean canPlaceOver() {
return placeover;
}
+ public boolean hasCustomMine() {
+ return customMine;
+ }
+
public boolean hasCustomPlace() {
return customPlace;
}
+ public boolean canPlaceInField() {
+ return fieldPlace;
+ }
+
public boolean isWhole() {
return whole;
}
@@ -355,10 +459,34 @@ public boolean isEntity() {
return entity;
}
+ public boolean usesSteam() {
+ return steam;
+ }
+
+ public boolean requiresOwnership() {
+ return ownership;
+ }
+
+ public boolean requiresMembership() {
+ return membership;
+ }
+
+ public int getSkillBonus(Skill skill) {
+ return skillBonuses.getOrDefault(skill, 0);
+ }
+
public Map getSkillBonuses() {
return skillBonuses;
}
+ public boolean hasPowerBonus() {
+ return powerBonus != null;
+ }
+
+ public Pair getPowerBonus() {
+ return powerBonus;
+ }
+
public boolean requiresMiningSkill() {
return miningSkill != null;
}
@@ -395,10 +523,22 @@ public Item getDecayInventoryItem() {
return decayInventoryItem == null ? this : decayInventoryItem.get();
}
+ public boolean hasModInventoryItem() {
+ return modInventoryItem != null;
+ }
+
+ public Item getModInventoryItem(int mod) {
+ return modInventoryItem == null ? this : mod >= modInventoryItem.getFirst() ? modInventoryItem.getLast().get() : Item.AIR;
+ }
+
public String[] getLootCategories() {
return lootCategories;
}
+ public double getRegenBonus() {
+ return regenBonus;
+ }
+
public double getToolBonus() {
return toolBonus;
}
@@ -423,6 +563,42 @@ public float getDamage() {
return isWeapon() ? damageInfo.getLast() : 0;
}
+ public boolean hasTimer() {
+ return timer != null;
+ }
+
+ public String getTimerType() {
+ return hasTimer() ? timer.getFirst() : null;
+ }
+
+ public int getTimerValue() {
+ return hasTimer() ? timer.getLast() : 0;
+ }
+
+ public int getTimerDelay() {
+ return timerDelay;
+ }
+
+ public boolean shouldProcessTimerOnBreak() {
+ return processTimerOnBreak;
+ }
+
+ public boolean hasFieldDamage() {
+ return fieldDamage != null;
+ }
+
+ public FieldDamage getFieldDamage() {
+ return fieldDamage;
+ }
+
+ public boolean hasSpacingItems() {
+ return !spacingItems.isEmpty();
+ }
+
+ public List
- getSpacingItems() {
+ return spacingItems.stream().map(LazyItemGetter::get).collect(Collectors.toList());
+ }
+
public boolean isCraftable() {
return !craftingIngredients.isEmpty();
}
@@ -456,4 +632,16 @@ public Object getUse(ItemUseType type) {
public Map getUses() {
return useConfigs;
}
+
+ public Map
- getConversions() {
+ return conversions.entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey().get(), entry -> entry.getValue().get()));
+ }
+
+ public boolean hasEntitySpawns() {
+ return !entitySpawns.isEmpty();
+ }
+
+ public WeightedMap getEntitySpawns() {
+ return entitySpawns;
+ }
}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/ItemRegistry.java b/gameserver/src/main/java/brainwine/gameserver/item/ItemRegistry.java
index 4b82d2c5..db35ed79 100644
--- a/gameserver/src/main/java/brainwine/gameserver/item/ItemRegistry.java
+++ b/gameserver/src/main/java/brainwine/gameserver/item/ItemRegistry.java
@@ -2,9 +2,11 @@
import static brainwine.shared.LogMarkers.SERVER_MARKER;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
@@ -15,6 +17,7 @@ public class ItemRegistry {
private static final Logger logger = LogManager.getLogger();
private static final Map items = new HashMap<>();
private static final Map itemsByCode = new HashMap<>();
+ private static final Map> itemsByCategory = new HashMap<>();
// TODO maybe just move the registry stuff here
public static void clear() {
@@ -36,6 +39,15 @@ public static boolean registerItem(Item item) {
return false;
}
+ String category = item.getCategory();
+ List
- categorizedItems = itemsByCategory.get(category);
+
+ if(categorizedItems == null) {
+ categorizedItems = new ArrayList<>();
+ itemsByCategory.put(category, categorizedItems);
+ }
+
+ categorizedItems.add(item);
items.put(id, item);
itemsByCode.put(code, item);
return true;
@@ -52,4 +64,8 @@ public static Item getItem(int code) {
public static Collection
- getItems() {
return Collections.unmodifiableCollection(items.values());
}
+
+ public static List
- getItemsByCategory(String category) {
+ return Collections.unmodifiableList(itemsByCategory.getOrDefault(category, Collections.emptyList()));
+ }
}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java b/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java
index f2995fed..2bbe7f76 100644
--- a/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java
+++ b/gameserver/src/main/java/brainwine/gameserver/item/ItemUseType.java
@@ -3,27 +3,80 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
+import brainwine.gameserver.item.interactions.BurstInteraction;
+import brainwine.gameserver.item.interactions.ChangeInteraction;
+import brainwine.gameserver.item.interactions.ComposterInteraction;
+import brainwine.gameserver.item.interactions.ContainerInteraction;
+import brainwine.gameserver.item.interactions.DialogInteraction;
+import brainwine.gameserver.item.interactions.ExpiatorInteraction;
+import brainwine.gameserver.item.interactions.GeckInteraction;
+import brainwine.gameserver.item.interactions.ItemInteraction;
+import brainwine.gameserver.item.interactions.LandmarkInteraction;
+import brainwine.gameserver.item.interactions.MinigameInteraction;
+import brainwine.gameserver.item.interactions.NoteInteraction;
+import brainwine.gameserver.item.interactions.RecyclerInteraction;
+import brainwine.gameserver.item.interactions.SpawnInteraction;
+import brainwine.gameserver.item.interactions.SpawnTeleportInteraction;
+import brainwine.gameserver.item.interactions.SwitchInteraction;
+import brainwine.gameserver.item.interactions.TargetTeleportInteraction;
+import brainwine.gameserver.item.interactions.TeleportInteraction;
+import brainwine.gameserver.item.interactions.TransmitInteraction;
+import brainwine.gameserver.item.interactions.WarmthInteraction;
+
+/**
+ * Much like with {@link Action}, block interactions depend on their use type.
+ */
public enum ItemUseType {
AFTERBURNER,
- CONTAINER,
- CREATE_DIALOG,
- DIALOG,
+ BREATH,
+ BURST(new BurstInteraction()),
+ COMPOSTER(new ComposterInteraction()),
+ CONTAINER(new ContainerInteraction()),
+ CREATE_DIALOG(new DialogInteraction(true)),
+ DESTROY,
+ DIALOG(new DialogInteraction(false)),
+ EXPIATOR(new ExpiatorInteraction()),
+ GECK(new GeckInteraction()),
GUARD,
- CHANGE,
+ CHANGE(new ChangeInteraction()),
FIELDABLE,
FLY,
+ LANDMARK(new LandmarkInteraction()),
+ MINIGAME(new MinigameInteraction()),
+ MOVE,
MULTI,
+ NOTE(new NoteInteraction()),
+ PET,
+ PLENTY,
PROTECTED,
PUBLIC,
- SWITCH,
+ RECYCLER(new RecyclerInteraction()),
+ SPAWN(new SpawnInteraction()),
+ SPAWN_TELEPORT(new SpawnTeleportInteraction()),
+ SWITCH(new SwitchInteraction()),
SWITCHED,
- TELEPORT,
+ TARGET_TELEPORT(new TargetTeleportInteraction()),
+ TELEPORT(new TeleportInteraction()),
+ TRIGGER,
+ TRANSMIT(new TransmitInteraction()),
+ TRANSMITTED,
+ WARMTH(new WarmthInteraction()),
ZONE_TELEPORT,
@JsonEnumDefaultValue
UNKNOWN;
-
+
+ private final ItemInteraction interaction;
+
+ private ItemUseType(ItemInteraction interaction) {
+ this.interaction = interaction;
+ }
+
+ private ItemUseType() {
+ this(null);
+ }
+
@JsonCreator
public static ItemUseType fromId(String id) {
String formatted = id.toUpperCase().replace(" ", "_");
@@ -36,4 +89,8 @@ public static ItemUseType fromId(String id) {
return UNKNOWN;
}
+
+ public ItemInteraction getInteraction() {
+ return interaction;
+ }
}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/MiningBonus.java b/gameserver/src/main/java/brainwine/gameserver/item/MiningBonus.java
index ee3388bf..65d59a85 100644
--- a/gameserver/src/main/java/brainwine/gameserver/item/MiningBonus.java
+++ b/gameserver/src/main/java/brainwine/gameserver/item/MiningBonus.java
@@ -4,7 +4,7 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
-import brainwine.gameserver.entity.player.Skill;
+import brainwine.gameserver.player.Skill;
@JsonIgnoreProperties(ignoreUnknown = true)
public class MiningBonus {
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/ModType.java b/gameserver/src/main/java/brainwine/gameserver/item/ModType.java
index 18069ac6..c8491007 100644
--- a/gameserver/src/main/java/brainwine/gameserver/item/ModType.java
+++ b/gameserver/src/main/java/brainwine/gameserver/item/ModType.java
@@ -6,6 +6,7 @@ public enum ModType {
DECAY,
ROTATION,
+ STACK,
@JsonEnumDefaultValue
NONE;
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/Tradeability.java b/gameserver/src/main/java/brainwine/gameserver/item/Tradeability.java
new file mode 100644
index 00000000..6a5d627f
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/item/Tradeability.java
@@ -0,0 +1,22 @@
+package brainwine.gameserver.item;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+public enum Tradeability {
+
+ TRUE,
+ FALSE,
+ LEVELED;
+
+ @JsonCreator
+ private static Tradeability create(String string) {
+ switch(string) {
+ default:
+ return TRUE;
+ case "false":
+ return FALSE;
+ case "leveled":
+ return LEVELED;
+ }
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/Consumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/Consumable.java
new file mode 100644
index 00000000..c519a301
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/Consumable.java
@@ -0,0 +1,9 @@
+package brainwine.gameserver.item.consumables;
+
+import brainwine.gameserver.item.Item;
+import brainwine.gameserver.player.Player;
+
+public interface Consumable {
+
+ public void consume(Item item, Player player, Object details);
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/ConvertConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/ConvertConsumable.java
new file mode 100644
index 00000000..4681999f
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/ConvertConsumable.java
@@ -0,0 +1,88 @@
+package brainwine.gameserver.item.consumables;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import brainwine.gameserver.dialog.Dialog;
+import brainwine.gameserver.dialog.DialogSection;
+import brainwine.gameserver.dialog.input.DialogSelectInput;
+import brainwine.gameserver.item.Item;
+import brainwine.gameserver.item.ItemRegistry;
+import brainwine.gameserver.player.Inventory;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.server.messages.InventoryMessage;
+
+/**
+ * Consumable handler for upgrade kits
+ */
+public class ConvertConsumable implements Consumable {
+
+ @Override
+ public void consume(Item item, Player player, Object details) {
+ Map
- conversions = item.getConversions();
+ Inventory inventory = player.getInventory();
+
+ // Find items in the player's inventory that can be upgraded
+ Set
- convertables = conversions.keySet().stream().filter(i -> inventory.hasItem(i)).collect(Collectors.toSet());
+
+ // Don't do anything if the player has no items that can be converted
+ if(convertables.isEmpty()) {
+ player.notify("You do not have any upgradeable items.");
+ player.sendMessage(new InventoryMessage(inventory.getClientConfig(item)));
+ return;
+ }
+
+ // Map item titles to their id
+ Map keyMap = convertables.stream().collect(Collectors.toMap(Item::getTitle, Item::getId, (a, b) -> a));
+
+ // Create upgrade dialog
+ Dialog dialog = new Dialog().addSection(new DialogSection()
+ .setTitle("Which item would you like to upgrade?")
+ .setInput(new DialogSelectInput()
+ .setOptions(convertables.stream().map(Item::getTitle).collect(Collectors.toList()))
+ .setMaxColumns(3)
+ .setKey("item")));
+
+ player.showDialog(dialog, data -> {
+ // Handle cancellation
+ if(data.length == 1 && data[0].equals("cancel")) {
+ player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item)));
+ return;
+ }
+
+ // Fail if there is no data
+ if(data.length == 0) {
+ fail(item, player);
+ return;
+ }
+
+ String key = keyMap.get(data[0]);
+
+ // Fail if the chosen item title doesn't map to an id
+ if(key == null) {
+ fail(item, player);
+ return;
+ }
+
+ Item itemToUpgrade = ItemRegistry.getItem(key);
+ Item targetItem = conversions.get(itemToUpgrade);
+
+ // Fail if the player doesn't have the item they want to upgrade or there is no upgrade for it
+ if(!inventory.hasItem(itemToUpgrade) || targetItem == null) {
+ fail(item, player);
+ return;
+ }
+
+ inventory.removeItem(item, true); // Remove the consumable
+ inventory.removeItem(itemToUpgrade, true); // Remove the item that was upgraded
+ inventory.addItem(targetItem, true); // Add the item that the item upgraded to :)
+ player.notify(String.format("%s upgraded to %s!", itemToUpgrade.getTitle(), targetItem.getTitle()));
+ });
+ }
+
+ private void fail(Item item, Player player) {
+ player.notify("Oops! There was a problem with the upgrade.");
+ player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item)));
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/HealConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/HealConsumable.java
new file mode 100644
index 00000000..6e0cc455
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/HealConsumable.java
@@ -0,0 +1,16 @@
+package brainwine.gameserver.item.consumables;
+
+import brainwine.gameserver.item.Item;
+import brainwine.gameserver.player.Player;
+
+/**
+ * Consumable handler for healing items
+ */
+public class HealConsumable implements Consumable {
+
+ @Override
+ public void consume(Item item, Player player, Object details) {
+ player.heal(item.getPower());
+ player.getInventory().removeItem(item);
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/NameChangeConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/NameChangeConsumable.java
new file mode 100644
index 00000000..336abbd3
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/NameChangeConsumable.java
@@ -0,0 +1,67 @@
+package brainwine.gameserver.item.consumables;
+
+import java.util.regex.Pattern;
+
+import brainwine.gameserver.GameServer;
+import brainwine.gameserver.dialog.Dialog;
+import brainwine.gameserver.dialog.DialogHelper;
+import brainwine.gameserver.item.Item;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.player.PlayerManager;
+import brainwine.gameserver.server.messages.EventMessage;
+import brainwine.gameserver.server.messages.InventoryMessage;
+
+/**
+ * Consumable handler for name changers
+ */
+public class NameChangeConsumable implements Consumable {
+
+ private static final Pattern namePattern = Pattern.compile("^[a-zA-Z0-9_.-]{4,20}$");
+
+ @Override
+ public void consume(Item item, Player player, Object details) {
+ PlayerManager playerManager = GameServer.getInstance().getPlayerManager();
+ Dialog dialog = DialogHelper.inputDialog("Change your name",
+ "Your in-game name can include letters, numbers, dashes and periods, and must be between 4 and 20 characters in length.");
+
+ player.showDialog(dialog, data -> {
+ // Handle cancellation
+ if(data.length == 1 && data[0].equals("cancel")) {
+ player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item)));
+ return;
+ }
+
+ String name = data.length == 1 ? "" + data[0] : null;
+
+ // Check if the data is present
+ if(name == null) {
+ fail(item, player, "Oops! There was a problem with your request.");
+ return;
+ }
+
+ // Check if the name is valid
+ if(!namePattern.matcher(name).matches()) {
+ fail(item, player, "Please enter a valid name.");
+ return;
+ }
+
+ // Check if name is already taken
+ if(playerManager.getPlayer(name) != null) {
+ fail(item, player, "That name is taken already.");
+ return;
+ }
+
+ player.getInventory().removeItem(item); // Remove the consumable
+ playerManager.changePlayerName(player, name); // Process the name change
+
+ // TODO this creates a race condition
+ player.sendMessage(new EventMessage("playerNameDidChange", name)); // Client side processing stuff
+ player.kick("Your name has been changed."); // Force the player to reconnect
+ });
+ }
+
+ private void fail(Item item, Player player, String message) {
+ player.showDialog(DialogHelper.messageDialog(message));
+ player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item)));
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/RefillConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/RefillConsumable.java
new file mode 100644
index 00000000..a75de86d
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/RefillConsumable.java
@@ -0,0 +1,16 @@
+package brainwine.gameserver.item.consumables;
+
+import brainwine.gameserver.item.Item;
+import brainwine.gameserver.player.Player;
+
+/**
+ * Consumable handler for steam canisters
+ */
+public class RefillConsumable implements Consumable {
+
+ @Override
+ public void consume(Item item, Player player, Object details) {
+ // All we do is remove the item because steam functionality is pretty much entirely client-side
+ player.getInventory().removeItem(item);
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillConsumable.java
new file mode 100644
index 00000000..60563b4a
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillConsumable.java
@@ -0,0 +1,93 @@
+package brainwine.gameserver.item.consumables;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.text.WordUtils;
+
+import brainwine.gameserver.dialog.Dialog;
+import brainwine.gameserver.dialog.DialogHelper;
+import brainwine.gameserver.dialog.DialogSection;
+import brainwine.gameserver.dialog.input.DialogSelectInput;
+import brainwine.gameserver.item.Item;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.player.Skill;
+import brainwine.gameserver.server.messages.InventoryMessage;
+
+/**
+ * Consumable handler for skill upgrade items
+ */
+public class SkillConsumable implements Consumable {
+
+ @Override
+ public void consume(Item item, Player player, Object details) {
+ List bumpedSkills = player.getBumpedSkills().getOrDefault(item, Collections.emptyList());
+
+ // Check if all skills have been bumped already
+ if(bumpedSkills.size() >= Skill.values().length) {
+ player.notify(String.format("You have already increased all of your skills with %ss.", item.getTitle().toLowerCase()));
+ player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item)));
+ return;
+ }
+
+ // Assemble a list of skills that can be upgraded with this consumable
+ List upgradeableSkills = Arrays.asList(Skill.values()).stream()
+ .filter(skill -> !bumpedSkills.contains(skill) && player.getSkillLevel(skill) < 10)
+ .collect(Collectors.toList());
+
+ // Check if there are any skills to upgrade
+ if(upgradeableSkills.isEmpty()) {
+ player.notify("You have maximized all skills available for mastery.");
+ player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item)));
+ return;
+ }
+
+ List upgradeableSkillNames = upgradeableSkills.stream()
+ .map(Skill::getId)
+ .map(WordUtils::capitalize)
+ .collect(Collectors.toList());
+
+ // Create dialog
+ Dialog dialog = new Dialog().addSection(new DialogSection()
+ .setTitle("Which skill would you like to increase?")
+ .setInput(new DialogSelectInput()
+ .setOptions(upgradeableSkillNames)
+ .setMaxColumns(3)
+ .setKey("skill")));
+
+ player.showDialog(dialog, data -> {
+ // Handle cancellation
+ if(data.length == 1 && data[0].equals("cancel")) {
+ player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item)));
+ return;
+ }
+
+ // Verify data
+ if(data.length != 1) {
+ fail(item, player);
+ return;
+ }
+
+ Skill skill = Skill.fromId("" + data[0]);
+
+ // Make sure that the skill is still eligible for upgrading
+ if(skill == null || player.hasSkillBeenBumped(item, skill) || player.getSkillLevel(skill) >= 10) {
+ fail(item, player);
+ return;
+ }
+
+ player.getInventory().removeItem(item, true); // Remove consumable
+ player.trackSkillBump(item, skill); // Track skill bump
+ player.setSkillLevel(skill, player.getSkillLevel(skill) + 1); // Increase skill level
+ player.showDialog(DialogHelper.messageDialog(String.format("%s increased!", WordUtils.capitalize(skill.toString().toLowerCase())),
+ String.format("You now have additional mastery of %s.", skill.toString().toLowerCase())));
+ });
+ }
+
+ private void fail(Item item, Player player) {
+ player.notify("Oops! There was a problem with upgrading your skill.");
+ player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item)));
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillResetConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillResetConsumable.java
new file mode 100644
index 00000000..d9e23d47
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/SkillResetConsumable.java
@@ -0,0 +1,62 @@
+package brainwine.gameserver.item.consumables;
+
+import java.util.Map.Entry;
+
+import brainwine.gameserver.dialog.Dialog;
+import brainwine.gameserver.dialog.DialogHelper;
+import brainwine.gameserver.dialog.DialogSection;
+import brainwine.gameserver.item.Item;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.player.Skill;
+import brainwine.gameserver.server.messages.InventoryMessage;
+
+/**
+ * Consumable handler for skill resets
+ */
+public class SkillResetConsumable implements Consumable {
+
+ @Override
+ public void consume(Item item, Player player, Object details) {
+ // Create dialog
+ Dialog dialog = new Dialog()
+ .setActions("yesno")
+ .addSection(new DialogSection()
+ .setTitle("Confirm skill reset")
+ .setText("Are you sure that you want to reset all of your skills back to level 1?"));
+
+ player.showDialog(dialog, data -> {
+ // Handle cancellation
+ if(data.length == 1 && data[0].equals("cancel")) {
+ player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item)));
+ return;
+ }
+
+ // Check if there are any skills to reset
+ if(!player.getSkills().values().stream().anyMatch(level -> level > 1)) {
+ player.showDialog(DialogHelper.messageDialog("You don't have any skills to reset."));
+ player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item)));
+ return;
+ }
+
+ int pointsToRefund = 0;
+
+ // Reset skill levels and calculate point refund total
+ for(Entry entry : player.getSkills().entrySet()) {
+ Skill skill = entry.getKey();
+ int level = entry.getValue();
+
+ // Skip if skill hasn't been upgraded at all
+ if(level <= 1) {
+ continue;
+ }
+
+ pointsToRefund += level - 1;
+ player.setSkillLevel(skill, 1); // Reset skill level
+ }
+
+ player.getInventory().removeItem(item, true); // Remove the consumable
+ player.setSkillPoints(player.getSkillPoints() + pointsToRefund); // Refund skill points
+ player.showDialog(DialogHelper.getDialog("skill_reset"));
+ });
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/StealthConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/StealthConsumable.java
new file mode 100644
index 00000000..aa3a0ab7
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/StealthConsumable.java
@@ -0,0 +1,26 @@
+package brainwine.gameserver.item.consumables;
+
+import brainwine.gameserver.item.Item;
+import brainwine.gameserver.player.Player;
+
+/**
+ * Consumable handler for stealth cloaks
+ */
+public class StealthConsumable implements Consumable {
+
+ @Override
+ public void consume(Item item, Player player, Object details) {
+ player.getInventory().removeItem(item);
+ player.setStealth(true);
+ float seconds = item.getPower();
+
+ // Apply skill power bonus
+ if(item.hasPowerBonus()) {
+ seconds += player.getTotalSkillLevel(item.getPowerBonus().getFirst()) * item.getPowerBonus().getLast();
+ }
+
+ // Create timer
+ long delay = (long)(seconds * 1000);
+ player.addTimer("end stealth", delay, () -> player.setStealth(false));
+ }
+}
diff --git a/gameserver/src/main/java/brainwine/gameserver/item/consumables/TeleportConsumable.java b/gameserver/src/main/java/brainwine/gameserver/item/consumables/TeleportConsumable.java
new file mode 100644
index 00000000..038b9897
--- /dev/null
+++ b/gameserver/src/main/java/brainwine/gameserver/item/consumables/TeleportConsumable.java
@@ -0,0 +1,53 @@
+package brainwine.gameserver.item.consumables;
+
+import java.util.List;
+
+import brainwine.gameserver.item.Item;
+import brainwine.gameserver.item.ItemUseType;
+import brainwine.gameserver.player.Player;
+import brainwine.gameserver.server.messages.InventoryMessage;
+import brainwine.gameserver.zone.MetaBlock;
+
+/**
+ * Consumable handler for portable teleporters.
+ *
+ * These do not seem to function correctly on v3 clients.
+ */
+public class TeleportConsumable implements Consumable {
+
+ @Override
+ public void consume(Item item, Player player, Object details) {
+ // Verify details
+ if(details == null || !(details instanceof List)) {
+ fail(item, player);
+ return;
+ }
+
+ @SuppressWarnings("unchecked")
+ List