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 -[![build](https://github.com/kuroppoi/brainwine/actions/workflows/build.yml/badge.svg)](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

+

+ build + release +

+ +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)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 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 coordinates = (List)details; + + // Verify coordinates + if(coordinates.size() != 2 || !(coordinates.get(0) instanceof Integer) || !(coordinates.get(1) instanceof Integer)) { + fail(item, player); + return; + } + + int x = (int)coordinates.get(0); + int y = (int)coordinates.get(1); + MetaBlock block = player.getZone().getMetaBlock(x, y); + + // Check if there is a teleporter at the target location + if(block == null || !block.getItem().hasUse(ItemUseType.TELEPORT, ItemUseType.ZONE_TELEPORT)) { + fail(item, player); + return; + } + + player.getInventory().removeItem(item); + player.teleport(x, y); + } + + private void fail(Item item, Player player) { + player.notify("Oops! There was a problem teleporting you to your target destination."); + player.sendMessage(new InventoryMessage(player.getInventory().getClientConfig(item))); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/BurstInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/BurstInteraction.java new file mode 100644 index 00000000..46eccdea --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/BurstInteraction.java @@ -0,0 +1,63 @@ +package brainwine.gameserver.item.interactions; + +import java.util.Map; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.DamageType; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.player.Skill; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.zone.Block; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for items that explode if you get too close + */ +@SuppressWarnings("unchecked") +public class BurstInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Do nothing if data is invalid + if(!(config instanceof Map)) { + return; + } + + Player player = (Player)entity; + Map configMap = (Map)config; + boolean dodge = MapHelper.getBoolean(configMap, "dodge"); + + // Do nothing if the player is lucky enough :) + if(dodge && Math.random() * Player.MAX_SKILL_LEVEL <= player.getTotalSkillLevel(Skill.AGILITY) / 2.0F) { + return; + } + + boolean natural = MapHelper.getBoolean(configMap, "natural"); + boolean enemy = MapHelper.getBoolean(configMap, "enemy"); + Block block = zone.getBlock(x, y); + + // Check if the block has to be be natural or triggered by an enemy + if((natural && !block.isNatural()) || (enemy && (player.isStealthy() || block.getOwnerHash() == player.getBlockHash()))) { + return; + } + + DamageType damageType = DamageType.fromName(MapHelper.getString(configMap, "damage_type")); + String effect = MapHelper.getString(configMap, "effect", "bomb"); + float range = MapHelper.getFloat(configMap, "range"); + float damage = MapHelper.getFloat(configMap, "damage"); + boolean destructive = MapHelper.getBoolean(configMap, "destructive"); + + // Create explosion and destroy block + zone.explode(x, y, range, null, destructive, damage, damageType, effect); + zone.updateBlock(x, y, layer, 0); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/ChangeInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ChangeInteraction.java new file mode 100644 index 00000000..71736788 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ChangeInteraction.java @@ -0,0 +1,26 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for blocks that can change between two states + */ +public class ChangeInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + Player player = (Player)entity; + zone.updateBlock(x, y, layer, item, mod == 0 ? 1 : 0, player); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/ComposterInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ComposterInteraction.java new file mode 100644 index 00000000..999b6412 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ComposterInteraction.java @@ -0,0 +1,40 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemRegistry; +import brainwine.gameserver.player.Inventory; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.zone.EcologicalMachine; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for the composter + */ +public class ComposterInteraction extends EcologicalMachineInteraction { + + public static final int COMPOST_EARTH_COST = 10; + public static final int COMPOST_GIBLETS_COST = 3; + + public ComposterInteraction() { + super(EcologicalMachine.COMPOSTER); + } + + @Override + public void interact(Zone zone, Player player, int x, int y) { + Inventory inventory = player.getInventory(); + Item earth = ItemRegistry.getItem("ground/earth"); + Item giblets = ItemRegistry.getItem("ground/giblets"); + Item compost = ItemRegistry.getItem("ground/earth-compost"); + + // Check if player has the required items + if(!inventory.hasItem(earth, COMPOST_EARTH_COST) || !inventory.hasItem(giblets, COMPOST_GIBLETS_COST)) { + player.notify(String.format("You need %s earth and %s giblets to generate compost.", COMPOST_EARTH_COST, COMPOST_GIBLETS_COST)); + return; + } + + inventory.removeItem(earth, COMPOST_EARTH_COST, true); + inventory.removeItem(giblets, COMPOST_GIBLETS_COST, true); + inventory.addItem(compost, true); + zone.spawnEffect(x + 2.0F, y, "area steam", 10); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/ContainerInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ContainerInteraction.java new file mode 100644 index 00000000..cda7e526 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ContainerInteraction.java @@ -0,0 +1,117 @@ +package brainwine.gameserver.item.interactions; + +import java.util.stream.Stream; + +import brainwine.gameserver.GameServer; +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemRegistry; +import brainwine.gameserver.item.ItemUseType; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.loot.Loot; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.zone.EcologicalMachine; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for lootable containers + */ +public class ContainerInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Check if the right data is present + if(metaBlock == null || data != null) { + return; + } + + Player player = (Player)entity; + String dungeonId = metaBlock.getStringProperty("@"); + + // Check if container is protected + if(item.hasUse(ItemUseType.FIELDABLE) && (zone.isBlockProtected(x, y, player) || (dungeonId != null && zone.isDungeonIntact(dungeonId)))) { + player.notify("This container is secured by protectors in the area."); + return; + } + + boolean plenty = item.hasUse(ItemUseType.PLENTY); + String lootCode = metaBlock.getStringProperty("y"); + + // Check loot code + if(plenty) { + if(lootCode == null) { + player.notify("This chest cannot be plundered."); + return; + } + + if(player.hasLootCode(lootCode)) { + player.notify("You've already plundered this chest."); + return; + } + } + + String specialItem = metaBlock.getStringProperty("$"); + + // Award loot + if(specialItem != null) { + if(specialItem.equals("?")) { + Loot loot = metaBlock.hasProperty("l") ? new Loot(Item.get(metaBlock.getStringProperty("l")), metaBlock.getIntProperty("q")) + : GameServer.getInstance().getLootManager().getRandomLoot(player, item.getLootCategories()); + int experience = metaBlock.getIntProperty("xp"); + + if(loot != null) { + if(plenty) { + player.addLootCode(lootCode); + } else { + metaBlock.removeProperty("$"); + metaBlock.removeProperty("xp"); + } + + player.awardLoot(loot, item.getLootGraphic(), "You found:"); + player.addExperience(experience); + player.getStatistics().trackContainerLooted(item); + } else { + player.notify("No eligible loot could be found for this container."); + } + } else { + Item machinePart = ItemRegistry.getItem(specialItem); + + if(zone.addMachinePart(machinePart)) { + EcologicalMachine machine = EcologicalMachine.fromPart(machinePart); + String machineName = machine.toString().toLowerCase(); + String determiner = Stream.of("a", "e", "i", "o", "u").filter(machineName::startsWith).findFirst().isPresent() ? "an" : "a"; + String text = String.format("You discovered %s %s component!", determiner, machineName); + + if(player.isV3()) { + player.notify(text, NotificationType.ACCOMPLISHMENT); + } else { + Object message = MapHelper.map(String.class, String.class, + "t", text, + "i", machinePart.getId()); + player.notify(message, NotificationType.ACCOMPLISHMENT); + } + + player.notifyPeers(String.format("%s discovered %s %s component.", player.getName(), determiner, machineName), NotificationType.PEER_ACCOMPLISHMENT); + player.getStatistics().trackDiscovery(machinePart); + metaBlock.removeProperty("$"); + } else { + // TODO how should we handle this...? + } + } + } + + // Update container mod + if(!plenty && !metaBlock.hasProperty("$")) { + zone.updateBlock(x, y, Layer.FRONT, item, 0, metaBlock.getOwner(), metaBlock.getMetadata()); + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/DialogInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/DialogInteraction.java new file mode 100644 index 00000000..9741c06f --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/DialogInteraction.java @@ -0,0 +1,125 @@ +package brainwine.gameserver.item.interactions; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for blocks that may be configured through a dialog + */ +@SuppressWarnings("unchecked") +public class DialogInteraction implements ItemInteraction { + + private boolean creationOnly; + + public DialogInteraction(boolean creationOnly) { + this.creationOnly = creationOnly; + } + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Do nothing if the required data isn't present + if(data == null || !(config instanceof Map)) { + return; + } + + // Do nothing if this block has already been configured and cannot be re-configured with this interaction + if(creationOnly && metaBlock != null && metaBlock.getBooleanProperty("cd")) { + return; + } + + Player player = (Player)entity; + Map configMap = (Map)config; + String target = MapHelper.getString(configMap, "target", "none"); + + // Do nothing for now if the target isn't the block's metadata + if(!target.equals("meta")) { + player.notify("Sorry, this action isn't implemented yet."); + return; + } + + // Update block metadata + Map metadata = new HashMap<>(); + List> sections = MapHelper.getList(configMap, "sections"); + + if(metaBlock != null) { + metadata.putAll(metaBlock.getMetadata()); + } + + if(sections != null && data.length == sections.size()) { + for(int i = 0; i < sections.size(); i++) { + Map section = sections.get(i); + String key = MapHelper.getString(section, "input.key"); + String type = MapHelper.getString(section, "input.type"); + + if(key != null && type != null) { + List options = MapHelper.getList(section, "input.options", Collections.EMPTY_LIST); + String text = String.valueOf(data[i]); + + switch(type) { + case "text": + int max = MapHelper.getInt(section, "input.max", MapHelper.getInt(section, "input.maxlength")); // Defaults to 0 = no limit + + // Get rid of text if player is currently muted + if(player.isMuted() && MapHelper.getBoolean(section, "input.sanitize")) { + text = text.replaceAll(".", "*"); + } + + // Shorten text if it is too long + if(max > 0 && text.length() > max) { + text = text.substring(0, max); + } + + metadata.put(key, text); + break; + case "text select": + case "select": + case "color": + // Check if input matches available options + if(!options.isEmpty() && !options.contains(text)) { + text = options.get(0); + } + + metadata.put(key, text); + break; + case "text index": + metadata.put(key, Math.max(0, Math.min(options.size() - 1, data[i] instanceof Integer ? (int)data[i] : 0))); + break; + } + } else if(MapHelper.getBoolean(section, "input.mod")) { + List options = MapHelper.getList(section, "input.options"); + + if(options != null) { + mod = options.indexOf(data[i]); + mod = mod == -1 ? 0 : mod; + mod *= MapHelper.getInt(section, "input.mod_multiple", 1); + zone.updateBlock(x, y, layer, item, mod, player); + } + } + } + } + + // Set configured flag + if(creationOnly) { + metadata.put("cd", true); + } + + // Update meta block + zone.setMetaBlock(x, y, item, player, metadata); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/EcologicalMachineInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/EcologicalMachineInteraction.java new file mode 100644 index 00000000..d466a253 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/EcologicalMachineInteraction.java @@ -0,0 +1,65 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.zone.EcologicalMachine; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +public abstract class EcologicalMachineInteraction implements ItemInteraction { + + protected final EcologicalMachine machine; + + public EcologicalMachineInteraction(EcologicalMachine machine) { + this.machine = machine; + } + + public abstract void interact(Zone zone, Player player, int x, int y); + + @Override + public final void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + Player player = (Player)entity; + + // Do nothing if the machine cannot be used yet. + if(!canUseMachine(zone, player, x, y, metaBlock)) { + return; + } + + // Handle interaction + interact(zone, player, x, y); + } + + private boolean canUseMachine(Zone zone, Player player, int x, int y, MetaBlock metaBlock) { + // Check if machine is already active + if(metaBlock.getBooleanProperty("activated")) { + return true; + } + + int totalParts = machine.getPartCount(); + int foundParts = zone.getDiscoveredParts(machine).size(); + + // Check if parts have been discovered + if(foundParts < totalParts) { + int remainingParts = totalParts - foundParts; + player.notify(String.format("%s part%s of the %s still need%s to be found.", + remainingParts, remainingParts == 1 ? "" : "s", machine.getId(), remainingParts == 1 ? "s" : "")); + return false; + } + + // Activate the machine! + metaBlock.setProperty("activated", true); + zone.updateBlock(x, y, Layer.FRONT, machine.getBase(), 2, null, metaBlock.getMetadata()); // TODO + player.notify(String.format("You activated the %s!", machine.getId()), NotificationType.ACCOMPLISHMENT); + player.notifyPeers(String.format("%s activated the %s!", player.getName(), machine.getId()), NotificationType.PEER_ACCOMPLISHMENT); + return false; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/ExpiatorInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ExpiatorInteraction.java new file mode 100644 index 00000000..5fb928ae --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ExpiatorInteraction.java @@ -0,0 +1,56 @@ +package brainwine.gameserver.item.interactions; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.zone.EcologicalMachine; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for the expiator + */ +public class ExpiatorInteraction extends EcologicalMachineInteraction { + + public ExpiatorInteraction() { + super(EcologicalMachine.EXPIATOR); + } + + @Override + public void interact(Zone zone, Player player, int x, int y) { + // TODO create a more generic function for this + List ghosts = zone.getNpcs().stream() + .filter(npc -> npc.getConfig().getName().equals("ghost") && npc.inRange(x, y, 5.0)) + .collect(Collectors.toList()); + + // Check if there are ghosts nearby + if(ghosts.isEmpty()) { + player.notify("No ghosts in range."); + return; + } + + List protectors = zone.getMetaBlocksWithItem("hell/dish"); + Collections.shuffle(protectors); + + // Expiate nearby ghosts + for(Entity ghost : ghosts) { + ghost.setHealth(0.0F); + + // Destroy a random infernal protector + if(!protectors.isEmpty()) { + MetaBlock protector = protectors.remove(0); + zone.updateBlock(protector.getX(), protector.getY(), Layer.FRONT, 0); + } + } + + zone.spawnEffect(x + 2.0F, y, "expiate", 10); + player.notify("You released a lost soul!", NotificationType.ACCOMPLISHMENT); + player.notifyPeers(String.format("%s released a lost soul.", player.getName()), NotificationType.PEER_ACCOMPLISHMENT); + player.getStatistics().trackDeliverances(ghosts.size()); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/GeckInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/GeckInteraction.java new file mode 100644 index 00000000..95107b84 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/GeckInteraction.java @@ -0,0 +1,20 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.player.Player; +import brainwine.gameserver.zone.EcologicalMachine; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for the purifier + */ +public class GeckInteraction extends EcologicalMachineInteraction { + + public GeckInteraction() { + super(EcologicalMachine.PURIFIER); + } + + @Override + public void interact(Zone zone, Player player, int x, int y) { + player.notify("The purifier is working."); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/ItemInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ItemInteraction.java new file mode 100644 index 00000000..ea95a942 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/ItemInteraction.java @@ -0,0 +1,13 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +public interface ItemInteraction { + + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data); +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/LandmarkInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/LandmarkInteraction.java new file mode 100644 index 00000000..9dd0419a --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/LandmarkInteraction.java @@ -0,0 +1,83 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.dialog.Dialog; +import brainwine.gameserver.dialog.DialogHelper; +import brainwine.gameserver.dialog.DialogSection; +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +import java.util.HashMap; +import java.util.Map; + +public class LandmarkInteraction implements ItemInteraction { + private static final int VOTING_INTERVAL = 1000; + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, Object config, Object[] data) { + if(!entity.isPlayer()) return; + + // Do nothing if data is invalid + if(data != null) { + return; + } + + Player player = (Player)entity; + + if(player.getLevel() < 10) { + player.notify("Sorry, you must be level 10 or higher to vote on landmarks."); + return; + } + + long now = System.currentTimeMillis(); + if(player.getLastLandmarkVoteAt() + VOTING_INTERVAL > now) { + player.notify("You must wait a bit before voting again."); + return; + } + + if(metaBlock.getOwner() == player) { + player.notify("Sorry, you cannot upvote your own landmark."); + return; + } + + Map v = MapHelper.getMap(metaBlock.getMetadata(), "v"); + if(v != null && v.containsKey(player.getDocumentId())) { + player.notify("You have already upvoted this landmark."); + return; + } + + String name = metaBlock.getStringProperty("n"); + Dialog dialog = new Dialog() + .setTitle("Landmark Upvote") + .setActions("Cancel", "Yes") + .addSection(new DialogSection().setTitle("Upvote " + name + "?")); + + player.showDialog(dialog, ans -> { + if(ans.length == 0) return; + if("Yes".equals(ans[0])) { + int current = metaBlock.getIntProperty("vc"); // will return 0 if null + metaBlock.setProperty("vc", current + 1); + + Map currentVotes = MapHelper.getMap(metaBlock.getMetadata(), "v", new HashMap<>()); + currentVotes.put(player.getDocumentId(), now); + metaBlock.getMetadata().put("v", currentVotes); + zone.updateBlockMod(metaBlock.getX(), metaBlock.getY(), Layer.FRONT, 1); + zone.sendBlockMetaUpdate(metaBlock); + + player.setLastLandmarkVoteAt(now); + player.addExperience(10); + player.getStatistics().trackLandmarksUpvoted(); + + Player owner = metaBlock.getOwner(); + if(owner != null) { + owner.getStatistics().trackLandmarkVotesReceived(); + } + + player.showDialog(DialogHelper.messageDialog("Vote Received", "Thanks for your upvote!")); + } + }); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/MinigameInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/MinigameInteraction.java new file mode 100644 index 00000000..6691fcd2 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/MinigameInteraction.java @@ -0,0 +1,94 @@ +package brainwine.gameserver.item.interactions; + +import java.util.Map; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.minigame.Minigame; +import brainwine.gameserver.minigame.Pandora; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +@SuppressWarnings("unchecked") +public class MinigameInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Check item (to prevent dialog spoof) + if(!zone.isChunkLoaded(x, y) || zone.getBlock(x, y).getFrontItem() != item) { + return; + } + + // Check & interact with active minigame + // TODO verifying the type wouldn't be a bad idea + Minigame activeMinigame = zone.getMinigame(x, y); + Player player = (Player)entity; + + if(activeMinigame != null) { + activeMinigame.onInteract(player); + return; + } + + // Do nothing if config is invalid + if(!(config instanceof Map)) { + return; + } + + Map configMap = (Map)config; + String type = MapHelper.getString(configMap, "type"); + + // Check if type is present + if(type == null) { + player.notify("Minigame type has not been configured."); + return; + } + + // Handle custom minigame + if(type.equals("custom")) { + player.notify("Sorry, custom minigames are not supported yet."); + return; + } + + Map startDialog = MapHelper.getMap(configMap, "start_dialog"); + + // Show start dialog if present and input hasn't been supplied + if(startDialog != null && data == null) { + player.showDialog(startDialog, input -> interact(zone, entity, x, y, layer, item, mod, metaBlock, config, input)); + return; + } else if(startDialog != null && data.length == 1 && "cancel".equals(data[0])) { + return; // Cancel action + } + + // Check if max number of minigames has been reached + if(zone.getMinigameCount() >= Zone.MAX_CONCURRENT_MINIGAMES) { + player.notify("Sorry, the maximum number of active minigames has been reached."); + return; + } + + Minigame minigame = null; + + // Create minigame session based on type + // TODO enum? + switch(type) { + case "pandora": + minigame = new Pandora(zone, player, x, y); + break; + default: + player.notify(String.format("Sorry, minigame type '%s' is not supported.", type)); + return; + } + + // Let's get this party started! + zone.startMinigame(minigame); + } + +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/NoteInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/NoteInteraction.java new file mode 100644 index 00000000..1885bc56 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/NoteInteraction.java @@ -0,0 +1,98 @@ +package brainwine.gameserver.item.interactions; + +import java.util.Collections; +import java.util.List; + +import brainwine.gameserver.dialog.Dialog; +import brainwine.gameserver.dialog.DialogHelper; +import brainwine.gameserver.dialog.DialogSection; +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +public class NoteInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Do nothing if the right data isn't present + if(metaBlock == null || data != null) { + return; + } + + Player player = (Player)entity; + + // Check if note contains a location + if(metaBlock.hasProperty("l")) { + List location = MapHelper.getList(metaBlock.getMetadata(), "l", Collections.emptyList()); + String text = metaBlock.getStringProperty("t"); + + // Do nothing if location data is invalid + if(location.size() != 2) { + return; + } + + int locationX = location.get(0); + int locationY = location.get(1); + + // Create dialog based on player version since v3 doesn't support map dialogs + if(player.isV3()) { + Dialog dialog = new Dialog() + .addSection(new DialogSection() + .setTitle("The note reads:") + .setText(text)) + .addSection(new DialogSection() + .setText(zone.getReadableCoordinates(locationX, locationY))); + player.showDialog(dialog); + } else { + // v2 dialog + Dialog dialog = new Dialog() + .addSection(new DialogSection() + .setTitle(text)) + .addSection(new DialogSection() + .setLocation(locationX, locationY)); + player.notify(dialog, NotificationType.NOTE); + } + + return; + } + + // Do nothing if player owns this note + if(metaBlock.isOwnedBy(player)) { + return; + } + + // Build string from note segments + String[] keys = { "t1", "t2", "t3", "t4", "t5", "t6" }; + StringBuilder builder = new StringBuilder(); + + for(int i = 0; i < keys.length; i++) { + String text = metaBlock.getStringProperty(keys[i]); + + // Skip if text is null or empty + if(text == null || text.isEmpty()) { + continue; + } + + // Append space if necessary + if(i > 0) { + builder.append(" "); + } + + builder.append(text); + } + + // Show note text in dialog + player.showDialog(DialogHelper.messageDialog("The note reads:", builder.toString())); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/RecyclerInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/RecyclerInteraction.java new file mode 100644 index 00000000..b561b40a --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/RecyclerInteraction.java @@ -0,0 +1,81 @@ +package brainwine.gameserver.item.interactions; + +import java.util.Map; +import java.util.Map.Entry; + +import brainwine.gameserver.dialog.Dialog; +import brainwine.gameserver.dialog.DialogListItem; +import brainwine.gameserver.dialog.DialogSection; +import brainwine.gameserver.dialog.DialogType; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemRegistry; +import brainwine.gameserver.player.Inventory; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.player.Skill; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.util.MathUtils; +import brainwine.gameserver.zone.EcologicalMachine; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for the recycler + */ +public class RecyclerInteraction extends EcologicalMachineInteraction { + + public RecyclerInteraction() { + super(EcologicalMachine.RECYCLER); + } + + @Override + public void interact(Zone zone, Player player, int x, int y) { + Map recyclables = MapHelper.map(Item.class, Item.class, + ItemRegistry.getItem("rubble/iron"), ItemRegistry.getItem("building/iron"), + ItemRegistry.getItem("rubble/copper"), ItemRegistry.getItem("building/copper"), + ItemRegistry.getItem("rubble/brass"), ItemRegistry.getItem("building/brass")); + int totalScrapRecycled = 0; + int scrapRequired = (int)MathUtils.lerp(10.0, 5.0, player.getTotalSkillLevel(Skill.BUILDING) / 9.0); + Inventory inventory = player.getInventory(); + DialogSection section = new DialogSection(); + + // Recycle scrap + for(Entry entry : recyclables.entrySet()) { + Item scrapItem = entry.getKey(); + int recycleCount = inventory.getQuantity(scrapItem) / scrapRequired; + + // Skip if there isn't enough scrap of this type to recycle + if(recycleCount == 0) { + continue; + } + + // Update inventory + Item resultItem = entry.getValue(); + int scrapCount = recycleCount * scrapRequired; + totalScrapRecycled += scrapCount; + inventory.removeItem(scrapItem, scrapCount, true); + inventory.addItem(resultItem, recycleCount, true); + + // Create dialog item + section.addItem(new DialogListItem() + .setItem(resultItem.getCode()) + .setText(String.format("%s x %s", resultItem.getTitle(), recycleCount))); + } + + // Check if anything was recycled + if(totalScrapRecycled == 0) { + player.notify("You do not have enough scrap to recycle."); + return; + } + + zone.spawnEffect(x + 2.0F, y, "area steam", 10); + Dialog dialog = new Dialog() + .addSection(section.setTitle(String.format("You recycled %s scrap into:", totalScrapRecycled))); + + // Show result dialog + if(player.isV3()) { + player.showDialog(dialog.setType(DialogType.LOOT)); + } else { + player.notify(dialog, NotificationType.REWARD); + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnInteraction.java new file mode 100644 index 00000000..52e7ae55 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnInteraction.java @@ -0,0 +1,27 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for items that can spawn entities + */ +public class SpawnInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if item can't spawn entities + if(!item.hasEntitySpawns() || mod != 0) { + return; + } + + // Try to spawn the entity and update block mod + if(zone.spawnEntity(item.getEntitySpawns().next(), x, y) != null) { + zone.updateBlock(x, y, layer, item, 1); + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnTeleportInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnTeleportInteraction.java new file mode 100644 index 00000000..c4f6fb80 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SpawnTeleportInteraction.java @@ -0,0 +1,27 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for that one white teleporter in the tutorial world + */ +public class SpawnTeleportInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Send the player to a suitable beginner zone + Player player = (Player)entity; + player.changeZone(null); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java new file mode 100644 index 00000000..a2eb16fd --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/SwitchInteraction.java @@ -0,0 +1,230 @@ +package brainwine.gameserver.item.interactions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.commons.text.WordUtils; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.item.DamageType; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemUseType; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.util.Vector2i; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for switches + */ +public class SwitchInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if the required data isn't present + if(data != null || metaBlock == null) { + return; + } + + int timer = metaBlock.getIntProperty("t"); + + // Do nothing if this switch has a timer and is already flipped + if(timer > 0 && mod % 2 == 1) { + return; + } + + // Show configured message to nearby players + String message = interpolateMessage(entity, metaBlock.getStringProperty("m")); + + if(message != null && !message.isEmpty()) { + float effectX = x + item.getBlockWidth() / 2.0F; + float effectY = y - item.getBlockHeight() + 1; + zone.spawnEffect(effectX, effectY, "emote", message); + } + + // Prepare list of targets + List> positions = MapHelper.getList(metaBlock.getMetadata(), ">", Collections.emptyList()); + List targets = new ArrayList<>(); + targets.add(new Vector2i(x, y)); + positions.stream().map(position -> new Vector2i(position.get(0), position.get(1))).forEach(targets::add); + int switchedMod = mod % 2 == 0 ? mod + 1 : mod - 1; + + // Switch all target blocks + for(Vector2i target : targets) { + switchBlock(zone, entity, target.getX(), target.getY(), switchedMod, metaBlock); + } + + // Create block timer if this is a timed switch + if(timer > 0) { + int unswitchedMod = switchedMod % 2 == 0 ? switchedMod + 1 : switchedMod - 1; + + zone.addBlockTimer(x, y, timer * 1000, () -> { + for(Vector2i target : targets) { + switchBlock(zone, entity, target.getX(), target.getY(), unswitchedMod, metaBlock); + } + }); + } + } + + private void switchBlock(Zone zone, Entity entity, int x, int y, int mod, MetaBlock switchMeta) { + // Do nothing if the target chunk isn't loaded + if(!zone.isChunkLoaded(x, y)) { + return; + } + + MetaBlock metaBlock = zone.getMetaBlock(x, y); + + // Do nothing if there is no metadata + if(metaBlock == null) { + return; + } + + Player owner = metaBlock == null ? null : metaBlock.getOwner(); + Map metadata = metaBlock == null ? null : metaBlock.getMetadata(); + Item item = metaBlock.getItem(); + Object config = item.getUse(ItemUseType.SWITCHED); + + if(config instanceof String) { + String type = (String)config; + + // Not the prettiest way to do this but it will have to do. + switch(type.toLowerCase()) { + case "spawner": switchSpawner(zone, metaBlock); break; + case "exploder": switchExploder(zone, entity, metaBlock); break; + case "messagesign": switchSign(zone, entity, metaBlock, switchMeta); break; + default: break; + } + } else if(item.hasUse(ItemUseType.SWITCH, ItemUseType.SWITCHED, ItemUseType.TRIGGER)) { + zone.updateBlock(x, y, Layer.FRONT, item, mod, owner, metadata); + } + } + + private void switchSpawner(Zone zone, MetaBlock metaBlock) { + // Kill existing entity + if(metaBlock.hasProperty("eid")) { + Entity entity = zone.getEntity(metaBlock.getIntProperty("eid")); + + if(entity instanceof Npc) { + Npc npc = (Npc)entity; + + // TODO an isArtificial() check will work well enough as a fix for #45 for now since spawners are the only things that use it + if(!npc.isDead() && npc.isArtificial()) { + npc.spawnEffect("bomb-teleport", 4); + npc.setHealth(0); + } + } + } + + Object config = metaBlock.getItem().getUse(ItemUseType.SPAWN); + + // Do nothing if use config data is invalid + if(!(config instanceof Map)) { + return; + } + + // Try to spawn entity + String entityType = MapHelper.getString((Map)config, metaBlock.getStringProperty("e")); + Npc npc = zone.spawnEntity(entityType, metaBlock.getX(), metaBlock.getY(), true); + + // Do nothing if entity failed to spawn + if(npc == null) { + return; + } + + npc.setArtificial(true); + metaBlock.setProperty("eid", npc.getId()); + } + + // TODO exploders were used to create lag machines back in the day, so maybe we should put a cooldown on this + private void switchExploder(Zone zone, Entity entity, MetaBlock metaBlock) { + String type = metaBlock.getStringProperty("e"); + + // Do nothing if explosion type doesn't exist in block metadata + if(type == null) { + return; + } + + int x = metaBlock.getX(); + int y = metaBlock.getY(); + + // Do nothing if exploder isn't activated + if(zone.getBlock(x, y).getFrontMod() == 0) { + return; + } + + // Create explosion + DamageType damageType = type.equalsIgnoreCase("electric") ? DamageType.ENERGY : DamageType.fromName(type); + String effect = String.format("bomb-%s", type.toLowerCase()); + zone.explode(x, y, 6, entity, false, 6, damageType, effect); + } + + private void switchSign(Zone zone, Entity entity, MetaBlock metaBlock, MetaBlock switchMeta) { + String message = interpolateMessage(entity, switchMeta.hasProperty("m") ? switchMeta.getStringProperty("m").trim() : ""); + boolean lock = metaBlock.hasProperty("lock") && metaBlock.getStringProperty("lock").equalsIgnoreCase("yes"); + Item item = metaBlock.getItem(); + + // Check and update lock status + if(lock) { + boolean locked = metaBlock.getBooleanProperty("locked"); + + if(!message.isEmpty()) { + if(locked) { + return; + } + + metaBlock.setProperty("locked", true); + } else if(locked) { + metaBlock.removeProperty("locked"); + } + } + + // Update sign text + String separator = "\n"; + String[] keys = {"t1", "t2", "t3", "t4"}; + String[] segments = WordUtils.wrap(message, 20, separator, true).split(separator, 4); + + for(int i = 0; i < keys.length; i++) { + String key = keys[i]; + String text = i < segments.length ? segments[i] : ""; + int separatorIndex = text.lastIndexOf(separator); + + if(separatorIndex != -1) { + text = text.substring(0, separatorIndex); + } + + metaBlock.setProperty(key, text); + } + + // Send data to players + float effectX = metaBlock.getX() + (float)item.getBlockWidth() / 2; + float effectY = metaBlock.getY() - (float)item.getBlockHeight() / 2 + 1; + zone.spawnEffect(effectX, effectY, "area steam", 10); + zone.sendBlockMetaUpdate(metaBlock); + } + + private String interpolateMessage(Entity entity, String message) { + if(message == null) return null; + + if(entity != null) { + String name; + if(entity.isPlayer()) { + name = entity.getName(); + } else { + Npc npc = (Npc) entity; + name = npc.getName() == null ? npc.getConfig().getTitle() : npc.getName(); + } + + if(name != null) { + message = message.replaceAll("\\*(player|mob)\\*", name); + } + } + + return message; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/TargetTeleportInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/TargetTeleportInteraction.java new file mode 100644 index 00000000..a8fb199a --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/TargetTeleportInteraction.java @@ -0,0 +1,115 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.GameServer; +import brainwine.gameserver.dialog.Dialog; +import brainwine.gameserver.dialog.DialogSection; +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.zone.Biome; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +public class TargetTeleportInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Do nothing if data is invalid + if(data != null) { + return; + } + + Player player = (Player)entity; + Zone targetZone = zone; + String zoneName = metaBlock.getStringProperty("pz"); + + // Validate target zone + if(zoneName != null) { + targetZone = GameServer.getInstance().getZoneManager().getZoneByName(zoneName); + + if(targetZone == null) { + player.notify(String.format("Cannot locate world '%s', please recalibrate.", zoneName)); + return; + } + + if(!targetZone.canJoin(player)) { + player.notify("Sorry, you can't enter this world right now."); + return; + } + } + + // Parse target position + int targetX = -1; + int targetY = metaBlock.getIntProperty("py") + (targetZone.getBiome() == Biome.DEEP ? -1000 : 200); + int centerX = targetZone.getWidth() / 2; + + try { + String strX = metaBlock.getStringProperty("px"); + + if(strX != null) { + if(strX.endsWith("w")) { + targetX = centerX - Integer.parseInt(strX.replace("w", "")); + } else { + targetX = centerX + Integer.parseInt(strX.replace("e", "")); + } + } + } catch(NumberFormatException e) { + // Discard silently + } + + // Do nothing if target is out of bounds + if(!targetZone.areCoordinatesInBounds(targetX, targetY)) { + player.notify("Cannot locate destination, please recalibrate."); + return; + } + + // Do nothing if target location is unexplored + if(!player.isGodMode() && !targetZone.isAreaExplored(targetX, targetY)) { + player.notify("That area hasn't been explored yet."); + return; + } + + // Check area protection + if(!player.isGodMode() && targetZone.isBlockProtected(targetX, targetY, player)) { + Player owner = metaBlock.getOwner(); + int setting = metaBlock.getIntProperty("pt"); + boolean ownerCanEdit = !targetZone.isBlockProtected(targetX, targetY, owner); + + // Check protection entry setting + if(owner == null || !ownerCanEdit || setting == 0 || (setting == 1 && !owner.isFollowing(player))) { + player.notify("That area is protected."); + return; + } + } + + // Teleport the player to the target location + if(targetZone == zone) { + player.teleport(targetX, targetY); + } else { + // Create confirmation dialog + Dialog dialog = new Dialog() + .setActions("yesno") + .addSection(new DialogSection() + .setTitle("Attention") + .setText(String.format("Teleport to world '%s'?", targetZone.getName()))); + + // Show confirmation dialog for zone change + Zone _targetZone = targetZone; + int _targetX = targetX; + int _targetY = targetY; + player.showDialog(dialog, input -> { + // TODO figure out this v2 quirk + if((!player.isV3() && input.length == 0) || (input.length == 1 && input[0].equals("Yes"))) { + player.changeZone(_targetZone, _targetX, _targetY); + } + }); + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/TeleportInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/TeleportInteraction.java new file mode 100644 index 00000000..43d8f635 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/TeleportInteraction.java @@ -0,0 +1,56 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemUseType; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for teleporters + */ +public class TeleportInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + Player player = (Player)entity; + + // Try to repair teleporter + if(mod == 0) { + zone.updateBlock(x, y, layer, item, 1); + player.getStatistics().trackDiscovery(item); + player.notify("You repaired a teleporter!", NotificationType.ACCOMPLISHMENT); + player.notifyPeers(String.format("%s repaired a teleporter.", player.getName()), NotificationType.SYSTEM); + return; + } + + // Verify data + if(data == null || data.length != 2 || mod != 1) { + return; + } + + int targetX = data[0] instanceof Integer ? (int)data[0] : -1; + int targetY = data[1] instanceof Integer ? (int)data[1] : -1; + MetaBlock targetMeta = zone.getMetaBlock(targetX, targetY); + + // Do nothing if target has no metadata + if(targetMeta == null) { + return; + } + + // Teleport the player if the target location is valid + if((targetMeta.getItem().hasUse(ItemUseType.TELEPORT) && zone.getBlock(targetX, targetY).getFrontMod() == 1) + || targetMeta.getItem().hasUse(ItemUseType.ZONE_TELEPORT)) { + player.teleport(targetX + 1, targetY); + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/TransmitInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/TransmitInteraction.java new file mode 100644 index 00000000..e50be386 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/TransmitInteraction.java @@ -0,0 +1,58 @@ +package brainwine.gameserver.item.interactions; + +import java.util.Collections; +import java.util.List; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemUseType; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for target teleporters + */ +public class TransmitInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Do nothing if the required data isn't present + if(data != null || metaBlock == null) { + return; + } + + Player player = (Player)entity; + List> positions = MapHelper.getList(metaBlock.getMetadata(), ">", Collections.emptyList()); + + // Do nothing if there is no linked position + if(positions.isEmpty()) { + return; + } + + List position = positions.get(0); + int targetX = position.get(0); + int targetY = position.get(1); + + // Make sure that the target location is in bounds + if(!zone.areCoordinatesInBounds(targetX, targetY)) { + return; + } + + // Notify the player if the target beacon is missing + if(!zone.getBlock(targetX, targetY).getFrontItem().hasUse(ItemUseType.TRANSMITTED)) { + player.notify("There is no beacon at the target location."); + return; + } + + player.teleport(targetX, targetY); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/interactions/WarmthInteraction.java b/gameserver/src/main/java/brainwine/gameserver/item/interactions/WarmthInteraction.java new file mode 100644 index 00000000..7ef4c495 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/interactions/WarmthInteraction.java @@ -0,0 +1,32 @@ +package brainwine.gameserver.item.interactions; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; + +/** + * Interaction handler for blocks that provide warmth + */ +public class WarmthInteraction implements ItemInteraction { + + @Override + public void interact(Zone zone, Entity entity, int x, int y, Layer layer, Item item, int mod, MetaBlock metaBlock, + Object config, Object[] data) { + // Do nothing if entity is not a player + if(!entity.isPlayer()) { + return; + } + + // Do nothing if object isn't lit or active + if(mod == 0) { + return; + } + + Player player = (Player)entity; + player.applyWarmth(); + player.notify("Ahh, toasty warmth."); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/loot/Loot.java b/gameserver/src/main/java/brainwine/gameserver/loot/Loot.java index f1a39004..90f7bbf5 100644 --- a/gameserver/src/main/java/brainwine/gameserver/loot/Loot.java +++ b/gameserver/src/main/java/brainwine/gameserver/loot/Loot.java @@ -28,6 +28,13 @@ public class Loot { @JsonCreator private Loot() {} + /** + * Arbitrary constructor for chests o' plenty + */ + public Loot(Item item, int quantity) { + this.items.put(item, quantity); + } + public Map getItems() { return items; } diff --git a/gameserver/src/main/java/brainwine/gameserver/loot/LootManager.java b/gameserver/src/main/java/brainwine/gameserver/loot/LootManager.java index b1e5fba7..3e648a15 100644 --- a/gameserver/src/main/java/brainwine/gameserver/loot/LootManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/loot/LootManager.java @@ -2,8 +2,8 @@ import static brainwine.shared.LogMarkers.SERVER_MARKER; -import java.io.File; import java.io.IOException; +import java.net.URL; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; @@ -18,15 +19,18 @@ import com.fasterxml.jackson.core.type.TypeReference; -import brainwine.gameserver.entity.player.Player; -import brainwine.gameserver.entity.player.Skill; -import brainwine.gameserver.util.ResourceUtils; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.player.Skill; +import brainwine.gameserver.resource.ResourceFinder; import brainwine.gameserver.util.WeightedMap; +import brainwine.gameserver.zone.Biome; import brainwine.shared.JsonHelper; public class LootManager { - public static final int[] MIN_FREQUENCIES_BY_LUCK = {18, 15, 12, 9, 7, 5, 4, 3, 2}; + public static final double LEVELS_PER_BONUS_ROLL = 6.0; + public static final int MAX_BONUS_ROLLS = 20; private static final Logger logger = LogManager.getLogger(); private final Map> lootTables = new HashMap<>(); @@ -36,37 +40,34 @@ public LootManager() { private void loadLootTables() { logger.info(SERVER_MARKER, "Loading loot tables ..."); - File file = new File("loottables.json"); - ResourceUtils.copyDefaults("loottables.json"); - if(file.isFile()) { - try { - Map> loot = JsonHelper.readValue(file, new TypeReference>>(){}); - lootTables.putAll(loot); - } catch (IOException e) { - logger.error(SERVER_MARKER, "Failed to load loot tables", e); - } + try { + URL url = ResourceFinder.getResourceUrl("loottables.json"); + Map> loot = JsonHelper.readValue(url, new TypeReference>>(){}); + lootTables.putAll(loot); + } catch (IOException e) { + logger.error(SERVER_MARKER, "Failed to load loot tables", e); } } public List getLootTable(String category) { - return lootTables.getOrDefault(category, Collections.emptyList()); + return lootTables.get(category); + } + + public Set getLootCategories() { + return Collections.unmodifiableSet(lootTables.keySet()); } - public List getEligibleLoot(Player player, String... categories) { - return getEligibleLoot(player, Arrays.asList(categories)); + public List getEligibleLoot(Biome biome, Set ignore, String... categories) { + return getEligibleLoot(biome, ignore, Arrays.asList(categories)); } - public List getEligibleLoot(Player player, Collection categories) { - int luck = player.getSkillLevel(Skill.LUCK); - int minFrequency = luck > MIN_FREQUENCIES_BY_LUCK.length ? 1 : MIN_FREQUENCIES_BY_LUCK[luck - 1]; + public List getEligibleLoot(Biome biome, Set ignore, Collection categories) { List eligibleLoot = lootTables.entrySet().stream() .filter(entry -> categories.contains(entry.getKey())) .map(Entry::getValue) .flatMap(Collection::stream) - .filter(loot -> (loot.getBiome() == null || loot.getBiome() == player.getZone().getBiome()) - && (Math.random() <= luck * 0.015 || loot.getFrequency() >= minFrequency) - && !player.getInventory().getWardrobe().containsAll(loot.getItems().keySet())) + .filter(loot -> (loot.getBiome() == null || loot.getBiome() == biome) && !ignore.containsAll(loot.getItems().keySet())) .collect(Collectors.toList()); return eligibleLoot; } @@ -76,6 +77,33 @@ public Loot getRandomLoot(Player player, String... categories) { } public Loot getRandomLoot(Player player, Collection categories) { - return new WeightedMap<>(getEligibleLoot(player, categories), Loot::getFrequency).next(); + return getRandomLoot(player.getTotalSkillLevel(Skill.LUCK), player.getZone().getBiome(), player.getInventory().getWardrobe(), categories); + } + + public Loot getRandomLoot(int luck, Biome biome, Set ignore, String... categories) { + return getRandomLoot(luck, biome, ignore, Arrays.asList(categories)); + } + + public Loot getRandomLoot(int luck, Biome biome, Set ignore, Collection categories) { + WeightedMap map = new WeightedMap<>(getEligibleLoot(biome, ignore, categories), Loot::getFrequency); + Loot loot = map.next(); + double rolls = (luck - 1) / LEVELS_PER_BONUS_ROLL; + int bonusRolls = Math.min(MAX_BONUS_ROLLS, (int)rolls); + + // Turn remainder into a chance to get an extra bonus roll + if(bonusRolls < MAX_BONUS_ROLLS && Math.random() < (rolls - bonusRolls)) { + bonusRolls++; + } + + // Perform bonus rolls and return the lowest frequency loot + for(int i = 0; i < bonusRolls; i++) { + Loot next = map.next(); + + if(loot == null || (next != null && next.getFrequency() < loot.getFrequency())) { + loot = next; + } + } + + return loot; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/minigame/Minigame.java b/gameserver/src/main/java/brainwine/gameserver/minigame/Minigame.java new file mode 100644 index 00000000..2d0f371a --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/minigame/Minigame.java @@ -0,0 +1,206 @@ +package brainwine.gameserver.minigame; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.EntityAttack; +import brainwine.gameserver.item.ItemUseType; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.util.MathUtils; +import brainwine.gameserver.util.Vector2i; +import brainwine.gameserver.zone.Zone; + +/** + * Base class for minigame sessions. + */ +public abstract class Minigame { + + public static final long LEADERBOARD_UPDATE_INTERVAL = 2000; + protected final Map participants = new HashMap<>(); + protected final List leaderboard = new ArrayList<>(); + protected final Zone zone; + protected final Player initiator; + protected final int x; + protected final int y; + protected boolean active; + protected Participant currentLeader; + protected long startedAt; + private long lastLeaderboardUpdateAt; + + public Minigame(Zone zone, Player initiator, int x, int y) { + this.zone = zone; + this.initiator = initiator; + this.x = x; + this.y = y; + } + + public abstract void onInteract(Player player); + protected abstract void onStart(); + protected abstract void onFinish(); + + public void tick(float deltaTime) { + long now = System.currentTimeMillis(); + + // Update leaderboard + if(now >= lastLeaderboardUpdateAt + LEADERBOARD_UPDATE_INTERVAL) { + updateLeaderboard(); + lastLeaderboardUpdateAt = now; + } + } + + /** + * Preferably, call {@link Zone#startMinigame(Minigame)}. + */ + public final void start() { + if(active) { + return; // Do nothing if already started + } + + active = true; + startedAt = System.currentTimeMillis(); + addParticipant(initiator); + addParticipantsInRange(); + onStart(); + } + + public final void finish() { + if(!active) { + return; // Do nothing if already inactive + } + + updateLeaderboard(); + + // Clean up participants + for(Participant participant : participants.values()) { + Player player = participant.getPlayer(); + + if(player.getMinigame() == this) { + player.setMinigame(null); + } + + participant.showInfo(""); + } + + onFinish(); + active = false; + } + + public void entityKilled(Entity entity, EntityAttack cause) { + // Override + } + + public void entityAttacked(Entity entity, EntityAttack attack, float damage) { + // Override + } + + public void updateLeaderboard() { + leaderboard.clear(); + participants.values().stream().sorted((a, b) -> Double.compare(b.getScore(), a.getScore())).forEach(leaderboard::add); + Participant leader = leaderboard.get(0); + + if(leader.getScore() > 0.0) { + if(leader != currentLeader) { + notifyParticipants(String.format("%s took the lead with %s!", leader.getPlayer().getName(), describeScore(leader.getScore())), NotificationType.PEER_ACCOMPLISHMENT); + } + + currentLeader = leader; + leaderboard.forEach(x -> x.showInfo(String.format("You are in %s place with %s", ordinalizeNumber(getLeaderboardPosition(x)), describeScore(x.getScore())))); + } + } + + public void notifyCreator(Object message) { + notifyCreator(message, NotificationType.POPUP); + } + + public void notifyCreator(Object message, NotificationType type) { + if(initiator != null) { + initiator.notify(message, type); + } + } + + public void notifyParticipants(Object message) { + notifyParticipants(message, NotificationType.POPUP); + } + + public void notifyParticipants(Object message, NotificationType type) { + for(Participant participant : participants.values()) { + if(participant.isParticipating()) { + participant.getPlayer().notify(message, type); + } + } + } + + public void addParticipantsInRange() { + for(Player player : zone.getPlayersInRange(x, y, getRange())) { + if(!player.hasActiveMinigame()) { + addParticipant(player); + } + } + } + + public Participant addParticipant(Player player) { + player.setMinigame(this); + return participants.computeIfAbsent(player, x -> new Participant(this, x)); + } + + public boolean hasParticipant(Player player) { + return participants.containsKey(player); + } + + public Participant getParticipant(Player player) { + return participants.get(player); + } + + public int getLeaderboardPosition(Participant participant) { + return leaderboard.indexOf(participant) + 1; + } + + public String describeScore(double score) { + return String.format(Locale.US, "%,.2f points", score); // Override + } + + public double getRange() { + return Math.max(zone.getWidth(), zone.getHeight()); // Override + } + + public Vector2i getSpawnPoint(Player player) { + List spawnPoints = zone.getMetaBlocks().stream() + .filter(block -> block.getItem().hasUse(ItemUseType.TELEPORT) && MathUtils.inRange(x, y, block.getX(), block.getY(), getRange())) + .map(block -> new Vector2i(block.getX(), block.getY())) + .collect(Collectors.toCollection(ArrayList::new)); + spawnPoints.add(new Vector2i(x, y)); + return spawnPoints.get((int)(Math.random() * spawnPoints.size())); // Override + } + + public String ordinalizeNumber(int number) { + String str = String.valueOf(number); + String ordinal = str.endsWith("11") || str.endsWith("12") || str.endsWith("13") ? "th" : str.endsWith("1") ? "st" : str.endsWith("2") ? "nd" : str.endsWith("3") ? "rd" : "th"; + return String.format("%s%s", number, ordinal); + } + + public Zone getZone() { + return zone; + } + + public Player getInitiator() { + return initiator; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public boolean isActive() { + return active; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/minigame/Pandora.java b/gameserver/src/main/java/brainwine/gameserver/minigame/Pandora.java new file mode 100644 index 00000000..a66db40c --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/minigame/Pandora.java @@ -0,0 +1,325 @@ +package brainwine.gameserver.minigame; + +import static brainwine.shared.LogMarkers.SERVER_MARKER; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.fasterxml.jackson.core.type.TypeReference; + +import brainwine.gameserver.GameServer; +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.EntityAttack; +import brainwine.gameserver.entity.EntityRegistry; +import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.item.DamageType; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.loot.Loot; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.resource.ResourceFinder; +import brainwine.gameserver.util.Vector2i; +import brainwine.gameserver.zone.Zone; +import brainwine.shared.JsonHelper; + +public class Pandora extends Minigame { + + public static final String PANDORA_OPEN_ID = "containers/pandora-open"; + public static final String[] REWARD_LOOT_CATEGORIES = { "armaments", "armaments+", "treasure", "treasure+" }; // TODO make configurable + public static final long GRACE_PERIOD = 60000; // 1 minute + public static final long MAX_ROUND_DURATION = 480000; // 8 minutes + public static final double MINIGAME_RANGE = 50.0; + public static final double MAX_ENEMY_DISTANCE = 50.0; // Maximum distance wave enemies can wander before they're teleported back + public static final double MAX_RESPAWN_DISTANCE = 30.0; // Maximum distance at which players respawn near the minigame on death + public static final int MIN_ROUNDS = 10; + public static final int MAX_ROUNDS = 20; + private static final Logger logger = LogManager.getLogger(); + private static final Map>> config = new HashMap<>(); + private final Map roundSpawns = new HashMap<>(); + private final Set potencyBumps = new HashSet<>(); + private final List spawns = new ArrayList<>(); + private final Random random = new Random(); + private int currentRound; + private int potencyLevel; + private long roundStartedAt; + private long nextActionAt; + + public static void loadConfig() { + logger.info(SERVER_MARKER, "Loading Pandora configuration ..."); + + try { + config.clear(); + config.putAll(JsonHelper.readValue(ResourceFinder.getResourceUrl("pandora.json"), new TypeReference>>>(){})); + + // Make sure there are spawns for round 1 + if(!config.containsKey(1)) { + throw new IllegalArgumentException("No round 1 spawns configured"); + } + + // Perform extensive error checking on load so we don't have to do it later + config.forEach((round, spawns) -> { + // Check for empty spawns + if(spawns.stream().anyMatch(spawn -> spawn.values().stream().reduce(Integer::sum).orElse(0) <= 0)) { + throw new IllegalArgumentException(String.format("Round %s+ config has one or more empty spawns", round)); + } + + // Check for invalid entity types + String invalidType = spawns.stream() + .flatMap(spawn -> spawn.keySet().stream()) + .filter(type -> EntityRegistry.getEntityConfig(type) == null) + .findFirst().orElse(null); + + if(invalidType != null) { + throw new IllegalArgumentException(String.format("Invalid entity type: %s", invalidType)); + } + }); + } catch(Exception e) { + logger.error(SERVER_MARKER, "Failed to load Pandora config", e); + config.clear(); + return; + } + } + + public Pandora(Zone zone, Player initiator, int x, int y) { + super(zone, initiator, x, y); + } + + @Override + public void tick(float deltaTime) { + super.tick(deltaTime); + long now = System.currentTimeMillis(); + + // End minigame if block state is invalid + if(!zone.isChunkLoaded(x, y) || !zone.getBlock(x, y).getFrontItem().hasId(PANDORA_OPEN_ID)) { + finish(); + return; + } + + // Wait for grace period to end + if(currentRound == 0) { + if(now >= startedAt + GRACE_PERIOD) { + zone.notifyPlayers("Pandora's Box is coming ALIVE!"); + zone.updateBlock(x, y, Layer.FRONT, PANDORA_OPEN_ID, 2); + zone.spawnEffect(x, y, "karma sound", 1); + nextRound(); + } + + return; + } + + // Cancel minigame if the maximum round duration has been reached + if(now >= roundStartedAt + MAX_ROUND_DURATION) { + cancel("Pandora could not be contained. Better luck next time."); + return; + } + + // Perform an action if it is time + if(now >= nextActionAt) { + nextAction(); + nextActionAt = (long)(now + (0.5 + currentRound * 0.05) * 1000); // Spawns become less frequent as the difficulty increases + } + + // Randomly explode + if(Math.random() < deltaTime * 0.123) { + explode(); + } + } + + @Override + public void onInteract(Player player) { + addParticipant(player); + + // Increase potency if the minigame hasn't started yet + if(currentRound == 0 && (potencyBumps.add(player.getDocumentId()) || player.isGodMode())) { + zone.notifyPlayers(String.format("%s increased Pandora's chaos level to %s!", player.getName(), ++potencyLevel), NotificationType.PEER_ACCOMPLISHMENT); + } + } + + @Override + protected void onStart() { + // Reject if config failed to load + if(config.isEmpty()) { + notifyCreator(String.format("Pandora has not been configured correctly.\nPlease %s.", initiator.isAdmin() ? "check the server log for more info" : "contact a server administrator")); + finish(); + return; + } + + zone.spawnEffect(x, y, "match start", 1); + zone.updateBlock(x, y, Layer.FRONT, PANDORA_OPEN_ID, 1); + potencyBumps.add(initiator.getDocumentId()); + potencyLevel++; + + // Notify all players in the zone + for(Player player : zone.getPlayers()) { + player.notifyProfile(String.format("%s opened Pandora's box at %s", initiator.getName(), zone.getReadableCoordinates(x, y)), "Tap on it in the next 60 seconds to build chaos!"); + } + } + + @Override + protected void onFinish() { + // Kill all spawned entities + for(Npc entity : spawns) { + entity.setMinigame(null); + entity.setHealth(0.0F); + } + + // Destroy block if it is still there + if(zone.getBlock(x, y).getFrontItem().hasId(PANDORA_OPEN_ID)) { + explode(8.0F); + zone.updateBlock(x, y, Layer.FRONT, 0); + zone.spawnEffect(x, y, "match end", 1); + } + } + + @Override + public void entityKilled(Entity entity, EntityAttack cause) { + spawns.remove(entity); + } + + @Override + public void entityAttacked(Entity entity, EntityAttack attack, float damage) { + // Do nothing if entity is not a wave enemy + if(!spawns.contains(entity)) { + return; + } + + Entity attacker = attack.getAttacker(); + + // Check if attacker is present + if(attacker == null || !attacker.isPlayer()) { + return; + }; + + Player player = (Player)attacker; + Participant participant = addParticipant(player); + participant.incrementScore(damage); + } + + @Override + public String describeScore(double score) { + return String.format(Locale.US, "%,.2f damage dealt", score); + } + + @Override + public double getRange() { + return MINIGAME_RANGE; + } + + @Override + public Vector2i getSpawnPoint(Player player) { + return player.inRange(x, y, MAX_RESPAWN_DISTANCE) ? super.getSpawnPoint(player) : null; + } + + private void nextAction() { + // Move on to the next round if all enemies have been killed + if(spawns.isEmpty() && roundSpawns.isEmpty()) { + nextRound(); + return; + } + + addParticipantsInRange(); + + // Spawn a random enemy if there are any remaining + if(!roundSpawns.isEmpty()) { + String entity = roundSpawns.keySet().stream().skip(random.nextInt(roundSpawns.size())).findAny().get(); + roundSpawns.compute(entity, (key, value) -> value <= 1 ? null : value - 1); + Npc npc = zone.spawnEntity(entity, x + random.nextInt(2), y - random.nextInt(3), true); + npc.setMinigame(this); + spawns.add(npc); + } + + // Teleport all out-of-range enemies back to the box + spawns.stream().filter(entity -> !entity.inRange(x, y, MAX_ENEMY_DISTANCE)).forEach(entity -> { + entity.spawnEffect("bomb-teleport", 4); + entity.setPosition(x + random.nextInt(2), y - random.nextInt(3)); + entity.spawnEffect("bomb-teleport", 4); + }); + } + + private void nextRound() { + addParticipantsInRange(); + int totalRounds = getTotalRounds(); + + // Finish if the final round has been cleared + if(currentRound >= totalRounds) { + complete(); + return; + } + + // Increment round and fetch spawn data + currentRound++; + int key = config.keySet().stream().filter(x -> currentRound >= x).max(Integer::compareTo).orElse(1); + List> configs = config.getOrDefault(key, Collections.emptyList()); + roundSpawns.clear(); + roundSpawns.putAll(configs.get(random.nextInt(configs.size()))); // Select a random wave of enemies + + // Randomly increase the number of spawns this round depending on the chaos level + int spawnBumps = potencyLevel / (currentRound < 10 ? 2 : 3); + + for(int i = 0; i < spawnBumps; i++) { + Entry spawn = roundSpawns.entrySet().stream().skip(random.nextInt(roundSpawns.size())).findFirst().get(); + roundSpawns.put(spawn.getKey(), spawn.getValue() + 1); + } + + // Notify all players in the zone that the next round is starting + zone.notifyPlayers(String.format("Pandora wave %s of %s is beginning!", currentRound, totalRounds)); + roundStartedAt = System.currentTimeMillis(); + } + + private void complete() { + finish(); + double luckMultiplier = Math.min(10.0, potencyLevel); + int baseLuck = Math.min(12, participants.size() * 4); + int position = 0; + + // Give out rewards + for(Participant participant : leaderboard) { + if(participant.isParticipating()) { + int luck = (int)(Math.max(1, baseLuck - position * 4) * luckMultiplier); + Player player = participant.getPlayer(); + Loot loot = GameServer.getInstance().getLootManager().getRandomLoot(luck, zone.getBiome(), player.getInventory().getWardrobe(), REWARD_LOOT_CATEGORIES); + + if(loot != null) { + player.awardLoot(loot, String.format("You won %s place!", ordinalizeNumber(position + 1))); + } else { + player.notify("Sorry, we couldn't find a suitable reward for you."); + } + } + + position++; + } + + // Broadcast leader's score + zone.notifyPlayers(String.format("Pandora has been contained! %s showed mastery with %s!", currentLeader.getPlayer().getName(), describeScore(currentLeader.getScore()))); + } + + private void cancel(String message) { + finish(); + zone.notifyPlayers(message); + } + + private void explode() { + explode(4.0F + (float)Math.random()); + } + + private void explode(float radius) { + int x = this.x - 1 + (int)(Math.random() * 3); + int y = this.y - 1 + (int)(Math.random() * 3); + zone.explode(x, y, radius, null, false, 0.0F, DamageType.ENERGY, "bomb-electric"); // TODO explosion should do damage, but only to players! + } + + public int getTotalRounds() { + return Math.min(MAX_ROUNDS, MIN_ROUNDS + potencyLevel); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/minigame/Participant.java b/gameserver/src/main/java/brainwine/gameserver/minigame/Participant.java new file mode 100644 index 00000000..1a29309d --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/minigame/Participant.java @@ -0,0 +1,49 @@ +package brainwine.gameserver.minigame; + +import brainwine.gameserver.player.Player; +import brainwine.gameserver.server.messages.EventMessage; + +/** + * Model for keeping track of player scores in minigames. + */ +public class Participant { + + private final Minigame minigame; + private final Player player; + private double score; + + public Participant(Minigame minigame, Player player) { + this.minigame = minigame; + this.player = player; + } + + public void showInfo(String message) { + if(isParticipating()) { + player.sendMessage(new EventMessage("mini", message)); + } + } + + public boolean isParticipating() { + return player.isOnline() && player.getZone() == minigame.getZone(); + } + + public Player getPlayer() { + return player; + } + + public void incrementScore(double score) { + this.score += score; + } + + public void deductScore(double score) { + this.score -= score; + } + + public void setScore(double score) { + this.score = score; + } + + public double getScore() { + return score; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/player/Appearance.java b/gameserver/src/main/java/brainwine/gameserver/player/Appearance.java new file mode 100644 index 00000000..bf30b2b5 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/player/Appearance.java @@ -0,0 +1,87 @@ +package brainwine.gameserver.player; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import brainwine.gameserver.GameConfiguration; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemRegistry; +import brainwine.gameserver.util.MapHelper; + +/** + * Utility class for player appearance related stuff. + * Ghosts also have a random appearance, which is why it's here instead of in the player class. + */ +public class Appearance { + + public static Map getRandomAppearance() { + return getRandomAppearance(null); + } + + public static Map getRandomAppearance(Player player) { + Map appearance = new HashMap<>(); + + for(AppearanceSlot slot : AppearanceSlot.values()) { + // Skip if slot cannot be changed by players + if(!slot.isChangeable()) { + continue; + } + + String category = slot.getCategory(); + + // Color handling + if(slot.isColor()) { + List colors = getAvailableColors(slot, player); + + // Change appearance to random color + if(!colors.isEmpty()) { + appearance.put(slot.getId(), colors.get((int)(Math.random() * colors.size()))); + } + + continue; + } + + // Fetch list of items in this slot's category that the player owns + List items = ItemRegistry.getItemsByCategory(category).stream() + .filter(item -> item.isBase() || (player != null && player.getInventory().hasItem(item))) + .collect(Collectors.toList()); + + // Change appearance to random clothing item + if(!items.isEmpty()) { + appearance.put(slot.getId(), items.get((int)(Math.random() * items.size())).getCode()); + } + } + + return appearance; + } + + public static List getAvailableColors(AppearanceSlot slot) { + return getAvailableColors(slot, null); + } + + public static List getAvailableColors(AppearanceSlot slot, Player player) { + List colors = new ArrayList<>(); + + // Return empty list if slot is not valid + if(!slot.isColor()) { + return colors; + } + + Map wardrobe = MapHelper.getMap(GameConfiguration.getBaseConfig(), "wardrobe", Collections.emptyMap()); + String category = slot.getCategory(); + + // Add base colors + colors.addAll(MapHelper.getList(wardrobe, category, Collections.emptyList())); + + // Add bonus colors + if(player != null && player.getInventory().hasItem(ItemRegistry.getItem("accessories/makeup"))) { + colors.addAll(MapHelper.getList(wardrobe, String.format("%s-bonus", category), Collections.emptyList())); + } + + return colors; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/player/AppearanceSlot.java b/gameserver/src/main/java/brainwine/gameserver/player/AppearanceSlot.java new file mode 100644 index 00000000..5c15f5fe --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/player/AppearanceSlot.java @@ -0,0 +1,61 @@ +package brainwine.gameserver.player; + +public enum AppearanceSlot { + + SKIN_COLOR("c*", "skin-color", true), + HAIR_COLOR("h*", "hair-color", true), + HAIR("h", "hair", true), + FACIAL_HAIR("fh", "facialhair", true), + TOPS("t", "tops", true), + BOTTOMS("b", "bottoms", true), + FOOTWEAR("fw", "footwear", true), + HEADGEAR("hg", "headgear", true), + FACIAL_GEAR("fg", "facialgear", true), + FACIAL_GEAR_GLOW("fg*", "facialgear-glow"), + SUIT("u", "suit"), + TOPS_OVERLAY("to", "tops-overlay"), + TOPS_OVERLAY_GLOW("to*", "tops-overlay-glow"), + ARMS_OVERLAY("ao", "arms-overlay"), + LEGS_OVERLAY("lo", "legs-overlay"), + FOOTWEAR_OVERLAY("fo", "footwear-overlay"); + + private final String id; + private final String category; + private final boolean changeable; + + private AppearanceSlot(String id, String category) { + this(id, category, false); + } + + private AppearanceSlot(String id, String category, boolean changeable) { + this.id = id; + this.category = category; + this.changeable = changeable; + } + + public static AppearanceSlot fromId(String id) { + for(AppearanceSlot value : values()) { + if(value.getId().equals(id)) { + return value; + } + } + + return null; + } + + public String getId() { + return id; + } + + public String getCategory() { + return category; + } + + public boolean isChangeable() { + return changeable; + } + + public boolean isColor() { + return id.endsWith("*"); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/ChatType.java b/gameserver/src/main/java/brainwine/gameserver/player/ChatType.java similarity index 88% rename from gameserver/src/main/java/brainwine/gameserver/entity/player/ChatType.java rename to gameserver/src/main/java/brainwine/gameserver/player/ChatType.java index d12f0ddd..cf83f5b7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/ChatType.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/ChatType.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.entity.player; +package brainwine.gameserver.player; import com.fasterxml.jackson.annotation.JsonValue; diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/ContainerType.java b/gameserver/src/main/java/brainwine/gameserver/player/ContainerType.java similarity index 88% rename from gameserver/src/main/java/brainwine/gameserver/entity/player/ContainerType.java rename to gameserver/src/main/java/brainwine/gameserver/player/ContainerType.java index 99ab9407..a9a876a5 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/ContainerType.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/ContainerType.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.entity.player; +package brainwine.gameserver.player; import com.fasterxml.jackson.annotation.JsonValue; diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Inventory.java b/gameserver/src/main/java/brainwine/gameserver/player/Inventory.java similarity index 90% rename from gameserver/src/main/java/brainwine/gameserver/entity/player/Inventory.java rename to gameserver/src/main/java/brainwine/gameserver/player/Inventory.java index d4e2202c..c2a96c9c 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Inventory.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/Inventory.java @@ -1,12 +1,15 @@ -package brainwine.gameserver.entity.player; +package brainwine.gameserver.player; import java.beans.ConstructorProperties; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import com.fasterxml.jackson.annotation.JsonIncludeProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -149,17 +152,21 @@ public int getQuantity(Item item) { public boolean isEmpty() { return items.isEmpty(); } - - public Item findJetpack() { + + public Item findAccessoryWithUse(ItemUseType use) { for(Item item : accessories.getItems()) { - if(item.hasUse(ItemUseType.FLY)) { + if(item.hasUse(use)) { return item; } } - + return Item.AIR; } + public Item findJetpack() { + return findAccessoryWithUse(ItemUseType.FLY); + } + public ItemContainer getHotbar() { return hotbar; } @@ -168,8 +175,18 @@ public ItemContainer getAccessories() { return accessories; } - public List getWardrobe() { - return items.keySet().stream().filter(item -> item.isClothing() && hasItem(item)).collect(Collectors.toList()); + // TODO hidden accessories + public int getSkillBonus(Skill skill) { + return Stream.of(accessories.getItems()).map(item -> item.getSkillBonus(skill)).max(Integer::compareTo).orElse(0); + } + + // TODO hidden accessories + public double getRegenBonus() { + return Stream.of(accessories.getItems()).map(Item::getRegenBonus).min(Double::compareTo).orElse(1.0); + } + + public Set getWardrobe() { + return items.keySet().stream().filter(item -> item.isClothing() && hasItem(item)).collect(Collectors.toCollection(HashSet::new)); } @JsonValue diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/ItemContainer.java b/gameserver/src/main/java/brainwine/gameserver/player/ItemContainer.java similarity index 96% rename from gameserver/src/main/java/brainwine/gameserver/entity/player/ItemContainer.java rename to gameserver/src/main/java/brainwine/gameserver/player/ItemContainer.java index c769d2fa..1d87192a 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/ItemContainer.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/ItemContainer.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.entity.player; +package brainwine.gameserver.player; import java.util.Arrays; diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/KarmaLevel.java b/gameserver/src/main/java/brainwine/gameserver/player/KarmaLevel.java similarity index 93% rename from gameserver/src/main/java/brainwine/gameserver/entity/player/KarmaLevel.java rename to gameserver/src/main/java/brainwine/gameserver/player/KarmaLevel.java index 06a3e0e2..bfc97036 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/KarmaLevel.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/KarmaLevel.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.entity.player; +package brainwine.gameserver.player; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import com.fasterxml.jackson.annotation.JsonValue; diff --git a/gameserver/src/main/java/brainwine/gameserver/player/NameChange.java b/gameserver/src/main/java/brainwine/gameserver/player/NameChange.java new file mode 100644 index 00000000..9d403440 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/player/NameChange.java @@ -0,0 +1,35 @@ +package brainwine.gameserver.player; + +import java.time.OffsetDateTime; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIncludeProperties; + +@JsonIncludeProperties({"new_name", "previous_name", "date"}) +public class NameChange { + + private String newName; + private String previousName; + private OffsetDateTime date; + + @JsonCreator + private NameChange() {} + + public NameChange(String newName, String previousName) { + this.newName = newName; + this.previousName = previousName; + date = OffsetDateTime.now(); + } + + public String getNewName() { + return newName; + } + + public String getPreviousName() { + return previousName; + } + + public OffsetDateTime getDate() { + return date; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/NotificationType.java b/gameserver/src/main/java/brainwine/gameserver/player/NotificationType.java similarity index 86% rename from gameserver/src/main/java/brainwine/gameserver/entity/player/NotificationType.java rename to gameserver/src/main/java/brainwine/gameserver/player/NotificationType.java index aaeebc18..1bad2741 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/NotificationType.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/NotificationType.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.entity.player; +package brainwine.gameserver.player; import com.fasterxml.jackson.annotation.JsonValue; @@ -13,6 +13,8 @@ public enum NotificationType { ACCOMPLISHMENT(10), PEER_ACCOMPLISHMENT(11), REWARD(12), // v2 only + NOTE(13), // v2 only + PROFILE(16), // v2 only CHAT(20), LEVEL_UP(21), // v3 only ACHIEVEMENT(22), // v3 only diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Placement.java b/gameserver/src/main/java/brainwine/gameserver/player/Placement.java similarity index 92% rename from gameserver/src/main/java/brainwine/gameserver/entity/player/Placement.java rename to gameserver/src/main/java/brainwine/gameserver/player/Placement.java index aa2411f8..7798430c 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Placement.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/Placement.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.entity.player; +package brainwine.gameserver.player; import brainwine.gameserver.item.Item; diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/player/Player.java similarity index 58% rename from gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java rename to gameserver/src/main/java/brainwine/gameserver/player/Player.java index dd55321a..a4cd54e1 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/Player.java @@ -1,4 +1,6 @@ -package brainwine.gameserver.entity.player; +package brainwine.gameserver.player; + +import static brainwine.shared.LogMarkers.SERVER_MARKER; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; @@ -10,33 +12,40 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import com.fasterxml.jackson.annotation.JsonCreator; import brainwine.gameserver.GameConfiguration; import brainwine.gameserver.GameServer; -import brainwine.gameserver.achievements.Achievement; -import brainwine.gameserver.achievements.AchievementManager; -import brainwine.gameserver.achievements.JourneymanAchievement; +import brainwine.gameserver.Timer; +import brainwine.gameserver.achievement.Achievement; +import brainwine.gameserver.achievement.AchievementManager; +import brainwine.gameserver.achievement.JourneymanAchievement; +import brainwine.gameserver.achievement.PositionAchievement; import brainwine.gameserver.command.CommandExecutor; import brainwine.gameserver.dialog.Dialog; import brainwine.gameserver.dialog.DialogListItem; import brainwine.gameserver.dialog.DialogSection; import brainwine.gameserver.dialog.DialogType; import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.entity.EntityAttack; import brainwine.gameserver.entity.EntityStatus; -import brainwine.gameserver.entity.FacingDirection; import brainwine.gameserver.entity.npc.Npc; -import brainwine.gameserver.item.Action; +import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemRegistry; import brainwine.gameserver.item.ItemUseType; import brainwine.gameserver.item.Layer; import brainwine.gameserver.item.MiningBonus; +import brainwine.gameserver.item.Tradeability; +import brainwine.gameserver.item.consumables.Consumable; import brainwine.gameserver.loot.Loot; import brainwine.gameserver.server.Message; import brainwine.gameserver.server.messages.AchievementMessage; @@ -50,6 +59,7 @@ import brainwine.gameserver.server.messages.EntityPositionMessage; import brainwine.gameserver.server.messages.EntityStatusMessage; import brainwine.gameserver.server.messages.EventMessage; +import brainwine.gameserver.server.messages.FollowMessage; import brainwine.gameserver.server.messages.HealthMessage; import brainwine.gameserver.server.messages.HeartbeatMessage; import brainwine.gameserver.server.messages.InventoryMessage; @@ -63,16 +73,23 @@ import brainwine.gameserver.server.messages.XpMessage; import brainwine.gameserver.server.messages.ZoneStatusMessage; import brainwine.gameserver.server.models.EntityStatusData; +import brainwine.gameserver.server.models.PlayerStat; import brainwine.gameserver.server.pipeline.Connection; import brainwine.gameserver.util.MapHelper; import brainwine.gameserver.util.MathUtils; import brainwine.gameserver.util.Vector2i; +import brainwine.gameserver.util.VersionUtils; +import brainwine.gameserver.zone.Biome; +import brainwine.gameserver.zone.Block; import brainwine.gameserver.zone.Chunk; import brainwine.gameserver.zone.MetaBlock; import brainwine.gameserver.zone.Zone; +import brainwine.gameserver.zone.ZoneManager; public class Player extends Entity implements CommandExecutor { + public static final int RECENT_ZONE_LIMIT = 12; + public static final int BOOKMARKED_ZONE_LIMIT = 50; public static final int MAX_SKILL_LEVEL = 15; public static final int MAX_NATURAL_SKILL_LEVEL = 10; public static final int MAX_SPEED_X = 12; @@ -80,13 +97,15 @@ public class Player extends Entity implements CommandExecutor { public static final int HEARTBEAT_TIMEOUT = 30000; public static final int MAX_AUTH_TOKENS = 3; public static final int TRACKED_ENTITY_UPDATE_INTERVAL = 100; - public static final int REGEN_NO_DAMAGE_TIME = 10000; public static final float ENTITY_VISIBILITY_RANGE = 40; - public static final float BASE_REGEN_AMOUNT = 0.1F; + public static final int BASE_REGEN_INTERVAL = 30000; + public static final float BASE_REGEN_AMOUNT = 1.0F / 3.0F; + private static final Logger logger = LogManager.getLogger(); private static int dialogDiscriminator; private final String documentId; private String email; private String password; + private String apiToken; private boolean admin; private int experience; private int skillPoints; @@ -95,26 +114,50 @@ public class Player extends Entity implements CommandExecutor { private Inventory inventory; private PlayerStatistics statistics; private List authTokens; + private List nameChanges; private List mutes; private List bans; + private List recentZones; + private List bookmarkedZones; + private Set followees; + private Set followers; + private Set lootCodes; private Set achievements; private Map ignoredHints; private Map skills; - private Map equippedClothing; - private Map equippedColors; + private Map> bumpedSkills; + private Map appearance; private final Map settings = new HashMap<>(); private final Set activeChunks = new HashSet<>(); private final Map> dialogs = new HashMap<>(); + private final List> timers = new ArrayList<>(); private final List trackedEntities = new ArrayList<>(); private String clientVersion; + private TradeSession tradeSession; private Placement lastPlacement; private Item heldItem = Item.AIR; - private Vector2i spawnPoint = new Vector2i(0, 0); + private double breath = 1.0; + private double thirst; + private double cold; + private int spawnX; + private int spawnY; + private int enterX; + private int enterY; private int teleportX; private int teleportY; + private boolean stealth; private boolean godMode; + private boolean customSpawn; + private boolean changingZones; + private long lastBreathMessage; + private long lastThirstMessage; + private long lastThirstDamageAt; + private long lastFreezeMessage; private long lastHeartbeat; private long lastTrackedEntityUpdate; + private long lastLandmarkVoteAt; + private long lastHealthRegenAt; + private Zone previousZone; private Zone nextZone; private Connection connection; @@ -124,6 +167,7 @@ protected Player(String documentId, PlayerConfigFile config) { this.name = config.getName(); this.email = config.getEmail(); this.password = config.getPasswordHash(); + this.apiToken = config.getApiToken(); this.admin = config.isAdmin(); this.experience = config.getExperience(); this.skillPoints = config.getSkillPoints(); @@ -132,13 +176,19 @@ protected Player(String documentId, PlayerConfigFile config) { this.inventory = config.getInventory(); this.statistics = config.getStatistics(); this.authTokens = config.getAuthTokens(); + this.nameChanges = config.getNameChanges(); this.mutes = config.getMutes(); this.bans = config.getBans(); + this.recentZones = config.getRecentZones(); + this.bookmarkedZones = config.getBookmarkedZones(); + this.followees = config.getFollowees(); + this.followers = config.getFollowers(); + this.lootCodes = config.getLootCodes(); this.achievements = config.getAchievements(); this.ignoredHints = config.getIgnoredHints(); this.skills = config.getSkills(); - this.equippedClothing = config.getEquippedClothing(); - this.equippedColors = config.getEquippedColors(); + this.bumpedSkills = config.getBumpedSkills(); + this.appearance = config.getAppearance(); health = getMaxHealth(); inventory.setPlayer(this); statistics.setPlayer(this); @@ -151,13 +201,19 @@ public Player(String documentId, String name, Zone zone) { this.inventory = new Inventory(this); this.statistics = new PlayerStatistics(this); this.authTokens = new ArrayList<>(); + this.nameChanges = new ArrayList<>(); this.mutes = new ArrayList<>(); this.bans = new ArrayList<>(); + this.recentZones = new ArrayList<>(); + this.bookmarkedZones = new ArrayList<>(); + this.followees = new HashSet<>(); + this.followers = new HashSet<>(); + this.lootCodes = new HashSet<>(); this.achievements = new HashSet<>(); this.ignoredHints = new HashMap<>(); this.skills = new HashMap<>(); - this.equippedClothing = new HashMap<>(); - this.equippedColors = new HashMap<>(); + this.bumpedSkills = new HashMap<>(); + this.appearance = Appearance.getRandomAppearance(); } @JsonCreator @@ -167,6 +223,7 @@ private static Player fromId(String id) { @Override public void tick(float deltaTime) { + super.tick(deltaTime); long now = System.currentTimeMillis(); statistics.trackPlayTime(deltaTime); @@ -177,10 +234,25 @@ public void tick(float deltaTime) { } } - // Regenerate health out of combat - if(!isDead() && now >= lastDamagedAt + REGEN_NO_DAMAGE_TIME) { - heal(BASE_REGEN_AMOUNT * deltaTime); + // Regenerate health out of combat + if(!isDead() && health < getMaxHealth() && now >= Math.max(lastHealthRegenAt, lastDamagedAt) + BASE_REGEN_INTERVAL * inventory.getRegenBonus()) { + heal(BASE_REGEN_AMOUNT); + lastHealthRegenAt = now; } + + if(!isDead()) { + applyBreath(deltaTime); + applyThirst(deltaTime); + applyFreeze(deltaTime); + } + + // Try to timeout trade + if(isTrading()) { + tradeSession.timeout(); + } + + // Process timers + timers.removeIf(Timer::process); // Update tracked entities if(now - lastTrackedEntityUpdate >= TRACKED_ENTITY_UPDATE_INTERVAL) { @@ -191,19 +263,29 @@ public void tick(float deltaTime) { } @Override - public void die(Player killer) { + public void die(EntityAttack cause) { + Entity killer = cause == null ? null : cause.getAttacker(); + String serverMessage = String.format("%s died.", name); + Map details = new HashMap<>(); + + if(killer != null) { + details.put("<", killer.getId()); + + if(killer.isPlayer()) { + // TODO track kill for killer achievement in pvp zones + serverMessage = String.format("%s killed %s.", killer.getName(), name); + } + } + + sendMessageToPeers(new EntityStatusMessage(this, EntityStatus.DEAD, details)); + GameServer.getInstance().notify(serverMessage, NotificationType.CHAT); statistics.trackDeath(); - sendMessageToPeers(new EntityStatusMessage(this, EntityStatus.DEAD)); // TODO killer id - GameServer.getInstance().notify(String.format("%s died", name), NotificationType.CHAT); } @Override public void notify(Object message, NotificationType type) { - if(type == NotificationType.SYSTEM && isV3()) { - sendMessage(new NotificationMessage(message, NotificationType.PEER_ACCOMPLISHMENT)); - } else { - sendMessage(new NotificationMessage(message, type)); - } + // TODO type SYSTEM (2) apparently plays the karma warning sound on v2 clients, so I guess we'll be mapping all of them to PEER_ACCOMPLISHMENT (11). + sendMessage(new NotificationMessage(message, type == NotificationType.SYSTEM ? NotificationType.PEER_ACCOMPLISHMENT : type)); } @Override @@ -223,6 +305,136 @@ public void setHealth(float health) { sendMessage(new HealthMessage(health)); } + @Override + public void blockPositionChanged() { + super.blockPositionChanged(); + updateAchievementProgress(PositionAchievement.class); // TODO check on interval rather than every block position change + } + + public double getBreathCapacity() { + return 15.0 + 1.25 * (getTotalSkillLevel(Skill.SURVIVAL) - 1); + } + + public boolean isSubmerged() { + Block headBlock = getZone().getBlock(getBlockX(), getBlockY() - 1); + + if(headBlock == null) return false; + + Item liquidItem = headBlock.getLiquidItem(); + + return !liquidItem.isAir() && headBlock.getLiquidMod() > 2; + } + + public void applyBreath(float deltaTime) { + if(isGodMode() || !inventory.findAccessoryWithUse(ItemUseType.BREATH).isAir()) { + breath = 1.0; + } else { + if(isSubmerged()) { + breath -= deltaTime / getBreathCapacity(); + } else { + breath += deltaTime / 5.0; + } + breath = MathUtils.clamp(breath, 0.0, 1.0); + + long currentTime = System.currentTimeMillis(); + if(lastBreathMessage + 1000 < currentTime) { + sendMessage(new StatMessage(PlayerStat.BREATH, breath)); + if(breath < 0.001) attack(null, null, 0.5f, DamageType.SUFFOCATION); + lastBreathMessage = currentTime; + } + } + } + + public void applyThirst(float deltaTime) { + long now = System.currentTimeMillis(); + + // Update thirst stat + if(isGodMode()) { + thirst = 0.0; + } else { + double thirstPeriod = MathUtils.lerp(5.0, 10.0, (getTotalSkillLevel(Skill.SURVIVAL) - 1) / 6.0) * 60; + int direction = zone.getBiome() == Biome.DESERT && !zone.isPurified() ? 1 : -1; + thirst = MathUtils.clamp(thirst + (direction * deltaTime / thirstPeriod), 0.0, 1.0); + } + + // Send message if it is time + if(now > lastThirstMessage + 1000) { + sendMessage(new StatMessage(PlayerStat.THIRST, (float)thirst)); + lastThirstMessage = now; + } + + if(thirst >= 1.0) { + Item waterJar = ItemRegistry.getItem("containers/jar-water"); + + // Consume a jar of water if the player has any and reset thirst + if(inventory.hasItem(waterJar)) { + inventory.removeItem(waterJar, true); + inventory.addItem(ItemRegistry.getItem("containers/jar"), true); // Refund empty jar + notify(String.format("-1 %s", waterJar.getTitle())); + thirst = 0.0; + return; + } + + // Damage the player every 3 seconds instead if they have no water in their inventory + if(now > lastThirstDamageAt + 3000) { + attack(null, null, 0.25F, DamageType.FIRE, true); // Apply as true damage + lastThirstDamageAt = now; + } + } + } + + public void applyFreeze(float deltaTime) { + long now = System.currentTimeMillis(); + + // Update freeze stat + if(isGodMode()) { + cold = 0.0; + } else { + double freezePeriod = MathUtils.lerp(3.0, 10.0, (getTotalSkillLevel(Skill.SURVIVAL) - 1) / 6.0) * 60; + int direction = zone.getBiome() == Biome.ARCTIC ? 1 : -2; // Warm back up twice as fast + cold = MathUtils.clamp(cold + (direction * deltaTime / freezePeriod), 0.0, 1.0); + } + + // Send message & perform damage tick if it is time + if(now > lastFreezeMessage + 1000) { + if(cold >= 1.0) { + attack(null, null, 0.25F, DamageType.COLD, true); // Apply as true damage + } + + sendMessage(new StatMessage(PlayerStat.FREEZE, (float)cold)); + lastFreezeMessage = now; + } + } + + public void applyWarmth() { + cold = 0.0; + sendMessage(new StatMessage(PlayerStat.FREEZE, (float)cold)); + } + + @Override + public float getAttackMultiplier(EntityAttack attack) { + return isGodMode() ? 9999.0F : 1.0F; + } + + @Override + public float getDefense(EntityAttack attack) { + return getNormalizedSkill(Skill.SURVIVAL) * 0.5F; + } + + @Override + public boolean isInvulnerable() { + return invulnerable || isGodMode(); + } + + @Override + public void setProperties(Map properties, boolean sendMessage) { + super.setProperties(properties, sendMessage); + + if(sendMessage) { + sendMessage(new EntityChangeMessage(id, properties)); + } + } + /** * @return A {@link Map} containing all the data necessary for use in {@link EntityStatusMessage}. */ @@ -230,23 +442,52 @@ public void setHealth(float health) { public Map getStatusConfig() { Map config = super.getStatusConfig(); config.put("id", documentId); - config.putAll(getAppearanceConfig()); + config.putAll(appearance); + config.put("u", inventory.findJetpack().getCode()); return config; } /** * Called by {@link Zone#addEntity(Entity)} when the player is added to it. */ - public void onZoneChanged() { - // TODO handle spawns better - MetaBlock spawn = zone.getRandomSpawnBlock(); + public void onZoneEntered() { + boolean spawnEffect = false; - if(spawn == null) { - x = zone.getWidth() / 2; - y = 2; - } else { - x = spawn.getX() + 1; - y = spawn.getY(); + // Find new spawn point if zone has changed + if(zone != previousZone) { + MetaBlock spawn = zone.getRandomSpawnBlock(); + + if(spawn == null) { + spawnX = zone.getWidth() / 2; + spawnY = 2; + } else { + spawnX = spawn.getX() + 1; + spawnY = spawn.getY(); + } + + x = spawnX; + y = spawnY; + spawnEffect = true; + } + + // Handle custom spawn location + if(zone != nextZone) { + x = spawnX; + y = spawnY; + spawnEffect = true; + } else if(customSpawn && zone == nextZone) { + x = enterX; + y = enterY; + } + + customSpawn = false; + + // Rescue player if they're out of bounds somehow + // blockX and blockY might not be assigned yet so we check the absolute position + if(!zone.areCoordinatesInBounds((int)x, (int)y)) { + x = spawnX; + y = spawnY; + spawnEffect = true; } // Set skills for new players @@ -269,31 +510,34 @@ public void onZoneChanged() { inventory.moveItemToContainer(jetpack, ContainerType.ACCESSORIES, 0); } - spawnPoint.setX((int)x); - spawnPoint.setY((int)y); + ZoneManager zoneManager = GameServer.getInstance().getZoneManager(); + PlayerManager playerManager = GameServer.getInstance().getPlayerManager(); + + // Issue an API token if the player doesn't have one + if(apiToken == null) { + playerManager.issueApiToken(this); + } + sendMessage(new ConfigurationMessage(id, getClientConfig(), GameConfiguration.getClientConfig(this), zone.getClientConfig(this))); - sendMessage(new ZoneStatusMessage(zone.getStatusConfig())); - sendMessage(new ZoneStatusMessage(zone.getStatusConfig())); + sendMessage(new ZoneStatusMessage(zone.getStatusConfig(this))); + zone.sendMachineStatus(this); sendMessage(new PlayerPositionMessage((int)x, (int)y)); sendMessage(new HealthMessage(health)); - sendMessage(new InventoryMessage(inventory)); - sendMessage(new WardrobeMessage(inventory.getWardrobe())); - sendMessage(new BlockMetaMessage(zone.getGlobalMetaBlocks())); // Send skill data for(Skill skill : skills.keySet()) { sendMessage(new SkillMessage(skill, skills.get(skill))); } + sendMessage(new InventoryMessage(inventory)); + sendMessage(new WardrobeMessage(inventory.getWardrobe())); + sendMessage(new BlockMetaMessage(zone.getGlobalMetaBlocks())); + // Send peer data Collection peers = zone.getPlayers(); sendMessage(new EntityStatusMessage(peers, EntityStatus.ENTERING)); sendMessage(new EntityPositionMessage(peers)); - - // TODO prepack this as well - for(Player peer : peers) { - sendMessage(new EntityItemUseMessage(peer.getId(), 0, peer.getHeldItem(), 0)); - } + sendMessage(new EntityItemUseMessage(peers)); // Send achievement data for(Achievement achievement : AchievementManager.getAchievements()) { @@ -316,26 +560,55 @@ public void onZoneChanged() { notify("Welcome to " + zone.getName(), NotificationType.WELCOME); } + if(spawnEffect) { + zone.spawnEffect(x + 0.5F, y - 0.75F, "spawn", 20); + } + + // Send social info + sendMessage(new FollowMessage(followees.stream().map(playerManager::getPlayerById).filter(Objects::nonNull).collect(Collectors.toList()), 0)); + sendMessage(new FollowMessage(followers.stream().map(playerManager::getPlayerById).filter(Objects::nonNull).collect(Collectors.toList()), 1)); + sendMessage(new EventMessage("socialInfoReady", null)); + + // Clear invalid bookmarks + bookmarkedZones.removeIf(bookmark -> zoneManager.getZone(bookmark) == null || !zoneManager.getZone(bookmark).canJoin(this)); + + // Misc stuff updateAchievementProgress(JourneymanAchievement.class); checkRegistration(); + recentZones.remove(zone.getDocumentId()); // Remove first in case the zone has already been visited recently + recentZones.add(0, zone.getDocumentId()); // Add at top so we don't have to reverse the list for the zone searcher + + while(recentZones.size() > RECENT_ZONE_LIMIT) { + recentZones.remove(recentZones.size() - 1); + } } /** * Called from {@link Connection} when the channel becomes inactive. + * + * TODO Should we force process all timers on disconnect? */ public void onDisconnect() { lastHeartbeat = 0; lastPlacement = null; clientVersion = null; + previousZone = zone; if(zone != null) { zone.removeEntity(this); } // Are we switching zones? Then set the new zone. - if(nextZone != null) { + if(changingZones) { zone = nextZone; - nextZone = null; + changingZones = false; + } else { + nextZone = zone; + } + + // Cancel existing trade session + if(isTrading()) { + tradeSession.cancel(this); } dialogs.clear(); @@ -393,7 +666,15 @@ public void sendMessageToPeers(Message message) { } public void changeZone(Zone zone) { + changeZone(zone, -1, -1); + } + + public void changeZone(Zone zone, int x, int y) { + changingZones = true; + customSpawn = x != -1 && y != -1; nextZone = zone; + enterX = x; + enterY = y; sendMessage(new EventMessage("playerWillChangeZone", null)); kick("Teleporting...", true); } @@ -403,30 +684,57 @@ public void showDialog(Dialog dialog) { } public void showDialog(Dialog dialog, Consumer handler) { + sendMessage(new DialogMessage(storeDialogHandler(handler), dialog)); + } + + public void showDialog(Map dialog) { + showDialog(dialog, null); + } + + public void showDialog(Map dialog, Consumer handler) { + sendMessage(new DialogMessage(storeDialogHandler(handler), dialog)); + } + + private int storeDialogHandler(Consumer handler) { int id = handler == null ? 0 : ++dialogDiscriminator; if(id != 0) { dialogs.put(id, handler); } - sendMessage(new DialogMessage(id, dialog)); + return id; } public void handleDialogInput(int id, Object[] input) { - if(id == 0 || (input.length == 1 && input[0].equals("cancel"))) { + if(id == 0) { return; } Consumer handler = dialogs.remove(id); if(handler == null) { - notify("Sorry, the request has expired."); + if(!(input.length == 1 && input[0].equals("cancel"))) { + notify("Sorry, the request has expired."); + } } else { - // TODO since we're dealing with user input, should we just try-catch this? - handler.accept(input); + try { + handler.accept(input); + } catch(Exception e) { + logger.error(SERVER_MARKER, "An error occured while handling dialog input", e); + notify("Oops! There was a problem processing your input."); + } } } + public void addTimer(String key, long delay, Runnable action) { + removeTimer(key); + timers.add(new Timer<>(key, delay, action)); + } + + public void removeTimer(String key) { + timers.removeIf(timer -> timer.getKey().equals(key)); + } + public void checkRegistration() { if(!isRegistered()) { sendMessage(new EventMessage("playerRegistered", false)); @@ -456,8 +764,12 @@ public String getClientVersion() { return clientVersion; } + public boolean hasClientVersion(String version) { + return clientVersion != null && VersionUtils.isGreaterOrEqualTo(clientVersion, version); + } + public boolean isV3() { - return clientVersion != null && clientVersion.startsWith("3"); + return hasClientVersion("3.0.0"); } /** @@ -468,15 +780,31 @@ public void rubberband() { } public void respawn() { + // Check minigame spawnpoint + if(hasActiveMinigame()) { + Vector2i spawnPoint = minigame.getSpawnPoint(this); + + if(spawnPoint != null) { + respawn(spawnPoint.getX(), spawnPoint.getY()); + return; + } + } + + // Respawn at default spawn point + respawn(spawnX, spawnY); + } + + public void respawn(int x, int y) { if(isDead()) { setHealth(getMaxHealth()); + breath = 1.0; + thirst = 0.0; + cold = 0.0; } - int x = spawnPoint.getX(); - int y = spawnPoint.getY(); sendMessage(new PlayerPositionMessage(x, y)); sendMessageToPeers(new EntityStatusMessage(this, EntityStatus.REVIVED)); - zone.sendMessage(new EffectMessage(x, y, "spawn", 20)); + zone.spawnEffect(x + 0.5F, y - 0.75F, "spawn", 20); } /** @@ -491,7 +819,7 @@ public void teleport(int x, int y) { teleportY = y; sendMessage(new TeleportMessage(x, y)); sendMessage(new PlayerPositionMessage(x, y)); - zone.sendMessage(new EffectMessage(x, y, "teleport", 20)); + zone.spawnEffect(x, y, "teleport", 20); } public int getTeleportX() { @@ -502,6 +830,15 @@ public int getTeleportY() { return teleportY; } + public void setStealth(boolean stealth) { + this.stealth = stealth; + setProperty("xs", stealth ? 1 : 0, true); + } + + public boolean isStealthy() { + return stealth; + } + public void setGodMode(boolean godMode) { this.godMode = godMode; } @@ -544,6 +881,14 @@ public void notify(Object message) { notify(message, NotificationType.POPUP); } + public void notifyProfile(String title, String description) { + if(isV3()) { + notify(String.format("%s\n%s", title, description)); + } else { + notify(MapHelper.map(String.class, String.class, "title", title, "desc", description), NotificationType.PROFILE); + } + } + public void setHeldItem(Item item) { heldItem = item; } @@ -552,6 +897,50 @@ public Item getHeldItem() { return heldItem; } + public void tradeItem(Player recipient, Item item) { + // Cannot trade with self + if(recipient == this) { + return; + } + + // Check if item is tradeable + if(!isGodMode() && item.getTradeability() == Tradeability.FALSE) { + notify("Sorry, you cannot trade this item."); + return; + } + + // Check if player is high enough level to trade this item + if(!isGodMode() && item.getTradeability() == Tradeability.LEVELED && getLevel() < 20) { + notify("You must be level 20+ to trade this item."); + return; + } + + // Cancel the current trade if the player is initiating a new trade + if(isTrading() && !tradeSession.isParticipant(recipient)) { + tradeSession.cancel(this); + } + + // Create a new trade session if it doesn't exist + if(!isTrading()) { + tradeSession = new TradeSession(this, recipient); + } + + // Process the offer + tradeSession.onItemOffered(this, item); + } + + public void setTradeSession(TradeSession tradeSession) { + this.tradeSession = tradeSession; + } + + public boolean isTrading() { + return tradeSession != null; + } + + public TradeSession getTradeSession() { + return tradeSession; + } + public void trackPlacement(int x, int y, Item item) { if(item.getUses().isEmpty() || !zone.areCoordinatesInBounds(x, y)) { return; @@ -562,6 +951,8 @@ public void trackPlacement(int x, int y, Item item) { if(lastPlacement != null) { if(item.hasUse(ItemUseType.SWITCHED) && !item.hasUse(ItemUseType.SWITCH)) { linked = tryLinkSwitchedItem(x, y, item); + } else if(item.hasUse(ItemUseType.TRANSMITTED)) { + linked = tryLinkTransmittedItem(x, y, item); } } @@ -576,7 +967,7 @@ private boolean tryLinkSwitchedItem(int x, int y, Item item) { Item pItem = lastPlacement.getItem(); boolean linked = false; - if(pItem.hasUse(ItemUseType.SWITCH)) { + if(pItem.hasUse(ItemUseType.SWITCH, ItemUseType.TRIGGER)) { MetaBlock metaBlock = zone.getMetaBlock(pX, pY); Map metadata = metaBlock == null ? null : metaBlock.getMetadata(); @@ -599,6 +990,39 @@ private boolean tryLinkSwitchedItem(int x, int y, Item item) { return linked; } + private boolean tryLinkTransmittedItem(int x, int y, Item item) { + int pX = lastPlacement.getX(); + int pY = lastPlacement.getY(); + Item pItem = lastPlacement.getItem(); + + // Do nothing if the last placed item is not a transmitter + if(!pItem.hasUse(ItemUseType.TRANSMIT)) { + return false; + } + + int maxTransmitDistance = getTotalSkillLevel(Skill.ENGINEERING) * 10; + + // Notify the player if the distance is beyond the maximum transmit distance + if(!isGodMode() && !MathUtils.inRange(x, y, pX, pY, maxTransmitDistance)) { + notify(String.format("You can only transmit %s blocks at your current engineering level.", maxTransmitDistance)); + return false; + } + + MetaBlock metaBlock = zone.getMetaBlock(pX, pY); + Map metadata = metaBlock == null ? null : metaBlock.getMetadata(); + + // Do nothing if metadata is null for whatever reason + if(metadata == null) { + return false; + } + + // Link transmitter to beacon + MapHelper.appendList(metadata, ">", Arrays.asList(x, y)); // Make it a list for compatibility reasons + zone.updateBlock(pX, pY, Layer.FRONT, pItem, 1, null, metadata); + lastPlacement = null; + return true; + } + public double getMiningRange() { return 5 + getTotalSkillLevel(Skill.MINING) / 3.0; } @@ -619,6 +1043,13 @@ public double getMiningBonusChance(MiningBonus bonus) { return bonus.getChance() * (getTotalSkillLevel(bonus.getSkill()) / (double)MAX_SKILL_LEVEL) * heldItem.getToolBonus(); } + /** + * @return The hash to be stored in blocks placed by this player. + */ + public int getBlockHash() { + return 1 + ((documentId.hashCode() & 2047) % 2047); + } + public String getDocumentId() { return documentId; } @@ -639,6 +1070,14 @@ protected String getPassword() { return password; } + protected void setApiToken(String apiToken) { + this.apiToken = apiToken; + } + + public String getApiToken() { + return apiToken; + } + protected void clearAuthTokens() { authTokens.clear(); } @@ -657,6 +1096,122 @@ protected List getAuthTokens() { return authTokens; } + public List getRecentZones() { + return Collections.unmodifiableList(recentZones); + } + + public void addZoneBookmark(Zone zone) { + addZoneBookmark(zone.getDocumentId()); + } + + public void addZoneBookmark(String zone) { + if(!isZoneBookmarked(zone)) { + bookmarkedZones.add(0, zone); // Add at top for zone searcher + } + } + + public void removeZoneBookmark(Zone zone) { + bookmarkedZones.remove(zone.getDocumentId()); + } + + public void removeZoneBookmark(String zone) { + bookmarkedZones.remove(zone); + } + + public boolean isZoneBookmarked(Zone zone) { + return isZoneBookmarked(zone.getDocumentId()); + } + + public boolean isZoneBookmarked(String zone) { + return bookmarkedZones.contains(zone); + } + + public int getBookmarkedZoneCount() { + return bookmarkedZones.size(); + } + + public List getBookmarkedZones() { + return Collections.unmodifiableList(bookmarkedZones); + } + + public void followPlayer(Player player) { + if(!followees.add(player.getDocumentId())) { + return; // Do nothing if player is already following + } + + player.addFollower(this); + sendMessage(new FollowMessage(player, 0, true)); + } + + public void unfollowPlayer(Player player) { + if(!followees.remove(player.getDocumentId())) { + return; // Do nothing if player is not following + } + + player.removeFollower(this); + sendMessage(new FollowMessage(player, 0, false)); + } + + public boolean isFollowing(Player player) { + return isFollowing(player.getDocumentId()); + } + + public boolean isFollowing(String followee) { + return followees.contains(followee); + } + + public Set getFollowees() { + return Collections.unmodifiableSet(followees); + } + + private void addFollower(Player player) { + followers.add(player.getDocumentId()); + + if(isOnline()) { + sendMessage(new FollowMessage(player, 1, true)); + } + } + + private void removeFollower(Player player) { + followers.remove(player.getDocumentId()); + + if(isOnline()) { + sendMessage(new FollowMessage(player, 1, false)); + } + } + + public boolean hasFollower(Player player) { + return hasFollower(player.getDocumentId()); + } + + public boolean hasFollower(String follower) { + return followers.contains(follower); + } + + public Set getFollowers() { + return Collections.unmodifiableSet(followers); + } + + public void addLootCode(String lootCode) { + lootCodes.add(lootCode); + } + + public boolean hasLootCode(String lootCode) { + return lootCodes.contains(lootCode); + } + + public Set getLootCodes() { + return Collections.unmodifiableSet(lootCodes); + } + + public void trackNameChange(String newName) { + nameChanges.add(new NameChange(newName, name)); + } + + public List getNameChanges() { + return nameChanges; + } + public void mute(String reason, OffsetDateTime until) { mute(null, reason, until); } @@ -698,7 +1253,7 @@ public void ban(String reason, OffsetDateTime until) { public void ban(Player issuer, String reason, OffsetDateTime endDate) { bans.add(new PlayerRestriction(issuer, reason, endDate)); - kick(String.format("You have been banned: %s", reason)); + kick(String.format("You have been banned: %s", reason), true); } public void unban() { @@ -711,6 +1266,10 @@ public void unban(Player issuer) { if(currentBan != null) { currentBan.pardon(issuer); } + + if(isOnline()) { + changeZone(null); + } } public boolean isBanned() { @@ -754,7 +1313,7 @@ public void setExperience(int experience, String message) { skillPoints += Math.max(0, newLevel - oldLevel); sendDelayedMessage(new LevelMessage(newLevel), 5000); sendDelayedMessage(new EffectMessage(0, 0, "levelup", 1), 5000); - sendDelayedMessage(new StatMessage("points", skillPoints), 5000); + sendDelayedMessage(new StatMessage(PlayerStat.POINTS, skillPoints), 5000); notifyPeers(String.format("%s leveled up to level %s!", name, newLevel), NotificationType.SYSTEM); } } @@ -792,7 +1351,7 @@ public int getLevel() { public void setSkillPoints(int skillPoints) { this.skillPoints = skillPoints; - sendMessage(new StatMessage("points", skillPoints)); + sendMessage(new StatMessage(PlayerStat.POINTS, skillPoints)); } public int getSkillPoints() { @@ -831,7 +1390,7 @@ public void removeCrowns(int crowns) { public void setCrowns(int crowns) { this.crowns = crowns; - sendMessage(new StatMessage("crowns", crowns)); + sendMessage(new StatMessage(PlayerStat.CROWNS, crowns)); } public int getCrowns() { @@ -853,7 +1412,7 @@ public Map getIgnoredHints() { public void updateAchievementProgress(Class achievementType) { List achievementsToCheck = AchievementManager.getAchievements().stream() .filter(achievement -> !hasAchievement(achievement) - && achievementType.isAssignableFrom(achievement.getClass()) + && achievementType == achievement.getClass() && (achievement.getPrevious() == null || hasAchievement(achievement.getPrevious()))) .collect(Collectors.toList()); @@ -920,27 +1479,18 @@ public Set getAchievements() { return Collections.unmodifiableSet(achievements); } - public void setClothing(ClothingSlot slot, Item item) { - if(!item.isClothing()) { - return; - } - - equippedClothing.put(slot, item); - zone.sendMessage(new EntityChangeMessage(id, getAppearanceConfig())); + public void randomizeAppearance() { + appearance.putAll(Appearance.getRandomAppearance(this)); + zone.sendMessage(new EntityChangeMessage(id, appearance)); } - public Map getEquippedClothing() { - return Collections.unmodifiableMap(equippedClothing); + public void updateAppearance(Map appearance) { + this.appearance.putAll(appearance); + zone.sendMessage(new EntityChangeMessage(id, appearance)); } - public void setColor(ColorSlot slot, String hex) { - // TODO check if the string is actually a valid hex color - equippedColors.put(slot, hex); - zone.sendMessage(new EntityChangeMessage(id, getAppearanceConfig())); - } - - public Map getEquippedColors() { - return Collections.unmodifiableMap(equippedColors); + public Map getAppearance() { + return Collections.unmodifiableMap(appearance); } public void setSkillLevel(Skill skill, int level) { @@ -949,19 +1499,11 @@ public void setSkillLevel(Skill skill, int level) { } public int getTotalSkillLevel(Skill skill) { - int accessorySkillLevel = 0; - - // Get the highest skill bonus accessory - for(Item accessory : inventory.getAccessories().getItems()) { - int skillBonus = accessory.getSkillBonuses().getOrDefault(skill, 0); - - if(skillBonus > accessorySkillLevel) { - accessorySkillLevel = skillBonus; - } - } - - // TODO account for exoskeleton bonuses - return getSkillLevel(skill) + accessorySkillLevel; + return getSkillLevel(skill) + inventory.getSkillBonus(skill); + } + + public float getNormalizedSkill(Skill skill) { + return getTotalSkillLevel(skill) / (float)MAX_SKILL_LEVEL; } public int getSkillLevel(Skill skill) { @@ -984,19 +1526,39 @@ public Map getSkills() { return Collections.unmodifiableMap(skills); } - public void consume(Item item) { - Action action = item.getAction(); + public void trackSkillBump(Item item, Skill skill) { + List skills = bumpedSkills.get(item); - // TODO some kind of abstraction for things like this would be pretty cool - switch(action) { - case HEAL: heal(item.getPower()); break; - default: break; + if(skills == null) { + skills = new ArrayList<>(); + bumpedSkills.put(item, skills); } - // (Temporary?) measure to prevent consuming unimplemented consumables - if(action != Action.NONE) { - inventory.removeItem(item); + skills.add(skill); + } + + public boolean hasSkillBeenBumped(Item item, Skill skill) { + return bumpedSkills.getOrDefault(item, Collections.emptyList()).contains(skill); + } + + public Map> getBumpedSkills() { + return bumpedSkills; + } + + public void consume(Item item) { + consume(item, null); + } + + public void consume(Item item, Object details) { + Consumable consumable = item.getAction().getConsumable(); + + if(consumable == null) { + sendMessage(new InventoryMessage(inventory.getClientConfig(item))); + notify("Sorry, this action hasn't been implemented yet."); + return; } + + consumable.consume(item, this, details); } public void awardLoot(Loot loot) { @@ -1004,6 +1566,14 @@ public void awardLoot(Loot loot) { } public void awardLoot(Loot loot, DialogType dialogType) { + awardLoot(loot, dialogType, "You received:"); + } + + public void awardLoot(Loot loot, String title) { + awardLoot(loot, DialogType.LOOT, title); + } + + public void awardLoot(Loot loot, DialogType dialogType, String title) { Dialog dialog = new Dialog(); DialogSection section = new DialogSection(); dialog.addSection(section); @@ -1030,10 +1600,10 @@ public void awardLoot(Loot loot, DialogType dialogType) { } if(v3) { - dialog.setTitle("You found:"); + dialog.setTitle(title); showDialog(dialog.setType(dialogType)); } else { - section.setTitle("You found:"); + section.setTitle(title); notify(dialog, NotificationType.REWARD); } } @@ -1114,7 +1684,15 @@ private void updateTrackedEntities() { trackedEntities.clear(); trackedEntities.addAll(entitiesInRange); } - + + public long getLastLandmarkVoteAt() { + return lastLandmarkVoteAt; + } + + public void setLastLandmarkVoteAt(long lastLandmarkVoteAt) { + this.lastLandmarkVoteAt = lastLandmarkVoteAt; + } + public boolean isTrackingEntity(Entity entity) { return trackedEntities.contains(entity); } @@ -1144,21 +1722,6 @@ public boolean isOnline() { return connection != null && connection.isOpen(); } - private Map getAppearanceConfig() { - Map appearance = new HashMap<>(); - - for(Entry entry : equippedClothing.entrySet()) { - appearance.put(entry.getKey().getId(), entry.getValue().getCode()); - } - - for(Entry entry : equippedColors.entrySet()) { - appearance.put(entry.getKey().getId(), entry.getValue()); - } - - appearance.put(ClothingSlot.SUIT.getId(), inventory.findJetpack().getCode()); // Jetpack - return appearance; - } - /** * @return A {@link Map} containing all the data necessary for use in {@link ConfigurationMessage}. */ @@ -1180,8 +1743,9 @@ public Map getClientConfig() { config.put("items_crafted", statistics.getTotalItemsCrafted()); config.put("play_time", (int)(statistics.getPlayTime())); config.put("deaths", statistics.getDeaths()); - config.put("appearance", getAppearanceConfig()); + config.put("appearance", appearance); config.put("settings", settings); + config.put("api_token", apiToken); return config; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java b/gameserver/src/main/java/brainwine/gameserver/player/PlayerConfigFile.java similarity index 65% rename from gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java rename to gameserver/src/main/java/brainwine/gameserver/player/PlayerConfigFile.java index 623a3216..c85d2bba 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/PlayerConfigFile.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.entity.player; +package brainwine.gameserver.player; import java.util.ArrayList; import java.util.HashMap; @@ -12,7 +12,7 @@ import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; -import brainwine.gameserver.achievements.Achievement; +import brainwine.gameserver.achievement.Achievement; import brainwine.gameserver.item.Item; import brainwine.gameserver.zone.Zone; @@ -22,6 +22,7 @@ public class PlayerConfigFile { private String name; private String email; private String passwordHash; + private String apiToken; private Zone currentZone; private boolean admin; private int experience; @@ -31,18 +32,25 @@ public class PlayerConfigFile { private Inventory inventory = new Inventory(); private PlayerStatistics statistics = new PlayerStatistics(); private List authTokens = new ArrayList<>(); + private List nameChanges = new ArrayList<>(); private List mutes = new ArrayList<>(); private List bans = new ArrayList<>(); + private List recentZones = new ArrayList<>(); + private List bookmarkedZones = new ArrayList<>(); + private Set followees = new HashSet<>(); + private Set followers = new HashSet<>(); + private Set lootCodes = new HashSet<>(); private Set achievements = new HashSet<>(); private Map ignoredHints = new HashMap<>(); private Map skills = new HashMap<>(); - private Map equippedClothing = new HashMap<>(); - private Map equippedColors = new HashMap<>(); + private Map> bumpedSkills = new HashMap<>(); + private Map appearance = new HashMap<>(); public PlayerConfigFile(Player player) { this.name = player.getName(); this.email = player.getEmail(); this.passwordHash = player.getPassword(); + this.apiToken = player.getApiToken(); this.currentZone = player.getZone(); this.admin = player.isAdmin(); this.experience = player.getExperience(); @@ -52,13 +60,19 @@ public PlayerConfigFile(Player player) { this.inventory = player.getInventory(); this.statistics = player.getStatistics(); this.authTokens = player.getAuthTokens(); + this.nameChanges = player.getNameChanges(); this.mutes = player.getMutes(); this.bans = player.getBans(); + this.recentZones = player.getRecentZones(); + this.bookmarkedZones = player.getBookmarkedZones(); + this.followees = player.getFollowees(); + this.followers = player.getFollowers(); + this.lootCodes = player.getLootCodes(); this.achievements = player.getAchievements(); this.ignoredHints = player.getIgnoredHints(); this.skills = player.getSkills(); - this.equippedClothing = player.getEquippedClothing(); - this.equippedColors = player.getEquippedColors(); + this.bumpedSkills = player.getBumpedSkills(); + this.appearance = player.getAppearance(); } @JsonCreator @@ -77,6 +91,10 @@ public String getPasswordHash() { return passwordHash; } + public String getApiToken() { + return apiToken; + } + public Zone getCurrentZone() { return currentZone; } @@ -90,6 +108,11 @@ public List getAuthTokens() { return authTokens; } + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public List getNameChanges() { + return nameChanges; + } + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) public List getMutes() { return mutes; @@ -100,6 +123,16 @@ public List getBans() { return bans; } + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public List getRecentZones() { + return recentZones; + } + + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public List getBookmarkedZones() { + return bookmarkedZones; + } + public int getExperience() { return experience; } @@ -126,6 +159,21 @@ public PlayerStatistics getStatistics() { return statistics; } + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public Set getFollowees() { + return followees; + } + + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public Set getFollowers() { + return followers; + } + + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + public Set getLootCodes() { + return lootCodes; + } + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) public Set getAchievements() { return achievements; @@ -142,12 +190,12 @@ public Map getSkills() { } @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) - public Map getEquippedClothing() { - return equippedClothing; + public Map> getBumpedSkills() { + return bumpedSkills; } @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) - public Map getEquippedColors() { - return equippedColors; + public Map getAppearance() { + return appearance; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java b/gameserver/src/main/java/brainwine/gameserver/player/PlayerManager.java similarity index 68% rename from gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java rename to gameserver/src/main/java/brainwine/gameserver/player/PlayerManager.java index b6a2f940..5f2eb7de 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/PlayerManager.java @@ -1,10 +1,12 @@ -package brainwine.gameserver.entity.player; +package brainwine.gameserver.player; import static brainwine.shared.LogMarkers.SERVER_MARKER; import java.io.File; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -14,27 +16,24 @@ import org.apache.logging.log4j.Logger; import org.mindrot.jbcrypt.BCrypt; -import brainwine.gameserver.GameServer; import brainwine.gameserver.server.pipeline.Connection; import brainwine.shared.JsonHelper; +import brainwine.shared.TokenGenerator; public class PlayerManager { // TODO check platforms as well - public static final List SUPPORTED_VERSIONS = Arrays.asList("1.12.1", "2.11.0.1", "2.11.1", "3.13.1"); + public static final List SUPPORTED_VERSIONS = Arrays.asList("1.13.3", "2.11.0.1", "2.11.1", "3.13.1"); private static final Logger logger = LogManager.getLogger(); private final Map playersById = new HashMap<>(); private final Map playersByName = new HashMap<>(); - private final Map playersByConnection = new HashMap<>(); + private final Map apiTokens = new HashMap<>(); + private final List onlinePlayers = new ArrayList<>(); public PlayerManager() { loadPlayers(); } - public void tick() { - - } - private void loadPlayers() { logger.info(SERVER_MARKER, "Loading player data ..."); File dataDir = new File("players"); @@ -55,11 +54,6 @@ private void loadPlayer(File file) { try { PlayerConfigFile configFile = JsonHelper.readValue(file, PlayerConfigFile.class); Player player = new Player(id, configFile); - - if(player.getZone() == null) { - player.setZone(GameServer.getInstance().getZoneManager().getRandomZone()); - } - String name = player.getName(); if(playersByName.containsKey(name)) { @@ -69,6 +63,10 @@ private void loadPlayer(File file) { playersById.put(id, player); playersByName.put(name.toLowerCase(), player); + + if(player.getApiToken() != null) { + apiTokens.put(player.getApiToken(), player); + } } catch (Exception e) { logger.error(SERVER_MARKER, "Could not load configuration for player id {}", id, e); } @@ -96,7 +94,7 @@ public String register(String name) { } String id = UUID.randomUUID().toString(); - Player player = new Player(id, name, GameServer.getInstance().getZoneManager().getRandomZone()); // TODO tutorial zone + Player player = new Player(id, name, null); // TODO tutorial zone playersById.put(id, player); playersByName.put(name.toLowerCase(), player); String authToken = UUID.randomUUID().toString(); @@ -120,6 +118,24 @@ public String login(String name, String password) { return authToken; } + public boolean issueApiToken(Player player) { + String apiToken = TokenGenerator.generateToken(10, apiTokens::containsKey); + String currentToken = player.getApiToken(); + + if(apiToken == null) { + player.notify("Oops, we couldn't issue an API token for you.", NotificationType.SYSTEM); + return false; + } + + if(currentToken != null && !apiTokens.remove(currentToken, player)) { + logger.warn(SERVER_MARKER, "Could not unindex API token {} for player {}", currentToken, player.getDocumentId()); + } + + player.setApiToken(apiToken); + apiTokens.put(apiToken, player); + return true; + } + public boolean verifyAuthToken(String name, String authToken) { Player player = getPlayer(name); @@ -139,15 +155,27 @@ public boolean verifyAuthToken(String name, String authToken) { return false; } + public void changePlayerName(Player player, String name) { + if(playersByName.containsKey(name)) { + logger.warn(SERVER_MARKER, "Tried to rename player {} to already existing name {}", player.getDocumentId(), name); + return; + } + + // Track name change and re-index the player + playersByName.remove(player.getName().toLowerCase()); + player.trackNameChange(name); + player.setName(name); + playersByName.put(name.toLowerCase(), player); + } + public void onPlayerConnect(Player player) { - Connection connection = player.getConnection(); - playersByConnection.put(connection, player); - logger.info(SERVER_MARKER, "{} logged into zone {} from {}", player.getName(), player.getZone().getName(), connection.getAddress()); + onlinePlayers.add(player); + logger.info(SERVER_MARKER, "{} logged into zone {}", player.getName(), player.getZone().getName()); } public void onPlayerDisconnect(Player player) { Connection connection = player.getConnection(); - playersByConnection.remove(connection); + onlinePlayers.remove(player); logger.info(SERVER_MARKER, "{} disconnected: {}", player.getName(), connection.getDisconnectReason()); } @@ -169,11 +197,19 @@ public Player getPlayerById(String id) { return playersById.get(id); } - public Player getPlayer(Connection connection) { - return playersByConnection.get(connection); + public Player getPlayerByApiToken(String apiToken) { + return apiTokens.get(apiToken); } public Collection getPlayers() { return playersById.values(); } + + public int getOnlinePlayerCount() { + return onlinePlayers.size(); + } + + public List getOnlinePlayers() { + return Collections.unmodifiableList(onlinePlayers); + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerRestriction.java b/gameserver/src/main/java/brainwine/gameserver/player/PlayerRestriction.java similarity index 97% rename from gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerRestriction.java rename to gameserver/src/main/java/brainwine/gameserver/player/PlayerRestriction.java index 8e4c0544..9f96ecad 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerRestriction.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/PlayerRestriction.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.entity.player; +package brainwine.gameserver.player; import java.time.OffsetDateTime; diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java b/gameserver/src/main/java/brainwine/gameserver/player/PlayerStatistics.java similarity index 68% rename from gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java rename to gameserver/src/main/java/brainwine/gameserver/player/PlayerStatistics.java index f028ad85..a667c3c5 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerStatistics.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/PlayerStatistics.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.entity.player; +package brainwine.gameserver.player; import java.util.Collections; import java.util.HashMap; @@ -11,16 +11,21 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import brainwine.gameserver.achievements.CraftingAchievement; -import brainwine.gameserver.achievements.DiscoveryAchievement; -import brainwine.gameserver.achievements.ExploringAchievement; -import brainwine.gameserver.achievements.HuntingAchievement; -import brainwine.gameserver.achievements.LooterAchievement; -import brainwine.gameserver.achievements.MiningAchievement; -import brainwine.gameserver.achievements.RaiderAchievement; -import brainwine.gameserver.achievements.ScavengingAchievement; -import brainwine.gameserver.achievements.SidekickAchievement; -import brainwine.gameserver.achievements.SpawnerStoppageAchievement; +import brainwine.gameserver.achievement.CraftingAchievement; +import brainwine.gameserver.achievement.DeliveranceAchievement; +import brainwine.gameserver.achievement.DiscoveryAchievement; +import brainwine.gameserver.achievement.ExploringAchievement; +import brainwine.gameserver.achievement.HuntingAchievement; +import brainwine.gameserver.achievement.LooterAchievement; +import brainwine.gameserver.achievement.MiningAchievement; +import brainwine.gameserver.achievement.RaiderAchievement; +import brainwine.gameserver.achievement.ScavengingAchievement; +import brainwine.gameserver.achievement.SidekickAchievement; +import brainwine.gameserver.achievement.SpawnerStoppageAchievement; +import brainwine.gameserver.achievement.TrappingAchievement; +import brainwine.gameserver.achievement.UndertakerAchievement; +import brainwine.gameserver.achievement.ArchitectAchievement; +import brainwine.gameserver.achievement.VotingAchievement; import brainwine.gameserver.entity.EntityConfig; import brainwine.gameserver.item.Item; @@ -29,17 +34,23 @@ public class PlayerStatistics { private Map itemsMined = new HashMap<>(); + private Map itemsScavenged = new HashMap<>(); private Map itemsCrafted = new HashMap<>(); private Map discoveries = new HashMap<>(); private Map kills = new HashMap<>(); private Map assists = new HashMap<>(); + private Map trappings = new HashMap<>(); private float playTime; private int itemsPlaced; private int areasExplored; private int containersLooted; private int dungeonsRaided; private int mawsPlugged; + private int undertakings; + private int deliverances; private int deaths; + private int landmarksUpvoted; + private int landmarkVotesReceived; @JsonIgnore private Player player; @@ -56,14 +67,8 @@ protected void setPlayer(Player player) { } public void trackItemMined(Item item) { - if(!itemsMined.containsKey(item)) { - player.addExperience(150, "New item mined!"); - } - itemsMined.put(item, getItemsMined(item) + 1); - player.addExperience(item.getExperienceYield()); player.updateAchievementProgress(MiningAchievement.class); - player.updateAchievementProgress(ScavengingAchievement.class); } public void setItemsMined(Map itemsMined) { @@ -90,6 +95,40 @@ public Map getItemsMined() { return Collections.unmodifiableMap(itemsMined); } + public void trackItemScavenged(Item item) { + if(!itemsScavenged.containsKey(item)) { + player.addExperience(150, "New item mined!"); + } + + itemsScavenged.put(item, getItemsScavenged(item) + 1); + player.addExperience(item.getExperienceYield()); + player.updateAchievementProgress(ScavengingAchievement.class); + } + + public void setItemsScavenged(Map itemsScavenged) { + this.itemsScavenged = itemsScavenged; + } + + public int getTotalItemsScavenged() { + return itemsScavenged.values().stream() + .reduce(Integer::sum) + .orElse(0); + } + + public int getUniqueItemsScavenged() { + return (int)itemsScavenged.entrySet().stream() + .filter(entry -> entry.getValue() > 0) + .count(); + } + + public int getItemsScavenged(Item item) { + return itemsScavenged.getOrDefault(item, 0); + } + + public Map getItemsScavenged() { + return Collections.unmodifiableMap(itemsScavenged); + } + public void trackItemCrafted(Item item) { trackItemCrafted(item, 1); } @@ -222,6 +261,30 @@ public Map getAssists() { return Collections.unmodifiableMap(assists); } + public void trackTrapping(EntityConfig entity) { + trappings.put(entity, getTrappings(entity) + 1); + player.addExperience(5); + player.updateAchievementProgress(TrappingAchievement.class); + } + + public void setTrappings(Map trappings) { + this.trappings = trappings; + } + + public int getTotalTrappings() { + return trappings.values().stream() + .reduce(Integer::sum) + .orElse(0); + } + + public int getTrappings(EntityConfig entity) { + return trappings.getOrDefault(entity, 0); + } + + public Map getTrappings() { + return Collections.unmodifiableMap(trappings); + } + public void trackPlayTime(float deltaTime) { playTime += deltaTime; } @@ -309,6 +372,56 @@ public int getMawsPlugged() { return mawsPlugged; } + public void trackUndertaking() { + undertakings++; + player.addExperience(25); + player.updateAchievementProgress(UndertakerAchievement.class); + } + + public void setUndertakings(int undertakings) { + this.undertakings = undertakings; + } + + public int getUndertakings() { + return undertakings; + } + + public void trackDeliverance() { + trackDeliverances(1); + } + + public void trackDeliverances(int amount) { + deliverances += amount; + player.addExperience(25 * amount); + player.updateAchievementProgress(DeliveranceAchievement.class); + } + + public void trackLandmarksUpvoted() { + landmarksUpvoted++; + player.updateAchievementProgress(VotingAchievement.class); + } + + public int getLandmarkVotesReceived() { + return landmarkVotesReceived; + } + + public void trackLandmarkVotesReceived() { + landmarkVotesReceived++; + player.updateAchievementProgress(ArchitectAchievement.class); + } + + public int getLandmarksUpvoted() { + return landmarksUpvoted; + } + + public void setDeliverances(int deliverances) { + this.deliverances = deliverances; + } + + public int getDeliverances() { + return deliverances; + } + public void trackDeath() { deaths++; } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Skill.java b/gameserver/src/main/java/brainwine/gameserver/player/Skill.java similarity index 94% rename from gameserver/src/main/java/brainwine/gameserver/entity/player/Skill.java rename to gameserver/src/main/java/brainwine/gameserver/player/Skill.java index 0f2aeec4..c95e8e64 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Skill.java +++ b/gameserver/src/main/java/brainwine/gameserver/player/Skill.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.entity.player; +package brainwine.gameserver.player; import com.fasterxml.jackson.annotation.JsonValue; diff --git a/gameserver/src/main/java/brainwine/gameserver/player/TradeSession.java b/gameserver/src/main/java/brainwine/gameserver/player/TradeSession.java new file mode 100644 index 00000000..aa7e8c4e --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/player/TradeSession.java @@ -0,0 +1,639 @@ +package brainwine.gameserver.player; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import brainwine.gameserver.dialog.Dialog; +import brainwine.gameserver.dialog.DialogHelper; +import brainwine.gameserver.dialog.DialogListItem; +import brainwine.gameserver.dialog.DialogSection; +import brainwine.gameserver.dialog.input.DialogSelectInput; +import brainwine.gameserver.item.Item; + +/** + * Manages a trade session between two players: an initiator and a recipient. + * + * The initiator is the one initiating the trade, and is the first to make an offer. + * The recipient is the player whom the initiator makes an offer to. + * + * The recipient will functionally not be aware of the trade until the initiator submits their offer, + * and will not be part of this trade (by holding the {@code TradeSession} object) until accepting it. + * + * After submitting their offer, the initiator should not be allowed to make any new offers + * to the recipient while the trade is active, and initiating a trade with a different player + * should result in a cancellation of the current trade first. + * + * When the recipient accepts the initiator's trade request, it is their turn to make a counter-offer. + * This works the same as before, but the "Give freely" option should be excluded from the dialog. + * Once the counter-offer is submitted, the recipient should be blocked from making new offers + * just like how it was with the initiator. + * + * If the initiator accepts the recipient's counter-offer, one final inventory check should be done + * and if both parties have the required items, the trade is finalized. + * + * TODO Find a good way to track trades & implement achievements. + */ +public class TradeSession { + + /** + * Trade states. + */ + private enum State { + /** + * The initiator is currently making an offer. + * The recipient is not aware of the trade at this point. + */ + INITIATOR_OFFERING, + + /** + * The initiator has submitted their offer and is being viewed by the recipient. + */ + RECIPIENT_VIEWING_OFFER, + + /** + * The recipient has accepted the initiator's trade request and is currently making a counter-offer. + */ + RECIPIENT_OFFERING, + + /** + * The recipient has submitted their counter-offer and is being viewed by the initiator. + * If the initiator accepts the offer, the trade will be finalized. + */ + INITIATOR_VIEWING_OFFER, + + /** + * The trade has ended and is no longer valid. + */ + TRADE_ENDED + } + + public static final int ITEM_LIMIT = 8; // Maximum number of different items + private final Map initiatorOffers = new HashMap<>(); + private final Map recipientOffers = new HashMap<>(); + private final Player initiator; + private final Player recipient; + private boolean isRecipientAware; // Whether or not the recipient is aware of this trade + private State state = State.INITIATOR_OFFERING; + private long timeoutAt; + + public TradeSession(Player initiator, Player recipient) { + this.initiator = initiator; + this.recipient = recipient; + } + + /** + * Called when one of the participating players drags an item to offer. + */ + protected void onItemOffered(Player player, Item item) { + // Do nothing if state is invalid + if((player == initiator && state != State.INITIATOR_OFFERING) || (player == recipient && state != State.RECIPIENT_OFFERING)) { + return; + } + + Map offers = getOffers(player); + + // Do nothing if item limit has been reached and item is not already part of the offer + if(offers.size() >= ITEM_LIMIT && !offers.containsKey(item)) { + player.notify("You cannot offer any more items."); + return; + } + + Player otherPlayer = getOtherPlayer(player); + player.showDialog(Dialogs.createQuantitySelectorDialog(player, otherPlayer, item), input -> { + // Handle cancellation + if(input.length == 0 || (input.length == 1 && input[0].equals("cancel"))) { + cancel(player); + return; + } + + // Parse quantity + int quantity = 0; + + try { + quantity = Integer.parseInt(String.valueOf(input[0])); + } catch(NumberFormatException e) { + abort(); + return; + } + + // Process input + onItemQuantitySelected(player, item, quantity); + }); + + // Update timeout + setTimeoutSeconds(10); + } + + /** + * Called by {@link #onItemOffered(Player, Item)} when the player submits an item quantity. + */ + private void onItemQuantitySelected(Player player, Item item, int quantity) { + // Do nothing if state is invalid + if((player == initiator && state != State.INITIATOR_OFFERING) || (player == recipient && state != State.RECIPIENT_OFFERING)) { + return; + } + + // Abort if quantiy is invalid or if the player does not have enough of this item + if(quantity <= 0 || !player.getInventory().hasItem(item, quantity)) { + abort(); + return; + } + + Map offers = getOffers(player); + + // Do nothing if item limit has been reached and item is not already part of the offer + if(offers.size() >= ITEM_LIMIT && !offers.containsKey(item)) { + return; + } + + // Store offer + offers.put(item, quantity); + + // Show offer status dialog + if(player == initiator) { + player.showDialog(Dialogs.createInitiatorOfferStatusDialog(recipient, offers), input -> { + // Validate input + if(input.length != 1) { + abort(); + return; + } + + // Handle action + switch(String.valueOf(input[0])) { + case "Request trade": + onInitiatorSendTradeRequest(); + break; + case "Give freely": + onInitiatorGiveFreely(); + break; + case "cancel": + // Quirk: V3 auto-cancels dialogs when a new one pops up, which isn't very helpful here... + if(!player.isV3()) { + cancel(player); + } + + break; + default: + abort(); + break; + } + }); + } else if(player == recipient) { + player.showDialog(Dialogs.createRecipientOfferStatusDialog(initiator, initiatorOffers, offers), input -> { + // Validate input + if(input.length != 1) { + abort(); + return; + } + + // Handle action + switch(String.valueOf(input[0])) { + case "Submit offer": + onRecipientSubmitOffer(); + break; + case "Cancel": + // Yes, the uppercase 'C' is important. + cancel(player); + break; + } + }); + } + + // Update timeout + setTimeoutSeconds(20); + } + + /** + * Called by {@link #onItemQuantitySelected(Player, Item, int)} when the initiator chooses to give their offer freely. + * TODO Should there be a confirmation dialog? + */ + private void onInitiatorGiveFreely() { + // Do nothing if state is invalid + if(state != State.INITIATOR_OFFERING) { + return; + } + + // End trade if recipient is currently unavailable + if(!canRecipientTrade()) { + initiator.showDialog(DialogHelper.messageDialog(String.format("%s cannot receive items right now -- try again in a minute.", recipient.getName()))); + end(); + return; + } + + // Check initiator's inventory + if(!checkInventory(initiator)) { + abort("The trade could not be fulfilled."); + return; + } + + // Try to end the trade + if(!end()) { + return; + } + + // Deliver goodies + initiatorOffers.forEach((item, quantity) -> { + initiator.getInventory().removeItem(item, quantity, true); + recipient.getInventory().addItem(item, quantity, true); + }); + + // Show feedback + initiator.showDialog(Dialogs.createOfferDialog(String.format("You sent free goodies to %s!", recipient.getName()), "Sent:", initiatorOffers)); + recipient.showDialog(Dialogs.createOfferDialog(String.format("You received goodies from %s!", initiator.getName()), "Received:", initiatorOffers)); + } + + /** + * Called by {@link #onItemQuantitySelected(Player, Item, int)} when the initiator submits their offer. + */ + private void onInitiatorSendTradeRequest() { + // Do nothing if state is invalid + if(state != State.INITIATOR_OFFERING) { + return; + } + + // Abort trade if no offers are present + if(initiatorOffers.isEmpty()) { + abort(); + return; + } + + // End trade if recipient is unavailable + if(!canRecipientTrade()) { + initiator.showDialog(DialogHelper.messageDialog(String.format("%s cannot trade right now -- try again in a minute.", recipient.getName()))); + end(); + return; + } + + // Update trade state + state = State.RECIPIENT_VIEWING_OFFER; + isRecipientAware = true; + + // Show feedback to initiator + initiator.showDialog(Dialogs.createOfferDialog("Your offer has been sent:", initiatorOffers)); + + // Show trade request dialog to recipient + recipient.showDialog(Dialogs.createOfferDialog(String.format("%s wants to trade:", initiator.getName()), null, "Are you interested?", initiatorOffers).setActions("yesno"), input -> { + // Handle cancellation + if(input.length == 1 && input[0].equals("cancel")) { + cancel(recipient); + return; + } + + onRecipientAcceptTradeRequest(); + }); + + // Update timeout + setTimeoutSeconds(10); + } + + /** + * Called by {@link #onInitiatorTradeRequest()} when the recipient accepts the initiator's trade request. + */ + private void onRecipientAcceptTradeRequest() { + // Do nothing if state is invalid + if(state != State.RECIPIENT_VIEWING_OFFER) { + return; + } + + // Cancel recipient's existing trade session if it exists + if(recipient.isTrading()) { + recipient.getTradeSession().cancel(recipient); + } + + // Update trade state + state = State.RECIPIENT_OFFERING; + recipient.setTradeSession(this); + + // Show feedback to initiator + initiator.showDialog(DialogHelper.messageDialog(String.format("%s accepted your trade request.", recipient.getName()), "Their offer will be along shortly.")); + + // Show feedback to recipient + recipient.showDialog(Dialogs.createOfferDialog("You accepted a trade request for:", null, + String.format("Drag the item you'd like to trade to %s, then select the amount to offer.", initiator.getName()), initiatorOffers)); + + // Update timeout + setTimeoutSeconds(20); + } + + /** + * Called by {@link #onItemQuantitySelected(Player, Item, int)} when the recipient submits their counter-offer. + */ + private void onRecipientSubmitOffer() { + // Do nothing if state is invalid + if(state != State.RECIPIENT_OFFERING) { + return; + } + + // Abort trade if no offers are present + if(recipientOffers.isEmpty()) { + abort(); + return; + } + + // Update state + state = State.INITIATOR_VIEWING_OFFER; + + // Show feedback to recipient + recipient.showDialog(Dialogs.createOfferDialog("Your offer has been sent:", recipientOffers)); + + // Show the recipient's offer to the initiator + initiator.showDialog(Dialogs.createFinalOfferDialog(initiator, recipient, initiatorOffers, recipientOffers), input -> { + // Handle cancellation + if(input.length == 1 && input[0].equals("cancel")) { + cancel(initiator); + return; + } + + // Finalize the trade + complete(); + }); + + // Update timeout + setTimeoutSeconds(20); + } + + /** + * Called by {@link #onRecipientSubmitOffer()} when the initiator accepts the recipient's counter-offer. + * Checks both parties' inventories and completes the trade if everything checks out. + */ + private void complete() { + // Check inventory of both players + if(!checkInventory(initiator) || !checkInventory(recipient)) { + abort("The trade could not be fulfilled."); + return; + } + + // Try to end the trade + if(!end()) { + return; + } + + // Update inventories + initiatorOffers.forEach((item, quantity) -> { + initiator.getInventory().removeItem(item, quantity, true); + recipient.getInventory().addItem(item, quantity, true); + }); + + recipientOffers.forEach((item, quantity) -> { + recipient.getInventory().removeItem(item, quantity, true); + initiator.getInventory().addItem(item, quantity, true); + }); + + // Show trade completion dialog + initiator.showDialog(Dialogs.createOfferDialog(String.format("You traded with %s.", recipient.getName()), "Received:", recipientOffers)); + recipient.showDialog(Dialogs.createOfferDialog(String.format("You traded with %s.", initiator.getName()), "Received:", initiatorOffers)); + } + + /** + * @param player The player whose inventory to be check. + * @return {@code true} if the player has enough of each item they have offered, otherwise {@code false}. + */ + private boolean checkInventory(Player player) { + Map offers = getOffers(player); + + for(Entry entry : offers.entrySet()) { + if(!player.getInventory().hasItem(entry.getKey(), entry.getValue())) { + return false; + } + } + + return true; + } + + /** + * Calls {@link #abort(String)} with a generic message. + */ + private void abort() { + abort("The trade was cancelled due to an error."); + } + + /** + * Aborts the trade and notifies the participants with the specified message. + * The recipient will not be notified of the cancellation if they aren't aware of the trade. + * + * @param message The message to display. Can be {@code null}, in which case no message will be shown. + */ + private void abort(String message) { + // Do nothing if the trade has already ended + if(!end()) { + return; + } + + // Don't notify if message isn't present + if(message == null) { + return; + } + + initiator.showDialog(DialogHelper.messageDialog(message)); + + // Only notify recipient if they are aware of this trade + if(isRecipientAware) { + recipient.showDialog(DialogHelper.messageDialog(message)); + } + } + + /** + * Cancels the trade and notifies the participants. + * The recipient will not be notified of the cancellation if they aren't aware of the trade. + * + * @param canceller The player who cancelled the trade. + */ + public void cancel(Player canceller) { + // Do nothing if the trade has already ended + if(!end()) { + return; + } + + String message = String.format("%s cancelled the trade.", canceller.getName()); + String cancellerMessage = String.format("You cancelled the trade with %s.", getOtherPlayer(canceller).getName()); + initiator.showDialog(DialogHelper.messageDialog(initiator == canceller ? cancellerMessage : message)); + + // Only notify recipient if they are aware of this trade + if(isRecipientAware) { + recipient.showDialog(DialogHelper.messageDialog(recipient == canceller ? cancellerMessage : message)); + } + } + + /** + * Ends the trade if it has timed out due to an action taking too long. + */ + public void timeout() { + // Do nothing if it's not time yet + if(System.currentTimeMillis() < timeoutAt) { + return; + } + + // Try to end the trade + if(!end()) { + return; + } + + initiator.showDialog(DialogHelper.messageDialog(String.format("Your trade with %s has timed out.", recipient.getName()))); + + // Only notify recipient if they are aware of this trade + if(isRecipientAware) { + recipient.showDialog(DialogHelper.messageDialog(String.format("Your trade with %s has timed out.", initiator.getName()))); + } + } + + /** + * Sets the state to {@link State#TRADE_ENDED} and sets the trade session of both players to {@code null} + * if they are in this trade according to {@link #isTradeCurrent(Player)} + * + * @return {@code false} if the trade has already been ended, otherwise {@code true}. + */ + private boolean end() { + if(state == State.TRADE_ENDED) { + return false; + } + + if(isTradeCurrent(initiator)) initiator.setTradeSession(null); + if(isTradeCurrent(recipient)) recipient.setTradeSession(null); + state = State.TRADE_ENDED; + return true; + } + + /** + * Updates the timeout value. + * + * @param seconds The amount of seconds until the trade should timeout. + */ + private void setTimeoutSeconds(int seconds) { + timeoutAt = System.currentTimeMillis() + seconds * 1000L; + } + + /** + * @return The current offers of the given player, or {@code null} if the player is not part of this trade. + */ + private Map getOffers(Player player) { + return player == initiator ? initiatorOffers : player == recipient ? recipientOffers : null; + } + + /** + * @return The other trade participant that is not the specified one, or {@code null} if the player is not part of this trade. + */ + private Player getOtherPlayer(Player player) { + return player == initiator ? recipient : player == recipient ? initiator : null; + } + + /** + * @return {@code true} if the given player is either the initiator or recipient of this trade, otherwise {@code false}. + */ + public boolean isParticipant(Player player) { + return player == initiator || player == recipient; + } + + /** + * @return {@code true} if the recipient available to trade right now, otherwise {@code false}. + */ + public boolean canRecipientTrade() { + return recipient.isOnline() && recipient.getZone() == initiator.getZone() && !recipient.isTrading(); + } + + /** + * @return {@code true} if the given player is currently in this trade, otherwise {@code false}. + * + * A player is considered to be part of the trade when they are either: + * - The initiator of the trade + * - The recipient and have accepted the initiator's request to trade + */ + private boolean isTradeCurrent(Player player) { + return player.getTradeSession() == this; + } + + /** + * Helper class for creating trading-related dialogs. + */ + private static class Dialogs { + + public static final List ITEM_QUANTITY_OPTIONS = + Arrays.asList("1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "15", "20", "25", "30", "40", "50", "75", "100", "200", "500", "1000", "5000", "25000", "100000"); + + public static Dialog createQuantitySelectorDialog(Player offerer, Player target, Item item) { + // Get quantity options that are available to the player + List quantityOptions = ITEM_QUANTITY_OPTIONS.stream() + .filter(quantity -> offerer.getInventory().hasItem(item, Integer.parseInt(quantity))) + .collect(Collectors.toList()); + + return new Dialog() + .addSection(new DialogSection() + .setTitle(String.format("Trade with %s", target.getName()))) + .addSection(new DialogSection() + .setText(String.format("Quantity of %s to trade:", item.getTitle())) + .setInput(new DialogSelectInput() + .setOptions(quantityOptions) + .setKey("quantity"))); + } + + public static Dialog createInitiatorOfferStatusDialog(Player recipient, Map offers) { + Dialog dialog = createOfferDialog("Your current offer:", offers); + + // Add multi-item trading hint if the item limit hasn't been reached yet + if(offers.size() < TradeSession.ITEM_LIMIT) { + dialog.addSection(new DialogSection() + .setText(String.format("Drag another item to %s to include it in your offer. " + + "You can trade up to %s different items at the same time this way.", recipient.getName(), TradeSession.ITEM_LIMIT))); + } + + dialog.addSection(new DialogSection() + .setInput(new DialogSelectInput() + .setOptions("Request trade", "Give freely") + .setKey("type"))); + + return dialog; + } + + public static Dialog createRecipientOfferStatusDialog(Player initiator, Map initiatorOffers, Map recipientOffers) { + Dialog dialog = createOfferDialog(String.format("%s's offer:", initiator.getName()), initiatorOffers).setActions("Cancel", "Submit offer"); + dialog.addSection(createOfferSection(recipientOffers).setTitle("Your current offer:")); + + // Add multi-item trading hint if the item limit hasn't been reached yet + if(recipientOffers.size() < TradeSession.ITEM_LIMIT) { + dialog.addSection(new DialogSection() + .setText(String.format("Drag another item to %s to include it in your offer. " + + "You can trade up to %s different items at the same time this way.", initiator.getName(), TradeSession.ITEM_LIMIT))); + } + + return dialog; + } + + public static Dialog createFinalOfferDialog(Player initiator, Player recipient, Map initiatorOffers, Map recipientOffers) { + return createOfferDialog(String.format("%s has offered:", recipient.getName()), recipientOffers).setActions("yesno") + .addSection(createOfferSection(initiatorOffers).setText("For your:")) + .addSection(new DialogSection().setText("Do you accept this trade?")); + } + + public static Dialog createOfferDialog(String title, Map offer) { + return createOfferDialog(title, null, offer); + } + + public static Dialog createOfferDialog(String title, String text, Map offer) { + return createOfferDialog(title, text, null, offer); + } + + public static Dialog createOfferDialog(String title, String text, String footer, Map offer) { + Dialog dialog = new Dialog().addSection(createOfferSection(offer).setTitle(title).setText(text)); + + if(footer != null) { + dialog.addSection(new DialogSection().setText(footer)); + } + + return dialog; + } + + private static DialogSection createOfferSection(Map offers) { + DialogSection section = new DialogSection(); + offers.forEach((item, quantity) -> { + section.addItem(new DialogListItem().setItem(item.getCode()) + .setImage(String.format("inventory/%s", item.getId())) + .setText(String.format("%s x %s", item.getTitle(), quantity))); + }); + return section; + } + } + +} diff --git a/gameserver/src/main/java/brainwine/gameserver/prefab/PrefabManager.java b/gameserver/src/main/java/brainwine/gameserver/prefab/PrefabManager.java index 07f18e5c..71c40739 100644 --- a/gameserver/src/main/java/brainwine/gameserver/prefab/PrefabManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/prefab/PrefabManager.java @@ -3,115 +3,105 @@ import static brainwine.shared.LogMarkers.SERVER_MARKER; import java.io.File; +import java.io.IOException; +import java.net.URL; import java.nio.file.Files; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.zip.InflaterInputStream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.msgpack.core.MessagePack; -import org.msgpack.core.MessageUnpacker; import org.msgpack.jackson.dataformat.MessagePackFactory; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.module.SimpleModule; +import brainwine.gameserver.resource.ResourceFinder; import brainwine.gameserver.serialization.BlockDeserializer; import brainwine.gameserver.serialization.BlockSerializer; -import brainwine.gameserver.util.ResourceUtils; import brainwine.gameserver.util.ZipUtils; import brainwine.gameserver.zone.Block; import brainwine.shared.JsonHelper; public class PrefabManager { + public static final String PREFAB_DIRECTORY_NAME = "prefabs"; private static final Logger logger = LogManager.getLogger(); private static final ObjectMapper mapper = JsonMapper.builder(new MessagePackFactory()) .addModule(new SimpleModule() .addDeserializer(Block.class, BlockDeserializer.INSTANCE) - .addSerializer(BlockSerializer.INSTANCE)) - .build(); - private final File dataDir = new File("prefabs"); + .addSerializer(BlockSerializer.INSTANCE)).build(); + private final File dataDirectory = new File(PREFAB_DIRECTORY_NAME); private final Map prefabs = new LinkedHashMap<>(); public PrefabManager() { + loadPrefabs(); + } + + private void loadPrefabs() { logger.info(SERVER_MARKER, "Loading prefabs ..."); - ResourceUtils.copyDefaults("prefabs/"); - if(dataDir.isDirectory()) { - for(File file : dataDir.listFiles()) { - if(file.isDirectory()) { - loadPrefab(file); - } - } - } + // Fetch prefab names and load prefabs + ResourceFinder.getResources(PREFAB_DIRECTORY_NAME).stream() + .map(x -> x.getParentDirectoryName()) + .filter(x -> x.length() > PREFAB_DIRECTORY_NAME.length()) + .map(x -> x.substring(PREFAB_DIRECTORY_NAME.length() + 1)) + .distinct() + .forEach(this::loadPrefab); - logger.info(SERVER_MARKER, "Successfully loaded {} prefab(s)", prefabs.size()); + logger.info(SERVER_MARKER, "Successfully loaded {} prefab{}", prefabs.size(), prefabs.size() == 1 ? "" : "s"); } - private void loadPrefab(File file) { - String name = file.getPath().substring(dataDir.getPath().length() + 1).replace(File.separatorChar, '/'); - File legacyBlocksFile = new File(file, "blocks.cmp"); - File configFile = new File(file, "config.json"); - File blocksFile = new File(file, "blocks.dat"); - + private void loadPrefab(String name) { try { + URL configUrl = ResourceFinder.getResourceUrl(String.format("prefabs/%s/config.json", name)); + URL blocksUrl = ResourceFinder.getResourceUrl(String.format("prefabs/%s/blocks.dat", name)); + PrefabConfigFile config = JsonHelper.readValue(configUrl, PrefabConfigFile.class); PrefabBlocksFile blockData = null; - if(legacyBlocksFile.exists() && !blocksFile.exists()) { - logger.info(SERVER_MARKER, "Updating blocks file for prefab '{}' ...", name); - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker( - ZipUtils.inflateBytes(Files.readAllBytes(legacyBlocksFile.toPath()))); - int width = unpacker.unpackInt(); - int height = unpacker.unpackInt(); - Block[] blocks = new Block[unpacker.unpackArrayHeader() / 3]; - - for(int i = 0; i < blocks.length; i++) { - blocks[i] = new Block(unpacker.unpackInt(), unpacker.unpackInt(), unpacker.unpackInt()); - } - - blockData = new PrefabBlocksFile(width, height, blocks); - Files.write(blocksFile.toPath(), ZipUtils.deflateBytes(mapper.writeValueAsBytes(blockData))); - legacyBlocksFile.delete(); - } else { - // If no config or blocks file exists we will assume that this is a category directory. - // TODO maybe create a separate function for all this? - if(!configFile.exists() || !blocksFile.exists()) { - for(File childFile : file.listFiles()) { - if(childFile.isDirectory()) { - loadPrefab(childFile); - } - } - - return; - } - - blockData = mapper.readValue(ZipUtils.inflateBytes(Files.readAllBytes(blocksFile.toPath())), PrefabBlocksFile.class); + // Load block data + try(InflaterInputStream inputStream = new InflaterInputStream(blocksUrl.openStream())) { + blockData = mapper.readValue(inputStream, PrefabBlocksFile.class); } - PrefabConfigFile config = JsonHelper.readValue(configFile, PrefabConfigFile.class); + // Add prefab prefabs.put(name, new Prefab(name, config, blockData)); } catch(Exception e) { - logger.error(SERVER_MARKER, "Could not load prefab {}:", name, e); + logger.error(SERVER_MARKER, "Could not load prefab '{}'", name, e); } } - public void addPrefab(Prefab prefab) throws Exception { - String name = prefab.getName().toLowerCase(); + public boolean createPrefab(Prefab prefab) throws IOException { + return createPrefab(prefab, false); + } + + public boolean createPrefab(Prefab prefab, boolean overwrite) throws IOException { + String name = prefab.getName(); - if(prefabs.containsKey(name)) { - logger.warn(SERVER_MARKER, "Duplicate prefab name: {}", name); - return; + // Do nothing if name already exists and overwriting is not allowed + if(!overwrite && prefabExists(name)) { + return false; } - File prefabDir = new File(dataDir, name); - prefabDir.mkdirs(); - JsonHelper.writeValue(new File(prefabDir, "config.json"), new PrefabConfigFile(prefab)); - Files.write(new File(prefabDir, "blocks.dat").toPath(), ZipUtils.deflateBytes(mapper.writeValueAsBytes(new PrefabBlocksFile(prefab)))); + // Serialize & write data + byte[] configBytes = JsonHelper.writeValueAsBytes(new PrefabConfigFile(prefab)); + byte[] blockBytes = ZipUtils.deflateBytes(mapper.writeValueAsBytes(new PrefabBlocksFile(prefab))); + File outputDirectory = new File(dataDirectory, name.toLowerCase()); + outputDirectory.mkdirs(); + Files.write(new File(outputDirectory, "config.json").toPath(), configBytes); + Files.write(new File(outputDirectory, "blocks.dat").toPath(), blockBytes); + + // Index prefab prefabs.put(name, prefab); + return true; + } + + public boolean prefabExists(String name) { + return prefabs.containsKey(name); } public Prefab getPrefab(String name) { diff --git a/gameserver/src/main/java/brainwine/gameserver/resource/Resource.java b/gameserver/src/main/java/brainwine/gameserver/resource/Resource.java new file mode 100644 index 00000000..45e17f25 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/resource/Resource.java @@ -0,0 +1,34 @@ +package brainwine.gameserver.resource; + +import java.net.URL; + +public class Resource { + + private final String name; + private final String simpleName; + private final String parentDirectoryName; + private final URL url; + + public Resource(String name, String simpleName, String parentDirectoryName, URL url) { + this.name = name; + this.simpleName = simpleName; + this.parentDirectoryName = parentDirectoryName; + this.url = url; + } + + public String getName() { + return name; + } + + public String getSimpleName() { + return simpleName; + } + + public String getParentDirectoryName() { + return parentDirectoryName; + } + + public URL getUrl() { + return url; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/resource/ResourceFinder.java b/gameserver/src/main/java/brainwine/gameserver/resource/ResourceFinder.java new file mode 100644 index 00000000..1f71b372 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/resource/ResourceFinder.java @@ -0,0 +1,172 @@ +package brainwine.gameserver.resource; + +import static brainwine.shared.LogMarkers.SERVER_MARKER; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.reflections.Reflections; +import org.reflections.scanners.Scanners; +import org.reflections.util.ClasspathHelper; +import org.reflections.util.ConfigurationBuilder; +import org.reflections.util.FilterBuilder; + +/** + * Helper class for finding the locations of gameserver configuration resources (loot tables, prefabs etc.) + */ +public class ResourceFinder { + + private static final Logger logger = LogManager.getLogger(); + + /** + * Calls {@link #getResourceUrl(String, boolean)} with {@code overridable true}. + */ + public static URL getResourceUrl(String name) { + return getResourceUrl(name, true); + } + + /** + * @param name The name of the resource. + * @param overridable If {@code true}, files in the working directory are counted and will take priority. + * @return The {@link URL} of the specified resource, or {@code null} if it doesn't exist. + */ + public static URL getResourceUrl(String name, boolean overridable) { + if(overridable) { + File file = new File(name); + + if(file.exists()) { + try { + return file.toURI().toURL(); + } catch(MalformedURLException e) { + logger.error(SERVER_MARKER, "Couldn't get URL for file '{}'", name); + return null; + } + } + } + + return ResourceFinder.class.getResource(String.format("/%s", name)); + } + + /** + * Calls {@link #getResource(String, boolean)} with {@code overridable true}. + */ + public static Resource getResource(String name) { + return getResource(name, true); + } + + /** + * @param name The name of the resource. + * @param overridable If {@code true}, files in the working directory are counted and will take priority. + * @return A {@link Resource} object for the specified resource, or {@code null} if it doesn't exist. + */ + public static Resource getResource(String name, boolean overridable) { + URL url = getResourceUrl(name, overridable); + + if(url == null) { + return null; + } + + String simpleName = name; + String parentDirectoryName = ""; // TODO null default? + int lastSeparatorIndex = name.lastIndexOf('/'); + + if(lastSeparatorIndex != -1) { + simpleName = name.substring(lastSeparatorIndex + 1); + parentDirectoryName = name.substring(0, lastSeparatorIndex); + } + + return new Resource(name, simpleName, parentDirectoryName, url); + } + + /** + * Calls {@link #getResources(String, boolean)} with {@code recursive true}. + */ + public static List getResources(String directory) { + return getResources(directory, true); + } + + /** + * Calls {@link #getResources(String, boolean, boolean)} with {@code overridable true}. + */ + public static List getResources(String directory, boolean recursive) { + return getResources(directory, recursive, true); + } + + /** + * @param directory The name of the directory containing the desired resources. + * @param recursive If {@code true}, resources in subdirectories will be counted as well. + * @param overridable If {@code true}, files in the working directory are counted and will take priority. + * @return A list of {@link #Resource} objects representing the found resources. + */ + public static List getResources(String directory, boolean recursive, boolean overridable) { + Set resourceNames = getResourceNames(directory, recursive, overridable); + return resourceNames.stream().map(x -> getResource(x, overridable)).collect(Collectors.toList()); + } + + /** + * Helper function to find resource names in a given resource directory. + */ + private static Set getResourceNames(String directory, boolean recursive, boolean overridable) { + Set resourceNames = overridable ? getFileNames(new File(directory), recursive) : new HashSet<>(); + FilterBuilder filter = new FilterBuilder().includePattern(String.format("%s/.*", directory)); + Reflections reflections = new Reflections(new ConfigurationBuilder() + .setUrls(ClasspathHelper.forResource(directory)) + .filterInputsBy(recursive ? filter : filter.excludePattern(String.format("%s/.*/.*", directory))) + .setScanners(Scanners.Resources)); + reflections.getResources(".*").stream() + .filter(x -> !resourceNames.contains(x)) + .forEach(resourceNames::add); + return resourceNames; + } + + /** + * Helper function to find file names in a given directory. + */ + private static Set getFileNames(File directory, boolean recursive) { + Set fileNames = new HashSet<>(); + Queue processQueue = new ArrayDeque<>(); + + if(directory.isDirectory()) { + for(File file : directory.listFiles()) { + processQueue.add(file); + } + } + + while(!processQueue.isEmpty()) { + File file = processQueue.poll(); + + if(file.isDirectory()) { + if(recursive) { + for(File child : file.listFiles()) { + processQueue.add(child); + } + } + } else { + String name = file.getPath().replace(File.separatorChar, '/'); // TODO will this always work and not do funny things with the path? + fileNames.add(name); + } + } + + return fileNames; + } + + /** + * @deprecated Will be moved to another location later. + */ + public static String removeFileSuffix(String string) { + if(!string.contains(".")) { + return string; + } + + return string.substring(0, string.lastIndexOf('.')); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/serialization/AchievementSerializer.java b/gameserver/src/main/java/brainwine/gameserver/serialization/AchievementSerializer.java index ca21aa80..580f6692 100644 --- a/gameserver/src/main/java/brainwine/gameserver/serialization/AchievementSerializer.java +++ b/gameserver/src/main/java/brainwine/gameserver/serialization/AchievementSerializer.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import brainwine.gameserver.achievements.Achievement; +import brainwine.gameserver.achievement.Achievement; /** * Seriously, Jackson has the stupidest problems sometimes. diff --git a/gameserver/src/main/java/brainwine/gameserver/serialization/MessageSerializer.java b/gameserver/src/main/java/brainwine/gameserver/serialization/MessageSerializer.java index 98d78a86..5f5b0316 100644 --- a/gameserver/src/main/java/brainwine/gameserver/serialization/MessageSerializer.java +++ b/gameserver/src/main/java/brainwine/gameserver/serialization/MessageSerializer.java @@ -11,8 +11,8 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; public class MessageSerializer extends StdSerializer { diff --git a/gameserver/src/main/java/brainwine/gameserver/serialization/RequestDeserializer.java b/gameserver/src/main/java/brainwine/gameserver/serialization/RequestDeserializer.java index cac7d6af..622a9594 100644 --- a/gameserver/src/main/java/brainwine/gameserver/serialization/RequestDeserializer.java +++ b/gameserver/src/main/java/brainwine/gameserver/serialization/RequestDeserializer.java @@ -9,7 +9,7 @@ import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import brainwine.gameserver.annotations.OptionalField; +import brainwine.gameserver.server.OptionalField; import brainwine.gameserver.server.Request; public class RequestDeserializer extends StdDeserializer { diff --git a/gameserver/src/main/java/brainwine/gameserver/annotations/MessageInfo.java b/gameserver/src/main/java/brainwine/gameserver/server/MessageInfo.java similarity index 97% rename from gameserver/src/main/java/brainwine/gameserver/annotations/MessageInfo.java rename to gameserver/src/main/java/brainwine/gameserver/server/MessageInfo.java index 0c130b17..246d4367 100644 --- a/gameserver/src/main/java/brainwine/gameserver/annotations/MessageInfo.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/MessageInfo.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.annotations; +package brainwine.gameserver.server; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/gameserver/src/main/java/brainwine/gameserver/server/NetworkRegistry.java b/gameserver/src/main/java/brainwine/gameserver/server/NetworkRegistry.java index d9996828..60bc67bb 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/NetworkRegistry.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/NetworkRegistry.java @@ -10,9 +10,6 @@ import org.apache.logging.log4j.Logger; import org.reflections.Reflections; -import brainwine.gameserver.annotations.MessageInfo; -import brainwine.gameserver.annotations.RequestInfo; - @SuppressWarnings("unchecked") public class NetworkRegistry { diff --git a/gameserver/src/main/java/brainwine/gameserver/annotations/OptionalField.java b/gameserver/src/main/java/brainwine/gameserver/server/OptionalField.java similarity index 86% rename from gameserver/src/main/java/brainwine/gameserver/annotations/OptionalField.java rename to gameserver/src/main/java/brainwine/gameserver/server/OptionalField.java index e7e7ff57..6faa8aeb 100644 --- a/gameserver/src/main/java/brainwine/gameserver/annotations/OptionalField.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/OptionalField.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.annotations; +package brainwine.gameserver.server; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/gameserver/src/main/java/brainwine/gameserver/server/PlayerRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/PlayerRequest.java index 1f018ae1..648c60cf 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/PlayerRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/PlayerRequest.java @@ -1,7 +1,6 @@ package brainwine.gameserver.server; -import brainwine.gameserver.GameServer; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.pipeline.Connection; /** @@ -12,7 +11,7 @@ public abstract class PlayerRequest extends Request { public abstract void process(Player player); public final void process(Connection connection) { - Player player = GameServer.getInstance().getPlayerManager().getPlayer(connection); + Player player = connection.getPlayer(); if(player == null) { connection.kick("No player instance found."); diff --git a/gameserver/src/main/java/brainwine/gameserver/annotations/RequestInfo.java b/gameserver/src/main/java/brainwine/gameserver/server/RequestInfo.java similarity index 87% rename from gameserver/src/main/java/brainwine/gameserver/annotations/RequestInfo.java rename to gameserver/src/main/java/brainwine/gameserver/server/RequestInfo.java index 0c4bb726..0298a7d8 100644 --- a/gameserver/src/main/java/brainwine/gameserver/annotations/RequestInfo.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/RequestInfo.java @@ -1,4 +1,4 @@ -package brainwine.gameserver.annotations; +package brainwine.gameserver.server; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/gameserver/src/main/java/brainwine/gameserver/server/Server.java b/gameserver/src/main/java/brainwine/gameserver/server/Server.java index 3fb4a3f4..d6469f56 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/Server.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/Server.java @@ -96,6 +96,6 @@ protected void initChannel(Channel channel) throws Exception { public void close() { logger.info(SERVER_MARKER, "Closing endpoints ..."); - eventLoopGroup.shutdownGracefully(); + eventLoopGroup.shutdownGracefully().awaitUninterruptibly(); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/AchievementMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/AchievementMessage.java index c440f4c3..f4cd1c63 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/AchievementMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/AchievementMessage.java @@ -1,7 +1,7 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 29, collection = true) public class AchievementMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/AchievementProgressMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/AchievementProgressMessage.java index 15e1156f..263f3cf4 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/AchievementProgressMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/AchievementProgressMessage.java @@ -1,7 +1,7 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 48, collection = true) public class AchievementProgressMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/BlockChangeMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/BlockChangeMessage.java index ab63a963..81f76b16 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/BlockChangeMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/BlockChangeMessage.java @@ -3,10 +3,10 @@ import java.util.Arrays; import java.util.Collection; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Layer; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; import brainwine.gameserver.server.models.BlockChangeData; @MessageInfo(id = 9, prepacked = true) @@ -18,7 +18,11 @@ public BlockChangeMessage(Collection blockChanges) { this.blockChanges = blockChanges; } + public BlockChangeMessage(int x, int y, Layer layer, int entityId, Item item, int mod) { + this(Arrays.asList(new BlockChangeData(x, y, layer, entityId, item, mod))); + } + public BlockChangeMessage(int x, int y, Layer layer, Item item, int mod) { - this(Arrays.asList(new BlockChangeData(x, y, layer, item, mod))); + this(x, y, layer, 0, item, mod); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/BlockMetaMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/BlockMetaMessage.java index e90282ae..99f8a7f3 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/BlockMetaMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/BlockMetaMessage.java @@ -3,8 +3,8 @@ import java.util.Arrays; import java.util.Collection; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; import brainwine.gameserver.zone.MetaBlock; @MessageInfo(id = 20, prepacked = true) diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/BlocksMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/BlocksMessage.java index 94875312..38f7b4fb 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/BlocksMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/BlocksMessage.java @@ -2,8 +2,8 @@ import java.util.Collection; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; import brainwine.gameserver.zone.Chunk; @MessageInfo(id = 3, compressed = true, prepacked = true) diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/ChatMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/ChatMessage.java index 471419a3..4e22f750 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/ChatMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/ChatMessage.java @@ -1,8 +1,8 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; -import brainwine.gameserver.entity.player.ChatType; +import brainwine.gameserver.player.ChatType; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 13, collection = true) public class ChatMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/ConfigurationMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/ConfigurationMessage.java index b22dd314..7605e85f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/ConfigurationMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/ConfigurationMessage.java @@ -2,8 +2,8 @@ import java.util.Map; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 2, json = true, compressed = true) public class ConfigurationMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/DialogMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/DialogMessage.java index fc91402c..edb38b14 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/DialogMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/DialogMessage.java @@ -2,9 +2,9 @@ import java.util.Map; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.dialog.Dialog; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 45, compressed = true) public class DialogMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/EffectMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/EffectMessage.java index ec3fc2c4..4848d8f6 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/EffectMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/EffectMessage.java @@ -1,8 +1,8 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.entity.Entity; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 30) public class EffectMessage extends Message { @@ -10,12 +10,12 @@ public class EffectMessage extends Message { public int x; public int y; public String name; - public int count; + public Object data; - public EffectMessage(float x, float y, String name, int count) { + public EffectMessage(float x, float y, String name, Object data) { this.x = (int)(x * Entity.POSITION_MODIFIER); this.y = (int)(y * Entity.POSITION_MODIFIER); this.name = name; - this.count = count; + this.data = data; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityChangeMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityChangeMessage.java index eeb49541..2ae472e7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityChangeMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityChangeMessage.java @@ -2,8 +2,8 @@ import java.util.Map; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 8, collection = true) public class EntityChangeMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityItemUseMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityItemUseMessage.java index aec81da2..cccb6f88 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityItemUseMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityItemUseMessage.java @@ -1,21 +1,29 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + import brainwine.gameserver.item.Item; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; +import brainwine.gameserver.server.models.EntityItemUseData; -@MessageInfo(id = 10, collection = true) +@MessageInfo(id = 10, prepacked = true) public class EntityItemUseMessage extends Message { - public int entityId; - public int type; - public Item item; - public int status; + public Collection data; + + public EntityItemUseMessage(Collection players) { + this.data = players.stream().map(EntityItemUseData::new).collect(Collectors.toList()); + } + + public EntityItemUseMessage(Player player) { + this.data = Arrays.asList(new EntityItemUseData(player)); + } - public EntityItemUseMessage(int entityId, int type, Item item, int status) { - this.entityId = entityId; - this.type = type; - this.item = item; - this.status = status; + public EntityItemUseMessage(int id, int type, Item item, int status) { + this.data = Arrays.asList(new EntityItemUseData(id, type, item, status)); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityPositionMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityPositionMessage.java index e9c6f438..cb62b1e0 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityPositionMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityPositionMessage.java @@ -4,10 +4,10 @@ import java.util.Collection; import java.util.stream.Collectors; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.FacingDirection; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; import brainwine.gameserver.server.models.EntityPositionData; @MessageInfo(id = 6, prepacked = true) diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityStatusMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityStatusMessage.java index accc0d7b..84556ad6 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityStatusMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityStatusMessage.java @@ -5,10 +5,10 @@ import java.util.Map; import java.util.stream.Collectors; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.EntityStatus; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; import brainwine.gameserver.server.models.EntityStatusData; @MessageInfo(id = 7, prepacked = true) @@ -28,6 +28,10 @@ public EntityStatusMessage(Entity entity, EntityStatus status) { this(Arrays.asList(new EntityStatusData(entity, status))); } + public EntityStatusMessage(Entity entity, EntityStatus status, Map details) { + this(Arrays.asList(new EntityStatusData(entity, status, details))); + } + public EntityStatusMessage(int id, int type, String name, EntityStatus status, Map details) { this(Arrays.asList(new EntityStatusData(id, type, name, status, details))); } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/EventMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/EventMessage.java index c35e1a01..d0abacba 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/EventMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/EventMessage.java @@ -1,7 +1,7 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 57) public class EventMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/FollowMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/FollowMessage.java new file mode 100644 index 00000000..b9688d21 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/FollowMessage.java @@ -0,0 +1,28 @@ +package brainwine.gameserver.server.messages; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +import brainwine.gameserver.player.Player; +import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; +import brainwine.gameserver.server.models.FollowData; + +@MessageInfo(id = 27, prepacked = true) +public class FollowMessage extends Message { + + public Collection followData; + + public FollowMessage(Collection followData) { + this.followData = followData; + } + + public FollowMessage(Player player, int direction, boolean following) { + this(Arrays.asList(new FollowData(player, direction, following))); + } + + public FollowMessage(Collection players, int direction) { + this(players.stream().map(player -> new FollowData(player, direction, true)).collect(Collectors.toList())); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/HealthMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/HealthMessage.java index 4cbe192a..49cebdac 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/HealthMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/HealthMessage.java @@ -1,7 +1,7 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 18) public class HealthMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/HeartbeatMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/HeartbeatMessage.java index c24c81ba..ae385761 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/HeartbeatMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/HeartbeatMessage.java @@ -1,7 +1,7 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 143) public class HeartbeatMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/InventoryMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/InventoryMessage.java index 638da1ef..2fa801cb 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/InventoryMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/InventoryMessage.java @@ -2,9 +2,9 @@ import java.util.Map; -import brainwine.gameserver.annotations.MessageInfo; -import brainwine.gameserver.entity.player.Inventory; +import brainwine.gameserver.player.Inventory; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 4) public class InventoryMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/KickMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/KickMessage.java index 95860011..faeb515e 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/KickMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/KickMessage.java @@ -1,7 +1,7 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 255) public class KickMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/LevelMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/LevelMessage.java index 4d19fe86..6ba336ba 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/LevelMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/LevelMessage.java @@ -1,7 +1,7 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 61) public class LevelMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/LightMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/LightMessage.java index 6b9d023b..0e477f92 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/LightMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/LightMessage.java @@ -1,7 +1,7 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 15, collection = true) public class LightMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/NotificationMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/NotificationMessage.java index ab514b67..46ca8ddb 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/NotificationMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/NotificationMessage.java @@ -1,8 +1,8 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; -import brainwine.gameserver.entity.player.NotificationType; +import brainwine.gameserver.player.NotificationType; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 33) public class NotificationMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/PlayerPositionMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/PlayerPositionMessage.java index 49a026f6..84defb19 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/PlayerPositionMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/PlayerPositionMessage.java @@ -1,7 +1,7 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 5) public class PlayerPositionMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/SkillMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/SkillMessage.java index a4c91fdd..e8a3ddb0 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/SkillMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/SkillMessage.java @@ -1,8 +1,8 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; -import brainwine.gameserver.entity.player.Skill; +import brainwine.gameserver.player.Skill; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 35, collection = true) public class SkillMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/StatMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/StatMessage.java index 8e82b9ce..2e10eebf 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/StatMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/StatMessage.java @@ -1,16 +1,17 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; +import brainwine.gameserver.server.models.PlayerStat; @MessageInfo(id = 44, collection = true) public class StatMessage extends Message { - public String key; + public PlayerStat stat; public Object value; - public StatMessage(String key, Object value) { - this.key = key; + public StatMessage(PlayerStat stat, Object value) { + this.stat = stat; this.value = value; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/TeleportMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/TeleportMessage.java index 3cc6793d..ff08df48 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/TeleportMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/TeleportMessage.java @@ -1,7 +1,7 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 50) public class TeleportMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/WardrobeMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/WardrobeMessage.java index 1398c35f..77b47034 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/WardrobeMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/WardrobeMessage.java @@ -3,9 +3,9 @@ import java.util.Arrays; import java.util.Collection; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.item.Item; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 39) public class WardrobeMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/XpMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/XpMessage.java index de7aa075..fbc80164 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/XpMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/XpMessage.java @@ -1,7 +1,7 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 60) public class XpMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneExploredMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneExploredMessage.java index 3672dc9a..e4fec542 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneExploredMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneExploredMessage.java @@ -1,7 +1,7 @@ package brainwine.gameserver.server.messages; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 53, collection = true) public class ZoneExploredMessage extends Message { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneSearchMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneSearchMessage.java index 2d7132f6..fe926287 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneSearchMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneSearchMessage.java @@ -1,13 +1,10 @@ package brainwine.gameserver.server.messages; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.List; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; -import brainwine.gameserver.zone.Zone; +import brainwine.gameserver.server.MessageInfo; +import brainwine.gameserver.server.models.ZoneSearchData; @MessageInfo(id = 23) public class ZoneSearchMessage extends Message { @@ -15,30 +12,12 @@ public class ZoneSearchMessage extends Message { public String type; public int typePosition = 0; public int totalTypes = 1; - public List> zones; + public Collection zones; public int followeesActive; - public ZoneSearchMessage(String type, Collection zones, int followeesActive) { + public ZoneSearchMessage(String type, Collection zones, int followeesActive) { this.type = type; - this.zones = new ArrayList<>(); - - for(Zone zone : zones) { - List info = new ArrayList<>(); - info.add(zone.getName()); // Should actually be the document ID, but we change zones based on name. - info.add(zone.getName()); - info.add(zone.getPlayers().size()); - info.add(0); // followees count - info.add(Collections.EMPTY_LIST); // followees - info.add(0); // active duration - info.add((int)(zone.getExplorationProgress() * 100)); - info.add(zone.getBiome()); - info.add("purified"); - info.add("a"); // accessibility - info.add(0); // protection level - info.add(null); // scenario - this.zones.add(info); - } - + this.zones = zones; this.followeesActive = followeesActive; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneStatusMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneStatusMessage.java index 635ba757..470bd5b6 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneStatusMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/ZoneStatusMessage.java @@ -1,16 +1,14 @@ package brainwine.gameserver.server.messages; -import java.util.Map; - -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; @MessageInfo(id = 17) public class ZoneStatusMessage extends Message { - public Map status; + public Object status; - public ZoneStatusMessage(Map status) { + public ZoneStatusMessage(Object status) { this.status = status; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/models/BlockChangeData.java b/gameserver/src/main/java/brainwine/gameserver/server/models/BlockChangeData.java index 9723e2bd..436ae4f0 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/models/BlockChangeData.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/models/BlockChangeData.java @@ -12,14 +12,15 @@ public class BlockChangeData { private final int x; private final int y; private final Layer layer; - private final int entityId = 0; + private final int entityId; private final Item item; private final int mod; - public BlockChangeData(int x, int y, Layer layer, Item item, int mod) { + public BlockChangeData(int x, int y, Layer layer, int entityId, Item item, int mod) { this.x = x; this.y = y; this.layer = layer; + this.entityId = entityId; this.item = item; this.mod = mod; } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/models/EntityItemUseData.java b/gameserver/src/main/java/brainwine/gameserver/server/models/EntityItemUseData.java new file mode 100644 index 00000000..294fabb7 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/server/models/EntityItemUseData.java @@ -0,0 +1,43 @@ +package brainwine.gameserver.server.models; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; + +import brainwine.gameserver.item.Item; +import brainwine.gameserver.player.Player; + +@JsonFormat(shape = Shape.ARRAY) +public class EntityItemUseData { + + private final int id; + private final int type; + private final Item item; + private final int status; + + public EntityItemUseData(Player player) { + this(player.getId(), 0, player.getHeldItem(), 0); + } + + public EntityItemUseData(int id, int type, Item item, int status) { + this.id = id; + this.type = type; + this.item = item; + this.status = status; + } + + public int getId() { + return id; + } + + public int getType() { + return type; + } + + public Item getItem() { + return item; + } + + public int getStatus() { + return status; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/server/models/EntityStatusData.java b/gameserver/src/main/java/brainwine/gameserver/server/models/EntityStatusData.java index e7539768..ccc9f341 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/models/EntityStatusData.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/models/EntityStatusData.java @@ -18,7 +18,11 @@ public class EntityStatusData { private final Map details; public EntityStatusData(Entity entity, EntityStatus status) { - this(entity.getId(), entity.getType(), entity.getName(), status, entity.getStatusConfig()); + this(entity, status, entity.getStatusConfig()); + } + + public EntityStatusData(Entity entity, EntityStatus status, Map details) { + this(entity.getId(), entity.getType(), entity.getName(), status, details); } public EntityStatusData(int id, int type, String name, EntityStatus status, Map details) { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/models/FollowData.java b/gameserver/src/main/java/brainwine/gameserver/server/models/FollowData.java new file mode 100644 index 00000000..752f44ce --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/server/models/FollowData.java @@ -0,0 +1,42 @@ +package brainwine.gameserver.server.models; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; + +import brainwine.gameserver.player.Player; + +@JsonFormat(shape = Shape.ARRAY) +public class FollowData { + + private final String playerName; + private final String playerId; + private final int direction; + private final boolean following; + + public FollowData(Player player, int direction, boolean following) { + this.playerName = player.getName(); + this.playerId = player.getDocumentId(); + this.direction = direction; + this.following = following; + } + + public FollowData(Player player, int direction) { + this(player, direction, true); + } + + public String getPlayerName() { + return playerName; + } + + public String getPlayerId() { + return playerId; + } + + public int getDirection() { + return direction; + } + + public boolean isFollowing() { + return following; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/server/models/PlayerStat.java b/gameserver/src/main/java/brainwine/gameserver/server/models/PlayerStat.java new file mode 100644 index 00000000..6b2805f5 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/server/models/PlayerStat.java @@ -0,0 +1,22 @@ +package brainwine.gameserver.server.models; + +import com.fasterxml.jackson.annotation.JsonValue; + +import brainwine.gameserver.server.messages.StatMessage; + +/** + * Easy enum for use with {@link StatMessage}. + */ +public enum PlayerStat { + + BREATH, + CROWNS, + FREEZE, + POINTS, + THIRST; + + @JsonValue + public String getClientId() { + return toString().toLowerCase(); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/server/models/ZoneSearchData.java b/gameserver/src/main/java/brainwine/gameserver/server/models/ZoneSearchData.java new file mode 100644 index 00000000..19bfba43 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/server/models/ZoneSearchData.java @@ -0,0 +1,40 @@ +package brainwine.gameserver.server.models; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; + +import brainwine.gameserver.player.Player; +import brainwine.gameserver.zone.Biome; +import brainwine.gameserver.zone.Zone; + +@JsonFormat(shape = Shape.ARRAY) +public class ZoneSearchData { + + public final String id; + public final String name; + public final int playerCount; + public final int followeeCount; + public final String[] followees; + public final int activeDuration; + public final int explorationProgress; + public final Biome biome; + public final String status; + public final String accessibility; // 'a' = all, 'p' = premium, 'i' = inaccessible + public final int protectionLevel; + public final String scenario; // Market, PvP, etc. + + public ZoneSearchData(Zone zone, Player player) { + this.id = zone.getDocumentId(); + this.name = zone.getName(); + this.playerCount = zone.getPlayerCount(); + this.followeeCount = 0; // TODO + this.followees = new String[0]; // TODO + this.activeDuration = 0; // TODO + this.explorationProgress = (int)(zone.getExplorationProgress() * 100); + this.biome = zone.getBiome(); + this.status = zone.getBiome() == Biome.PLAIN ? (zone.isPurified() ? "purified" : "toxic") : null; + this.accessibility = "a"; + this.protectionLevel = zone.isProtected(player) ? 10 : 0; + this.scenario = zone.isPvp() ? "PvP" : null; // TODO market scenario + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/server/pipeline/Connection.java b/gameserver/src/main/java/brainwine/gameserver/server/pipeline/Connection.java index fc657bd3..9db78094 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/pipeline/Connection.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/pipeline/Connection.java @@ -11,7 +11,7 @@ import org.apache.logging.log4j.Logger; import brainwine.gameserver.GameServer; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.Message; import brainwine.gameserver.server.Request; import brainwine.gameserver.server.messages.KickMessage; @@ -80,7 +80,9 @@ public ChannelFuture sendMessage(Message message) { public void sendDelayedMessage(Message message, int delay) { channel.eventLoop().schedule(() -> { - sendMessage(message); + if(channel.isOpen()) { + sendMessage(message); + } }, delay, TimeUnit.MILLISECONDS); } @@ -99,6 +101,10 @@ public void setPlayer(Player player) { this.player = player; } + public Player getPlayer() { + return player; + } + public boolean isV3() { return player != null && player.isV3(); } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/pipeline/MessageEncoder.java b/gameserver/src/main/java/brainwine/gameserver/server/pipeline/MessageEncoder.java index 0cd9ec9d..bda7accc 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/pipeline/MessageEncoder.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/pipeline/MessageEncoder.java @@ -7,8 +7,8 @@ import com.fasterxml.jackson.databind.ObjectWriter; -import brainwine.gameserver.annotations.MessageInfo; import brainwine.gameserver.server.Message; +import brainwine.gameserver.server.MessageInfo; import brainwine.gameserver.server.NetworkRegistry; import brainwine.gameserver.util.ZipUtils; import brainwine.shared.JsonHelper; diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/AdminRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/AdminRequest.java index 157aeb6a..9c822b34 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/AdminRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/AdminRequest.java @@ -1,13 +1,20 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; +import java.util.Collection; +import java.util.stream.Collectors; + +import brainwine.gameserver.command.CommandManager; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.server.OptionalField; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; @RequestInfo(id = 254) public class AdminRequest extends PlayerRequest { public String key; + + @OptionalField public Object data; @Override @@ -17,8 +24,24 @@ public void process(Player player) { } switch(key) { - case "god": player.setGodMode(data == null || data.equals(1)); - default: break; + case "god": + player.setGodMode(data == null || data.equals(1)); + break; + case "admin": + // This is a client-sided fuck-up + if(player.isV3()) { + key = "grow"; + } + default: + // Delegate request to the command manager + if(data == null) { + CommandManager.executeCommand(player, String.format("/%s", key)); + } else { + String parameters = data instanceof Collection ? String.join(" ", + ((Collection)data).stream().map(String::valueOf).collect(Collectors.toList())) : String.valueOf(data); + CommandManager.executeCommand(player, String.format("/%s %s", key, parameters)); + } + break; } } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/AuthenticateRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/AuthenticateRequest.java index 6278ffeb..f391b0a6 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/AuthenticateRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/AuthenticateRequest.java @@ -1,14 +1,17 @@ package brainwine.gameserver.server.requests; import java.time.format.DateTimeFormatter; +import java.util.Locale; import brainwine.gameserver.GameServer; -import brainwine.gameserver.annotations.OptionalField; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; -import brainwine.gameserver.entity.player.PlayerRestriction; -import brainwine.gameserver.entity.player.PlayerManager; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.player.PlayerManager; +import brainwine.gameserver.player.PlayerRestriction; +import brainwine.gameserver.server.OptionalField; import brainwine.gameserver.server.Request; +import brainwine.gameserver.server.RequestInfo; +import brainwine.gameserver.server.messages.NotificationMessage; import brainwine.gameserver.server.pipeline.Connection; import brainwine.gameserver.zone.Zone; @@ -44,29 +47,34 @@ public void process(Connection connection) { PlayerRestriction ban = player.getCurrentBan(); Zone zone = player.getZone(); - // Deny access if the player is currently banned if(ban != null) { - connection.kick(String.format("You are banned from the server until %s for: %s", - ban.getEndDate().format(DateTimeFormatter.RFC_1123_DATE_TIME), ban.getReason())); - return; - } - - // Try to put player in a random zone if current zone is null - // TODO default zone - if(zone == null) { - zone = server.getZoneManager().getRandomZone(); - } - - // Kick player if zone is still null (aka it failed to find a suitable random zone) - if(zone == null) { - connection.kick("No default zone could be found."); - return; + // Send player to jail world if they're banned + zone = server.getZoneManager().getZoneByName("Hell"); + String banMessage = String.format("You are banned from the server until\n%s for: %s", + ban.getEndDate().format(DateTimeFormatter.ofPattern("d MMMM uuuu HH:mm:ss", Locale.ENGLISH)), ban.getReason()); + + // Kick player with ban message if no jail world exists + if(zone == null) { + connection.kick(banMessage); + return; + } + + connection.sendDelayedMessage(new NotificationMessage(banMessage, NotificationType.MAINTENANCE), 5000); // Slightly hacky but shouldn't cause any issues + } else if(zone == null || !zone.canJoin(player)) { + // Try to put player in a random zone if current zone is null or cannot be joined + zone = server.getZoneManager().findBeginnerZone(); + + // Kick player if zone is still null (aka it failed to find a suitable random zone) + if(zone == null) { + connection.kick("Sorry, we couldn't find a world for you to join."); + return; + } } player.setConnection(connection); player.setClientVersion(version); - playerManager.onPlayerConnect(player); zone.addEntity(player); + playerManager.onPlayerConnect(player); }); }); } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java index 777f5d6c..2bf9f5d5 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockMineRequest.java @@ -1,13 +1,8 @@ package brainwine.gameserver.server.requests; -import java.util.Collections; -import java.util.List; import java.util.Map; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.NotificationType; -import brainwine.gameserver.entity.player.Player; -import brainwine.gameserver.entity.player.Skill; +import brainwine.gameserver.entity.Entity; import brainwine.gameserver.item.Action; import brainwine.gameserver.item.Fieldability; import brainwine.gameserver.item.Item; @@ -15,7 +10,11 @@ import brainwine.gameserver.item.Layer; import brainwine.gameserver.item.MiningBonus; import brainwine.gameserver.item.ModType; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.player.Skill; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; import brainwine.gameserver.server.messages.BlockChangeMessage; import brainwine.gameserver.server.messages.InventoryMessage; import brainwine.gameserver.util.MapHelper; @@ -61,8 +60,10 @@ public void process(Player player) { return; } - // TODO block ownership & 'placed' fieldability - if(!player.isGodMode() && !digging && item.getFieldability() == Fieldability.TRUE && zone.isBlockProtected(x, y, player)) { + Fieldability fieldability = item.getFieldability(); + boolean fieldable = fieldability == Fieldability.TRUE || (fieldability == Fieldability.PLACED && !block.isNatural()); + + if(!player.isGodMode() && !digging && fieldable && zone.isBlockProtected(x, y, player)) { fail(player, "This block is protected."); return; } @@ -86,29 +87,54 @@ public void process(Player player) { } } + // Check custom mine + if(item.hasCustomMine()) { + switch(item.getId()) { + case "mechanical/zone-teleporter": + if(!player.isGodMode() && zone.getMetaBlocksWithItem("mechanical/zone-teleporter").size() < 2) { + fail(player, "You must keep at least one world teleporter active."); + return; + } + + break; + default: break; + } + } + if(digging) { zone.digBlock(x, y); return; } + // Apply decay if block is being mined with a hatchet + if(item.getMod() == ModType.DECAY && player.getHeldItem().getAction() == Action.SMASH) { + int nextMod = Math.min(4, block.getMod(layer) + 1); + zone.updateBlock(x, y, layer, item, nextMod); + + // Send inventory message for v3 players + if(player.isV3()) { + player.sendDelayedMessage(new InventoryMessage(player.getInventory().getClientConfig(item))); + + Item decayItem = item.getDecayInventoryItem(); + + if(!decayItem.isAir()) { + player.sendDelayedMessage(new InventoryMessage(player.getInventory().getClientConfig(decayItem))); + } + } + + return; + } + if(metaBlock != null) { Map metadata = metaBlock.getMetadata(); // Check if block is a natural switch with an active door if(!player.isGodMode() && !metaBlock.hasOwner() && item.hasUse(ItemUseType.SWITCH)) { - List> positions = MapHelper.getList(metadata, ">", Collections.emptyList()); - - for(List position : positions) { - Block target = zone.getBlock(position.get(0), position.get(1)); - - if(target != null) { - Item switchedItem = target.getFrontItem(); - - if(switchedItem.hasUse(ItemUseType.SWITCHED)) { - fail(player, String.format("This switch cannot be mined before its %s.", switchedItem.getTitle().toLowerCase())); - return; - } - } + Item switchedItem = zone.getSwitchedItem(metaBlock); + + if(!switchedItem.isAir()) { + fail(player, String.format("This switch cannot be mined before its %s.", switchedItem.getTitle().toLowerCase())); + return; } } @@ -125,19 +151,63 @@ public void process(Player player) { } } - zone.updateBlock(x, y, layer, 0, 0, player); - player.getStatistics().trackItemMined(item); - Item inventoryItem = item.getMod() == ModType.DECAY && block.getMod(layer) > 0 ? item.getDecayInventoryItem() : item.getInventoryItem(); + if(item.shouldProcessTimerOnBreak()) { + zone.processBlockTimer(x, y); + } + + // Pretty much only used for spawners + if(item.hasUse(ItemUseType.DESTROY)) { + Object config = item.getUse(ItemUseType.DESTROY); + + if(config instanceof String) { + String type = (String)config; + + switch(type.toLowerCase()) { + case "spawner": destroySpawner(zone, metaBlock); break; + default: break; + } + } + } + + // Check for entity spawns + if(item.hasEntitySpawns() && block.getMod(layer) == 0 && !item.hasTimer() && !item.hasUse(ItemUseType.SPAWN)) { + zone.spawnEntity(item.getEntitySpawns().next(), x, y); + } + + // Determine inventory item + Item inventoryItem; + + if(item.getMod() == ModType.DECAY && block.getMod(layer) > 0) { + inventoryItem = item.getDecayInventoryItem(); + } else if(item.hasModInventoryItem()) { + inventoryItem = item.getModInventoryItem(block.getMod(layer)); + } else { + inventoryItem = item.getInventoryItem(); + } + int quantity = 1; + player.getStatistics().trackItemMined(item); + + if(block.isNatural()) { + player.getStatistics().trackItemScavenged(item); + } + + // Check stack mod + if(item.getMod() == ModType.STACK) { + quantity = Math.max(1, block.getMod(layer)); + } + + zone.updateBlock(x, y, layer, 0, 0, player); // Apply mining bonus if there is one if(item.hasMiningBonus()) { MiningBonus bonus = item.getMiningBonus(); + if(Math.random() < player.getMiningBonusChance(bonus)) { if(!bonus.getItem().isAir()) { inventoryItem = bonus.getItem(); } - + if(bonus.isDoubleLoot()) { quantity *= 2; } @@ -151,6 +221,21 @@ public void process(Player player) { } } + private void destroySpawner(Zone zone, MetaBlock metaBlock) { + // Do nothing if spawner doesn't have an entity + if(!metaBlock.hasProperty("eid")) { + return; + } + + Entity entity = zone.getEntity(metaBlock.getIntProperty("eid")); + + // Kill entity if it exists + if(entity != null && !entity.isDead()) { + entity.spawnEffect("bomb-teleport", 4); + entity.setHealth(0); + } + } + private void fail(Player player, String reason) { player.notify(reason); Block block = player.getZone().getBlock(x, y); diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java index d74fbca4..96e542d2 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockPlaceRequest.java @@ -1,17 +1,25 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; -import brainwine.gameserver.entity.player.Skill; +import java.util.UUID; + +import brainwine.gameserver.entity.EntityConfig; +import brainwine.gameserver.entity.npc.Npc; +import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemGroup; +import brainwine.gameserver.item.ItemUseType; import brainwine.gameserver.item.Layer; import brainwine.gameserver.item.ModType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.player.Skill; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; import brainwine.gameserver.server.messages.BlockChangeMessage; import brainwine.gameserver.server.messages.InventoryMessage; import brainwine.gameserver.util.MathUtils; import brainwine.gameserver.util.Pair; import brainwine.gameserver.zone.Block; +import brainwine.gameserver.zone.MetaBlock; import brainwine.gameserver.zone.Zone; @RequestInfo(id = 12) @@ -56,7 +64,29 @@ public void process(Player player) { return; } - if(!player.isGodMode() && zone.isBlockProtected(x, y, player)) { + if(!player.isGodMode() && item.requiresOwnership() && !zone.isOwner(player)) { + fail(player, "You can only place this in worlds you own."); + return; + } + + if(!player.isGodMode() && item.requiresMembership() && !zone.isOwner(player) && !zone.isMember(player)) { + fail(player, "You can only place these in owned or member worlds."); + return; + } + + if(!player.isGodMode() && item.hasSpawnSpacing() && zone.isSpawnInRange(x, y, item.getSpawnSpacing())) { + fail(player, String.format("%s must be at least %s blocks away from spawns.", item.getTitle(), item.getSpawnSpacing())); + return; + } + + if(!player.isGodMode() && item.hasSpacing() && zone.getMetaBlocks().stream().anyMatch(block + -> (item.hasSpacingItems() ? item.getSpacingItems().contains(block.getItem()) : block.getItem() == item) + && MathUtils.inRange(block.getX(), block.getY(), x, y, item.getSpacing()))) { + fail(player, String.format("%s must be at least %s blocks away from other %ss.", item.getTitle(), item.getSpacing(), item.getTitle().toLowerCase())); + return; + } + + if(!player.isGodMode() && !item.canPlaceInField() && zone.isBlockProtected(x, y, player)) { fail(player, "This block is protected."); return; } @@ -83,16 +113,7 @@ public void process(Player player) { if(layer == Layer.LIQUID) { mod = 5; } else if(item.getMod() == ModType.ROTATION && !item.isMirrorable()) { - // Automatically orient rotatable blocks based on adjacent block - if(zone.isChunkLoaded(x, y + 1) && zone.getBlock(x, y + 1).getFrontItem().isWhole()) { - mod = 0; - } else if(zone.isChunkLoaded(x, y - 1) && zone.getBlock(x, y - 1).getFrontItem().isWhole()) { - mod = 2; - } else if(zone.isChunkLoaded(x - 1, y) && zone.getBlock(x - 1, y).getFrontItem().isWhole()) { - mod = 1; - } else if(zone.isChunkLoaded(x + 1, y) && zone.getBlock(x + 1, y).getFrontItem().isWhole()) { - mod = 3; - } + mod = findRotationMod(zone, x, y, item.getBlockWidth(), item.getBlockHeight()); } zone.updateBlock(x, y, layer, item, mod, player); @@ -100,18 +121,215 @@ public void process(Player player) { player.getStatistics().trackItemPlaced(); player.trackPlacement(x, y, item); + // Create block timer if applicable + if(item.hasTimer()) { + createBlockTimer(zone, player); + } + // Process custom place if applicable if(item.hasCustomPlace()) { - processCustomPlace(player); + processCustomPlace(zone, player); + } + + // Misc processing + if(item.getGroup() == ItemGroup.GRAVESTONE) { + processBurial(zone, player); + } else if(item.getGroup() == ItemGroup.CAGE) { + processTrapping(zone, player); } } - private void processCustomPlace(Player player) { - Zone zone = player.getZone(); + /** + * Automatically finds a suitable rotation mod based on adjacent blocks. + * Priority order is bottom -> top -> left -> right. + */ + private int findRotationMod(Zone zone, int x, int y, int width, int height) { + boolean bottom = true; + boolean top = true; + boolean left = true; + boolean right = true; + + // Check top and bottom + for(int i = 0; i < width; i++) { + bottom &= zone.isChunkLoaded(x + i, y + 1) && zone.getBlock(x + i, y + 1).getFrontItem().isWhole(); + top &= zone.isChunkLoaded(x + i, y - height) && zone.getBlock(x + i, y - height).getFrontItem().isWhole(); + } + + // Check left and right + for(int i = 0; i < height; i++) { + left &= zone.isChunkLoaded(x - 1, y - i) && zone.getBlock(x - 1, y - i).getFrontItem().isWhole(); + right &= zone.isChunkLoaded(x + width, y - i) && zone.getBlock(x + width, y - i).getFrontItem().isWhole(); + } + + return bottom ? 0 : top ? 2 : left ? 1 : right ? 3 : 0; + } + + private void processTrapping(Zone zone, Player player) { + // Check bounds + if(x <= 0 || x + 1 >= zone.getWidth() || y <= 0 || y + 1 >= zone.getHeight()) { + return; + } + + // Do nothing if cage is not surrounded by whole blocks + if(!zone.isBlockWhole(x - 1, y - 1) || !zone.isBlockWhole(x, y - 1) || !zone.isBlockWhole(x + 1, y - 1) || !zone.isBlockWhole(x - 1, y) || !zone.isBlockWhole(x + 1, y) + || !zone.isBlockWhole(x - 1, y + 1) || !zone.isBlockWhole(x, y + 1) || !zone.isBlockWhole(x + 1, y + 1)) { + return; + } + + // Find random trappable entity at this location + // TODO we have to do an isDead() check here because dead NPCs aren't always cleared immediately + Npc entity = zone.getNpcs().stream() + .filter(npc -> !npc.isDead() && !npc.isArtificial() && npc.getBlockX() == x && npc.getBlockY() == y && npc.getConfig().isTrappable()) + .findFirst().orElse(null); + + // Do nothing if no eligible entity was found + if(entity == null) { + return; + } + + EntityConfig config = entity.getConfig(); + + // Try to turn entity into a pet cage + if(item.hasUse(ItemUseType.PET)) { + // Don't waste it if entity has no pet variant + if(!config.hasTrappablePetItem()) { + return; + } + + entity.setHealth(0.0F); + zone.updateBlock(x, y, layer, 0); + player.getInventory().addItem(config.getTrappablePetItem(), true); + player.getStatistics().trackTrapping(config); + return; + } + + // Otherwise, kill the entity and place some fur + // TODO v2 stores the quantity in the mod of "piled" items, but this functionality is not implemented here at all! + entity.attack(player, item, entity.getHealth(), DamageType.ACID, true); + zone.updateBlock(x, y, Layer.FRONT, "ground/fur"); + player.getStatistics().trackTrapping(config); + } + + private void processBurial(Zone zone, Player player) { + // Check bounds + if(x <= 0 || x + 2 >= zone.getWidth() || y + 2 >= zone.getHeight()) { + return; + } + + // Do nothing if there is no skeleton underneath the gravestone + if(!zone.getBlock(x, y + 1).getFrontItem().hasId("rubble/skeleton")) { + return; + } + + // Do nothing if the skeleton is obstructed + if(zone.isBlockOccupied(x + 1, y + 1, Layer.FRONT)) { + return; + } + + // Do nothing if the skeleton isn't underground + if(!zone.isUnderground(x, y + 1) || !zone.isUnderground(x + 1, y + 1)) { + return; + } + + // Do nothing if the gravestone isn't above ground + if(zone.isUnderground(x, y) || zone.isUnderground(x + 1, y)) { + return; + } + + // Do nothing if the skeleton isn't surrounded by earth + if(!zone.isBlockEarthy(x - 1, y + 1) || !zone.isBlockEarthy(x + 2, y + 1) || !zone.isBlockEarthy(x, y + 2) || !zone.isBlockEarthy(x + 1, y + 2)) { + return; + } + + // Everything checks out -- fill the grave! + zone.updateBlock(x, y + 1, Layer.FRONT, "ground/earth"); + zone.updateBlock(x + 1, y + 1, Layer.FRONT, "ground/earth"); + zone.spawnEffect(x + 1.0F, y + 0.5F, "expiate", 20); + zone.spawnEffect(x + 1.0F, y + 0.5F, "sparkle up", 20); + + // ~33% chance to spawn a ghost + if(Math.random() < 0.334) { + zone.spawnEntity("ghost", x + 1, y); + } + + player.getStatistics().trackUndertaking(); + } + + private void createBlockTimer(Zone zone, Player player) { + String type = item.getTimerType(); + int value = item.getTimerValue(); + Runnable task = null; + + switch(type) { + case "front mod": + task = () -> zone.updateBlock(x, y, layer, item, value); + break; + case "bomb": + task = () -> zone.explode(x, y, value, player, true, value, DamageType.FIRE, value >= 6 ? "bomb-large" : "bomb"); + break; + case "bomb-fire": + task = () -> zone.explode(x, y, value, player, false, value, DamageType.FIRE, "bomb-fire"); + break; + case "bomb-electric": + task = () -> zone.explode(x, y, value, player, false, value, DamageType.ENERGY, "bomb-electric"); + break; + case "bomb-frost": + task = () -> zone.explode(x, y, value, player, false, value, DamageType.COLD, "bomb-frost"); + break; + case "bomb-dig": + task = () -> { + zone.explode(x, y, value, player, "bomb-fire"); + int distance = value * 10; + + // Dig until we reach the maximum distance or hit a solid block + for(int i = 1; i <= distance; i++) { + if(!zone.digBlock(x, y + i)) { + break; + } + } + }; + break; + case "bomb-spawner": + task = () -> { + zone.explode(x, y, value, player, false, value, DamageType.FIRE, "bomb-fire"); + + // Spawn a bunch of entities + for(int i = 0; i < value; i++) { + zone.spawnEntity(item.getEntitySpawns().next(), x, y); + } + }; + break; + case "bomb-water": + task = () -> { + zone.explode(x, y, value, player, false, value, DamageType.COLD, "bomb-large"); + zone.explodeLiquid(x, y, 4, "liquid/water"); + }; + break; + case "bomb-acid": + task = () -> { + zone.explode(x, y, value, player, false, value, DamageType.ACID, "bomb-large"); + zone.explodeLiquid(x, y, 4, "liquid/acid"); + }; + break; + case "bomb-lava": + task = () -> { + zone.explode(x, y, value, player, false, value, DamageType.FIRE, "bomb-large"); + zone.explodeLiquid(x, y, 4, "liquid/magma"); + }; + break; + default: + break; + } + if(task != null) { + zone.addBlockTimer(x, y, item.getTimerDelay() * 1000, task); + } + } + + private void processCustomPlace(Zone zone, Player player) { switch(item.getId()) { - // See if we can plug a maw or pipe case "building/plug": + // See if we can plug a maw or pipe Item baseItem = zone.getBlock(x, y).getBaseItem(); String plugged = baseItem.hasId("base/maw") ? "base/maw-plugged" : baseItem.hasId("base/pipe") ? "base/pipe-plugged" : null; @@ -122,6 +340,16 @@ private void processCustomPlace(Player player) { player.getStatistics().trackMawPlugged(); } + break; + case "containers/chest-plenty": + case "containers/sack-plenty": + // Create additional metadata for chests o' plenty + MetaBlock metaBlock = zone.getMetaBlock(x, y); + + if(metaBlock != null) { + metaBlock.setProperty("y", UUID.randomUUID().toString()); // Generate random loot code + metaBlock.setProperty("$", "?"); + } break; // No valid item; do nothing default: break; diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockUseRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockUseRequest.java index 6f4a00cc..73318b40 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockUseRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlockUseRequest.java @@ -1,27 +1,19 @@ package brainwine.gameserver.server.requests; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import brainwine.gameserver.GameServer; -import brainwine.gameserver.annotations.OptionalField; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.NotificationType; -import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemUseType; import brainwine.gameserver.item.Layer; -import brainwine.gameserver.loot.Loot; +import brainwine.gameserver.item.interactions.ItemInteraction; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.server.OptionalField; import brainwine.gameserver.server.PlayerRequest; -import brainwine.gameserver.util.MapHelper; +import brainwine.gameserver.server.RequestInfo; import brainwine.gameserver.zone.Block; import brainwine.gameserver.zone.MetaBlock; import brainwine.gameserver.zone.Zone; -@SuppressWarnings("unchecked") @RequestInfo(id = 21) public class BlockUseRequest extends PlayerRequest { @@ -36,10 +28,17 @@ public class BlockUseRequest extends PlayerRequest { public void process(Player player) { Zone zone = player.getZone(); + // Do nothing if player is dead or if the target chunk is not active if(player.isDead() || !player.isChunkActive(x, y)) { return; } + // Do nothing if player is too far away + if(!player.isGodMode() && !player.inRange(x, y, player.getMiningRange())) { + return; + } + + // Transform usage data if necessary if(data != null && data.length == 1 && data[0] instanceof Map) { data = ((Map)data[0]).values().toArray(); } @@ -49,161 +48,43 @@ public void process(Player player) { Item item = block.getItem(layer); int mod = block.getMod(layer); + // Check if block is owned by another player if(metaBlock != null && item.hasUse(ItemUseType.PROTECTED)) { - Player owner = GameServer.getInstance().getPlayerManager().getPlayerById(metaBlock.getOwner()); + Player owner = metaBlock.getOwner(); if(player != owner) { if(item.hasUse(ItemUseType.PUBLIC)) { String publicUse = item.getUse(ItemUseType.PUBLIC).toString(); + // TODO implement other cases switch(publicUse) { - case "owner": - player.notify(String.format("This %s is owned by %s.", - item.getTitle().toLowerCase(), owner == null ? "nobody.." : owner.getName())); - break; + case "owner": + player.notify(String.format("This %s is owned by %s.", + item.getTitle().toLowerCase(), owner == null ? "somebody else" : owner.getName())); + break; + case "note": + ItemUseType.NOTE.getInteraction().interact(zone, player, x, y, layer, item, mod, metaBlock, null, data); + break; + case "landmark": + ItemUseType.LANDMARK.getInteraction().interact(zone, player, x, y, layer, item, mod, metaBlock, null, data); + break; + default: break; } } else { player.notify("Sorry, that belongs to somebody else."); - return; } + + return; } } - for(Entry entry : item.getUses().entrySet()) { - ItemUseType use = entry.getKey(); - Object value = entry.getValue(); + // Try to interact with the block + item.getUses().forEach((use, config) -> { + ItemInteraction interaction = use.getInteraction(); - switch(use) { - case DIALOG: - case CREATE_DIALOG: - if(data != null && value instanceof Map) { - Map config = (Map)value; - String target = MapHelper.getString(config, "target", "none"); - - switch(target) { - case "meta": - Map metadata = new HashMap<>(); - List> sections = MapHelper.getList(config, "sections"); - - if(sections != null && data.length == sections.size()) { - for(int i = 0; i < sections.size(); i++) { - Map section = sections.get(i); - String key = MapHelper.getString(section, "input.key"); - - if(key != null) { - String text = String.valueOf(data[i]); - - // Get rid of text if player is currently muted - if(player.isMuted() && MapHelper.getBoolean(section, "input.sanitize")) { - text = text.replaceAll(".", "*"); - } - - metadata.put(key, text); - } else if(MapHelper.getBoolean(section, "input.mod")) { - List options = MapHelper.getList(section, "input.options"); - - if(options != null) { - mod = options.indexOf(data[i]); - mod = mod == -1 ? 0 : mod; - mod *= MapHelper.getInt(section, "input.mod_multiple", 1); - zone.updateBlock(x, y, layer, item, mod, player); - } - } - } - } - - // TODO find out what this is for - if(use == ItemUseType.CREATE_DIALOG) { - metadata.put("cd", true); - } - - zone.setMetaBlock(x, y, item, player, metadata); - break; - } - } - break; - case CHANGE: - zone.updateBlock(x, y, layer, item, mod == 0 ? 1 : 0, player); - break; - case CONTAINER: - if(metaBlock != null) { - Map metadata = metaBlock.getMetadata(); - String specialItem = MapHelper.getString(metadata, "$"); - - if(specialItem != null) { - String dungeonId = MapHelper.getString(metadata, "@"); - - if(dungeonId != null && item.hasUse(ItemUseType.FIELDABLE) && zone.isDungeonIntact(dungeonId)) { - player.notify("This container is secured by protectors in the area."); - break; - } - - if(specialItem.equals("?")) { - Loot loot = GameServer.getInstance().getLootManager().getRandomLoot(player, item.getLootCategories()); - - if(loot == null) { - player.notify("No eligible loot could be found for this container."); - } else { - metadata.remove("$"); - player.awardLoot(loot, item.getLootGraphic()); - player.getStatistics().trackContainerLooted(item); - } - } else { - player.notify("Sorry, this container can't be looted right now."); - } - - if(mod != 0) { - zone.updateBlock(x, y, Layer.FRONT, item, 0); - } - } - } - break; - case TELEPORT: - if(data != null && mod == 1 && data.length == 2 && data[0] instanceof Integer && data[1] instanceof Integer) { - int tX = (int)data[0]; - int tY = (int)data[1]; - MetaBlock target = zone.getMetaBlock(tX, tY); - - if(target != null && target.getItem().hasUse(ItemUseType.TELEPORT, ItemUseType.ZONE_TELEPORT)) { - player.teleport(tX + 1, tY); - } - } else if(mod == 0) { - zone.updateBlock(x, y, layer, item, 1); - player.getStatistics().trackDiscovery(item); - player.notify("You repaired a teleporter!", NotificationType.ACCOMPLISHMENT); - player.notifyPeers(String.format("%s repaired a teleporter.", player.getName()), NotificationType.SYSTEM); - } - break; - case SWITCH: - if(data == null) { - if(metaBlock != null) { - // TODO timed switches - Map metadata = metaBlock.getMetadata(); - List> positions = MapHelper.getList(metadata, ">", Collections.emptyList()); - zone.updateBlock(x, y, layer, item, mod % 2 == 0 ? mod + 1 : mod - 1, player, metadata); - - for(List position : positions) { - int sX = position.get(0); - int sY = position.get(1); - Block target = zone.getBlock(sX, sY); - - if(target != null) { - Item switchedItem = target.getFrontItem(); - - if(switchedItem.hasUse(ItemUseType.SWITCHED)) { - if(!(item.getUse(ItemUseType.SWITCHED) instanceof String)) { - int switchedMod = target.getFrontMod(); - zone.updateBlock(sX, sY, Layer.FRONT, switchedItem, switchedMod % 2 == 0 ? switchedMod + 1 : switchedMod - 1, null); - } - } - } - } - } - } - break; - default: - break; + if(interaction != null) { + interaction.interact(zone, player, x, y, layer, item, mod, metaBlock, config, data); } - } + }); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlocksIgnoreRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlocksIgnoreRequest.java index 78c299ed..6cb39494 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlocksIgnoreRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlocksIgnoreRequest.java @@ -1,8 +1,8 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; @RequestInfo(id = 25) public class BlocksIgnoreRequest extends PlayerRequest { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlocksRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlocksRequest.java index 706a8bb8..9f20d7a1 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/BlocksRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BlocksRequest.java @@ -3,9 +3,9 @@ import java.util.ArrayList; import java.util.List; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; import brainwine.gameserver.server.messages.BlockMetaMessage; import brainwine.gameserver.server.messages.BlocksMessage; import brainwine.gameserver.server.messages.LightMessage; @@ -47,6 +47,13 @@ public void process(Player player) { } Chunk chunk = zone.getChunk(index); + + // Kick player if chunk is null (load failure) + if(chunk == null) { + player.kick("Chunk load failure."); + return; + } + chunks.add(chunk); metaBlocks.addAll(zone.getLocalMetaBlocksInChunk(index)); player.addActiveChunk(index); diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/BookmarkRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/BookmarkRequest.java new file mode 100644 index 00000000..5b74bdf9 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/BookmarkRequest.java @@ -0,0 +1,40 @@ +package brainwine.gameserver.server.requests; + +import brainwine.gameserver.GameServer; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; + +@RequestInfo(id = 62) +public class BookmarkRequest extends PlayerRequest { + + public String type; // Looks like multiple bookmark types were planned, but only "zone" ended up being added. + public String id; + public boolean active; + + @Override + public void process(Player player) { + // Check type + if(!type.equals("zone")) { + return; + } + + // Check if zone exists + if(GameServer.getInstance().getZoneManager().getZone(id) == null) { + return; + } + + // Add or remove bookmark + if(active) { + if(player.getBookmarkedZoneCount() > Player.BOOKMARKED_ZONE_LIMIT) { + player.notify("Error: Bookmark limit reached, please delete some bookmarks."); + return; + } + + player.addZoneBookmark(id); + } else { + player.removeZoneBookmark(id); + } + } + +} diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/ChangeAppearanceRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/ChangeAppearanceRequest.java index beb7de26..22885ed2 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/ChangeAppearanceRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/ChangeAppearanceRequest.java @@ -3,31 +3,30 @@ import java.util.Map; import java.util.Map.Entry; -import brainwine.gameserver.annotations.RequestInfo; import brainwine.gameserver.dialog.DialogHelper; -import brainwine.gameserver.entity.player.ClothingSlot; -import brainwine.gameserver.entity.player.ColorSlot; -import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemRegistry; +import brainwine.gameserver.player.Appearance; +import brainwine.gameserver.player.AppearanceSlot; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; +import brainwine.gameserver.server.messages.EntityChangeMessage; +import brainwine.gameserver.util.MapHelper; -/** - * TODO we should actually check if the sent value is even compatible with the slot. - * We wouldn't want to allow players to equip pants for hats! - */ @RequestInfo(id = 22) public class ChangeAppearanceRequest extends PlayerRequest { - public Map data; + public Map appearance; @Override public void process(Player player) { - if(data.containsKey("meta")) { - String meta = "" + data.get("meta"); + // Handle special cases + if(appearance.containsKey("meta")) { + String meta = MapHelper.getString(appearance, "meta", ""); if(meta.equals("randomize")) { - player.notify("Sorry, you can't randomize your appearance yet."); + player.randomizeAppearance(); } else { player.showDialog(DialogHelper.getWardrobeDialog(meta)); } @@ -35,36 +34,54 @@ public void process(Player player) { return; } - for(Entry entry : data.entrySet()) { - String key = entry.getKey(); + // Validate appearance data + for(Entry entry : appearance.entrySet()) { + AppearanceSlot slot = AppearanceSlot.fromId(entry.getKey()); Object value = entry.getValue(); - if(value instanceof Integer) { - ClothingSlot slot = ClothingSlot.fromId(key); - - if(slot == null) { - continue; - } - - Item item = ItemRegistry.getItem((int)value); - - if(!item.isBase() && !player.getInventory().hasItem(item)) { - player.notify("Sorry, but you do not own this."); + // Fail if slot is not valid + if(slot == null || !slot.isChangeable()) { + fail(player); + return; + } + + // Handle color data + if(slot.isColor()) { + // Fail if color value is not a string + if(!(value instanceof String)) { + fail(player); return; } - player.setClothing(slot, item); - } else if(value instanceof String) { - // TODO check if player owns color - ColorSlot slot = ColorSlot.fromId(key); - String color = (String)value; - - if(slot == null) { - continue; + // Fail if player doesn't own color + if(!Appearance.getAvailableColors(slot, player).contains((String)value)) { + fail(player); + return; } - player.setColor(slot, color); + continue; + } + + // Fail if item value is not an integer (item code) + if(!(value instanceof Integer)) { + fail(player); + return; + } + + Item item = ItemRegistry.getItem((int)value); + + // Do nothing if item isn't valid clothing or player doesn't own it + if(!item.isClothing() || !slot.getCategory().equals(item.getCategory()) || (!item.isBase() && !player.getInventory().hasItem(item))) { + fail(player); + return; } } + + // Update player appearance + player.updateAppearance(appearance); + } + + private void fail(Player player) { + player.sendMessage(new EntityChangeMessage(player.getId(), player.getAppearance())); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/ChatRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/ChatRequest.java index 267244b3..d03e04bf 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/ChatRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/ChatRequest.java @@ -1,11 +1,11 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.OptionalField; -import brainwine.gameserver.annotations.RequestInfo; import brainwine.gameserver.command.CommandManager; -import brainwine.gameserver.entity.player.NotificationType; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.server.OptionalField; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; @RequestInfo(id = 13) public class ChatRequest extends PlayerRequest { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/ConsoleRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/ConsoleRequest.java index 4ff46b9a..b1bd6a9b 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/ConsoleRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/ConsoleRequest.java @@ -1,9 +1,9 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.RequestInfo; import brainwine.gameserver.command.CommandManager; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; @RequestInfo(id = 47) public class ConsoleRequest extends PlayerRequest { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/CraftRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/CraftRequest.java index 4e4f9eab..618f8da1 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/CraftRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/CraftRequest.java @@ -1,22 +1,21 @@ package brainwine.gameserver.server.requests; import java.util.List; +import java.util.stream.Collectors; -import brainwine.gameserver.annotations.OptionalField; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Inventory; -import brainwine.gameserver.entity.player.Player; -import brainwine.gameserver.entity.player.Skill; import brainwine.gameserver.item.CraftingRequirement; import brainwine.gameserver.item.Item; +import brainwine.gameserver.player.Inventory; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.player.Skill; +import brainwine.gameserver.server.OptionalField; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; import brainwine.gameserver.util.MathUtils; import brainwine.gameserver.util.Pair; import brainwine.gameserver.zone.MetaBlock; +import brainwine.gameserver.zone.Zone; -/** - * TODO Account for skills, bonuses etc.. - */ @RequestInfo(id = 19) public class CraftRequest extends PlayerRequest { @@ -54,30 +53,52 @@ public void process(Player player) { return; } } - + // Check if required crafting helpers are nearby if(!player.isGodMode() && item.requiresWorkshop()) { - List workshop = player.getZone().getMetaBlocks(metaBlock - -> MathUtils.inRange(player.getX(), player.getY(), metaBlock.getX(), metaBlock.getY(), 10)); + Zone zone = player.getZone(); + + // Fetch list of all meta blocks in the player's vicinity + List workshop = zone.getMetaBlocks(metaBlock -> zone.isChunkLoaded(metaBlock.getX(), metaBlock.getY()) + && MathUtils.inRange(player.getX(), player.getY(), metaBlock.getX(), metaBlock.getY(), 20)); + // Check for each crafting helper if it is present in the workshop and available for use for(CraftingRequirement craftingHelper : item.getCraftingHelpers()) { - int quantityMissing = craftingHelper.getQuantity() - (int)workshop.stream().filter(metaBlock - -> metaBlock.getItem() == craftingHelper.getItem()).count(); + int quantityRequired = craftingHelper.getQuantity(); + // Fetch list of crafting helpers of this type that are present in the workshop + List presentCraftingHelpers = workshop.stream() + .filter(metaBlock -> metaBlock.getItem() == craftingHelper.getItem()).collect(Collectors.toList()); + int quantityMissing = quantityRequired - presentCraftingHelpers.size(); + + // Check if workshop is still missing crafting helpers of this type and notify the player if this is the case if(quantityMissing > 0) { - player.notify(String.format("You can't craft this item because your workshop is lacking %sx %s.", + player.notify(String.format("You can't craft this item because your workshop is still lacking %sx %s.", quantityMissing, craftingHelper.getItem().getTitle())); return; } + + // Perform additional checks if the crafting helper requires steam to function + if(craftingHelper.getItem().usesSteam()) { + quantityMissing = quantityRequired - (int)presentCraftingHelpers.stream() + .filter(metaBlock -> zone.getBlock(metaBlock.getX(), metaBlock.getY()).getFrontMod() == 1).count(); + + // Notify the player if not enough crafting helpers are powered + if(quantityMissing > 0) { + player.notify(String.format("You can't craft this item because your workshop still needs to provide steam power to %sx %s.", + quantityMissing, craftingHelper.getItem().getTitle())); + return; + } + } } } for(CraftingRequirement ingredient : ingredients) { - inventory.removeItem(ingredient.getItem(), ingredient.getQuantity() * quantity); + inventory.removeItem(ingredient.getItem(), ingredient.getQuantity() * quantity, item.requiresWorkshop()); } int totalQuantity = item.getCraftingQuantity() * quantity; - inventory.addItem(item, totalQuantity); + inventory.addItem(item, totalQuantity, item.requiresWorkshop()); player.getStatistics().trackItemCrafted(item, totalQuantity); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/DialogRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/DialogRequest.java index 44e1b218..49dfc1c3 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/DialogRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/DialogRequest.java @@ -7,15 +7,17 @@ import org.apache.commons.text.WordUtils; -import brainwine.gameserver.annotations.OptionalField; -import brainwine.gameserver.annotations.RequestInfo; +import brainwine.gameserver.GameServer; import brainwine.gameserver.dialog.Dialog; import brainwine.gameserver.dialog.DialogHelper; import brainwine.gameserver.dialog.DialogSection; import brainwine.gameserver.dialog.input.DialogSelectInput; -import brainwine.gameserver.entity.player.Player; -import brainwine.gameserver.entity.player.Skill; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.player.Skill; +import brainwine.gameserver.server.OptionalField; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; +import brainwine.gameserver.zone.Zone; @RequestInfo(id = 45) public class DialogRequest extends PlayerRequest { @@ -33,12 +35,15 @@ public void process(Player player) { if(id instanceof String) { switch((String)id) { - case "skill_upgrade": - onSkillUpgrade(player); - break; - default: - player.notify("Sorry, this action is not implemented yet."); - break; + case "skill_upgrade": + onSkillUpgrade(player); + break; + case "player": + showPlayerDialog(player); + break; + default: + player.notify("Sorry, this action is not implemented yet."); + break; } return; } else if(id instanceof Integer) { @@ -48,6 +53,59 @@ public void process(Player player) { } } + private void showPlayerDialog(Player player) { + // Do nothing if there is no valid input data + if(input == null || input.length == 0 || !(input[0] instanceof String)) { + return; + } + + // Create player info dialog + Player subject = GameServer.getInstance().getPlayerManager().getPlayer((String)input[0]); + Dialog dialog = new Dialog().setTitle(subject.getName()); + + // Online status section + if(subject.isOnline()) { + dialog.addSection(new DialogSection().setText(String.format("Online\nCurrently in %s", subject.getZone().getName()))); + + if(player.getZone() != subject.getZone()) { + dialog.addSection(new DialogSection().setText(String.format("Goto %s", subject.getZone().getName())).setChoice("visit")); + } + } else { + dialog.addSection(new DialogSection().setText("Offline")); + } + + // Follow section + String followText = player.isFollowing(subject) ? "Unfollow" : "Follow"; + dialog.addSection(new DialogSection().setText(followText).setChoice(followText.toLowerCase())); + + // Show player info dialog + player.showDialog(dialog, input -> { + // Handle cancellation + if(input.length == 0 || (input.length == 1 && input[0].equals("cancel"))) { + return; + } + + String choice = String.valueOf(input[0]); + + // Handle selection + switch(choice) { + case "follow": player.followPlayer(subject); break; + case "unfollow": player.unfollowPlayer(subject); break; + case "visit": + Zone zone = subject.getZone(); + + // TODO maybe perform checks in changeZone function and add a force flag for bypassing? + if(!zone.canJoin(player)) { + player.notify("Sorry, you can't enter this world right now."); + break; + } + + player.changeZone(subject.getZone()); + break; + } + }); + } + private void onSkillUpgrade(Player player) { if(player.getSkillPoints() <= 0) { player.notify("Sorry, you are out of skill points. Level up to earn some more!"); @@ -73,10 +131,11 @@ private void onSkillUpgrade(Player player) { "Note: Additional skills like Combat and Engineering are unlocked as you progress." : null) .setInput(new DialogSelectInput() .setOptions(upgradeableSkillNames) + .setMaxColumns(3) .setKey("skill"))); player.showDialog(dialog, input -> { - if(input.length == 0) { + if(input.length == 0 || input[0].equals("cancel")) { return; } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/EntitiesRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/EntitiesRequest.java index 0d0c65ce..ee5cb799 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/EntitiesRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/EntitiesRequest.java @@ -3,10 +3,10 @@ import java.util.ArrayList; import java.util.List; -import brainwine.gameserver.annotations.RequestInfo; import brainwine.gameserver.entity.Entity; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; import brainwine.gameserver.server.messages.EntityStatusMessage; import brainwine.gameserver.server.models.EntityStatusData; diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/EntityUseRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/EntityUseRequest.java new file mode 100644 index 00000000..e5654b29 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/EntityUseRequest.java @@ -0,0 +1,44 @@ +package brainwine.gameserver.server.requests; + +import java.util.List; + +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemRegistry; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; + +@RequestInfo(id = 46) +public class EntityUseRequest extends PlayerRequest { + + public int entityId; + public Object data; + + @Override + public void process(Player player) { + Entity entity = player.getZone().getEntity(entityId); + + // Check if entity exists + if(entity == null) { + return; + } + + // Check if entity is player + if(entity.isPlayer()) { + Player targetPlayer = (Player)entity; + + if(data instanceof List) { + List data = (List)this.data; + + // Handle trade + if(data.size() == 2 && "trade".equals(data.get(0)) && data.get(1) instanceof Integer) { + Item item = ItemRegistry.getItem((int)data.get(1)); + player.tradeItem(targetPlayer, item); + } + } + + return; + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/EventRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/EventRequest.java index 7f9e5f81..362cf7de 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/EventRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/EventRequest.java @@ -1,8 +1,8 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; @RequestInfo(id = 57) public class EventRequest extends PlayerRequest { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/FollowRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/FollowRequest.java new file mode 100644 index 00000000..23b31509 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/FollowRequest.java @@ -0,0 +1,31 @@ +package brainwine.gameserver.server.requests; + +import brainwine.gameserver.GameServer; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; + +@RequestInfo(id = 27) +public class FollowRequest extends PlayerRequest { + + public String recipientName; + public boolean following; + + @Override + public void process(Player player) { + Player recipient = GameServer.getInstance().getPlayerManager().getPlayer(recipientName); + + // Check if recipient exists + if(recipient == null) { + player.notify(String.format("Couldn't find a player named %s", recipientName)); + return; + } + + // Follow or unfollow player + if(following) { + player.followPlayer(recipient); + } else { + player.unfollowPlayer(recipient); + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/HealthRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/HealthRequest.java index b1e0e94a..1b2c2f07 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/HealthRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/HealthRequest.java @@ -1,9 +1,12 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.OptionalField; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.entity.Entity; +import brainwine.gameserver.item.DamageType; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.server.OptionalField; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; @RequestInfo(id = 18) public class HealthRequest extends PlayerRequest { @@ -18,10 +21,18 @@ public class HealthRequest extends PlayerRequest { public void process(Player player) { float health = this.health / 1000.0F; - if(!player.isGodMode() && health >= player.getHealth()) { + // Prevent self-healing unless player has god mode enabled + if(health >= player.getHealth()) { + if(player.isGodMode()) { + player.setHealth(health); + } + return; } - player.damage(player.getHealth() - health, null); + // TODO attacker ID is always zero on v3 and damage type seems to do nothing on both v2 and v3 so we'll just have to do what we can here + Entity attacker = player.getZone().getEntity(attackerId); + float damage = player.getHealth() - health; + player.attack(attacker, Item.AIR, damage, DamageType.ACID, true); // Deal true damage; the client should have already applied any damage modifiers } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/HeartbeatRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/HeartbeatRequest.java index 9c298f19..43f80a7a 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/HeartbeatRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/HeartbeatRequest.java @@ -1,8 +1,8 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; @RequestInfo(id = 143) public class HeartbeatRequest extends PlayerRequest { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/HintRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/HintRequest.java index 9adea563..49866ad8 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/HintRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/HintRequest.java @@ -1,8 +1,8 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; @RequestInfo(id = 36) public class HintRequest extends PlayerRequest { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryMoveRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryMoveRequest.java index 4934c26a..ba285333 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryMoveRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryMoveRequest.java @@ -1,11 +1,11 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.OptionalField; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.ContainerType; -import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; +import brainwine.gameserver.player.ContainerType; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.server.OptionalField; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; /** * TODO This request may be sent *before* a {@link CraftRequest} is sent. diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java index 7847ebcc..5b800eac 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java @@ -3,12 +3,13 @@ import java.util.Arrays; import java.util.Collection; -import brainwine.gameserver.annotations.OptionalField; -import brainwine.gameserver.annotations.RequestInfo; +import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.npc.Npc; -import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.server.OptionalField; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; import brainwine.gameserver.server.messages.EntityItemUseMessage; /** @@ -29,14 +30,14 @@ public class InventoryUseRequest extends PlayerRequest { @Override public void process(Player player) { // Don't do anything if the player is dead or doesn't own this item - if(player.isDead() || !player.getInventory().hasItem(item)) { + if((player.isDead() && status == 1) || (!item.isAir() && !player.getInventory().hasItem(item))) { return; } // Try to consume item if it is a consumable if(item.isConsumable()) { if(status == 1) { - player.consume(item); + player.consume(item, details); } } else { // Set current held item if applicable @@ -44,35 +45,35 @@ public void process(Player player) { player.setHeldItem(item); } - // Send item use data to other players in the zone - player.sendMessageToPeers(new EntityItemUseMessage(player.getId(), type, item, status)); - // Lovely type ambiguity. Always nice. if(item.isWeapon() && status == 1) { Collection entityIds = details instanceof Collection ? (Collection)details : details instanceof Integer ? Arrays.asList((int)details) : null; - // Skip if null aka details was of an invalid type - if(entityIds == null) { - return; - } - - int maxTargetableEntities = player.getMaxTargetableEntities(); - - for(Object id : entityIds) { - if(id instanceof Integer) { - Npc npc = player.getZone().getNpc((int)id); + // Attack enemies if details are present + if(entityIds != null && !entityIds.isEmpty()) { + int maxTargetableEntities = player.getMaxTargetableEntities(); + + for(Object id : entityIds) { + if(id instanceof Integer) { + Npc npc = player.getZone().getNpc((int)id); + + if(npc != null && (player.isGodMode() || (player.canSee(npc) && !npc.wasAttackedRecently(player, Entity.ATTACK_INVINCIBLE_TIME)))) { + npc.attack(player, item, item.getDamage(), item.getDamageType()); + } + } - if(npc != null && (player.isGodMode() || player.canSee(npc))) { - npc.attack(player, item); + if(--maxTargetableEntities <= 0) { + break; } } - if(--maxTargetableEntities <= 0) { - break; - } + return; } } + + // Send item use data to other players in the zone if no details are present + player.sendMessageToTrackers(new EntityItemUseMessage(player.getId(), type, item, status)); } } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/MoveRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/MoveRequest.java index ed0115fc..ef1833e2 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/MoveRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/MoveRequest.java @@ -1,10 +1,10 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.RequestInfo; import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.FacingDirection; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; import brainwine.gameserver.server.messages.EntityPositionMessage; import brainwine.gameserver.zone.Zone; diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/RespawnRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/RespawnRequest.java index 981519eb..882cc80f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/RespawnRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/RespawnRequest.java @@ -1,8 +1,8 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; @RequestInfo(id = 26) public class RespawnRequest extends PlayerRequest { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/StatusRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/StatusRequest.java index 3d925435..d3125a8f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/StatusRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/StatusRequest.java @@ -1,8 +1,8 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; @RequestInfo(id = 54) public class StatusRequest extends PlayerRequest { diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/TransactionRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/TransactionRequest.java index 2c5759db..d0bbb3ef 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/TransactionRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/TransactionRequest.java @@ -1,9 +1,9 @@ package brainwine.gameserver.server.requests; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; -import brainwine.gameserver.server.messages.StatMessage; +import brainwine.gameserver.server.RequestInfo; +import brainwine.gameserver.shop.ShopManager; @RequestInfo(id = 41) public class TransactionRequest extends PlayerRequest { @@ -12,7 +12,6 @@ public class TransactionRequest extends PlayerRequest { @Override public void process(Player player) { - player.notify("Sorry, the crown store has not been implemented yet."); - player.sendMessage(new StatMessage("crowns", player.getCrowns())); + ShopManager.purchaseProduct(player, key); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneChangeRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneChangeRequest.java index cbf048e8..efa7bd4e 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneChangeRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneChangeRequest.java @@ -1,25 +1,39 @@ package brainwine.gameserver.server.requests; import brainwine.gameserver.GameServer; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; import brainwine.gameserver.zone.Zone; +import brainwine.gameserver.zone.ZoneManager; @RequestInfo(id = 24) public class ZoneChangeRequest extends PlayerRequest { - public String zoneName; + public String zoneId; @Override public void process(Player player) { - Zone zone = GameServer.getInstance().getZoneManager().getZoneByName(zoneName); + ZoneManager manager = GameServer.getInstance().getZoneManager(); + Zone zone = manager.getZone(zoneId); + // Get zone by name if ID search yielded no result if(zone == null) { - player.notify("Sorry, could not find a zone with name " + zoneName); + zone = manager.getZoneByName(zoneId); + } + + if(zone == null) { + player.notify("Couldn't locate world."); + return; + } + + if(zone == player.getZone()) { + player.notify("You're already in " + zone.getName()); return; - } else if(zone == player.getZone()) { - player.notify("You're already in " + zoneName); + } + + if(!player.isGodMode() && !zone.canJoin(player)) { + player.notify("You do not belong to that world."); return; } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneSearchRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneSearchRequest.java index 7ccfbeea..7a3edbf7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneSearchRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/ZoneSearchRequest.java @@ -1,16 +1,18 @@ package brainwine.gameserver.server.requests; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; +import java.util.Collection; +import java.util.Comparator; import java.util.List; -import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Predicate; +import java.util.stream.Collectors; import brainwine.gameserver.GameServer; -import brainwine.gameserver.annotations.RequestInfo; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.RequestInfo; import brainwine.gameserver.server.messages.ZoneSearchMessage; +import brainwine.gameserver.server.models.ZoneSearchData; import brainwine.gameserver.zone.Biome; import brainwine.gameserver.zone.Zone; import brainwine.gameserver.zone.ZoneManager; @@ -22,65 +24,81 @@ public class ZoneSearchRequest extends PlayerRequest { @Override public void process(Player player) { - List result = new ArrayList<>(); - List zones = searchZones(GameServer.getInstance().getZoneManager()); - zones.remove(player.getZone()); - Collections.shuffle(zones); - Set indices = new HashSet<>(); - int index = (int)(Math.random() * zones.size()); - int amount = Math.min(9, zones.size()); + int searchLimit = 200; + int displayLimit = 10; + ZoneManager zoneManager = GameServer.getInstance().getZoneManager(); + Collection zoneIds = null; + Collection zones = null; - for(int i = 0; i < amount; i++) { - while(indices.contains(index)) { - index = (int)(Math.random() * zones.size()); - } - - indices.add(index); - result.add(zones.get(i)); + // Get list of zones to filter + // TODO friends + switch(type) { + case "Recent": zoneIds = player.getRecentZones(); break; + case "Bookmarked": zoneIds = player.getBookmarkedZones(); break; + default: break; + } + + if(zoneIds != null) { + // Get zones from pre-assembled list of zone IDs + zones = zoneIds.stream() + .map(zoneManager::getZone) + .filter(zone -> zone != null && zone != player.getZone() && zone.canJoin(player)) + .collect(Collectors.toList()); + } else { + // Find zones matching the filter + zones = zoneManager.getZones().stream() + .filter(getFilter(player).and(zone -> zone != player.getZone())) + .limit(searchLimit) + .collect(Collectors.toList()); } - player.sendDelayedMessage(new ZoneSearchMessage(type, result, 0)); + // Get random zones to display + List data = zones.stream() + .skip(ThreadLocalRandom.current().nextInt(Math.max(1, 1 + zones.size() - displayLimit))) + .limit(displayLimit) + .sorted(getComparator()) + .map(zone -> new ZoneSearchData(zone, player)) + .collect(Collectors.toList()); + + // Send data + player.sendDelayedMessage(new ZoneSearchMessage(type, data, 0)); } - private List searchZones(ZoneManager manager) { - List zones = new ArrayList<>(); - + private Predicate getFilter(Player player) { switch(type) { case "Random": - zones.addAll(manager.searchZones(null, null)); - break; + return zone -> zone.isPublic(); + case "Popular": + return zone -> zone.isPublic() && zone.isPopular(); + case "Unexplored": + return zone -> zone.isPublic() && !zone.isProtected() && zone.isUnexplored(); + case "Owned": + return zone -> zone.isOwner(player); + case "Member": + return zone -> zone.isMember(player); + case "PvP": + return zone -> zone.isPublic() && zone.isPvp(); case "Plain": - zones.addAll(manager.searchZones(zone -> zone.getBiome() == Biome.PLAIN)); - break; - case "Arctic": - zones.addAll(manager.searchZones(zone -> zone.getBiome() == Biome.ARCTIC)); - break; case "Hell": - zones.addAll(manager.searchZones(zone -> zone.getBiome() == Biome.HELL)); - break; + case "Arctic": case "Desert": - zones.addAll(manager.searchZones(zone -> zone.getBiome() == Biome.DESERT)); - break; case "Brain": - zones.addAll(manager.searchZones(zone -> zone.getBiome() == Biome.BRAIN)); - break; case "Deep": - zones.addAll(manager.searchZones(zone -> zone.getBiome() == Biome.DEEP)); - break; case "Space": - zones.addAll(manager.searchZones(zone -> zone.getBiome() == Biome.SPACE)); - break; - case "Unexplored": - zones.addAll(manager.searchZones(zone -> zone.getExplorationProgress() < 0.7, (a, b) -> Float.compare(a.getExplorationProgress(), b.getExplorationProgress()))); - break; + return zone -> zone.isPublic() && !zone.isProtected() && zone.getBiome() == Biome.valueOf(type.toUpperCase()); + default: + return zone -> zone.isPublic() && zone.getName().toLowerCase().contains(type.toLowerCase()); + } + } + + private Comparator getComparator() { + switch(type) { case "Popular": - zones.addAll(manager.searchZones(zone -> zone.getPlayers().size() > 0, (a, b) -> Integer.compare(b.getPlayers().size(), a.getPlayers().size()))); - break; + return (a, b) -> Integer.compare(b.getPlayerCount(), a.getPlayerCount()); // Most players first + case "Unexplored": + return (a, b) -> Integer.compare(a.getChunksExploredCount(), b.getChunksExploredCount()); // Least explored first default: - zones.addAll(manager.searchZones(zone -> zone.getName().toLowerCase().contains(type.toLowerCase()))); - break; + return (a, b) -> 0; } - - return zones; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/shop/ItemProduct.java b/gameserver/src/main/java/brainwine/gameserver/shop/ItemProduct.java new file mode 100644 index 00000000..adadc637 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/shop/ItemProduct.java @@ -0,0 +1,58 @@ +package brainwine.gameserver.shop; + +import java.util.Collections; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import brainwine.gameserver.dialog.Dialog; +import brainwine.gameserver.dialog.DialogListItem; +import brainwine.gameserver.dialog.DialogSection; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; + +public class ItemProduct extends Product { + + private final Map items; + + @JsonCreator + public ItemProduct( + @JsonProperty(value = "name", required = true) String name, + @JsonProperty(value = "cost", required = true) int cost, + @JsonProperty(value = "items", required = true) Map items) { + super(name, cost); + this.items = items; + } + + @Override + public void purchase(Player player) { + DialogSection section = new DialogSection().setTitle("You received:"); + Dialog dialog = new Dialog().addSection(section); + + // Add items to inventory + items.forEach((item, quantity) -> { + if(quantity <= 0) { + return; + } + + section.addItem(new DialogListItem() + .setItem(item.getCode()) + .setImage(String.format("inventory/%s", item.getId())) + .setText(String.format("%s x %s", item.getTitle(), quantity))); + player.getInventory().addItem(item, quantity, true); + }); + + // Show dialog + if(player.isV3()) { + player.showDialog(dialog); + } else { + player.notify(dialog, NotificationType.REWARD); + } + } + + public Map getItems() { + return Collections.unmodifiableMap(items); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/shop/Product.java b/gameserver/src/main/java/brainwine/gameserver/shop/Product.java new file mode 100644 index 00000000..97529639 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/shop/Product.java @@ -0,0 +1,50 @@ +package brainwine.gameserver.shop; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; + +import brainwine.gameserver.player.Player; + +@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type") +@JsonSubTypes({ + @Type(name = "item", value = ItemProduct.class), + @Type(name = "zone", value = ZoneProduct.class), +}) +public abstract class Product { + + protected final String name; + protected final int cost; + protected boolean available = true; + protected String description = "No description is available for this product."; + protected ProductImage image = new ProductImage("inventory/air"); + + public Product(String name, int cost) { + this.name = name; + this.cost = cost; + } + + public abstract void purchase(Player player); + + public String getName() { + return name; + } + + public int getCost() { + return cost; + } + + public boolean isAvailable() { + return available; + } + + public String getDescription() { + return description; + } + + public ProductImage getImage() { + return image; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/shop/ProductImage.java b/gameserver/src/main/java/brainwine/gameserver/shop/ProductImage.java new file mode 100644 index 00000000..7e46b0c0 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/shop/ProductImage.java @@ -0,0 +1,59 @@ +package brainwine.gameserver.shop; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +public class ProductImage { + + private final Layer[] layers; + + @JsonCreator + public ProductImage(Layer[] layers) { + this.layers = layers; + } + + @JsonCreator + public ProductImage(String sprite) { + layers = new Layer[1]; + layers[0] = new Layer(sprite, null); + } + + public boolean isLayered() { + return layers.length > 1; + } + + public String getBaseSprite() { + return layers.length == 0 ? "inventory/air" : layers[0].getSprite(); + } + + @JsonValue + private Layer[] getJsonValue() { + return layers; + } + + private static class Layer { + + private final String sprite; + private final String color; + + @JsonCreator + public Layer( + @JsonProperty(value = "sprite", required = true) String sprite, + @JsonProperty(value = "color") String color) { + this.sprite = sprite; + this.color = color; + } + + @JsonGetter("frame") + public String getSprite() { + return sprite; + } + + @SuppressWarnings("unused") + public String getColor() { + return color; + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/shop/ShopManager.java b/gameserver/src/main/java/brainwine/gameserver/shop/ShopManager.java new file mode 100644 index 00000000..84f7954a --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/shop/ShopManager.java @@ -0,0 +1,125 @@ +package brainwine.gameserver.shop; + +import static brainwine.shared.LogMarkers.SERVER_MARKER; + +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.fasterxml.jackson.core.type.TypeReference; + +import brainwine.gameserver.GameConfiguration; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.resource.ResourceFinder; +import brainwine.gameserver.server.messages.StatMessage; +import brainwine.gameserver.server.models.PlayerStat; +import brainwine.gameserver.util.MapHelper; +import brainwine.shared.JsonHelper; + +/** + * Manages the Crown Store. + * Since no real money is involved, we can afford to be pretty careless with its implementation. + */ +public class ShopManager { + + private static final Logger logger = LogManager.getLogger(); + private static final Map sections = new LinkedHashMap<>(); + private static final Map products = new LinkedHashMap<>(); + + public static void loadShopData() { + logger.info(SERVER_MARKER, "Loading shop data ..."); + sections.clear(); + products.clear(); + + try { + URL url = ResourceFinder.getResourceUrl("shop.json"); + Map data = JsonHelper.readValue(url, new TypeReference>(){}); + sections.putAll(JsonHelper.readValue(data.getOrDefault("sections", Collections.emptyMap()), new TypeReference>(){})); + products.putAll(JsonHelper.readValue(data.getOrDefault("products", Collections.emptyMap()), new TypeReference>(){})); + } catch(Exception e) { + logger.error(SERVER_MARKER, "Could not load shop data", e); + return; + } + + try { + // Create section data + for(Entry entry : sections.entrySet()) { + Map data = JsonHelper.readValue(entry.getValue(), new TypeReference>(){}); + data.put("key", entry.getKey()); + data.put("items", data.remove("products")); + MapHelper.appendList(GameConfiguration.getBaseConfig(), "shop.sections", data); + } + + // Create product data + for(Entry entry : products.entrySet()) { + Product product = entry.getValue(); + ProductImage image = product.getImage(); + + // Skip product if it isn't available + if(!product.isAvailable()) { + continue; + } + + Map data = JsonHelper.readValue(product, new TypeReference>(){}); + data.put("key", entry.getKey()); + + // Convert inventory data + if(data.containsKey("items")) { + Map inventoryData = (Map)data.remove("items"); // TODO this is kinda shit + inventoryData.forEach((item, quantity) -> MapHelper.appendList(data, "inventory", Arrays.asList(item, quantity))); + } + + // Convert image data + if(image != null && data.containsKey("image")) { + if(image.isLayered()) { + data.put("images", data.remove("image")); + } + + data.put("image", image.getBaseSprite()); + } + + MapHelper.appendList(GameConfiguration.getBaseConfig(), "shop.items", data); + } + } catch(Exception e) { + logger.error(SERVER_MARKER, "An error occured while converting shop data", e); + products.clear(); // Clear products so purchases can't be made + return; + } + + logger.info(SERVER_MARKER, "Successfully loaded {} product{}", products.size(), products.size() == 1 ? "" : "s"); + } + + public static boolean purchaseProduct(Player player, Product product) { + // Check if item is available + if(product == null || !product.isAvailable()) { + player.notify("Oops! There was an error with your purchase."); + player.sendMessage(new StatMessage(PlayerStat.CROWNS, player.getCrowns())); + return false; + } + + // Check if player has enough crowns + if(player.getCrowns() < product.getCost()) { + player.notify("You do not have enough crowns to buy this."); + player.sendMessage(new StatMessage(PlayerStat.CROWNS, player.getCrowns())); + return false; + } + + player.setCrowns(player.getCrowns() - product.getCost()); + product.purchase(player); + return true; + } + + public static boolean purchaseProduct(Player player, String productKey) { + return purchaseProduct(player, products.get(productKey)); + } + + public static Product getProduct(String key) { + return products.get(key); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/shop/ShopSection.java b/gameserver/src/main/java/brainwine/gameserver/shop/ShopSection.java new file mode 100644 index 00000000..6dc57511 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/shop/ShopSection.java @@ -0,0 +1,33 @@ +package brainwine.gameserver.shop; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ShopSection { + + private final String name; + private final String icon; + private final String[] products; + + @JsonCreator + public ShopSection( + @JsonProperty(value = "name", required = true) String name, + @JsonProperty(value = "icon", required = true) String icon, + @JsonProperty(value = "products", required = true) String... products) { + this.name = name; + this.icon = icon; + this.products = products; + } + + public String getName() { + return name; + } + + public String getIcon() { + return icon; + } + + public String[] getProducts() { + return products; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/shop/ZoneProduct.java b/gameserver/src/main/java/brainwine/gameserver/shop/ZoneProduct.java new file mode 100644 index 00000000..e9a4eee7 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/shop/ZoneProduct.java @@ -0,0 +1,104 @@ +package brainwine.gameserver.shop; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import brainwine.gameserver.GameServer; +import brainwine.gameserver.dialog.DialogHelper; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.zone.Biome; +import brainwine.gameserver.zone.gen.ZoneGenerator; + +public class ZoneProduct extends Product { + + private final Settings settings; + + @JsonCreator + public ZoneProduct( + @JsonProperty(value = "name", required = true) String name, + @JsonProperty(value = "cost", required = true) int cost, + @JsonProperty(value = "zone", required = true) Settings settings) { + super(name, cost); + this.settings = settings; + } + + @Override + public void purchase(Player player) { + ZoneGenerator generator = settings.getGenerator() == null ? ZoneGenerator.getZoneGenerator(settings.getBiome()) : ZoneGenerator.getZoneGenerator(settings.getGenerator()); + + // Refund purchase if generator doesn't exist + if(generator == null) { + player.notify("Oops! There was a problem with your purchase."); + player.setCrowns(player.getCrowns() + cost); + return; + } + + player.showDialog(DialogHelper.messageDialog("World purchased!", "We've started generating your private world. We'll let you know when it's ready for exploration!")); + generator.generateZoneAsync(settings.getBiome(), settings.getWidth(), settings.getHeight(), zone -> { + // Refund purchase if world failed to generate + if(zone == null) { + player.showDialog(DialogHelper.messageDialog("There was a problem generating your private world. Your crowns have been refunded.")); + player.setCrowns(player.getCrowns() + cost); + return; + } + + // Update world ownership + zone.setOwner(player); + zone.setPrivate(true); + zone.setProtected(true); + GameServer.getInstance().getZoneManager().addZone(zone); + + // Ask player if they want to travel to their newly purchased world + player.showDialog(DialogHelper.messageDialog("Your private world is ready!", String.format( + "Your private world '%s' is ready for exploration and adventure! Let's head there now.", zone.getName())).setActions("yesno"), input -> { + // Check cancellation + if(input.length == 1 && "cancel".equals(input[0])) { + return; + } + + // Send player to purchased zone + player.changeZone(zone); + }); + }); + } + + /** + * Zone generator settings for the product. + */ + private static class Settings { + + private final Biome biome; + private final int width; + private final int height; + private final String generator; + + @JsonCreator + public Settings( + @JsonProperty(value = "biome", required = true) Biome biome, + @JsonProperty(value = "width", required = true) int width, + @JsonProperty(value = "height", required = true) int height, + @JsonProperty(value = "generator") String generator) { + this.biome = biome; + this.width = width; + this.height = height; + this.generator = generator; + } + + public Biome getBiome() { + return biome; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public String getGenerator() { + return generator; + } + } + +} diff --git a/gameserver/src/main/java/brainwine/gameserver/util/MapHelper.java b/gameserver/src/main/java/brainwine/gameserver/util/MapHelper.java index 5f0b88e5..4890cf20 100644 --- a/gameserver/src/main/java/brainwine/gameserver/util/MapHelper.java +++ b/gameserver/src/main/java/brainwine/gameserver/util/MapHelper.java @@ -50,6 +50,10 @@ public static Map map(Class keyType, Class valueType, Object. } public static void put(Map map, String path, Object value) { + if(path == null) { + return; + } + String[] segments = path.split("\\."); Map current = (Map)map; @@ -76,6 +80,10 @@ public static T get(Map map, String path, Class type) { } public static T get(Map map, String path, Class type, T def) { + if(path == null) { + return def; + } + String[] segments = path.split("\\."); Map current = (Map)map; diff --git a/gameserver/src/main/java/brainwine/gameserver/util/MathUtils.java b/gameserver/src/main/java/brainwine/gameserver/util/MathUtils.java index f7d68e78..508c3560 100644 --- a/gameserver/src/main/java/brainwine/gameserver/util/MathUtils.java +++ b/gameserver/src/main/java/brainwine/gameserver/util/MathUtils.java @@ -34,7 +34,11 @@ public static double clamp01(double value) { return clamp(value, 0.0F, 1.0F); } + public static double distance(double x, double y, double x2, double y2) { + return Math.hypot(x - x2, y - y2); + } + public static boolean inRange(double x, double y, double x2, double y2, double range) { - return Math.hypot(x - x2, y - y2) <= range; + return distance(x, y, x2, y2) <= range; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/util/ResourceUtils.java b/gameserver/src/main/java/brainwine/gameserver/util/ResourceUtils.java deleted file mode 100644 index ee6609a5..00000000 --- a/gameserver/src/main/java/brainwine/gameserver/util/ResourceUtils.java +++ /dev/null @@ -1,60 +0,0 @@ -package brainwine.gameserver.util; - -import java.io.File; -import java.nio.file.Files; -import java.util.Set; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.reflections.Reflections; -import org.reflections.scanners.Scanners; -import org.reflections.util.ClasspathHelper; -import org.reflections.util.ConfigurationBuilder; - -public class ResourceUtils { - - private static final Logger logger = LogManager.getLogger(); - - public static void copyDefaults(String path) { - copyDefaults(path, false); - } - - public static void copyDefaults(String path, boolean force) { - try { - File file = new File(path); - - if(!file.exists() || force) { - Reflections reflections = new Reflections(new ConfigurationBuilder() - .setUrls(ClasspathHelper.forResource("defaults")) - .setInputsFilter(x -> x.matches(String.format("defaults/%s.*", path))) - .setScanners(Scanners.Resources)); - Set fileNames = reflections.getResources(".*"); - - for(String fileName : fileNames) { - File output = new File(fileName.substring(9)); - File parent = output.getAbsoluteFile().getParentFile(); - - if(parent != null) { - parent.mkdirs(); - } - - try { - Files.copy(ResourceUtils.class.getResourceAsStream(String.format("/%s", fileName)), output.toPath()); - } catch (Exception e) { - logger.error("Couldn't copy resource '{}'", fileName, e); - } - } - } - } catch(Exception e) { - logger.error("Couldn't copy defaults '{}'", path, e); - } - } - - public static String removeFileSuffix(String string) { - if(!string.contains(".")) { - return string; - } - - return string.substring(0, string.lastIndexOf('.')); - } -} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Block.java b/gameserver/src/main/java/brainwine/gameserver/zone/Block.java index 5ca3c8cf..7d3db1db 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Block.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Block.java @@ -4,40 +4,39 @@ import brainwine.gameserver.item.ItemRegistry; import brainwine.gameserver.item.Layer; -/** - * TODO store block owners. - */ public class Block { private Item baseItem; private Item backItem; - private int backMod; + private byte backMod; private Item frontItem; - private int frontMod; + private byte frontMod; private Item liquidItem; - private int liquidMod; + private byte liquidMod; + private short ownerHash; public Block() { - this(0, 0, 0, 0, 0, 0, 0); + this(0, 0, 0, 0, 0, 0, 0, 0); } public Block(int base, int back, int front) { - this(base & 15, back & 65535, back >> 16 & 31, front & 65535, front >> 16 & 31, base >> 8 & 255, base >> 16 & 31); + this(base & 15, back & 65535, back >> 16 & 31, front & 65535, front >> 16 & 31, base >> 8 & 255, base >> 16 & 31, front >> 21 & 2047); } - public Block(int baseItem, int backItem, int backMod, int frontItem, int frontMod, int liquidItem, int liquidMod) { + public Block(int baseItem, int backItem, int backMod, int frontItem, int frontMod, int liquidItem, int liquidMod, int ownerHash) { this(ItemRegistry.getItem(baseItem), ItemRegistry.getItem(backItem), backMod, - ItemRegistry.getItem(frontItem), frontMod, ItemRegistry.getItem(liquidItem), liquidMod); + ItemRegistry.getItem(frontItem), frontMod, ItemRegistry.getItem(liquidItem), liquidMod, ownerHash); } - public Block(Item baseItem, Item backItem, int backMod, Item frontItem, int frontMod, Item liquidItem, int liquidMod) { - this.baseItem = baseItem; - this.backItem = backItem; - this.backMod = backMod; - this.frontItem = frontItem; - this.frontMod = frontMod; - this.liquidItem = liquidItem; - this.liquidMod = liquidMod; + public Block(Item baseItem, Item backItem, int backMod, Item frontItem, int frontMod, Item liquidItem, int liquidMod, int ownerHash) { + updateLayer(Layer.BASE, baseItem, 0, ownerHash); + updateLayer(Layer.BACK, backItem, backMod, ownerHash); + updateLayer(Layer.FRONT, frontItem, frontMod, ownerHash); + updateLayer(Layer.LIQUID, liquidItem, liquidMod, ownerHash); + } + + public boolean isSolid() { + return (frontItem.isDoor() && frontMod % 2 == 0) || (!frontItem.isDoor() && frontItem.isSolid()); } public void updateLayer(Layer layer, int item) { @@ -45,7 +44,11 @@ public void updateLayer(Layer layer, int item) { } public void updateLayer(Layer layer, int item, int mod) { - updateLayer(layer, ItemRegistry.getItem(item), mod); + updateLayer(layer, item, mod, 0); + } + + public void updateLayer(Layer layer, int item, int mod, int owner) { + updateLayer(layer, ItemRegistry.getItem(item), mod, owner); } public void updateLayer(Layer layer, Item item) { @@ -53,21 +56,26 @@ public void updateLayer(Layer layer, Item item) { } public void updateLayer(Layer layer, Item item, int mod) { + updateLayer(layer, item, mod, 0); + } + + public void updateLayer(Layer layer, Item item, int mod, int owner) { switch(layer) { case BASE: baseItem = item; break; case BACK: backItem = item; - backMod = mod; + backMod = (byte)(mod & 31); break; case FRONT: frontItem = item; - frontMod = mod; + frontMod = (byte)(mod & 31); + ownerHash = (short)(item.isAir() ? 0 : owner & 2047); break; case LIQUID: liquidItem = item; - liquidMod = mod; + liquidMod = (byte)(mod & 31); break; default: break; @@ -115,13 +123,13 @@ public Item getItem(Layer layer) { public void setMod(Layer layer, int mod) { switch(layer) { case BACK: - backMod = mod; + backMod = (byte)(mod & 31); break; case FRONT: - frontMod = mod; + frontMod = (byte)(mod & 31); break; case LIQUID: - liquidMod = mod; + liquidMod = (byte)(mod & 31); break; default: break; @@ -170,7 +178,7 @@ public int getFrontMod() { } public int getFront() { - return frontItem.getCode() | ((frontMod & 31) << 16); + return frontItem.getCode() | ((ownerHash & 2047) << 21) | ((frontMod & 31) << 16); } public Item getLiquidItem() { @@ -180,4 +188,12 @@ public Item getLiquidItem() { public int getLiquidMod() { return liquidMod; } + + public boolean isNatural() { + return ownerHash == 0; + } + + public int getOwnerHash() { + return ownerHash; + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Chunk.java b/gameserver/src/main/java/brainwine/gameserver/zone/Chunk.java index 954ddd70..e594e62d 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Chunk.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Chunk.java @@ -16,6 +16,7 @@ public class Chunk { private final int width; private final int height; private final Block[] blocks; + private long saveTime; private boolean modified; @ConstructorProperties({"x", "y", "width", "height", "blocks"}) @@ -83,4 +84,13 @@ private boolean isIndexInBounds(int index) { public Block[] getBlocks() { return blocks; } + + public void setSaveTime(long saveTime) { + this.saveTime = saveTime; + } + + @JsonIgnore + public long getSaveTime() { + return saveTime; + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java index d299dd39..ff0fa07d 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java @@ -17,83 +17,118 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.zip.DataFormatException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.msgpack.core.MessagePack; -import org.msgpack.core.MessageUnpacker; import org.msgpack.jackson.dataformat.MessagePackFactory; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.player.Player; import brainwine.gameserver.serialization.BlockDeserializer; import brainwine.gameserver.serialization.BlockSerializer; import brainwine.gameserver.util.ZipUtils; public class ChunkManager { + public static final int FILE_SIGNATURE = 0x44574344; + public static final int FILE_HEADER_SIZE = 64; + public static final int LATEST_FILE_VERSION = 0x00000001; + public static final int DEFAULT_CHUNK_ALLOC_SIZE = 2048; + public static final int CHUNK_HEADER_SIZE = 32; + public static final byte[] FILE_HEADER_PADDING = new byte[FILE_HEADER_SIZE - 12]; + public static final byte[] CHUNK_HEADER_PADDING = new byte[CHUNK_HEADER_SIZE - 12]; private static final Logger logger = LogManager.getLogger(); - private static final int allocSize = 2048; private static final ObjectMapper mapper = new ObjectMapper(new MessagePackFactory()) .registerModule(new SimpleModule() .addDeserializer(Block.class, BlockDeserializer.INSTANCE) .addSerializer(BlockSerializer.INSTANCE)); private final Map chunks = new HashMap<>(); private final Zone zone; - private final File blocksFile; private RandomAccessFile file; - private int dataOffset; + private int allocSize; public ChunkManager(Zone zone) { this.zone = zone; - blocksFile = new File(zone.getDirectory(), "blocks.dat"); - File legacyBlocksFile = new File(zone.getDirectory(), "blocks"); + } + + private void initialize() throws IOException, DataFormatException { + // Do nothing if already initialized + if(file != null) { + return; + } + + File dataDirectory = zone.getDirectory(); + File chunksFileV1 = new File(dataDirectory, "blocks"); // Legacy version (no longer supported) + File chunksFileV2 = new File(dataDirectory, "blocks.dat"); // Previous version + File chunksFile = new File(dataDirectory, "chunks.bin"); // Latest version - if(!blocksFile.exists() && legacyBlocksFile.exists()) { - logger.info(SERVER_MARKER, "Updating blocks file for zone {} ...", zone.getDocumentId()); - DataInputStream inputStream = null; - DataOutputStream outputStream = null; + // Check outdated legacy format + if(!chunksFileV2.exists() && chunksFileV1.exists()) { + throw new IOException("Chunk data is outdated. Please try to load this zone with an older server version to update it."); + } + + // Load or initialize header data + if(chunksFile.exists()) { + try(DataInputStream inputStream = new DataInputStream(new FileInputStream(chunksFile))) { + // Check file signature + if(inputStream.readInt() != FILE_SIGNATURE) { + throw new IOException("Invalid file signature"); + } + + int fileVersion = inputStream.readInt(); + allocSize = inputStream.readInt(); + inputStream.skip(FILE_HEADER_PADDING.length); + + // Update chunk data if necessary + if(fileVersion != LATEST_FILE_VERSION) { + throw new IOException("Invalid file version"); // Throw exception for now since there is only one version + } + } + } else { + allocSize = DEFAULT_CHUNK_ALLOC_SIZE; - try { - inputStream = new DataInputStream(new FileInputStream(legacyBlocksFile)); - outputStream = new DataOutputStream(new FileOutputStream(blocksFile)); - int chunkCount = zone.getChunkCount(); + try(DataOutputStream outputStream = new DataOutputStream(new FileOutputStream(chunksFile))) { + outputStream.writeInt(FILE_SIGNATURE); + outputStream.writeInt(LATEST_FILE_VERSION); + outputStream.writeInt(allocSize); + outputStream.write(FILE_HEADER_PADDING); - for(int i = 0; i < chunkCount; i++) { - short length = inputStream.readShort(); - byte[] chunkBytes = new byte[length]; - inputStream.read(chunkBytes); - inputStream.skipBytes(2048 - length - 2); - chunkBytes = ZipUtils.inflateBytes(chunkBytes); - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(chunkBytes); - unpacker.unpackArrayHeader(); - int x = unpacker.unpackInt(); - int y = unpacker.unpackInt(); - int width = unpacker.unpackInt(); - int height = unpacker.unpackInt(); - Block[] blocks = new Block[unpacker.unpackArrayHeader() / 3]; + // Update chunk data from previous version if it is present + if(chunksFileV2.exists()) { + logger.info(SERVER_MARKER, "Updating chunk data for zone {} ...", zone.getDocumentId()); + int chunkCount = zone.getChunkCount(); + long now = System.currentTimeMillis(); - for(int j = 0; j < blocks.length; j++) { - blocks[j] = new Block(unpacker.unpackInt(), unpacker.unpackInt(), unpacker.unpackInt()); + try(DataInputStream inputStream = new DataInputStream(new FileInputStream(chunksFileV2))) { + for(int i = 0; i < chunkCount; i++) { + // Read chunk data + byte[] chunkBytes = new byte[inputStream.readShort()]; + inputStream.read(chunkBytes); + inputStream.skip(DEFAULT_CHUNK_ALLOC_SIZE - chunkBytes.length - 2); // Skip reserved chunk space + + // Write chunk header + outputStream.writeLong(now); // Save time + outputStream.writeInt(chunkBytes.length); + outputStream.write(CHUNK_HEADER_PADDING); + + // Write chunk data + outputStream.write(chunkBytes); + + // Write chunk padding + if(i + 1 < chunkCount) { + outputStream.write(new byte[allocSize - chunkBytes.length - CHUNK_HEADER_SIZE]); + } + } } - - unpacker.close(); - byte[] bytes = ZipUtils.deflateBytes(mapper.writeValueAsBytes(new Chunk(x, y, width, height, blocks))); - outputStream.writeShort(bytes.length); - outputStream.write(bytes); - outputStream.write(new byte[allocSize - bytes.length - 2]); } - - inputStream.close(); - outputStream.close(); - } catch(Exception e) { - logger.error(SERVER_MARKER, "Could not update blocks file for zone {}", zone.getDocumentId(), e); } - - legacyBlocksFile.delete(); } + + // Create random access file stream + file = new RandomAccessFile(chunksFile, "rw"); } protected void closeStream() { @@ -112,27 +147,16 @@ public void saveChunks() { List inactiveChunks = new ArrayList<>(); for(Chunk chunk : chunks.values()) { - if(chunk.isModified()) { - saveChunk(chunk); - } - - boolean active = false; - - for(Player player : zone.getPlayers()) { - if(player.isChunkActive(chunk)) { - active = true; - break; - } - } - - if(!active) { + if(!isChunkActive(chunk)) { inactiveChunks.add(chunk); + zone.onChunkUnloaded(chunk); // Perform cleanup *before* the chunk is saved and unindexed in case any last-minute changes need to be made } + + saveChunk(chunk); } for(Chunk chunk : inactiveChunks) { chunks.remove(getChunkIndex(chunk.getX(), chunk.getY())); - zone.onChunkUnloaded(chunk); } } @@ -140,35 +164,40 @@ private void saveChunk(Chunk chunk) { int index = zone.getChunkIndex(chunk.getX(), chunk.getY()); try { - if(file == null) { - file = new RandomAccessFile(blocksFile, "rw"); - } - - byte[] bytes = ZipUtils.deflateBytes(mapper.writeValueAsBytes(chunk)); + initialize(); + file.seek(FILE_HEADER_SIZE + index * allocSize); + file.writeLong(System.currentTimeMillis()); // Write save time - if(bytes.length > allocSize) { - throw new IOException("WARNING: bigger than alloc size: " + bytes.length); + // Write block data if chunk has been modified + if(chunk.isModified()) { + byte[] bytes = ZipUtils.deflateBytes(mapper.writeValueAsBytes(chunk)); + + // TODO reformat entire file with bigger alloc size + if(bytes.length > allocSize - CHUNK_HEADER_SIZE) { + throw new IOException("WARNING: bigger than alloc size: " + bytes.length); + } + + file.writeInt(bytes.length); + file.write(CHUNK_HEADER_PADDING); + file.write(bytes); + chunk.setModified(false); } - - file.seek(dataOffset + index * allocSize); - file.writeShort(bytes.length); - file.write(bytes); - chunk.setModified(false); - } catch (IOException e) { + } catch(Exception e) { logger.error(SERVER_MARKER, "Could not save chunk {} of zone {}", index, zone.getDocumentId(), e); } } private Chunk loadChunk(int index) { try { - if(file == null) { - file = new RandomAccessFile(blocksFile, "rw"); - } - - file.seek(dataOffset + index * allocSize); - byte[] bytes = new byte[file.readShort()]; + initialize(); + file.seek(FILE_HEADER_SIZE + index * allocSize); + long saveTime = file.readLong(); + byte[] bytes = new byte[file.readInt()]; + file.skipBytes(CHUNK_HEADER_PADDING.length); file.read(bytes); - return mapper.readValue(ZipUtils.inflateBytes(bytes), Chunk.class); + Chunk chunk = mapper.readValue(ZipUtils.inflateBytes(bytes), Chunk.class); + chunk.setSaveTime(saveTime); + return chunk; } catch(Exception e) { logger.error(SERVER_MARKER, "Could not load chunk {} of zone {}", index, zone.getDocumentId(), e); } @@ -210,6 +239,18 @@ public void putChunk(int index, Chunk chunk) { } } + public boolean isChunkActive(int x, int y) { + return zone.areCoordinatesInBounds(x, y) && isChunkActive(getChunkIndex(x, y)); + } + + public boolean isChunkActive(int index) { + return zone.getPlayers().stream().anyMatch(player -> player.isChunkActive(index)); + } + + public boolean isChunkActive(Chunk chunk) { + return zone.getPlayers().stream().anyMatch(player -> player.isChunkActive(chunk)); + } + public boolean isChunkLoaded(int x, int y) { return zone.areCoordinatesInBounds(x, y) && isChunkLoaded(getChunkIndex(x, y)); } @@ -237,10 +278,15 @@ public Chunk getChunk(int index) { Chunk chunk = chunks.get(index); + // Load chunk if it isn't cached if(chunk == null) { chunk = loadChunk(index); - chunks.put(index, chunk); - zone.onChunkLoaded(chunk); + + // Index chunk if it was loaded successfully + if(chunk != null) { + chunks.put(index, chunk); + zone.onChunkLoaded(chunk); + } } return chunk; diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/DugBlock.java b/gameserver/src/main/java/brainwine/gameserver/zone/DugBlock.java deleted file mode 100644 index fb385764..00000000 --- a/gameserver/src/main/java/brainwine/gameserver/zone/DugBlock.java +++ /dev/null @@ -1,40 +0,0 @@ -package brainwine.gameserver.zone; - -import brainwine.gameserver.item.Item; - -public class DugBlock { - - private final int x; - private final int y; - private final Item item; - private final int mod; - private final long time; - - public DugBlock(int x, int y, Item item, int mod, long time) { - this.x = x; - this.y = y; - this.item = item; - this.mod = mod; - this.time = time; - } - - public int getX() { - return x; - } - - public int getY() { - return y; - } - - public Item getItem() { - return item; - } - - public int getMod() { - return mod; - } - - public long getTime() { - return time; - } -} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/EcologicalMachine.java b/gameserver/src/main/java/brainwine/gameserver/zone/EcologicalMachine.java new file mode 100644 index 00000000..ffbc8450 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/EcologicalMachine.java @@ -0,0 +1,107 @@ +package brainwine.gameserver.zone; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemRegistry; + +public enum EcologicalMachine { + + PURIFIER("p", "mechanical/geck-tub", + "mechanical/geck-tank-base", + "mechanical/geck-tank", + "mechanical/geck-hoses", + "mechanical/geck-tree-base", + "mechanical/geck-tree-top", + "mechanical/geck-cog-large", + "mechanical/geck-cog-small"), + + COMPOSTER("c", "mechanical/composter-chamber", + "mechanical/composter-cover", + "mechanical/composter-fuel-tank", + "mechanical/composter-input-tube", + "mechanical/composter-output", + "mechanical/composter-turbine"), + + RECYCLER("r", "mechanical/recycler-chamber", + "mechanical/recycler-tubes", + "mechanical/recycler-pipe-base", + "mechanical/recycler-pipe", + "mechanical/recycler-gear-base", + "mechanical/recycler-gear"), + + // Not exactly 'ecological' but whatever. + EXPIATOR("e", "hell/expiator-face", + "hell/expiator-pipe", + "hell/expiator-frame", + "hell/expiator-tank", + "hell/expiator-tubes", + "hell/expiator-tube", + "hell/expiator-gear", + "hell/expiator-exhaust"); + + private final String clientId; + private final String base; + private final List parts; + + private EcologicalMachine(String clientId, String base, String... parts) { + this.clientId = clientId; + this.base = base; + this.parts = Arrays.asList(parts); + } + + @JsonCreator + public static EcologicalMachine fromName(String id) { + for(EcologicalMachine value : values()) { + if(value.getId().equalsIgnoreCase(id)) { + return value; + } + } + + return null; + } + + public static EcologicalMachine fromBase(Item base) { + return Stream.of(values()).filter(x -> base.hasId(x.base)).findFirst().orElse(null); + } + + public static EcologicalMachine fromPart(Item part) { + return Stream.of(values()).filter(x -> x.isMachinePart(part)).findFirst().orElse(null); + } + + public static boolean isMachine(Item item) { + return fromBase(item) != null; + } + + @JsonValue + public String getId() { + return toString().toLowerCase(); + } + + public String getClientId() { + return clientId; + } + + public Item getBase() { + return ItemRegistry.getItem(base); + } + + public boolean isMachinePart(Item item) { + return parts.contains(item.getId()); + } + + public int getPartCount() { + return parts.size(); + } + + public List getParts() { + return parts.stream().map(ItemRegistry::getItem).collect(Collectors.toCollection(ArrayList::new)); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java index 0b71f8d1..82ae7fc9 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java @@ -2,8 +2,8 @@ import static brainwine.shared.LogMarkers.SERVER_MARKER; -import java.io.File; import java.io.IOException; +import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -20,21 +20,20 @@ import com.fasterxml.jackson.core.type.TypeReference; -import brainwine.gameserver.GameServer; import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.EntityConfig; import brainwine.gameserver.entity.EntityRegistry; import brainwine.gameserver.entity.EntityStatus; import brainwine.gameserver.entity.npc.Npc; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.entity.npc.NpcData; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Layer; import brainwine.gameserver.item.ModType; -import brainwine.gameserver.server.messages.EffectMessage; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.resource.ResourceFinder; import brainwine.gameserver.server.messages.EntityPositionMessage; import brainwine.gameserver.server.messages.EntityStatusMessage; import brainwine.gameserver.util.MapHelper; -import brainwine.gameserver.util.ResourceUtils; import brainwine.gameserver.util.Vector2i; import brainwine.gameserver.util.WeightedMap; import brainwine.shared.JsonHelper; @@ -62,31 +61,29 @@ public EntityManager(Zone zone) { public static void loadEntitySpawns() { spawns.clear(); logger.info(SERVER_MARKER, "Loading entity spawns ..."); - File file = new File("spawning.json"); - ResourceUtils.copyDefaults("spawning.json"); - if(file.isFile()) { - try { - spawns.putAll(JsonHelper.readValue(file, new TypeReference>>(){})); - } catch (IOException e) { - logger.error(SERVER_MARKER, "Failed to load entity spawns", e); - } + try { + URL url = ResourceFinder.getResourceUrl("spawning.json", true); + spawns.putAll(JsonHelper.readValue(url, new TypeReference>>(){})); + } catch (IOException e) { + logger.error(SERVER_MARKER, "Failed to load entity spawns", e); } } - private static List getEligibleEntitySpawns(Biome biome, String locale, double depth, Item baseItem) { + private static List getEligibleEntitySpawns(Biome biome, String locale, double depth, double acidity, Item baseItem) { return spawns.entrySet().stream() .filter(entry -> entry.getKey() == biome) .map(Entry::getValue) .flatMap(Collection::stream) .filter(spawn -> locale.equalsIgnoreCase(spawn.getLocale()) && depth >= spawn.getMinDepth() && depth <= spawn.getMaxDepth() + && acidity >= spawn.getMinAcidity() && acidity <= spawn.getMaxAcidity() && ((!baseItem.hasId("base/maw") && !baseItem.hasId("base/pipe")) || spawn.getOrifice() == baseItem)) .collect(Collectors.toList()); } - private static EntitySpawn getRandomEligibleEntitySpawn(Biome biome, String locale, double depth, Item baseItem) { - return new WeightedMap<>(getEligibleEntitySpawns(biome, locale, depth, baseItem), EntitySpawn::getFrequency).next(); + private static EntitySpawn getRandomEligibleEntitySpawn(Biome biome, String locale, double depth, double acidity, Item baseItem) { + return new WeightedMap<>(getEligibleEntitySpawns(biome, locale, depth, acidity, baseItem), EntitySpawn::getFrequency).next(); } public void tick(float deltaTime) { @@ -136,7 +133,7 @@ private void spawnRandomEntity() { Block block = chunk.getBlock(x, y); String locale = block.getBaseItem().isAir() ? "sky" : "cave"; EntitySpawn spawn = getRandomEligibleEntitySpawn( - zone.getBiome(), locale, y / (double)zone.getHeight(), block.getBaseItem()); + zone.getBiome(), locale, y / (double)zone.getHeight(), zone.getAcidity(), block.getBaseItem()); if(immediate) { if(tryBustOrifice(x, y, Layer.BACK) || tryBustOrifice(x, y, Layer.FRONT)) { @@ -175,22 +172,22 @@ private boolean tryBustOrifice(int x, int y, Layer layer) { private void clearEntities() { npcs.values().stream() - .filter(npc -> npc.isDead() || !zone.isChunkLoaded((int)npc.getX(), (int)npc.getY()) || - (npc.isTransient() && System.currentTimeMillis() > npc.getLastTrackedAt() + ENTITY_CLEAR_TIME)) + .filter(npc -> npc.isDead() || (!npc.isPersistent() && !npc.hasActiveMinigame() && (!zone.isChunkLoaded(npc.getBlockX(), npc.getBlockY()) || + (npc.isTransient() && System.currentTimeMillis() > npc.getLastTrackedAt() + ENTITY_CLEAR_TIME)))) .collect(Collectors.toList()) .forEach(this::removeEntity); } - public List getEntitiesInRange(float x, float y, float range) { + public List getEntitiesInRange(float x, float y, double range) { return getEntities().stream().filter(entity -> entity.inRange(x, y, range)).collect(Collectors.toList()); } - public Player getRandomPlayerInRange(float x, float y, float range) { + public Player getRandomPlayerInRange(float x, float y, double range) { List players = getPlayersInRange(x, y, range); return players.isEmpty() ? null : players.get(random.nextInt(players.size())); } - public List getPlayersInRange(float x, float y, float range) { + public List getPlayersInRange(float x, float y, double range) { return getPlayers().stream().filter(player -> player.inRange(x, y, range)).collect(Collectors.toList()); } @@ -210,12 +207,10 @@ public void trySpawnBlockEntity(int x, int y) { List guardians = MapHelper.getList(metaBlock.getMetadata(), "!", Collections.emptyList()); for(String guardian : guardians) { - EntityConfig config = EntityRegistry.getEntityConfig(guardian); + Npc entity = spawnEntity(guardian, x, y); - if(config != null) { - Npc entity = new Npc(zone, config); + if(entity != null) { entity.setGuardBlock(x, y); - spawnEntity(entity, x, y); } } } @@ -231,36 +226,60 @@ public void trySpawnBlockEntity(int x, int y) { // Check for mounted entity (turrets & geysers) if(item.isEntity()) { - EntityConfig config = EntityRegistry.getEntityConfig(item.getId()); + Npc entity = spawnEntity(item.getId(), x, y); - if(config != null) { - Npc entity = new Npc(zone, config); + if(entity != null) { MetaBlock metaBlock = zone.getMetaBlock(x, y); // Set owner entity if it has one if(metaBlock != null && metaBlock.hasOwner()) { - entity.setOwner(GameServer.getInstance().getPlayerManager().getPlayerById(metaBlock.getOwner())); + entity.setOwner(metaBlock.getOwner()); } entity.setMountBlock(x, y); - spawnEntity(entity, x, y); mountedNpcs.put(index, entity); } } } + public void spawnPersistentNpcs(Collection data) { + for(NpcData entry : data) { + if(entry.getType() == null) { + continue; + } + + Npc npc = new Npc(zone, entry.getType()); + npc.setName(entry.getName()); + spawnEntity(npc, entry.getX(), entry.getY()); + } + } + + public Npc spawnEntity(String type, int x, int y) { + return spawnEntity(type, x, y, false); + } + + public Npc spawnEntity(String type, int x, int y, boolean effect) { + EntityConfig config = EntityRegistry.getEntityConfig(type); + + if(config == null) { + return null; + } + + Npc entity = new Npc(zone, config); + spawnEntity(entity, x, y, effect); + return entity; + } + public void spawnEntity(Entity entity, int x, int y) { spawnEntity(entity, x, y, false); } public void spawnEntity(Entity entity, int x, int y, boolean effect) { - if(zone.isChunkLoaded(x, y)) { - addEntity(entity); - entity.setPosition(x, y); - - if(effect) { - zone.sendMessageToChunk(new EffectMessage(x + 0.5F, y + 0.5F, "bomb-teleport", 4), zone.getChunk(x, y)); - } + addEntity(entity); + entity.setPosition(x, y); + + if(effect && zone.isChunkLoaded(x, y)) { + zone.spawnEffect(x + 0.5F, y + 0.5F, "bomb-teleport", 4); } } @@ -275,9 +294,9 @@ public void addEntity(Entity entity) { if(entity instanceof Player) { Player player = (Player)entity; - player.onZoneChanged(); + player.onZoneEntered(); players.put(entityId, player); - playersByName.put(player.getName(), player); + playersByName.put(player.getName().toLowerCase(), player); player.sendMessageToPeers(new EntityStatusMessage(player, EntityStatus.ENTERING)); player.sendMessageToPeers(new EntityPositionMessage(player)); } else if(entity instanceof Npc) { @@ -296,7 +315,7 @@ public void removeEntity(Entity entity) { if(entity instanceof Player) { players.remove(entityId); - playersByName.remove(entity.getName()); + playersByName.remove(entity.getName().toLowerCase()); zone.sendMessage(new EntityStatusMessage(entity, EntityStatus.EXITING)); } else { npcs.remove(entityId); @@ -332,19 +351,23 @@ public int getNpcCount() { } public int getTransientNpcCount() { - return (int)npcs.values().stream().filter(npc -> npc.isTransient()).count(); + return (int)npcs.values().stream().filter(Npc::isTransient).count(); } public Collection getNpcs() { return Collections.unmodifiableCollection(npcs.values()); } + public List getPersistentNpcs() { + return npcs.values().stream().filter(Npc::isPersistent).collect(Collectors.toList()); + } + public Player getPlayer(int entityId) { return players.get(entityId); } public Player getPlayer(String name) { - return playersByName.get(name); + return playersByName.get(name.toLowerCase()); } public int getPlayerCount() { diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/EntitySpawn.java b/gameserver/src/main/java/brainwine/gameserver/zone/EntitySpawn.java index 0b2fe792..8908ea81 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/EntitySpawn.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/EntitySpawn.java @@ -21,6 +21,12 @@ public class EntitySpawn { @JsonProperty("max_depth") private double maxDepth = 1; + + @JsonProperty("min_acidity") + private double minAcidity = 0.0; + + @JsonProperty("max_acidity") + private double maxAcidity = 1.0; @JsonProperty("orifice") private Item orifice; @@ -43,6 +49,14 @@ public double getMinDepth() { public double getMaxDepth() { return maxDepth; } + + public double getMinAcidity() { + return minAcidity; + } + + public double getMaxAcidity() { + return maxAcidity; + } public Item getOrifice() { return orifice; @@ -51,4 +65,5 @@ public Item getOrifice() { public double getFrequency() { return frequency; } + } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Growable.java b/gameserver/src/main/java/brainwine/gameserver/zone/Growable.java new file mode 100644 index 00000000..c7cddc66 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Growable.java @@ -0,0 +1,31 @@ +package brainwine.gameserver.zone; + +import java.beans.ConstructorProperties; + +import brainwine.gameserver.item.Item; + +public class Growable { + + private final int maxMod; + private final double chance; + private final Item replaceSource; + + @ConstructorProperties({"max_mod", "chance", "replace_source"}) + public Growable(int maxMod, double chance, Item replaceSource) { + this.maxMod = maxMod; + this.chance = chance; + this.replaceSource = replaceSource; + } + + public int getMaxMod() { + return maxMod; + } + + public double getChance() { + return chance; + } + + public Item getReplaceSource() { + return replaceSource; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/GrowthManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/GrowthManager.java new file mode 100644 index 00000000..1d4af2f2 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/GrowthManager.java @@ -0,0 +1,152 @@ +package brainwine.gameserver.zone; + +import static brainwine.shared.LogMarkers.SERVER_MARKER; + +import java.net.URL; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.fasterxml.jackson.core.type.TypeReference; + +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; +import brainwine.gameserver.resource.ResourceFinder; +import brainwine.gameserver.util.WeightedMap; +import brainwine.shared.JsonHelper; + +/** + * Manages plant growth in a zone. + */ +public class GrowthManager { + + public static final int MAX_RAIN_CYCLES = 500; // Maximum number of rain cycles that are permitted in a single growth update + private static final Logger logger = LogManager.getLogger(); + private static final Map growables = new HashMap<>(); + private static final Map>> sourcesByBiome = new HashMap<>(); + private final Set sourceIndices = new HashSet<>(); + private final Map> sources; + private final Zone zone; + + public GrowthManager(Zone zone) { + this.sources = sourcesByBiome.getOrDefault(zone.getBiome(), Collections.emptyMap()); + this.zone = zone; + } + + public static void loadGrowthData() { + growables.clear(); + sourcesByBiome.clear(); + + try { + URL url = ResourceFinder.getResourceUrl("growth.json"); + Map data = JsonHelper.readValue(url, new TypeReference>(){}); + growables.putAll(JsonHelper.readValue(data.get("growables"), new TypeReference>(){})); + sourcesByBiome.putAll(JsonHelper.readValue(data.get("sources"), new TypeReference>>>(){})); + } catch(Exception e) { + logger.error(SERVER_MARKER, "Could not load growth data", e); + } + } + + /** + * Calls {@link #updateGrowables(int, Collection)} where {@code sourceIndices} is the currently indexed growables. + */ + public void updateGrowables(int rainCycles) { + updateGrowables(rainCycles, sourceIndices); + } + + /** + * Updates the specified growables {@code n} times where {@code n} is the number of rain cycles. + */ + public void updateGrowables(int rainCycles, Collection sourceIndices) { + // Do nothing if zone isn't purified + if(!zone.isPurified() && zone.getBiome() != Biome.HELL) { + return; + } + + // Do nothing if there's nothing to do... duh! + if(rainCycles < 1 || sourceIndices.isEmpty()) { + return; + } + + // Reduce overhead by reducing the number of iterations in exchange for a growth chance boost + rainCycles = Math.min(MAX_RAIN_CYCLES, rainCycles); + int growthChanceBoost = Math.min(10, rainCycles); + rainCycles /= growthChanceBoost; + + // Update growth for each rain cycle + for(int i = 0; i < rainCycles; i++) { + Iterator iterator = sourceIndices.iterator(); + + while(iterator.hasNext()) { + int index = iterator.next(); + int x = index % zone.getWidth(); + int y = index / zone.getWidth(); + + // Unindex if chunk is not loaded + if(y == 0 || !zone.isChunkLoaded(x, y)) { + iterator.remove(); + continue; + } + + // Skip if sunlight can't reach this source + if(zone.getSunlight()[x] < y) { + continue; + } + + Block sourceBlock = zone.getBlock(x, y); + Item sourceItem = sourceBlock.getFrontItem(); + + // Unindex if block is not a source + if(!sources.containsKey(sourceItem)) { + iterator.remove(); + continue; + } + + Block growableBlock = zone.getBlock(x, y - 1); + Item growableItem = growableBlock.getFrontItem(); + + // Place a random growable if block isn't occupied or try to grow it if it is a valid growable + if(growableItem.isAir()) { + growableItem = sources.get(sourceItem).next(); + + // Update block if item exists + if(growableItem != null) { + zone.updateBlock(x, y - 1, Layer.FRONT, growableItem); + } + } else if(growables.containsKey(growableItem)) { + Growable growable = growables.get(growableItem); + int mod = growableBlock.getFrontMod(); + + // Try to apply a growth stage if the plant can still grow + if(mod < growable.getMaxMod() && Math.random() < growable.getChance() * growthChanceBoost) { + zone.updateBlock(x, y - 1, Layer.FRONT, growableItem, ++mod); + + // Replace source block if max mod has been reached + if(growable.getReplaceSource() != null && mod >= growable.getMaxMod()) { + zone.updateBlock(x, y, Layer.FRONT, growable.getReplaceSource()); + } + } + } + } + } + } + + /** + * Attempts to index the block and returns {@code true} if the block's front item is a valid growth source. + */ + public boolean indexBlock(int x, int y, Item item) { + if(!sources.containsKey(item)) { + return false; + } + + sourceIndices.add(zone.getBlockIndex(x, y)); + return true; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/MachineManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/MachineManager.java new file mode 100644 index 00000000..bd36f436 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/MachineManager.java @@ -0,0 +1,201 @@ +package brainwine.gameserver.zone; + +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 java.util.Map.Entry; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import brainwine.gameserver.item.Item; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.server.messages.ZoneStatusMessage; +import brainwine.gameserver.util.MapHelper; + +/** + * Manages ecological machines in a zone. + */ +public class MachineManager { + + public static final float PURIFICATION_TIME_SECONDS = 60.0F * 60.0F * 24.0F * 3.0F; // 72 hours (3 days) + private final Map> discoveredParts = new HashMap<>(); + private final Map machineBlocks = new HashMap<>(); + private final Zone zone; + + public MachineManager(Zone zone) { + this.zone = zone; + } + + /** + * Decreases the zone's acidity if the purifier has been activated. + */ + public void updatePurifier(float deltaTime) { + // Do nothing if purifier isn't active + if(!isMachineActive(EcologicalMachine.PURIFIER)) { + return; + } + + // Reduce acidity + float amount = deltaTime / PURIFICATION_TIME_SECONDS; + zone.setAcidity(Math.max(0.0F, zone.getAcidity() - amount)); + } + + /** + * @return {@code true} if the specified machine has been activated, otherwise {@code false}. + */ + public boolean isMachineActive(EcologicalMachine machine) { + return machineBlocks.values().stream() + .filter(block -> block.getItem() == machine.getBase() && block.getBooleanProperty("activated")) + .findFirst().isPresent(); + } + + /** + * This will show the number of discovered components on the minimap + * as well as update the machine sprite on V2 clients. + */ + public void sendMachineStatus(Player player) { + // V3 unfortunately doesn't seem to track machine progress + if(player.isV3()) { + return; + } + + // Create client data + Map> data = discoveredParts.entrySet().stream() + .collect(Collectors.toMap( + entry -> entry.getKey().getClientId(), // Map machine type to client ID + entry -> entry.getValue().stream() + .map(Item::getCode) // Map item instance to item code + .collect(Collectors.toCollection(ArrayList::new)))); + + // Sneakily add base parts of active machines to client data + List activeMachines = Stream.of(EcologicalMachine.values()) + .filter(this::isMachineActive) + .collect(Collectors.toList()); + activeMachines.forEach(machine -> MapHelper.appendList(data, machine.getClientId(), machine.getBase().getCode())); + + // Send data + player.sendMessage(new ZoneStatusMessage(MapHelper.map("machines", data))); + } + + /** + * Sync machine status with all players in the zone. + */ + private void updateMachineStatus(EcologicalMachine machine) { + // Find all machines of this type in the zone + List metaBlocks = machineBlocks.values().stream() + .filter(block -> block.getItem() == machine.getBase()) + .collect(Collectors.toList()); + + // Get list of discovered parts and transform to client data + List parts = discoveredParts.getOrDefault(machine, Collections.emptyList()).stream() + .filter(machine::isMachinePart) + .map(Item::getCode) + .collect(Collectors.toList()); + parts.add(machine.getBase().getCode()); + + // Update the machine sprite for V3 clients + for(MetaBlock metaBlock : metaBlocks) { + metaBlock.setProperty("spr", parts); + zone.sendBlockMetaUpdate(metaBlock); + } + + // Send machine status update to players + for(Player player : zone.getPlayers()) { + sendMachineStatus(player); + } + } + + // TODO rethink + public boolean addMachinePart(Item part) { + EcologicalMachine machine = EcologicalMachine.fromPart(part); + + // Do nothing if machine doesn't exist + if(machine == null) { + return false; + } + + List parts = discoveredParts.computeIfAbsent(machine, x -> new ArrayList<>()); + + // Do nothing if part has already been discovered + if(parts.contains(part)) { + return false; + } + + // Update machine status + parts.add(part); + updateMachineStatus(machine); + return true; + } + + // TODO rethink + public boolean removeMachinePart(Item part) { + EcologicalMachine machine = EcologicalMachine.fromPart(part); + + // Do nothing if machine doesn't exist + if(machine == null) { + return false; + } + + List parts = discoveredParts.get(machine); + + if(parts != null) { + parts.remove(part); + + // Remove key if there are no parts left + if(parts.isEmpty()) { + discoveredParts.remove(machine); + } + + // Update machine status + updateMachineStatus(machine); + return true; + } + + return false; + } + + /** + * @return An immutable view of the discovered parts for the specified machine. + */ + public Collection getDiscoveredParts(EcologicalMachine machine) { + return Collections.unmodifiableCollection(discoveredParts.getOrDefault(machine, Collections.emptyList())); + } + + /** + * @return An immutable view of all of the discovered machine parts. + */ + public Map> getDiscoveredParts() { + return Collections.unmodifiableMap(discoveredParts); + } + + protected void loadData(ZoneConfigFile config) { + // Filter out invalid parts + Map> discoveredParts = config.getDiscoveredParts().entrySet().stream() + .collect(Collectors.toMap( + Entry::getKey, + entry -> entry.getValue().stream() + .filter(item -> entry.getKey().isMachinePart(item)) + .collect(Collectors.toCollection(ArrayList::new)))); + this.discoveredParts.putAll(discoveredParts); + } + + protected void indexMetaBlock(int index, MetaBlock metaBlock) { + EcologicalMachine machine = EcologicalMachine.fromBase(metaBlock.getItem()); + + if(machine != null) { + machineBlocks.put(index, metaBlock); + updateMachineStatus(machine); + } + } + + protected void unindexMetaBlock(int index) { + MetaBlock metaBlock = machineBlocks.remove(index); + + if(metaBlock != null) { + updateMachineStatus(EcologicalMachine.fromBase(metaBlock.getItem())); + } + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/MetaBlock.java b/gameserver/src/main/java/brainwine/gameserver/zone/MetaBlock.java index f16ae052..f1784821 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/MetaBlock.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/MetaBlock.java @@ -2,6 +2,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.function.Function; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -10,9 +11,13 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.GameServer; import brainwine.gameserver.item.Item; +import brainwine.gameserver.player.Player; +/** + * I hate this class and everything in it. + */ @JsonInclude(Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public class MetaBlock { @@ -67,7 +72,17 @@ public boolean hasOwner() { return owner != null; } - public String getOwner() { + public boolean isOwnedBy(Player player) { + return hasOwner() && player != null && player.getDocumentId().equals(owner); + } + + @JsonIgnore + public Player getOwner() { + return GameServer.getInstance().getPlayerManager().getPlayerById(owner); + } + + @JsonProperty("owner") + private String getOwnerId() { return owner; } @@ -83,6 +98,60 @@ public Item getItem() { return item; } + public void setProperty(String key, Object value) { + metadata.put(key, value); + } + + public void removeProperty(String key) { + metadata.remove(key); + } + + public boolean hasProperty(String key) { + return metadata.containsKey(key); + } + + public Object getProperty(String key) { + return metadata.get(key); + } + + public int getIntProperty(String key) { + return tryParse(key, Integer::parseInt, 0); + } + + public float getFloatProperty(String key) { + return tryParse(key, Float::parseFloat, 0.0f); + } + + public boolean getBooleanProperty(String key) { + return Boolean.parseBoolean(String.valueOf(getProperty(key))); + } + + public String getStringProperty(String key) { + Object value = metadata.get(key); + return value != null && value instanceof String ? (String)value : null; + } + + /** + * Generic function for parsing a number from a string. + */ + private T tryParse(String key, Function parseFunction, T def) { + Object value = metadata.get(key); + + if(value == null) { + return def; + } + + T result = def; + + try { + result = parseFunction.apply(String.valueOf(value)); + } catch(NumberFormatException e) { + // Discard silently + } + + return result; + } + public void setMetadata(Map metadata) { this.metadata = metadata; } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/SteamIteration.java b/gameserver/src/main/java/brainwine/gameserver/zone/SteamIteration.java new file mode 100644 index 00000000..67280438 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/SteamIteration.java @@ -0,0 +1,35 @@ +package brainwine.gameserver.zone; + +/** + * Used by {@link SteamManager} to keep track of things. + */ +public class SteamIteration { + + private int x; + private int y; + private byte direction; + private short depth; + + public SteamIteration(int x, int y, int direction, int depth) { + this.x = x; + this.y = y; + this.direction = (byte)direction; + this.depth = (short)depth; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public byte getDirection() { + return direction; + } + + public short getDepth() { + return depth; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/SteamManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/SteamManager.java new file mode 100644 index 00000000..03c2ac25 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/SteamManager.java @@ -0,0 +1,226 @@ +package brainwine.gameserver.zone; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Queue; +import java.util.Set; + +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.Layer; + +/** + * Distributes steam through collectors to nearby machines via pipes. + */ +public class SteamManager { + + public static final int STEAM_UPDATE_INTERVAL = 3000; // Update interval in milliseconds + public static final int MAX_ITERATIONS = 300; // Maximum number of iterations before giving up + public static final int MAX_COLLECTOR_DISTANCE = 350; // Collectors that are not within this distance of any players in the zone will be skipped + public static final byte STATE_EMPTY = 0x0; // Nothing or unrelated + public static final byte STATE_PIPE = 0x1; // Pipe + public static final byte STATE_COLLECTOR = 0x2; // Active collector + private final Set collectorIndices = new HashSet<>(); + private final Set steamableIndices = new HashSet<>(); + private final Set processedIndices = new HashSet<>(); + private final List expiredSteamableIndices = new ArrayList<>(); + private final Queue processQueue = new ArrayDeque<>(); + private final Zone zone; + private byte[] data; + private long lastUpdateAt; + + public SteamManager(Zone zone) { + this.zone = zone; + this.data = new byte[(zone.getWidth() * zone.getHeight()) >> 2]; + } + + public void tick(double deltaTime) { + long now = System.currentTimeMillis(); + + // Check if it's time to update steam yet + if(now > lastUpdateAt + STEAM_UPDATE_INTERVAL) { + updateSteam(); + lastUpdateAt = now; + } + } + + private void updateSteam() { + // Do nothing if there are no players in this zone + if(zone.getPlayerCount() == 0) { + return; + } + + // Clear data from previous run + processedIndices.clear(); + expiredSteamableIndices.clear(); + + // Turn off all steam-powered objects + for(int index : steamableIndices) { + int x = index % zone.getWidth(); + int y = index / zone.getWidth(); + + // Skip if chunk isn't loaded + if(!zone.isChunkLoaded(x, y)) { + expiredSteamableIndices.add(index); + continue; + } + + Block block = zone.getBlock(x, y); + Item item = block.getFrontItem(); + + // Skip if front item doesn't use steam + if(!item.usesSteam()) { + expiredSteamableIndices.add(index); + continue; + } + + // Directly set the block mod + zone.updateBlockMod(x, y, Layer.FRONT, 0); + } + + // Unindex expired steamables + for(int index : expiredSteamableIndices) { + steamableIndices.remove(index); + } + + // Enqueue blocks at the spouts of all collectors + for(int index : collectorIndices) { + int x = index % zone.getWidth(); + int y = index / zone.getWidth(); + + // Skip if no player is close to this collector + if(zone.getPlayersInRange(x, y, MAX_COLLECTOR_DISTANCE).isEmpty()) { + continue; + } + + // Queue spouts + processQueue.add(new SteamIteration(x + 1, y - 3, 0, 0)); // Top + processQueue.add(new SteamIteration(x + 3, y - 1, 1, 0)); // Right + processQueue.add(new SteamIteration(x + 1, y + 1, 2, 0)); // Bottom + processQueue.add(new SteamIteration(x - 1, y - 1, 3, 0)); // Left + } + + // Travel down the pipeline and power on any machines that are reached by it + while(!processQueue.isEmpty()) { + SteamIteration iteration = processQueue.poll(); + int depth = iteration.getDepth(); + + // Skip if depth limit has been reached + if(depth >= MAX_ITERATIONS) { + continue; + } + + int x = iteration.getX(); + int y = iteration.getY(); + + // Skip if coordinates are out of bounds + if(!zone.areCoordinatesInBounds(x, y)) { + continue; + } + + int index = zone.getBlockIndex(x, y); + + // Skip if block has already been processed + if(processedIndices.contains(index)) { + continue; + } + + processedIndices.add(index); + + // Skip if block is not a pipe but activate it first if it uses steam + if(getState(x, y) != STATE_PIPE) { + if(steamableIndices.contains(index)) { + zone.updateBlockMod(x, y, Layer.FRONT, 1); // Directly set the block mod + } + + continue; + } + + byte direction = iteration.getDirection(); + int nextDepth = depth + 1; + + // Enqueue adjacent blocks for processing + if(direction != 2) processQueue.add(new SteamIteration(x, y - 1, 0, nextDepth)); // Top + if(direction != 3) processQueue.add(new SteamIteration(x + 1, y, 1, nextDepth)); // Right + if(direction != 0) processQueue.add(new SteamIteration(x, y + 1, 2, nextDepth)); // Bottom + if(direction != 1) processQueue.add(new SteamIteration(x - 1, y, 3, nextDepth)); // Left + } + } + + public void indexBlock(int x, int y, Item item) { + int index = zone.getBlockIndex(x, y); + + // Does it use steam? + if(!item.usesSteam()) { + steamableIndices.remove(index); + + // Is it a pipe? + if(!item.hasId("mechanical/pipe")) { + + // Is it a collector and is it on top of a steam vent? + if(!item.hasId("mechanical/collector") || !isCollectorActive(x, y)) { + collectorIndices.remove(index); + setState(index, STATE_EMPTY); + return; + } + + collectorIndices.add(index); + setState(index, STATE_COLLECTOR); + return; + } + + setState(index, STATE_PIPE); + return; + } + + steamableIndices.add(index); + setState(index, STATE_EMPTY); + } + + private boolean isCollectorActive(int x, int y) { + return zone.isChunkLoaded(x + 1, y - 1) && zone.getBlock(x + 1, y - 1).getBaseItem().hasId("base/vent"); + } + + protected void setData(byte[] data) { + // Do nothing if data is null + if(data == null) { + return; + } + + int size = zone.getWidth() * zone.getHeight(); + + // Do nothing if data size is incorrect + if(data.length << 2 != size) { + return; + } + + this.data = data; + + // Index active collectors + for(int i = 0; i < size; i++) { + if(getState(i) == STATE_COLLECTOR) { + collectorIndices.add(i); + } + } + } + + private void setState(int index, byte state) { + int byteOffset = index >> 2; + int bitOffset = (index % 4) << 1; + data[byteOffset] &= ~(0x3 << bitOffset); // Clear bits + data[byteOffset] |= (state & 0x3) << bitOffset; // Set bits + } + + private int getState(int x, int y) { + return getState(zone.getBlockIndex(x, y)); + } + + private int getState(int index) { + return (data[index >> 2] >> ((index % 4) << 1)) & 0x3; + } + + protected byte[] getData() { + return data; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/WeatherManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/WeatherManager.java index 5bd5c624..d4b3d72f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/WeatherManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/WeatherManager.java @@ -9,12 +9,14 @@ public class WeatherManager { private static final ThreadLocalRandom random = ThreadLocalRandom.current(); + private final Zone zone; private long rainStart; private long rainDuration; private float rainPower; private float precipitation; - public WeatherManager() { + public WeatherManager(Zone zone) { + this.zone = zone; createRandomRain(random.nextBoolean()); } @@ -22,7 +24,12 @@ public void tick(float deltaTime) { long now = System.currentTimeMillis(); if(now > rainStart + rainDuration) { - createRandomRain(rainPower > 0 ? true : false); + boolean dry = rainPower > 0; + createRandomRain(dry); + + if(dry) { + zone.updateGrowables(1); + } } float lerp = (float)(deltaTime * MathUtils.lerp(0.02F, 0.1F, (now - rainStart) / (float)rainDuration)); diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index 97cedf59..f72540be 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -2,16 +2,17 @@ import java.io.File; import java.time.OffsetDateTime; -import java.util.ArrayDeque; +import java.time.temporal.TemporalUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Queue; +import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.UUID; @@ -24,24 +25,31 @@ import com.fasterxml.jackson.annotation.JsonValue; import brainwine.gameserver.GameServer; +import brainwine.gameserver.Timer; import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.npc.Npc; -import brainwine.gameserver.entity.player.ChatType; -import brainwine.gameserver.entity.player.NotificationType; -import brainwine.gameserver.entity.player.Player; +import brainwine.gameserver.entity.npc.NpcData; +import brainwine.gameserver.item.DamageType; +import brainwine.gameserver.item.Fieldability; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemRegistry; import brainwine.gameserver.item.ItemUseType; import brainwine.gameserver.item.Layer; import brainwine.gameserver.item.MetaType; import brainwine.gameserver.item.ModType; +import brainwine.gameserver.minigame.Minigame; +import brainwine.gameserver.player.ChatType; +import brainwine.gameserver.player.NotificationType; +import brainwine.gameserver.player.Player; import brainwine.gameserver.prefab.Prefab; import brainwine.gameserver.server.Message; import brainwine.gameserver.server.messages.BlockChangeMessage; import brainwine.gameserver.server.messages.BlockMetaMessage; import brainwine.gameserver.server.messages.ChatMessage; import brainwine.gameserver.server.messages.ConfigurationMessage; +import brainwine.gameserver.server.messages.EffectMessage; import brainwine.gameserver.server.messages.LightMessage; +import brainwine.gameserver.server.messages.NotificationMessage; import brainwine.gameserver.server.messages.ZoneExploredMessage; import brainwine.gameserver.server.messages.ZoneStatusMessage; import brainwine.gameserver.server.models.BlockChangeData; @@ -49,9 +57,14 @@ import brainwine.gameserver.util.MathUtils; import brainwine.gameserver.util.SimplexNoise; import brainwine.gameserver.util.Vector2i; +import brainwine.gameserver.zone.gen.models.RubbleType; +/** + * TODO Zone class is getting kinda big. I want to split it into more smaller classes to make it more manageable. + */ public class Zone { + public static final int MAX_CONCURRENT_MINIGAMES = 20; public static final int DEFAULT_CHUNK_WIDTH = 20; public static final int DEFAULT_CHUNK_HEIGHT = 20; private static final ThreadLocalRandom random = ThreadLocalRandom.current(); @@ -68,22 +81,37 @@ public class Zone { private int[] sunlight; private int[] depths; private boolean[] chunksExplored; + private int chunksExploredCount; private OffsetDateTime creationDate = OffsetDateTime.now(); private float time = (float)Math.random(); // TODO temporary private float temperature; private float acidity; + private boolean isPrivate; + private boolean isProtected; + private boolean pvp; + private String entryCode; + private String owner; private final ChunkManager chunkManager; - private final WeatherManager weatherManager = new WeatherManager(); + private final SteamManager steamManager; + private final GrowthManager growthManager; + private final WeatherManager weatherManager = new WeatherManager(this); private final EntityManager entityManager = new EntityManager(this); private final LiquidManager liquidManager = new LiquidManager(this); - private final Queue digQueue = new ArrayDeque<>(); - private final List blockChanges = new ArrayList<>(); + private final MachineManager machineManager = new MachineManager(this); private final Set pendingSunlight = new HashSet<>(); + private final List members = new ArrayList<>(); + private final List> blockTimers = new ArrayList<>(); private final Map dungeons = new HashMap<>(); private final Map metaBlocks = new HashMap<>(); private final Map globalMetaBlocks = new HashMap<>(); private final Map fieldBlocks = new HashMap<>(); + private final Map damageFieldBlocks = new HashMap<>(); + private final Map blockChanges = new HashMap<>(); + private final Map minigames = new HashMap<>(); + private final Map actionHistory = new HashMap<>(); private long lastStatusUpdate = System.currentTimeMillis(); + private int ticksElapsed; + private boolean modified; protected Zone(String documentId, ZoneConfigFile config, ZoneDataFile data) { this(documentId, config.getName(), config.getBiome(), config.getWidth(), config.getHeight()); @@ -95,8 +123,18 @@ protected Zone(String documentId, ZoneConfigFile config, ZoneDataFile data) { this.sunlight = sunlight != null && sunlight.length == width ? sunlight : this.sunlight; this.depths = depths != null && depths.length == 3 ? depths : this.depths; this.chunksExplored = chunksExplored != null && chunksExplored.length == getChunkCount() ? chunksExplored : this.chunksExplored; + recalculateChunksExploredCount(); + steamManager.setData(data.getSteamData()); + machineManager.loadData(config); pendingSunlight.addAll(data.getPendingSunlight()); + entryCode = config.getEntryCode(); + owner = config.getOwner(); + members.addAll(config.getMembers()); + actionHistory.putAll(config.getActionHistory()); acidity = biome == Biome.ARCTIC || biome == Biome.SPACE ? 0 : config.getAcidity(); + isPrivate = config.isPrivate(); + isProtected = config.isProtected(); + pvp = config.isPvp(); creationDate = config.getCreationDate(); } @@ -112,8 +150,10 @@ public Zone(String documentId, String name, Biome biome, int width, int height) surface = new int[width]; sunlight = new int[width]; chunksExplored = new boolean[numChunksWidth * numChunksHeight]; - chunkManager = new ChunkManager(this); acidity = biome == Biome.ARCTIC || biome == Biome.SPACE ? 0 : 1; + chunkManager = new ChunkManager(this); + steamManager = new SteamManager(this); + growthManager = new GrowthManager(this); Arrays.fill(surface, height); Arrays.fill(sunlight, height); } @@ -128,6 +168,8 @@ public void tick(float deltaTime) { weatherManager.tick(deltaTime); entityManager.tick(deltaTime); liquidManager.tick(deltaTime); + steamManager.tick(deltaTime); + simulate(deltaTime); // One full cycle = 1200 seconds = 20 minutes time += deltaTime * (1.0F / 1200.0F); @@ -136,32 +178,45 @@ public void tick(float deltaTime) { time -= 1.0F; } + // Send zone status update if(!getPlayers().isEmpty()) { if(now >= lastStatusUpdate + 4000) { - sendMessage(new ZoneStatusMessage(getStatusConfig())); + for(Player player : getPlayers()) { + sendMessage(new ZoneStatusMessage(getStatusConfig(player))); + } + lastStatusUpdate = now; } } - if(!digQueue.isEmpty()) { - DugBlock dugBlock = digQueue.peek(); + // Update minigames + if(!minigames.isEmpty()) { + Iterator iterator = minigames.values().iterator(); - if(now >= dugBlock.getTime()) { - digQueue.poll(); - int x = dugBlock.getX(); - int y = dugBlock.getY(); - Block block = getBlock(x, y); + while(iterator.hasNext()) { + Minigame minigame = iterator.next(); - if(block != null && block.getFrontItem().hasId("ground/earth-dug")) { - updateBlock(x, y, Layer.FRONT, dugBlock.getItem(), dugBlock.getMod()); + // Remove inactive minigames + if(!minigame.isActive()) { + iterator.remove(); + continue; } + + minigame.tick(deltaTime); } } + // Process block timers + if(!blockTimers.isEmpty()) { + List> readyTimers = blockTimers.stream().filter(timer -> now >= timer.getTime()).collect(Collectors.toList()); + blockTimers.removeAll(readyTimers); + readyTimers.forEach(Timer::process); + } + // Send block changes to players who they are relevant to if(!blockChanges.isEmpty()) { for(Player player : getPlayers()) { - List blockChangesNearPlayer = blockChanges.stream() + List blockChangesNearPlayer = blockChanges.values().stream() .filter(blockChange -> player.isChunkActive(blockChange.getX(), blockChange.getY())) .collect(Collectors.toList()); @@ -172,6 +227,38 @@ public void tick(float deltaTime) { blockChanges.clear(); } + + + // Process field damage (every 1 second) + if(ticksElapsed % 8 == 0) { + for(Player player : getPlayers()) { + for(MetaBlock block : damageFieldBlocks.values()) { + Item item = block.getItem(); + float distance = (float)MathUtils.distance(player.getX(), player.getY(), block.getX(), block.getY()); + float radius = item.getFieldDamage().getRadius(); + float maxDamage = item.getFieldDamage().getMaxDamage(); + + if(maxDamage == 0.0F) { + maxDamage = 2.0F; + } + + // Deal true damage to the target, scaling with distance from field block + if(distance < radius) { + float damage = maxDamage * (1.0F - distance / radius); + player.attack(null, item, damage, item.getFieldDamage().getType(), true); + } + } + } + } + + ticksElapsed++; + } + + /** + * Simulate happenings that take longer periods of time + */ + protected void simulate(float deltaTime) { + machineManager.updatePurifier(deltaTime); } /** @@ -191,7 +278,7 @@ public void sendMessage(Message message) { * @param message The message to send. * @param chunk The chunk near which players must be. */ - public void sendMessageToChunk(Message message, Chunk chunk) { + public void sendLocalMessage(Message message, Chunk chunk) { for(Player player : getPlayers()) { if(player.isChunkActive(chunk)) { player.sendMessage(message); @@ -199,6 +286,22 @@ public void sendMessageToChunk(Message message, Chunk chunk) { } } + public void sendLocalMessage(Message message, float x, float y) { + sendLocalMessage(message, (int)x, (int)y); + } + + public void sendLocalMessage(Message message, int x, int y) { + if(!isChunkLoaded(x, y)) { + return; + } + + sendLocalMessage(message, getChunk(x, y)); + } + + public void sendBlockMetaUpdate(MetaBlock metaBlock) { + sendLocalMessage(new BlockMetaMessage(metaBlock), metaBlock.getX(), metaBlock.getY()); + } + public void sendChatMessage(Player sender, String text) { sendChatMessage(sender, text, ChatType.CHAT); } @@ -215,6 +318,24 @@ public void sendChatMessage(Player sender, String text, ChatType type) { GameServer.getInstance().notify(String.format("%s: %s", sender.getName(), text), NotificationType.CHAT); } + public void notifyPlayers(String message) { + notifyPlayers(message, NotificationType.POPUP); + } + + public void notifyPlayers(String message, NotificationType type) { + sendMessage(new NotificationMessage(message, type)); + } + + public void spawnEffect(float x, float y, String type, Object data) { + sendLocalMessage(new EffectMessage(x, y, type, data), x, y); + } + + public void kickAllPlayers(String reason, boolean shouldReconnect) { + for(Player player : getPlayers()) { + player.kick(reason, shouldReconnect); + } + } + public boolean isPointVisibleFrom(int x1, int y1, int x2, int y2) { return raycast(x1, y1, x2, y2) == null; } @@ -299,48 +420,233 @@ private List raycast(int x1, int y1, int x2, int y2, boolean path, boo return all ? coords : null; } - public boolean isBlockSolid(int x, int y) { - return isBlockSolid(x, y, true); + public void explode(int x, int y, float radius, Entity cause, String effect) { + explode(x, y, radius, cause, false, 0, null, effect); } - public boolean isBlockSolid(int x, int y, boolean checkAdjacents) { - if(!areCoordinatesInBounds(x, y) || !isChunkLoaded(x, y)) { - return true; + public void explode(int x, int y, float radius, Entity cause, boolean destructive, float baseDamage, DamageType damageType, String effect) { + // Do nothing if the chunk at the target location isn't loaded + if(!isChunkLoaded(x, y)) { + return; } - Block block = getBlock(x, y); - Item item = block.getItem(Layer.FRONT); + spawnEffect(x + 0.5F, y + 0.5F, effect, radius); + Player player = cause instanceof Player ? (Player)cause : null; + Item item = getBlock(x, y).getFrontItem(); - if(item.isDoor() && block.getFrontMod() % 2 == 0) { - return true; - } else if(!item.isDoor() && item.isSolid()) { - return true; + // Try to destroy the block at the source of the explosion + if(item.getFieldability() == Fieldability.FALSE) { + updateBlock(x, y, Layer.FRONT, 0); + + if(destructive && !isBlockProtected(x, y, player)) { + updateBlock(x, y, Layer.BACK, 0); + } } - if(checkAdjacents) { - for(int i = -3; i <= 0; i++) { - for(int j = 0; j <= 2; j++) { - int x1 = x + i; - int y1 = y + j; + // Destroy blocks within range if the explosion is destructive + if(destructive) { + int rayCount = (int)Math.ceil(radius * 8); + List> rays = new ArrayList<>(); + List affectedBlocks = new ArrayList<>(); + Set processed = new HashSet<>(); + + // Determine the outer points of the blast circle and cast rays to them + for(int i = 0; i < rayCount; i++) { + float rayDistance = (float)(radius * (Math.random() * 0.4F + 0.8F)); + float angle = (float)Math.toRadians(i * (360.0F / rayCount)); + int targetX = (int)(x + rayDistance * Math.sin(angle)); + int targetY = (int)(y + rayDistance * Math.cos(angle)); + rays.add(raycast(x, y, targetX, targetY, true, true, false)); + } + + // Fetch list of field blocks that are within range of the explosion (drastically speeds up the protection check) + Collection fieldBlocksInRange = fieldBlocks.values().stream() + .filter(metaBlock -> MathUtils.inRange(x, y, metaBlock.getX(), metaBlock.getY(), metaBlock.getItem().getField() + radius * 2)) + .collect(Collectors.toList()); + + // Determine which blocks to destroy by figuring out where each ray should stop + for(List ray : rays) { + for(Vector2i position : ray) { + int positionX = position.getX(); + int positionY = position.getY(); + int index = positionY * width + positionX; - if(!areCoordinatesInBounds(x1, y1) || !isChunkLoaded(x1, y1)) { + // Skip if block has been processed + if(processed.contains(index)) { continue; } - block = getBlock(x1, y1); - item = block.getFrontItem(); + // Skip if not in bounds + if(!areCoordinatesInBounds(positionX, positionY)) { + break; + } - if(item.getBlockWidth() > Math.abs(i) && item.getBlockHeight() > Math.abs(j) - && isBlockSolid(x1, y1, false)) { - return true; + Item frontItem = getBlock(positionX, positionY).getFrontItem(); + double distance = MathUtils.distance(x, y, positionX, positionY); + double power = radius - distance; + + // Do not destroy block if it invulnerable or too tough + if(!frontItem.isAir() && (frontItem.isInvulnerable() || frontItem.getToughness() >= power)) { + break; + } + + // Do not destroy block if it is protected + if(isBlockProtected(positionX, positionY, player, fieldBlocksInRange) || frontItem.hasField()) { + // Keep following this ray if the block isn't occupied + if(!frontItem.isWhole()) { + continue; + } + + break; } + + // Only count as processed if the ray can no longer be stopped + processed.add(index); + + // Metadata check + MetaBlock metaBlock = getMetaBlock(positionX, positionY); + + if(metaBlock != null) { + // Do not destroy block if it is a container with loot + if(frontItem.hasUse(ItemUseType.CONTAINER) && metaBlock.hasProperty("$")) { + continue; + } + + // Do not destroy block if it is a natural dungeon switch with an active linked item + if(!metaBlock.hasOwner() && !getSwitchedItem(metaBlock).isAir()) { + continue; + } + } + + affectedBlocks.add(position); } } + + // Sort affected blocks by their distance from the explosion center + affectedBlocks.sort((a, b) -> { + double distanceA = MathUtils.distance(x, y, a.getX(), a.getY()); + double distanceB = MathUtils.distance(x, y, b.getX(), b.getY()); + return distanceA > distanceB ? 1 : distanceB > distanceA ? -1 : 0; + }); + + // Destroy affected blocks + for(Vector2i position : affectedBlocks) { + updateBlock(position.getX(), position.getY(), Layer.FRONT, 0); + updateBlock(position.getX(), position.getY(), Layer.BACK, 0); + } } - return false; + // Fetch list of nearby entities + List nearbyEntities = getEntitiesInRange(x, y, radius); + + // Damage nearby entities based on their distance from the explosion + for(Entity entity : nearbyEntities) { + // Cast a ray from the explosion to the entity and damage it if it reaches it + if(entity.canSee(x, y)) { + double distance = MathUtils.distance(x, y, entity.getX(), entity.getY()); + float damage = (float)(baseDamage - distance); + entity.attack(cause, item, damage, damageType); + } + } + } + + public void explodeLiquid(int x, int y, int range, int liquid) { + explodeLiquid(x, y, range, ItemRegistry.getItem(liquid)); + } + + public void explodeLiquid(int x, int y, int range, String liquid) { + explodeLiquid(x, y, range, ItemRegistry.getItem(liquid)); + } + + public void explodeLiquid(int x, int y, int range, Item liquid) { + // Do nothing if liquid isn't actually a liquid + if(liquid.getLayer() != Layer.LIQUID) { + return; + } + + // Place liquid blocks around the explosion + for(int i = x - range; i <= x + range; i++) { + for(int j = y - range; j <= y + range; j++) { + // Skip if not in range + if(!MathUtils.inRange(x, y, i, j, range)) { + continue; + } + + // Place liquid if target block isn't solid + if(!isBlockSolid(i, j, true)) { + updateBlock(i, j, Layer.LIQUID, liquid, 5); + } + } + } + } + + public Item getSwitchedItem(MetaBlock metaBlock) { + // Do nothing if meta block is not a switch + if(!metaBlock.getItem().hasUse(ItemUseType.SWITCH)) { + return Item.AIR; + } + + // TODO this implementation assumes that all switched items have metadata + List> positions = MapHelper.getList(metaBlock.getMetadata(), ">", Collections.emptyList()); + return positions.stream() + .map(position -> getMetaBlock(position.get(0), position.get(1))) // Map to meta block + .filter(Objects::nonNull) // Remove null meta blocks + .map(MetaBlock::getItem) // Map to item + .filter(item -> item.hasUse(ItemUseType.SWITCHED)) // Remove non-switched items + .findFirst().orElse(Item.AIR); // Return first entry or air if none exist } + public boolean isBlockWhole(int x, int y) { + return areCoordinatesInBounds(x, y) && getBlock(x, y).getFrontItem().isWhole(); + } + + public boolean isBlockEarthy(int x, int y) { + return areCoordinatesInBounds(x, y) && getBlock(x, y).getFrontItem().isEarthy(); + } + + public boolean isBlockNatural(int x, int y) { + return areCoordinatesInBounds(x, y) && getBlock(x, y).isNatural(); + } + + public boolean isBlockSolid(int x, int y) { + return isBlockSolid(x, y, true); + } + + public boolean isBlockSolid(int x, int y, boolean checkAdjacents) { + if(!areCoordinatesInBounds(x, y) || !isChunkLoaded(x, y)) { + return true; + } + + Block block = getBlock(x, y); + return block.isSolid() || (checkAdjacents && findBlock(x, y, Block::isSolid) != null); + } + + /** + * Find block with item occupying the block position that satisfies the predicate. + * Closer blocks are prioritized in row major order. + */ + public Block findBlock(int x, int y, Predicate predicate) { + for(int i = 0; i >= -3; i--) { + for(int j = 0; j <= 2; j++) { + int x1 = x + i; + int y1 = y + j; + + if(!areCoordinatesInBounds(x1, y1) || !isChunkLoaded(x1, y1)) { + continue; + } + + Block block = getBlock(x1, y1); + Item item = block.getFrontItem(); + + if(item.getBlockWidth() > Math.abs(i) && item.getBlockHeight() > Math.abs(j) && predicate.test(block)) { + return block; + } + } + } + + return null; + } + public boolean isBlockOccupied(int x, int y, Layer layer) { if(!areCoordinatesInBounds(x, y)) { return false; @@ -356,14 +662,38 @@ public boolean isBlockProtected(int x, int y) { return isBlockProtected(x, y, null); } - public boolean isBlockProtected(int x, int y, Player from) { - for(MetaBlock fieldBlock : fieldBlocks.values()) { + public boolean isBlockProtected(int x, int y, Player player) { + return isBlockProtected(x, y, player, fieldBlocks.values()); + } + + public boolean isBlockProtected(int x, int y, Player player, Collection fieldBlocks) { + // Check bounds + if(!areCoordinatesInBounds(x, y)) { + return true; + } + + // Check protection at zone level + if(player != null && isProtected(player)) { + return true; + } + + Item frontItem = getBlock(x, y).getFrontItem(); // TODO can load chunks! + MetaBlock metaBlock = getMetaBlock(x, y); + + // Check block owner if it has a field + if(frontItem.hasField() && (metaBlock == null || !metaBlock.isOwnedBy(player))) { + return true; + } + + // Check field blocks + for(MetaBlock fieldBlock : fieldBlocks) { Item item = fieldBlock.getItem(); int fX = fieldBlock.getX(); int fY = fieldBlock.getY(); int field = fieldBlock.getItem().getField(); - if(from == null || !ownsMetaBlock(fieldBlock, from)) { + if(player == null || (!fieldBlock.isOwnedBy(player) + && !(fieldBlock.getIntProperty("t") == 1 && player.hasFollower(fieldBlock.getOwner())))) { if(item.isDish()) { if(MathUtils.inRange(x, y, fX, fY, field)) { return true; @@ -385,7 +715,7 @@ public boolean willDishOverlap(int x, int y, int field, Player player) { int fY = fieldBlock.getY(); int fField = fieldBlock.getItem().getField(); - if(MathUtils.inRange(x, y, fX, fY, field + fField) && !ownsMetaBlock(fieldBlock, player)) { + if(MathUtils.inRange(x, y, fX, fY, field + fField) && !fieldBlock.isOwnedBy(player)) { return true; } } @@ -405,7 +735,7 @@ public Prefab createPrefabFromSection(String name, int x, int y, int width, int for(int j = 0; j < height; j++) { int index = j * width + i; Block block = getBlock(x + i, y + j); - blocks[index] = new Block(block.getBaseItem(), block.getBackItem(), block.getBackMod(), block.getFrontItem(), block.getFrontMod(), block.getLiquidItem(), block.getLiquidMod()); + blocks[index] = new Block(block.getBaseItem(), block.getBackItem(), block.getBackMod(), block.getFrontItem(), block.getFrontMod(), block.getLiquidItem(), block.getLiquidMod(), 0); MetaBlock metaBlock = metaBlocks.get(getBlockIndex(x + i, j + y)); if(metaBlock != null) { @@ -437,13 +767,16 @@ public void placePrefab(Prefab prefab, int x, int y, Random random) { } public void placePrefab(Prefab prefab, int x, int y, Random random, long seed) { + placePrefab(prefab, x, y, random, prefab.isMirrorable() && random.nextBoolean(), seed); + } + + public void placePrefab(Prefab prefab, int x, int y, Random random, boolean mirrored, long seed) { int width = prefab.getWidth(); int height = prefab.getHeight(); Block[] blocks = prefab.getBlocks(); int guardBlocks = 0; String dungeonId = prefab.isDungeon() ? UUID.randomUUID().toString() : null; boolean decay = prefab.hasDecay(); - boolean mirrored = prefab.isMirrorable() && random.nextBoolean(); Map replacedItems = new HashMap<>(); // Replacements @@ -477,8 +810,8 @@ public void placePrefab(Prefab prefab, int x, int y, Random random, long seed) { Item backItem = replacedItems.getOrDefault(block.getBackItem(), block.getBackItem()); Item frontItem = replacedItems.getOrDefault(block.getFrontItem(), block.getFrontItem()); Item liquidItem = replacedItems.getOrDefault(block.getLiquidItem(), block.getLiquidItem()); - int backMod = block.getBackMod(); - int frontMod = block.getFrontMod(); + int backMod = backItem.getMod() == block.getBackItem().getMod() ? block.getBackMod() : 0; + int frontMod = frontItem.getMod() == block.getFrontItem().getMod() ? block.getFrontMod() : 0; int liquidMod = block.getLiquidMod(); // Update base item if it isn't empty @@ -510,6 +843,15 @@ public void placePrefab(Prefab prefab, int x, int y, Random random, long seed) { frontMod = random.nextInt(4) + 1; } + // Try to place rubble + if(decay && frontItem.isWhole() && !isBlockOccupied(x + i, y + j - 1, Layer.FRONT) && random.nextDouble() <= 0.2) { + RubbleType[] types = RubbleType.values(); + RubbleType type = types[random.nextInt(types.length)]; + String[] itemIds = type.getItemIds(); + Item item = ItemRegistry.getItem(itemIds[random.nextInt(itemIds.length)]); + updateBlock(x + i, y + j - 1, Layer.FRONT, item); + } + int offset = mirrored ? -(frontItem.getBlockWidth() - 1) : 0; // Clear the block it would normally occupy @@ -655,14 +997,70 @@ public boolean isDungeonIntact(String id) { return dungeons.containsKey(id); } - public void digBlock(int x, int y) { + public boolean digBlock(int x, int y) { if(!areCoordinatesInBounds(x, y)) { - return; + return false; } Block block = getBlock(x, y); - digQueue.add(new DugBlock(x, y, block.getFrontItem(), block.getFrontMod(), System.currentTimeMillis() + 10000)); + Item item = block.getFrontItem(); + + if(!item.isDiggable()) { + return !item.isWhole(); + } + + int mod = block.getFrontMod(); updateBlock(x, y, Layer.FRONT, "ground/earth-dug"); + addBlockTimer(x, y, 10000, () -> { + if(block.getFrontItem().hasId("ground/earth-dug")) { + updateBlock(x, y, Layer.FRONT, item, mod); + } + }); + + return true; + } + + public void addBlockTimer(int x, int y, long delay, Runnable task) { + removeBlockTimer(x, y); + blockTimers.add(new Timer<>(getBlockIndex(x, y), delay, task)); + } + + public void removeBlockTimer(int x, int y) { + blockTimers.removeIf(timer -> timer.getKey() == getBlockIndex(x, y)); + } + + public void processBlockTimer(int x, int y) { + Timer timer = blockTimers.stream().filter(t -> t.getKey() == getBlockIndex(x, y)).findFirst().orElse(null); + + if(timer != null) { + timer.process(true); + } + } + + public void startMinigame(Minigame minigame) { + int index = getBlockIndex(minigame.getX(), minigame.getY()); + Minigame currentMinigame = minigames.get(index); + + // Don't start minigame if a minigame is already active at this location + if(currentMinigame != null && currentMinigame.isActive()) { + minigame.notifyCreator("Another minigame is already in progress at that location."); + return; + } + + minigames.put(index, minigame); + minigame.start(); + } + + public Minigame getMinigame(int x, int y) { + return getMinigame(getBlockIndex(x, y)); + } + + public Minigame getMinigame(int index) { + return minigames.get(index); + } + + public int getMinigameCount() { + return minigames.size(); } public void updateBlock(int x, int y, Layer layer, int item) { @@ -715,13 +1113,16 @@ public void updateBlock(int x, int y, Layer layer, Item item, int mod, Player ow } Chunk chunk = getChunk(x, y); - chunk.getBlock(x, y).updateLayer(layer, item, mod); + chunk.getBlock(x, y).updateLayer(layer, item, mod, owner == null ? 0 : owner.getBlockHash()); // TODO owner hash should get updated on place only!! chunk.setModified(true); + modified = true; // TODO this alone is NOT sufficient! // Queue block update if there are players in this zone. // TODO maybe check if the block update was in an active chunk, too? if(!getPlayers().isEmpty()) { - blockChanges.add(new BlockChangeData(x, y, layer, item, mod)); + int z = layer.ordinal(); + int changeIndex = z * width * height + getBlockIndex(x, y); + blockChanges.put(changeIndex, new BlockChangeData(x, y, layer, 0, item, mod)); // TODO entity id } if(layer == Layer.FRONT) { @@ -739,7 +1140,10 @@ public void updateBlock(int x, int y, Layer layer, Item item, int mod, Player ow removeMetaBlock(x, y); } + removeBlockTimer(x, y); entityManager.trySpawnBlockEntity(x, y); + steamManager.indexBlock(x, y, item); + growthManager.indexBlock(x, y, item); if(item.isWhole() && y < sunlight[x]) { sunlight[x] = y; @@ -747,7 +1151,7 @@ public void updateBlock(int x, int y, Layer layer, Item item, int mod, Player ow recalculateSunlight(x, sunlight[x]); } - sendMessageToChunk(new LightMessage(x, getSunlight(x, 1)), chunk); + sendLocalMessage(new LightMessage(x, getSunlight(x, 1)), chunk); } else if(layer == Layer.LIQUID) { if(!item.isAir() && mod > 0) { liquidManager.indexLiquidBlock(x, y); @@ -755,6 +1159,22 @@ public void updateBlock(int x, int y, Layer layer, Item item, int mod, Player ow } } + // TODO better block update methods + public void updateBlockMod(int x, int y, Layer layer, int mod) { + if(!areCoordinatesInBounds(x, y)) { + return; + } + + Block block = getBlock(x, y); + block.setMod(layer, mod); + + if(!getPlayers().isEmpty()) { + int z = layer.ordinal(); + int changeIndex = z * width * height + getBlockIndex(x, y); + blockChanges.put(changeIndex, new BlockChangeData(x, y, layer, 0, block.getItem(layer), mod)); + } + } + /** * @param x The x position of the block. * @param y The y position of the block. @@ -813,8 +1233,7 @@ public void setMetaBlock(int x, int y, Item item, Player owner, Map metaBlocks) { @@ -860,14 +1288,6 @@ protected void setMetaBlocks(List metaBlocks) { indexDungeons(); } - private boolean ownsMetaBlock(MetaBlock metaBlock, Player player) { - if(!metaBlock.hasOwner()) { - return false; - } - - return player.getDocumentId().equals(metaBlock.getOwner()); - } - public MetaBlock getMetaBlock(int x, int y) { return metaBlocks.get(getBlockIndex(x, y)); } @@ -882,6 +1302,11 @@ public MetaBlock getRandomSpawnBlock() { return spawnBlocks.isEmpty() ? null : spawnBlocks.get((int)(Math.random() * spawnBlocks.size())); } + public boolean isSpawnInRange(int x, int y, double range) { + return metaBlocks.values().stream().anyMatch(block -> (block.getItem().hasId("mechanical/zone-teleporter") + || block.getItem().hasId("signs/obelisk-spawn")) && MathUtils.inRange(block.getX(), block.getY(), x, y, range)); + } + public List getMetaBlocksWithUse(ItemUseType useType) { return getMetaBlocks(metaBlock -> metaBlock.getItem().hasUse(useType)); } @@ -915,18 +1340,30 @@ public Collection getGlobalMetaBlocks() { return Collections.unmodifiableCollection(globalMetaBlocks.values()); } - public List getEntitiesInRange(float x, float y, float range) { + public List getEntitiesInRange(float x, float y, double range) { return entityManager.getEntitiesInRange(x, y, range); } - public Player getRandomPlayerInRange(float x, float y, float range) { + public Player getRandomPlayerInRange(float x, float y, double range) { return entityManager.getRandomPlayerInRange(x, y, range); } - public List getPlayersInRange(float x, float y, float range) { + public List getPlayersInRange(float x, float y, double range) { return entityManager.getPlayersInRange(x, y, range); } + public void spawnPersistentNpcs(Collection data) { + entityManager.spawnPersistentNpcs(data); + } + + public Npc spawnEntity(String type, int x, int y) { + return entityManager.spawnEntity(type, x, y); + } + + public Npc spawnEntity(String type, int x, int y, boolean effect) { + return entityManager.spawnEntity(type, x, y, effect); + } + public void spawnEntity(Entity entity, int x, int y) { entityManager.spawnEntity(entity, x, y); } @@ -967,6 +1404,10 @@ public Collection getNpcs() { return entityManager.getNpcs(); } + public List getPersistentNpcs() { + return entityManager.getPersistentNpcs(); + } + public Player getPlayer(int entityId) { return entityManager.getPlayer(entityId); } @@ -987,6 +1428,64 @@ public int settleLiquids() { return liquidManager.settleLiquids(); } + public void updateGrowables(int rainCycles) { + growthManager.updateGrowables(rainCycles); + } + + public boolean isMachineActive(EcologicalMachine machine) { + return machineManager.isMachineActive(machine); + } + + public void sendMachineStatus(Player player) { + machineManager.sendMachineStatus(player); + } + + public boolean addMachinePart(Item part) { + return machineManager.addMachinePart(part); + } + + public boolean removeMachinePart(Item part) { + return machineManager.removeMachinePart(part); + } + + public Collection getDiscoveredParts(EcologicalMachine machine) { + return machineManager.getDiscoveredParts(machine); + } + + public Map> getDiscoveredParts() { + return machineManager.getDiscoveredParts(); + } + + public MachineManager getMachineManager() { + return machineManager; + } + + public void recordActionTime(String name) { + actionHistory.put(name.toLowerCase(), OffsetDateTime.now()); + } + + public boolean isActionOnCooldown(String name, long cooldown, TemporalUnit unit) { + return actionHistory.containsKey(name.toLowerCase()) && !OffsetDateTime.now().isAfter(actionHistory.get(name.toLowerCase()).plus(cooldown, unit)); + } + + public Map getActionHistory() { + return Collections.unmodifiableMap(actionHistory); + } + + /** + * @return The specified coordinates in a player-readable format + * For example, {@code x: 200 y: 300} in a plain biome becomes {@code 800 west, 100 below} + */ + public String getReadableCoordinates(int x, int y) { + int center = width / 2; + int surface = biome == Biome.DEEP ? -1000 : 200; + String directionX = x < center ? "west" : x > center ? "east" : "central"; + String directionY = y > surface ? "below" : "above"; + String coordX = String.format("%s %s", Math.abs(x - center), directionX); + String coordY = String.format("%s %s", Math.abs(y - surface), directionY); + return String.format("%s, %s", coordX, coordY); + } + public boolean areCoordinatesInBounds(int x, int y) { return x >= 0 && y >= 0 && x < width && y < height; } @@ -994,31 +1493,51 @@ public boolean areCoordinatesInBounds(int x, int y) { protected void onChunkLoaded(Chunk chunk) { int chunkX = chunk.getX(); int chunkY = chunk.getY(); + List growthSourceIndices = new ArrayList<>(); for(int x = chunkX; x < chunkX + chunk.getWidth(); x++) { // Update pending sunlight if(pendingSunlight.contains(x)) { recalculateSunlight(x, sunlight[x]); - sendMessageToChunk(new LightMessage(x, getSunlight(x, 1)), chunk); + sendLocalMessage(new LightMessage(x, getSunlight(x, 1)), chunk); } for(int y = chunkY; y < chunkY + chunk.getHeight(); y++) { // Spawn block-related entities entityManager.trySpawnBlockEntity(x, y); - - // Index liquids Block block = chunk.getBlock(x, y); + // Index front item + Item item = block.getFrontItem(); + steamManager.indexBlock(x, y, item); + + if(growthManager.indexBlock(x, y, item)) { + growthSourceIndices.add(getBlockIndex(x, y)); + } + + // Index liquids if(!block.getLiquidItem().isAir() && block.getLiquidMod() > 0) { liquidManager.indexLiquidBlock(x, y); } } } + + // Simulate plant growth based on time passed since chunk was last loaded + int cycles = (int)((System.currentTimeMillis() - chunk.getSaveTime()) / 1200000); // One cycle per 20 minutes + growthManager.updateGrowables(cycles, growthSourceIndices); } - protected void onChunkUnloaded(Chunk chunk) { - // TODO is this function ever gonna be necessary? - // It seems that most (if not all) thingies are unindexed automatically. + protected void onChunkUnloaded(Chunk chunk) { + for(int x = 0; x < chunk.getWidth(); x++) { + for(int y = 0; y < chunk.getHeight(); y++) { + int index = getBlockIndex(chunk.getX() + x, chunk.getY() + y); + Minigame minigame = getMinigame(index); + + if(minigame != null) { + minigame.finish(); // Unload active minigame + } + } + } } public void saveChunks() { @@ -1072,6 +1591,10 @@ public WeatherManager getWeatherManager() { return weatherManager; } + public boolean isUnderground(int x, int y) { + return areCoordinatesInBounds(x, y) && y >= surface[x]; + } + public void setSurface(int x, int surface) { if(areCoordinatesInBounds(x, surface)) { this.surface[x] = surface; @@ -1161,10 +1684,19 @@ public boolean exploreArea(int x, int y, Player explorer) { explorer.getStatistics().trackAreaExplored(); } + chunksExploredCount++; sendMessage(new ZoneExploredMessage(chunkIndex)); return chunksExplored[chunkIndex] = true; } + public boolean isAreaExplored(int x, int y) { + return areCoordinatesInBounds(x, y) && chunksExplored[getChunkIndex(x, y)]; + } + + protected byte[] getSteamData() { + return steamManager.getData(); + } + public File getDirectory() { return new File("zones", documentId); } @@ -1181,15 +1713,17 @@ public boolean[] getChunksExplored() { } public int getChunksExploredCount() { - int count = 0; + return chunksExploredCount; + } + + private void recalculateChunksExploredCount() { + chunksExploredCount = 0; for(boolean explored : chunksExplored) { if(explored) { - count++; + chunksExploredCount++; } } - - return count; } @JsonValue @@ -1201,8 +1735,13 @@ public int getSeed() { return (int)(UUID.fromString(documentId).getMostSignificantBits() >> 32); } + /** + * @deprecated DO NOT CALL DIRECTLY. + * If you have to rename a zone, please use {@link ZoneManager#renameZone(Zone, String)}. + */ public void setName(String name) { this.name = name; + kickAllPlayers("Zone name changed.", true); } public String getName() { @@ -1257,9 +1796,132 @@ public float getAcidity() { return acidity; } + public void setPrivate(boolean value) { + this.isPrivate = value; + kickAllPlayers("Accessibility status changed.", true); // The login handler will kick non-members out of the zone if the world is made private + } + + public boolean canJoin(Player player) { + return player.isGodMode() || isPublic() || isOwner(player) || isMember(player); + } + + public boolean isPublic() { + return !isPrivate(); + } + + public boolean isPrivate() { + return isPrivate; + } + + public void setProtected(boolean value) { + this.isProtected = value; + kickAllPlayers("Protection status changed.", true); + } + + public boolean isProtected(Player player) { + return isProtected && !isOwner(player) && !isMember(player); + } + + public boolean isProtected() { + return isProtected; + } + + public void setPvp(boolean pvp) { + this.pvp = pvp; + kickAllPlayers("PvP status changed.", true); + } + + public boolean isPvp() { + return pvp; + } + + protected void setEntryCode(String entryCode) { + this.entryCode = entryCode; + } + + public boolean hasEntryCode() { + return entryCode != null; + } + + public String getEntryCode() { + return entryCode; + } + + public boolean isOwner(Player player) { + return isOwned() && player.getDocumentId().equals(owner); + } + + public boolean isOwned() { + return owner != null; + } + + public void setOwner(Player player) { + this.owner = player.getDocumentId(); + + // Update spawn teleporter ownership + for(MetaBlock block : getMetaBlocksWithItem("mechanical/zone-teleporter")) { + block.setOwner(owner); + sendBlockMetaUpdate(block); + } + } + + public String getOwner() { + return owner; + } + + public void addMember(Player player) { + members.add(player.getDocumentId()); + + // Force player to reconnect if they're currently in this zone + if(player.getZone() == this) { + player.kick("Member status changed.", true); + } + } + + public void removeMember(Player player) { + members.remove(player.getDocumentId()); + + // Kick the player from the world if they are currently in it or force them to reconnect if the world is public + if(player.getZone() == this) { + if(isPublic()) { + player.kick("Member status changed.", true); + } else { + player.changeZone(null); + } + } + } + + public boolean isMember(Player player) { + return members.contains(player.getDocumentId()); + } + + public List getMembers() { + return Collections.unmodifiableList(members); + } + public OffsetDateTime getCreationDate() { return creationDate; } + + public boolean isUnexplored() { + return this.getExplorationProgress() < 0.7; + } + + public boolean isPopular() { + return this.getPlayers().size() > 0; + } + + public boolean isPurified() { + return acidity < 0.05F; + } + + public void setModified(boolean modified) { + this.modified = modified; + } + + public boolean isModified() { + return modified; + } /** * @return A {@link Map} containing all the data necessary for use in {@link ConfigurationMessage}. @@ -1274,6 +1936,13 @@ public Map getClientConfig(Player player) { config.put("surface", surface); config.put("chunks_explored", chunksExplored); config.put("chunks_explored_count", getChunksExploredCount()); + config.put("private", isPrivate); + config.put("protected", isProtected(player)); + config.put("protected_player", isProtected(player)); + config.put("owner", isOwner(player)); + config.put("member", isMember(player)); + config.put("pvp", pvp); + config.put("bookmarked", player.isZoneBookmarked(this)); Map depth = new HashMap<>(); List earth = new ArrayList<>(); @@ -1297,16 +1966,16 @@ public Map getClientConfig(Player player) { /** * @return A {@link Map} containing all the data necessary for use in {@link ZoneStatusMessage}. */ - public Map getStatusConfig() { - Map config = new HashMap<>(); - config.put("w", new int[] { - (int)(time * 10000), - (int)(temperature * 10000), - (int)(weatherManager.getPrecipitation() * 10000), - (int)(weatherManager.getPrecipitation() * 10000), - (int)(weatherManager.getPrecipitation() * 10000), - (int)(acidity * 10000) - }); - return config; + public Object getStatusConfig(Player player) { + int[] status = { + (int)(time * 10000), + (int)(temperature * 10000), + (int)(weatherManager.getPrecipitation() * 10000), + (int)(weatherManager.getPrecipitation() * 10000), + (int)(weatherManager.getPrecipitation() * 10000), + (int)(acidity * 10000) + }; + + return player.hasClientVersion("2.1.0") ? MapHelper.map("w", status) : status; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneConfigFile.java b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneConfigFile.java index 567bc9a4..f45cddb9 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneConfigFile.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneConfigFile.java @@ -1,6 +1,10 @@ package brainwine.gameserver.zone; import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -8,6 +12,8 @@ import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; +import brainwine.gameserver.item.Item; + @JsonIgnoreProperties(ignoreUnknown = true) public class ZoneConfigFile { @@ -26,21 +32,35 @@ public class ZoneConfigFile { @JsonSetter(nulls = Nulls.SKIP) private float acidity = 1.0F; + @JsonSetter(value = "private") + private boolean isPrivate; + + @JsonSetter(value = "protected") + private boolean isProtected; + @JsonSetter(nulls = Nulls.SKIP) - private OffsetDateTime creationDate = OffsetDateTime.now(); + private boolean pvp; - public ZoneConfigFile(Zone zone) { - this(zone.getName(), zone.getBiome(), zone.getWidth(), zone.getHeight(), zone.getAcidity(), zone.getCreationDate()); - } + @JsonSetter(nulls = Nulls.SKIP) + private String entryCode; - public ZoneConfigFile(String name, Biome biome, int width, int height, float acidity, OffsetDateTime creationDate) { - this.name = name; - this.biome = biome; - this.width = width; - this.height = height; - this.acidity = acidity; - this.creationDate = creationDate; - } + @JsonSetter(nulls = Nulls.SKIP) + private String owner; + + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + private List members = new ArrayList<>(); + + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + private Map> discoveredParts = new HashMap<>(); + + @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) + private Map actionHistory = new HashMap<>(); + + @JsonSetter(nulls = Nulls.SKIP) + private OffsetDateTime creationDate = OffsetDateTime.now(); + + @JsonSetter(nulls = Nulls.SKIP) + private OffsetDateTime lastActiveDate = OffsetDateTime.now(); @JsonCreator private ZoneConfigFile(@JsonProperty(value = "name", required = true) String name, @@ -51,6 +71,23 @@ private ZoneConfigFile(@JsonProperty(value = "name", required = true) String nam this.height = height; } + public ZoneConfigFile(Zone zone) { + this.name = zone.getName(); + this.biome = zone.getBiome(); + this.width = zone.getWidth(); + this.height = zone.getHeight(); + this.acidity = zone.getAcidity(); + this.isPrivate = zone.isPrivate(); + this.isProtected = zone.isProtected(); + this.pvp = zone.isPvp(); + this.entryCode = zone.getEntryCode(); + this.owner = zone.getOwner(); + this.members = zone.getMembers(); + this.discoveredParts = zone.getDiscoveredParts(); + this.actionHistory = zone.getActionHistory(); + this.creationDate = zone.getCreationDate(); + } + public String getName() { return name; } @@ -71,7 +108,43 @@ public float getAcidity() { return acidity; } + public boolean isPrivate() { + return isPrivate; + } + + public boolean isProtected() { + return isProtected; + } + + public boolean isPvp() { + return pvp; + } + + public String getEntryCode() { + return entryCode; + } + + public String getOwner() { + return owner; + } + + public List getMembers() { + return members; + } + + public Map> getDiscoveredParts() { + return discoveredParts; + } + + public Map getActionHistory() { + return actionHistory; + } + public OffsetDateTime getCreationDate() { return creationDate; } + + public OffsetDateTime getLastActiveDate() { + return lastActiveDate; + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneDataFile.java b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneDataFile.java index 6eeaf4a6..1e5fc977 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneDataFile.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneDataFile.java @@ -26,16 +26,20 @@ public class ZoneDataFile { @JsonSetter(nulls = Nulls.AS_EMPTY) private boolean[] chunksExplored = {}; + @JsonSetter(nulls = Nulls.AS_EMPTY) + private byte[] steamData = {}; + public ZoneDataFile(Zone zone) { - this(zone.getSurface(), zone.getSunlight(), zone.getDepths(), zone.getPendingSunlight(), zone.getChunksExplored()); + this(zone.getSurface(), zone.getSunlight(), zone.getDepths(), zone.getPendingSunlight(), zone.getChunksExplored(), zone.getSteamData()); } - public ZoneDataFile(int[] surface, int[] sunlight, int[] depths, Collection pendingSunlight, boolean[] chunksExplored) { + public ZoneDataFile(int[] surface, int[] sunlight, int[] depths, Collection pendingSunlight, boolean[] chunksExplored, byte[] steamData) { this.surface = surface; this.sunlight = sunlight; this.depths = depths; this.pendingSunlight = pendingSunlight; this.chunksExplored = chunksExplored; + this.steamData = steamData; } @JsonCreator @@ -60,4 +64,8 @@ public Collection getPendingSunlight() { public boolean[] getChunksExplored() { return chunksExplored; } + + public byte[] getSteamData() { + return steamData; + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java index 9c8e1eb0..afa056d7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/ZoneManager.java @@ -5,6 +5,8 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; @@ -13,20 +15,21 @@ import java.util.List; import java.util.Map; import java.util.function.Predicate; -import java.util.zip.DataFormatException; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.msgpack.core.MessagePack; -import org.msgpack.core.MessageUnpacker; import org.msgpack.jackson.dataformat.MessagePackFactory; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import brainwine.gameserver.GameServer; +import brainwine.gameserver.entity.npc.NpcData; import brainwine.gameserver.util.ZipUtils; import brainwine.gameserver.zone.gen.ZoneGenerator; import brainwine.shared.JsonHelper; +import brainwine.shared.TokenGenerator; public class ZoneManager { @@ -36,6 +39,9 @@ public class ZoneManager { private final File dataDir = new File("zones"); private Map zones = new HashMap<>(); private Map zonesByName = new HashMap<>(); + private Map entryCodes = new HashMap<>(); + private long lastZoneGenerationTime = System.currentTimeMillis(); + private boolean generatingZone = false; public ZoneManager() { logger.info(SERVER_MARKER, "Loading zone data ..."); @@ -63,7 +69,7 @@ public void tryGenerateDefaultZone() { generator = ZoneGenerator.getDefaultZoneGenerator(); } - Zone zone = generator.generateZone(Biome.PLAIN, 2000, 600); + Zone zone = generator.generateZone(Biome.PLAIN); addZone(zone); } @@ -71,6 +77,40 @@ public void tick(float deltaTime) { for(Zone zone : getZones()) { zone.tick(deltaTime); } + + long timeSinceLastGeneration = (System.currentTimeMillis() - lastZoneGenerationTime) / 1000; + + // zero players interval has to be greater than the min generation interval + final long MIN_GENERATION_INTERVAL_SECONDS = 10 * 60; + final long GENERATION_INTERVAL_ZERO_PLAYERS_SECONDS = 30 * 60; + // player count influence has to be positive and a greater value means + // more players are needed for a given increase in generation rate + final long PLAYER_COUNT_INFLUENCE = 16; + + if (!generatingZone && timeSinceLastGeneration > MIN_GENERATION_INTERVAL_SECONDS) { + int playerCount = GameServer.getInstance().getPlayerManager().getOnlinePlayerCount(); + long requiredInterval = Math.max( + MIN_GENERATION_INTERVAL_SECONDS, + GENERATION_INTERVAL_ZERO_PLAYERS_SECONDS - (playerCount - 1) * (GENERATION_INTERVAL_ZERO_PLAYERS_SECONDS - MIN_GENERATION_INTERVAL_SECONDS) / PLAYER_COUNT_INFLUENCE + ); + + if (timeSinceLastGeneration > requiredInterval) { + if (shouldGenerateUnexploredZone() && !generatingZone) { + generatingZone = true; + Biome biome = Biome.getRandomBiome(); + ZoneGenerator generator = ZoneGenerator.getZoneGenerator(biome); + generator.generateZoneAsync(biome, zone -> { + if (zone != null) { + this.addZone(zone); + lastZoneGenerationTime = System.currentTimeMillis(); + } else { + logger.warn(SERVER_MARKER, "Automatic zone generation failed. See the previous logs for more information."); + } + generatingZone = false; + }); + } + } + } } public void onShutdown() { @@ -84,62 +124,38 @@ private void loadZone(File file) { String id = file.getName(); File dataFile = new File(file, "zone.dat"); File legacyDataFile = new File(file, "shape.cmp"); + File configFile = new File(file, "config.json"); + File metaBlocksFile = new File(file, "metablocks.json"); + File charactersFile = new File(file, "characters.json"); - try { - ZoneDataFile data = null; - + try { if(legacyDataFile.exists() && !dataFile.exists()) { - data = convertLegacyDataFile(legacyDataFile, dataFile); - // legacyDataFile.delete(); Let's just keep it.. - } else { - data = mapper.readValue(ZipUtils.inflateBytes(Files.readAllBytes(dataFile.toPath())), ZoneDataFile.class); + throw new IOException("Zone data format is outdated. Please try to load this zone with an older server version to update it."); } - ZoneConfigFile config = JsonHelper.readValue(new File(file, "config.json"), ZoneConfigFile.class); + ZoneDataFile data = mapper.readValue(ZipUtils.inflateBytes(Files.readAllBytes(dataFile.toPath())), ZoneDataFile.class); + ZoneConfigFile config = JsonHelper.readValue(configFile, ZoneConfigFile.class); Zone zone = new Zone(id, config, data); - zone.setMetaBlocks(JsonHelper.readList(new File(file, "metablocks.json"), MetaBlock.class)); + + // Load meta blocks + if(metaBlocksFile.exists()) { + zone.setMetaBlocks(JsonHelper.readList(metaBlocksFile, MetaBlock.class)); + } + + // Load characters + if(charactersFile.exists()) { + zone.spawnPersistentNpcs(JsonHelper.readList(charactersFile, NpcData.class)); + } + + zone.simulate(ChronoUnit.SECONDS.between(config.getLastActiveDate(), OffsetDateTime.now())); addZone(zone); } catch (Exception e) { logger.error(SERVER_MARKER, "Zone load failure. id: {}", id, e); } } - private ZoneDataFile convertLegacyDataFile(File legacyFile, File outputFile) throws IOException, DataFormatException { - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(ZipUtils.inflateBytes(Files.readAllBytes(legacyFile.toPath()))); - int[] surface = new int[unpacker.unpackArrayHeader()]; - - for(int i = 0; i < surface.length; i++) { - surface[i] = unpacker.unpackInt(); - } - - int[] sunlight = new int[unpacker.unpackArrayHeader()]; - - for(int i = 0; i < sunlight.length; i++) { - sunlight[i] = unpacker.unpackInt(); - } - - List pendingSunlight = new ArrayList<>(); - int pendingSunlightSize = unpacker.unpackArrayHeader(); - - for(int i = 0; i < pendingSunlightSize; i++) { - pendingSunlight.add(unpacker.unpackInt()); - } - - boolean[] chunksExplored = new boolean[unpacker.unpackArrayHeader()]; - - for(int i = 0; i < chunksExplored.length; i++) { - chunksExplored[i] = unpacker.unpackBoolean(); - } - - ZoneDataFile data = new ZoneDataFile(surface, sunlight, null, pendingSunlight, chunksExplored); - Files.write(outputFile.toPath(), ZipUtils.deflateBytes(mapper.writeValueAsBytes(data))); - return data; - } - public void saveZones() { - for(Zone zone : getZones()) { - saveZone(zone); - } + zones.values().stream().filter(Zone::isModified).forEach(this::saveZone); } public void saveZone(Zone zone) { @@ -147,10 +163,19 @@ public void saveZone(Zone zone) { file.mkdirs(); try { + // Serialize everything before writing to disk to minimize risk of data corruption if something goes wrong + byte[] charactersBytes = JsonHelper.writeValueAsBytes(zone.getPersistentNpcs().stream().map(NpcData::new).collect(Collectors.toList())); + byte[] metaBlocksBytes = JsonHelper.writeValueAsBytes(zone.getMetaBlocks()); + byte[] configBytes = JsonHelper.writeValueAsBytes(new ZoneConfigFile(zone)); + byte[] dataBytes = ZipUtils.deflateBytes(mapper.writeValueAsBytes(new ZoneDataFile(zone))); + + // Write data to files zone.saveChunks(); - JsonHelper.writeValue(new File(file, "metablocks.json"), zone.getMetaBlocks()); - JsonHelper.writeValue(new File(file, "config.json"), new ZoneConfigFile(zone)); - Files.write(new File(file, "zone.dat").toPath(), ZipUtils.deflateBytes(mapper.writeValueAsBytes(new ZoneDataFile(zone)))); + Files.write(new File(file, "characters.json").toPath(), charactersBytes); + Files.write(new File(file, "metablocks.json").toPath(), metaBlocksBytes); + Files.write(new File(file, "config.json").toPath(), configBytes); + Files.write(new File(file, "zone.dat").toPath(), dataBytes); + zone.setModified(false); } catch(Exception e) { logger.error(SERVER_MARKER, "Zone save failure. id: {}", zone.getDocumentId(), e); } @@ -167,20 +192,90 @@ public void addZone(Zone zone) { zones.put(id, zone); zonesByName.put(name.toLowerCase(), zone); + + if(zone.hasEntryCode()) { + entryCodes.put(zone.getEntryCode(), zone); + } + } + + /** + * Should the automatic zone generator generate a new zone? + * TODO could be slow, it might be a better idea to just check the most recently auto-generated zone instead. + * + * @return {@code true} if all unowned worlds are at least 40% explored, otherwise {@code false}. + */ + public boolean shouldGenerateUnexploredZone() { + return getZones().stream().filter(zone -> !zone.isOwned()).allMatch(zone -> zone.getExplorationProgress() >= 0.4); + } + + /** + * Renames the specified zone and re-indexes it. + * + * @return {@code true} if the renaming was successful, otherwise {@code false}. + */ + @SuppressWarnings("deprecation") + public boolean renameZone(Zone zone, String name) { + if(doesZoneExist(name)) { + return false; // Return false if name is already taken + } + + if(!zonesByName.remove(zone.getName().toLowerCase(), zone)) { + return false; // Sanity check + } + + zone.setName(name); + zonesByName.put(name.toLowerCase(), zone); + return true; + } + + /** + * Generates a new entry code for the specified zone and re-indexes it. + * + * @return {@code true} if the entry code was generated successfully, otherwise {@code false}. + */ + public boolean issueEntryCode(Zone zone) { + String entryCode = String.format("z%s", TokenGenerator.generateToken(6, entryCodes::containsKey)); + String currentCode = zone.getEntryCode(); + + if(entryCode == null) { + return false; + } + + if(currentCode != null && !entryCodes.remove(currentCode, zone)) { + logger.warn(SERVER_MARKER, "Could not unindex entry code {} for zone {}", currentCode, zone.getDocumentId()); + } + + zone.setEntryCode(entryCode); + entryCodes.put(entryCode, zone); + return true; } public Zone getZone(String id) { return zones.get(id); } + public boolean doesZoneExist(String name) { + return zonesByName.containsKey(name.toLowerCase()); + } + public Zone getZoneByName(String name) { return zonesByName.get(name.toLowerCase()); } - public Zone getRandomZone() { - List zones = new ArrayList<>(); - zones.addAll(getZones()); - return zones.get((int)(Math.random() * zones.size())); + public Zone getZoneByEntryCode(String entryCode) { + return entryCodes.get(entryCode); + } + + /** + * @return A public, non-owned, recently-generated temperate world (with players if possible) or {@code null} if no such world exists. + */ + public Zone findBeginnerZone() { + return zones.values().stream() + .filter(zone -> zone.isPublic() && !zone.isOwned() && zone.isUnexplored() && zone.getBiome() == Biome.PLAIN) + .sorted((a, b) -> b.getCreationDate().compareTo(a.getCreationDate())) + .limit(50) + .sorted((a, b) -> Integer.compare(b.getPlayerCount(), a.getPlayerCount())) + .findFirst().orElse(null); } public List searchZones(Predicate predicate) { diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/AsyncZoneGenerator.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/AsyncZoneGenerator.java index fdd637c3..364c0818 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/AsyncZoneGenerator.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/AsyncZoneGenerator.java @@ -49,14 +49,11 @@ public void run() { if(callback != null) { GameServer gameServer = GameServer.getInstance(); + gameServer.queueSynchronousTask(() -> callback.accept(generated)); + // Shouldn't be an issue, but log a warning anyway. if(gameServer.shouldStop()) { - logger.warn(SERVER_MARKER, "Server shutdown has been requested while generating a zone!" - + " Callback will be fired immediately on the async zone generator thread." - + " Don't blame me for what happens!"); - callback.accept(generated); - } else { - gameServer.queueSynchronousTask(() -> callback.accept(generated)); + logger.warn(SERVER_MARKER, "Server shutdown has been requested while generating a zone!"); } } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/GeneratorConfig.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/GeneratorConfig.java index eafb3f13..9fbbd197 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/GeneratorConfig.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/GeneratorConfig.java @@ -15,6 +15,7 @@ import brainwine.gameserver.zone.gen.caves.CaveDecorator; import brainwine.gameserver.zone.gen.caves.CaveType; import brainwine.gameserver.zone.gen.models.Deposit; +import brainwine.gameserver.zone.gen.models.LayerSeparator; import brainwine.gameserver.zone.gen.models.OreDeposit; import brainwine.gameserver.zone.gen.models.SpecialStructure; import brainwine.gameserver.zone.gen.models.StoneType; @@ -33,6 +34,7 @@ public class GeneratorConfig { private double dungeonChance = 0.25; private double backgroundAccentChance = 0.033; private double backgroundDrawingChance = 0.001; + private LayerSeparator layerSeparator; private WeightedMap stoneTypes = new WeightedMap<>(); private WeightedMap spawnBuildings = new WeightedMap<>(); private WeightedMap dungeons = new WeightedMap<>(); @@ -86,6 +88,10 @@ public double getBackgroundDrawingChance() { return backgroundDrawingChance; } + public LayerSeparator getLayerSeparator() { + return layerSeparator; + } + @JsonSetter(value = "stone_types", nulls = Nulls.SKIP) public WeightedMap getStoneTypes() { return stoneTypes; diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/GeneratorContext.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/GeneratorContext.java index b6f219ad..dac32a47 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/GeneratorContext.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/GeneratorContext.java @@ -1,29 +1,27 @@ package brainwine.gameserver.zone.gen; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Random; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Layer; import brainwine.gameserver.prefab.Prefab; import brainwine.gameserver.util.SimplexNoise; -import brainwine.gameserver.util.Vector2i; import brainwine.gameserver.zone.Biome; import brainwine.gameserver.zone.Block; import brainwine.gameserver.zone.Chunk; import brainwine.gameserver.zone.Zone; import brainwine.gameserver.zone.gen.caves.Cave; +import brainwine.gameserver.zone.gen.models.Structure; import brainwine.gameserver.zone.gen.surface.SurfaceRegion; public class GeneratorContext { private final List surfaceRegions = new ArrayList<>(); private final List caves = new ArrayList<>(); - private final Map prefabRegions = new HashMap<>(); + private final List structures = new ArrayList<>(); private final Zone zone; private final int seed; private final Random random; @@ -58,11 +56,12 @@ public List getCaves() { public boolean placePrefab(Prefab prefab, int x, int y) { x = Math.max(1, Math.min(x, getWidth() - prefab.getWidth() - 1)); - y = Math.max(1, Math.min(y, getHeight() - prefab.getHeight() - 3)); + y = Math.max(3, Math.min(y, getHeight() - prefab.getHeight() - 3)); if(!willPrefabOverlap(prefab, x, y)) { - zone.placePrefab(prefab, x, y, random, seed); - prefabRegions.put(new Vector2i(x, y), new Vector2i(prefab.getWidth(), prefab.getHeight())); + boolean mirrored = random.nextBoolean(); + zone.placePrefab(prefab, x, y, random, mirrored, seed); + structures.add(new Structure(prefab, x, y, mirrored)); return true; } @@ -147,13 +146,8 @@ private void placeScaffolding(int x, int y, int width, boolean ruin) { } public boolean willPrefabOverlap(Prefab prefab, int x, int y) { - for(Entry entry : prefabRegions.entrySet()) { - Vector2i position = entry.getKey(); - Vector2i size = entry.getValue(); - int x2 = position.getX(); - int y2 = position.getY(); - - if(x + prefab.getWidth() >= x2 && x <= x2 + size.getX() && y + prefab.getHeight() >= y2 && y <= y2 + size.getY()) { + for(Structure structure : structures) { + if(x + prefab.getWidth() >= structure.getX() && x <= structure.getX() + structure.getWidth() && y + prefab.getHeight() >= structure.getY() && y <= structure.getY() + structure.getHeight()) { return true; } } @@ -161,6 +155,21 @@ public boolean willPrefabOverlap(Prefab prefab, int x, int y) { return false; } + public boolean isStructureMirrored(int x, int y) { + Structure structure = getStructure(x, y); + return structure != null && structure.isMirrored(); + } + + public Structure getStructure(int x, int y) { + for(Structure structure : structures) { + if(x >= structure.getX() && x <= structure.getX() + structure.getWidth() && y >= structure.getY() && y <= structure.getY() + structure.getHeight()) { + return structure; + } + } + + return null; + } + public void updateBlock(int x, int y, Layer layer, int item) { zone.updateBlock(x, y, layer, item); } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/ZoneGenerator.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/ZoneGenerator.java index 0daaa6de..88ddc2a1 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/ZoneGenerator.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/ZoneGenerator.java @@ -2,7 +2,6 @@ import static brainwine.shared.LogMarkers.SERVER_MARKER; -import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.Random; @@ -13,10 +12,13 @@ import org.apache.logging.log4j.Logger; import brainwine.gameserver.GameServer; +import brainwine.gameserver.StringGenerator; import brainwine.gameserver.item.Layer; -import brainwine.gameserver.util.ResourceUtils; +import brainwine.gameserver.resource.Resource; +import brainwine.gameserver.resource.ResourceFinder; import brainwine.gameserver.zone.Biome; import brainwine.gameserver.zone.Zone; +import brainwine.gameserver.zone.gen.models.TerrainType; import brainwine.gameserver.zone.gen.tasks.CaveGeneratorTask; import brainwine.gameserver.zone.gen.tasks.DecorGeneratorTask; import brainwine.gameserver.zone.gen.tasks.GeneratorTask; @@ -26,38 +28,11 @@ public class ZoneGenerator { - // TODO Collect more names and create a name generator that's actually proper lmao - private static final String[] 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[] 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" - }; - private static final Logger logger = LogManager.getLogger(); private static final Map generators = new HashMap<>(); private static final ZoneGenerator defaultGenerator = new ZoneGenerator(); private static AsyncZoneGenerator asyncGenerator; + private final GeneratorConfig config; private final GeneratorTask terrainGenerator; private final GeneratorTask caveGenerator; private final GeneratorTask decorGenerator; @@ -68,6 +43,7 @@ public ZoneGenerator() { } public ZoneGenerator(GeneratorConfig config) { + this.config = config; terrainGenerator = new TerrainGeneratorTask(config); caveGenerator = new CaveGeneratorTask(config); decorGenerator = new DecorGeneratorTask(config); @@ -77,24 +53,20 @@ public ZoneGenerator(GeneratorConfig config) { public static void init() { generators.clear(); logger.info(SERVER_MARKER, "Loading zone generator configurations ..."); - ResourceUtils.copyDefaults("generators/"); - File dataDir = new File("generators"); - if(dataDir.isDirectory()) { - for(File file : dataDir.listFiles()) { - try { - String name = ResourceUtils.removeFileSuffix(file.getName()).toLowerCase(); - - if(generators.containsKey(name)) { - logger.warn(SERVER_MARKER, "Duplicate generator config name '{}'", name); - continue; - } - - GeneratorConfig config = JsonHelper.readValue(file, GeneratorConfig.class); - generators.put(name, new ZoneGenerator(config)); - } catch(Exception e) { - logger.error(SERVER_MARKER, "Failed to load generator config '{}'", file.getName(), e); - } + for(Resource resource : ResourceFinder.getResources("generators", false)) { + String name = ResourceFinder.removeFileSuffix(resource.getSimpleName()).toLowerCase(); + + if(generators.containsKey(name)) { + logger.warn(SERVER_MARKER, "Duplicate generator config name '{}'", name); + continue; + } + + try { + GeneratorConfig config = JsonHelper.readValue(resource.getUrl(), GeneratorConfig.class); + generators.put(name, new ZoneGenerator(config)); + } catch(Exception e) { + logger.error(SERVER_MARKER, "Failed to load generator config '{}'", name, e); } } @@ -149,7 +121,7 @@ public Zone generateZone() { } public Zone generateZone(Biome biome) { - return generateZone(biome, 2000, 600); + return generateZone(biome, biome == Biome.DEEP ? 1200 : 2000, biome == Biome.DEEP ? 1000 : 600); } public Zone generateZone(Biome biome, int width, int height) { @@ -158,18 +130,11 @@ public Zone generateZone(Biome biome, int width, int height) { public Zone generateZone(Biome biome, int width, int height, int seed) { String id = generateDocumentId(seed); - String name = getRandomName(); - int retryCount = 0; - - while(GameServer.getInstance().getZoneManager().getZoneByName(name) != null) { - if(retryCount >= 10) { - name = id; - logger.warn(SERVER_MARKER, "Could not generate a unique name for zone {}", id); - break; - } - - name = getRandomName(); - retryCount++; + String name = StringGenerator.getRandomZoneName(x -> GameServer.getInstance().getZoneManager().getZoneByName(x) != null, 20); + + if(name == null) { + name = id; + logger.warn(SERVER_MARKER, "Could not generate a unique name for zone {}", id); } Zone zone = new Zone(id, name, biome, width, height); @@ -182,6 +147,10 @@ public Zone generateZone(Biome biome, int width, int height, int seed) { // Bedrock for(int x = 0; x < width; x++) { ctx.updateBlock(x, height - 1, Layer.FRONT, "ground/bedrock"); + + if(config.getTerrainType() == TerrainType.FILLED) { + ctx.updateBlock(x, 0, Layer.FRONT, "ground/bedrock"); + } } return zone; @@ -192,7 +161,7 @@ public void generateZoneAsync(Consumer callback) { } public void generateZoneAsync(Biome biome, Consumer callback) { - generateZoneAsync(biome, 2000, 600, callback); + generateZoneAsync(biome, biome == Biome.DEEP ? 1200 : 2000, biome == Biome.DEEP ? 1000 : 600, callback); } public void generateZoneAsync(Biome biome, int width, int height, Consumer callback) { @@ -210,12 +179,6 @@ private static String generateDocumentId(int seed) { return new UUID(mostSigBits, leastSigBits).toString(); } - private static String getRandomName() { - String firstName = FIRST_NAMES[(int)(Math.random() * FIRST_NAMES.length)]; - String lastName = LAST_NAMES[(int)(Math.random() * LAST_NAMES.length)]; - return firstName + " " + lastName; - } - private static int getRandomSeed() { return (int)(Math.random() * Integer.MAX_VALUE); } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/models/LayerSeparator.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/models/LayerSeparator.java new file mode 100644 index 00000000..8bc144a6 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/models/LayerSeparator.java @@ -0,0 +1,51 @@ +package brainwine.gameserver.zone.gen.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import brainwine.gameserver.item.Item; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class LayerSeparator { + + @JsonProperty("item") + private Item item; + + @JsonProperty("min_thickness") + private int minThickness = 3; + + @JsonProperty("max_thickness") + private int maxThickness = 6; + + @JsonProperty("min_amplitude") + private double minAmplitude = 20; + + @JsonProperty("max_amplitude") + private double maxAimplitude = 20; + + @JsonCreator + private LayerSeparator(@JsonProperty(value = "item", required = true) Item item) { + this.item = item; + } + + public Item getItem() { + return item; + } + + public int getMinThickness() { + return minThickness; + } + + public int getMaxThickness() { + return maxThickness; + } + + public double getMinAmplitude() { + return minAmplitude; + } + + public double getMaxAmplitude() { + return maxAimplitude; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/models/Structure.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/models/Structure.java new file mode 100644 index 00000000..4156d364 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/models/Structure.java @@ -0,0 +1,47 @@ +package brainwine.gameserver.zone.gen.models; + +import brainwine.gameserver.prefab.Prefab; + +/** + * Instance of a placed structure in a zone for use in generator contexts. + * + * TODO we could store more information here in the future to allow for more specific structure edits after the fact. + */ +public class Structure { + + private final Prefab prefab; + private final int x; + private final int y; + private final boolean mirrored; + + public Structure(Prefab prefab, int x, int y, boolean mirrored) { + this.prefab = prefab; + this.x = x; + this.y = y; + this.mirrored = mirrored; + } + + public Prefab getPrefab() { + return prefab; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public int getWidth() { + return prefab.getWidth(); + } + + public int getHeight() { + return prefab.getHeight(); + } + + public boolean isMirrored() { + return mirrored; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/CaveGeneratorTask.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/CaveGeneratorTask.java index 40f5868a..3921ee75 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/CaveGeneratorTask.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/CaveGeneratorTask.java @@ -99,11 +99,11 @@ public void generate(GeneratorContext ctx) { // Generate a cave wall with a thickness depending on the size of the cave if(asteroids || stoneType != StoneType.DEFAULT) { ctx.updateBlock(x, y, Layer.BASE, stoneType.getBaseItem()); - int checkDistance = asteroids? 5 : 3; + int checkDistance = asteroids ? 5 : 3; for(int i = x - checkDistance; i <= x + checkDistance; i++) { for(int j = y - checkDistance; j <= y + checkDistance; j++) { - if(ctx.inBounds(i, j) && !cells[i][j]) { + if((asteroids ? ctx.isAir(i, j, Layer.FRONT) : ctx.isEarthy(i, j)) && !cells[i][j]) { double maxDistance = asteroids ? 4.5 + ctx.nextDouble() - 1 : MathUtils.clamp(cave.getSize() / 16.0, 1.8, checkDistance) + (ctx.nextDouble() - 0.5); double distance = Math.hypot(i - x, j - y); @@ -169,7 +169,8 @@ public boolean[][] generateCells(GeneratorContext ctx, double cellRate, int smoo for(int x = 0; x < width; x++) { for(int y = 0; y < height; y++) { - if((y >= ctx.getSurface(x) + ctx.nextInt(3)) && ctx.nextDouble() <= cellRate) { + if((terrainType == TerrainType.ASTEROIDS ? ctx.isAir(x, y, Layer.FRONT) : ctx.isEarthy(x, y)) + && (y >= ctx.getSurface(x) + ctx.nextInt(3)) && ctx.nextDouble() <= cellRate) { cells[x][y] = true; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/StructureGeneratorTask.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/StructureGeneratorTask.java index f484ffef..4384e25a 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/StructureGeneratorTask.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/StructureGeneratorTask.java @@ -1,12 +1,19 @@ package brainwine.gameserver.zone.gen.tasks; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.stream.Collectors; +import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Layer; import brainwine.gameserver.prefab.Prefab; import brainwine.gameserver.util.Vector2i; import brainwine.gameserver.util.WeightedMap; +import brainwine.gameserver.zone.Biome; +import brainwine.gameserver.zone.EcologicalMachine; import brainwine.gameserver.zone.MetaBlock; import brainwine.gameserver.zone.gen.GeneratorConfig; import brainwine.gameserver.zone.gen.GeneratorContext; @@ -14,8 +21,8 @@ import brainwine.gameserver.zone.gen.caves.CaveDecorator; import brainwine.gameserver.zone.gen.caves.CaveType; import brainwine.gameserver.zone.gen.caves.StructureCaveDecorator; -import brainwine.gameserver.zone.gen.models.TerrainType; import brainwine.gameserver.zone.gen.models.SpecialStructure; +import brainwine.gameserver.zone.gen.models.TerrainType; import brainwine.gameserver.zone.gen.surface.StructureSurfaceDecorator; import brainwine.gameserver.zone.gen.surface.SurfaceDecorator; import brainwine.gameserver.zone.gen.surface.SurfaceRegion; @@ -130,29 +137,131 @@ public void generate(GeneratorContext ctx) { } } - // Populate world with broken teleporters (TODO: world machines and component chests) - // Eligible containers are containers that are below surface and are either a mech chest or a non-dungeon red chest - List replaceableContainers = ctx.getZone().getMetaBlocks(metaBlock + // Fetch list of replaceable containers + List containers = ctx.getZone().getMetaBlocks(metaBlock -> ctx.isUnderground(metaBlock.getX(), metaBlock.getY()) && (metaBlock.getItem().hasId("containers/chest-mechanical-large") || (metaBlock.getItem().hasId("containers/chest") && !metaBlock.getMetadata().containsKey("@")))); - Collections.shuffle(replaceableContainers, ctx.getRandom()); - int brokenTeleporterCount = Math.min(replaceableContainers.size(), Math.max(1, width * height / (ctx.nextInt(80000) + 120000))); + Collections.shuffle(containers, ctx.getRandom()); + + // TODO + placeComponentChests(ctx, containers); + placeBrokenTeleporters(ctx, containers); + + if(ctx.getZone().getBiome() == Biome.HELL) { + placeInfernalProtectors(ctx, containers); + } + + } + + private void placeComponentChests(GeneratorContext ctx, List containers) { + // TODO remove test code + Biome biome = ctx.getZone().getBiome(); + List machines = new ArrayList<>(); + + switch(biome) { + case PLAIN: + machines.add(EcologicalMachine.PURIFIER); + machines.add(ctx.nextDouble() < 0.5 ? EcologicalMachine.RECYCLER : EcologicalMachine.COMPOSTER); + break; + case ARCTIC: + machines.add(EcologicalMachine.RECYCLER); + break; + case HELL: + machines.add(EcologicalMachine.EXPIATOR); + break; + case DESERT: + machines.add(EcologicalMachine.PURIFIER); + machines.add(EcologicalMachine.RECYCLER); + break; + case DEEP: + machines.add(EcologicalMachine.PURIFIER); + break; + default: + break; + } + + // Get list of machine parts to distribute + List parts = machines.stream().map(EcologicalMachine::getParts).flatMap(Collection::stream).collect(Collectors.toList()); + Iterator iterator = parts.iterator(); + + // Distribute parts + while(iterator.hasNext() && !containers.isEmpty()) { + MetaBlock container = containers.remove(0); + String dungeonId = container.getStringProperty("@"); + int x = container.getX(); + int y = container.getY(); + int offset = getContainerOffset(ctx, x, y); + ctx.updateBlock(x, y, Layer.FRONT, 0); + ctx.updateBlock(x + offset, y, Layer.FRONT, "containers/chest-industrial", 1); + MetaBlock componentChest = ctx.getZone().getMetaBlock(x + offset, y); + componentChest.setProperty("@", dungeonId); + componentChest.setProperty("$", iterator.next().getId()); + iterator.remove(); + } + + // If there are still parts left but no containers, just add 'em. + for(Item part : parts) { + ctx.getZone().addMachinePart(part); + } + } + + private void placeBrokenTeleporters(GeneratorContext ctx, List containers) { + // One teleporter per 150,000 blocks (8 teleporters in a normal sized world) + int amount = Math.min(containers.size(), Math.max(1, ctx.getWidth() * ctx.getHeight() / 150000)); + + // Place teleporters + for(int i = 0; i < amount; i++) { + MetaBlock container = containers.remove(0); + int x = container.getX(); + int y = container.getY(); + int offset = getContainerOffset(ctx, x, y); + ctx.updateBlock(x, y, Layer.FRONT, 0); + ctx.updateBlock(x + offset, y, Layer.FRONT, "mechanical/teleporter"); + ctx.getZone().removeMetaBlock(x + offset, y); // Broken teleporters should have no metadata + } + } + + private void placeInfernalProtectors(GeneratorContext ctx, List containers) { + // One dish per 100,000 blocks (12 dishes in a normal sized world) + int amount = Math.min(containers.size(), Math.max(1, ctx.getWidth() * ctx.getHeight() / 100000)); - for(int i = 0; i < brokenTeleporterCount; i++) { - MetaBlock container = replaceableContainers.remove(0); + for(int i = 0; i < amount; i++) { + MetaBlock container = containers.remove(0); int x = container.getX(); int y = container.getY(); - ctx.updateBlock(x, y, Layer.FRONT, "mechanical/teleporter"); - ctx.getZone().removeMetaBlock(x, y); // Broken teleporters should have no metadata + int offset = getContainerOffset(ctx, x, y); + + // Replace with large chest if container is a small chest + if(container.getItem().hasId("containers/chest")) { + ctx.updateBlock(x, y, Layer.FRONT, 0); + ctx.updateBlock(x + offset, y, Layer.FRONT, "containers/chest-large", 1, container.getMetadata()); + } + + // Place dish on top of the container + ctx.updateBlock(x + offset, y - 1, Layer.FRONT, "hell/dish"); } } + /** + * Crappy fix for badly placed teleporters, component chests etc. in mirrored structures. + * + * @return X offset for large containers. + */ + private int getContainerOffset(GeneratorContext ctx, int x, int y) { + if(ctx.isOccupied(x - 1, y, Layer.FRONT) || ctx.getBlock(x, y).getFrontItem().getBlockWidth() > 1) { + return 0; // TODO will cause issues if original container is larger than 2 blocks + } + + return ctx.isStructureMirrored(x, y) ? -1 : 0; + } + private void placeRandomSpawnBuilding(GeneratorContext ctx, int x) { Prefab spawnBuilding = spawnBuildings.next(ctx.getRandom()); if(filled) { - int y = ctx.getHeight() / 8 + ctx.nextInt(Math.max(1, ctx.nextInt(ctx.getHeight() / 8))); + int min = ctx.getHeight() / 32; + int y = min + ctx.nextInt(Math.max(1, ctx.nextInt(min))); ctx.placePrefab(spawnBuilding, x, y); } else { ctx.placePrefabSurface(spawnBuilding, x); diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/TerrainGeneratorTask.java b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/TerrainGeneratorTask.java index 26783f86..281dc861 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/TerrainGeneratorTask.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/gen/tasks/TerrainGeneratorTask.java @@ -1,10 +1,12 @@ package brainwine.gameserver.zone.gen.tasks; +import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Layer; import brainwine.gameserver.util.SimplexNoise; import brainwine.gameserver.util.WeightedMap; import brainwine.gameserver.zone.gen.GeneratorConfig; import brainwine.gameserver.zone.gen.GeneratorContext; +import brainwine.gameserver.zone.gen.models.LayerSeparator; import brainwine.gameserver.zone.gen.models.TerrainType; import brainwine.gameserver.zone.gen.surface.SurfaceRegion; import brainwine.gameserver.zone.gen.surface.SurfaceRegionType; @@ -14,6 +16,7 @@ public class TerrainGeneratorTask implements GeneratorTask { private final TerrainType type; private final double minAmplitude; private final double maxAmplitude; + private final LayerSeparator layerSeparator; private final int surfaceRegionSize; private final WeightedMap surfaceRegionTypes; @@ -21,6 +24,7 @@ public TerrainGeneratorTask(GeneratorConfig config) { type = config.getTerrainType(); minAmplitude = config.getMinAmplitude(); maxAmplitude = config.getMaxAmplitude(); + layerSeparator = config.getLayerSeparator(); surfaceRegionSize = config.getSurfaceRegionSize(); surfaceRegionTypes = config.getSurfaceRegionTypes(); } @@ -29,7 +33,7 @@ public TerrainGeneratorTask(GeneratorConfig config) { public void generate(GeneratorContext ctx) { int width = ctx.getWidth(); int height = ctx.getHeight(); - int surfaceLevel = height < 600 ? height / 3 : 200; + int surfaceLevel = type == TerrainType.ASTEROIDS ? (height < 600 ? height / 6 : 100) : (height < 600 ? height / 3 : 200); int lowestSurfaceLevel = 0; // Determine surface first, then start placing blocks. @@ -72,5 +76,24 @@ public void generate(GeneratorContext ctx) { ctx.updateBlock(x, y, Layer.BASE, "base/earth"); } } + + // Generate layer separators + if(layerSeparator != null) { + Item item = layerSeparator.getItem(); + int minThickness = layerSeparator.getMinThickness(); + int maxThickness = layerSeparator.getMaxThickness(); + double amplitude = ctx.nextDouble() * (layerSeparator.getMaxAmplitude() - layerSeparator.getMinAmplitude()) + layerSeparator.getMinAmplitude(); + + for(int depth : ctx.getZone().getDepths()) { + for(int x = 0; x < width; x++) { + int start = (int)(SimplexNoise.noise2(ctx.getSeed(), x / 256.0, 0, 7) * amplitude) + depth - maxThickness / 2; + int size = ctx.nextInt(maxThickness - minThickness) + minThickness; + + for(int y = start; y < start + size; y++) { + ctx.updateBlock(x, y, item.getLayer(), item); + } + } + } + } } } diff --git a/gameserver/src/main/resources/defaults/generators/arctic.json b/gameserver/src/main/resources/generators/arctic.json similarity index 100% rename from gameserver/src/main/resources/defaults/generators/arctic.json rename to gameserver/src/main/resources/generators/arctic.json diff --git a/gameserver/src/main/resources/defaults/generators/brain.json b/gameserver/src/main/resources/generators/brain.json similarity index 100% rename from gameserver/src/main/resources/defaults/generators/brain.json rename to gameserver/src/main/resources/generators/brain.json diff --git a/gameserver/src/main/resources/defaults/generators/deep.json b/gameserver/src/main/resources/generators/deep.json similarity index 97% rename from gameserver/src/main/resources/defaults/generators/deep.json rename to gameserver/src/main/resources/generators/deep.json index a218fd0e..f23768fe 100644 --- a/gameserver/src/main/resources/defaults/generators/deep.json +++ b/gameserver/src/main/resources/generators/deep.json @@ -4,6 +4,13 @@ "dungeon_chance": 0.4, "background_accent_chance": 0.033, "background_drawing_chance": 0.001, + "layer_separator": { + "item": "ground/blackrock", + "min_thickness": 3, + "max_thickness": 6, + "min_amplitude": 20, + "max_amplitude": 20 + }, "stone_types": { "default": 17, "limestone": 4 diff --git a/gameserver/src/main/resources/defaults/generators/desert.json b/gameserver/src/main/resources/generators/desert.json similarity index 100% rename from gameserver/src/main/resources/defaults/generators/desert.json rename to gameserver/src/main/resources/generators/desert.json diff --git a/gameserver/src/main/resources/defaults/generators/hell.json b/gameserver/src/main/resources/generators/hell.json similarity index 98% rename from gameserver/src/main/resources/defaults/generators/hell.json rename to gameserver/src/main/resources/generators/hell.json index c577dc75..67c5ddf5 100644 --- a/gameserver/src/main/resources/defaults/generators/hell.json +++ b/gameserver/src/main/resources/generators/hell.json @@ -7,6 +7,13 @@ "dungeon_chance": 0.375, "background_accent_chance": 0.033, "background_drawing_chance": 0.001, + "layer_separator": { + "item": "ground/blackrock", + "min_thickness": 3, + "max_thickness": 6, + "min_amplitude": 20, + "max_amplitude": 20 + }, "stone_types": { "default": 1 }, diff --git a/gameserver/src/main/resources/defaults/generators/plain.json b/gameserver/src/main/resources/generators/plain.json similarity index 100% rename from gameserver/src/main/resources/defaults/generators/plain.json rename to gameserver/src/main/resources/generators/plain.json diff --git a/gameserver/src/main/resources/defaults/generators/space.json b/gameserver/src/main/resources/generators/space.json similarity index 100% rename from gameserver/src/main/resources/defaults/generators/space.json rename to gameserver/src/main/resources/generators/space.json diff --git a/gameserver/src/main/resources/growth.json b/gameserver/src/main/resources/growth.json new file mode 100644 index 00000000..e277909c --- /dev/null +++ b/gameserver/src/main/resources/growth.json @@ -0,0 +1,196 @@ +{ + "growables": { + "vegetation/grass": { + "max_mod": 1, + "chance": 0.1 + }, + "vegetation/arctic-growth": { + "max_mod": 1, + "chance": 0.1 + }, + "vegetation/shrub-a": { + "max_mod": 11, + "chance": 0.1 + }, + "vegetation/shrub-b": { + "max_mod": 11, + "chance": 0.1 + }, + "vegetation/shrub-c": { + "max_mod": 14, + "chance": 0.1 + }, + "vegetation/shrub-d": { + "max_mod": 14, + "chance": 0.1 + }, + "vegetation/shrub-e": { + "max_mod": 14, + "chance": 0.1 + }, + "vegetation/shrub-f": { + "max_mod": 14, + "chance": 0.1 + }, + "vegetation/shrub-g": { + "max_mod": 14, + "chance": 0.1 + }, + "vegetation/flower-amaryllis": { + "max_mod": 4, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/flower-asphodelus": { + "max_mod": 4, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/flower-birds-foot-trefoil": { + "max_mod": 4, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/flower-carnations": { + "max_mod": 4, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/flower-coxcomb": { + "max_mod": 4, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/flower-delphinium": { + "max_mod": 4, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/flower-lobelia": { + "max_mod": 4, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/flower-tuberose": { + "max_mod": 4, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/flower-bird-of-paradise": { + "max_mod": 8, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/flower-cactus": { + "max_mod": 8, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/flower-echinacea": { + "max_mod": 8, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/flower-sunflower": { + "max_mod": 8, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/flower-trumpets": { + "max_mod": 8, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/tree-bonsai": { + "max_mod": 8, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/tree-hellish": { + "max_mod": 8, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/pumpkin": { + "max_mod": 4, + "chance": 0.1, + "replace_source": "ground/earth-compost" + }, + "vegetation/pine-tree": { + "max_mod": 10, + "chance": 0.05 + } + }, + "sources": { + "arctic": { + "ground/earth-compost": { + "vegetation/arctic-growth": 10, + "vegetation/pine-tree": 1 + } + }, + "hell": { + "vegetation/tree-hellish-bulb": { + "vegetation/tree-hellish": 1 + } + }, + "desert": { + "vegetation/flower-cactus-bulb": { + "vegetation/flower-cactus": 1 + } + }, + "plain": { + "ground/earth-compost": { + "vegetation/grass": 120, + "vegetation/shrub-a": 33, + "vegetation/shrub-b": 34, + "vegetation/shrub-c": 33, + "vegetation/shrub-d": 25, + "vegetation/shrub-e": 20, + "vegetation/shrub-f": 15, + "vegetation/shrub-g": 15 + }, + "vegetation/flower-amaryllis-bulb": { + "vegetation/flower-amaryllis": 1 + }, + "vegetation/flower-asphodelus-bulb": { + "vegetation/flower-asphodelus": 1 + }, + "vegetation/flower-birds-foot-trefoil-bulb": { + "vegetation/flower-birds-foot-trefoil": 1 + }, + "vegetation/flower-carnations-bulb": { + "vegetation/flower-carnations": 1 + }, + "vegetation/flower-coxcomb-bulb": { + "vegetation/flower-coxcomb": 1 + }, + "vegetation/flower-delphinium-bulb": { + "vegetation/flower-delphinium": 1 + }, + "vegetation/flower-lobelia-bulb": { + "vegetation/flower-lobelia": 1 + }, + "vegetation/flower-tuberose-bulb": { + "vegetation/flower-tuberose": 1 + }, + "vegetation/flower-bird-of-paradise-bulb": { + "vegetation/flower-bird-of-paradise": 1 + }, + "vegetation/flower-echinacea-bulb": { + "vegetation/flower-echinacea": 1 + }, + "vegetation/flower-sunflower-bulb": { + "vegetation/flower-sunflower": 1 + }, + "vegetation/flower-trumpets-bulb": { + "vegetation/flower-trumpets": 1 + }, + "vegetation/tree-bonsai-bulb": { + "vegetation/tree-bonsai": 1 + }, + "vegetation/pumpkin-seeds": { + "vegetation/pumpkin": 1 + } + } + } +} diff --git a/gameserver/src/main/resources/defaults/loottables.json b/gameserver/src/main/resources/loottables.json similarity index 100% rename from gameserver/src/main/resources/defaults/loottables.json rename to gameserver/src/main/resources/loottables.json diff --git a/gameserver/src/main/resources/pandora.json b/gameserver/src/main/resources/pandora.json new file mode 100644 index 00000000..75287e97 --- /dev/null +++ b/gameserver/src/main/resources/pandora.json @@ -0,0 +1,146 @@ +{ + "1": [ + { + "creatures/crow-auto": 10 + }, + { + "creatures/roach-large": 10 + }, + { + "automata/tiny": 4, + "automata/small": 2 + }, + { + "revenant": 3 + }, + { + "terrapus/adult": 5, + "terrapus/fire": 1 + }, + { + "terrapus/fire": 4 + }, + { + "terrapus/frost": 4 + } + ], + "4": [ + { + "automata/tiny": 8, + "automata/small": 4 + }, + { + "brains/tiny-crawler": 3, + "brains/tiny-flyer": 3 + }, + { + "revenant": 4, + "dire-revenant": 1 + }, + { + "terrapus/adult": 8, + "terrapus/fire": 3 + }, + { + "terrapus/fire": 4, + "terrapus/frost": 4 + }, + { + "terrapus/fire": 3, + "brains/small": 1 + }, + { + "terrapus/skeleton": 4, + "terrapus/frost": 2 + } + ], + "7": [ + { + "automata/medium": 6, + "automata/large": 2 + }, + { + "brains/tiny-crawler": 7, + "brains/tiny-flyer": 6 + }, + { + "revenant": 5, + "dire-revenant": 3 + }, + { + "brains/small": 4, + "terrapus/frost": 3 + }, + { + "brains/medium": 1, + "brains/small": 2 + }, + { + "terrapus/skeleton": 8, + "terrapus/acid": 4 + } + ], + "10": [ + { + "automata/medium": 3, + "automata/large": 3 + }, + { + "revenant": 8, + "dire-revenant": 4 + }, + { + "brains/medium-dire": 1, + "brains/small": 3, + "brains/tiny-crawler": 4 + }, + { + "brains/medium": 3, + "brains/small": 4, + "brains/tiny-crawler": 3 + } + ], + "13": [ + { + "automata/medium": 4, + "automata/large": 5 + }, + { + "revenant": 10, + "dire-revenant": 5 + }, + { + "brains/medium-dire": 2, + "brains/small": 7 + }, + { + "brains/medium": 3, + "brains/small": 6 + }, + { + "terrapus/skeleton": 12, + "terrapus/acid": 8 + } + ], + "17": [ + { + "automata/large": 6 + }, + { + "brains/medium-dire": 4, + "brains/small": 2 + }, + { + "brains/medium-dire": 2, + "brains/medium": 5 + }, + { + "brains/large": 1, + "brains/medium": 2 + }, + { + "brains/large": 1, + "brains/small": 3 + } + ] +} diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/arctic_surface_1/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/arctic_surface_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/arctic_surface_1/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/arctic_surface_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/arctic_surface_1/config.json b/gameserver/src/main/resources/prefabs/dungeons/arctic_surface_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/arctic_surface_1/config.json rename to gameserver/src/main/resources/prefabs/dungeons/arctic_surface_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_1/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_large_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_1/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_large_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_1/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_large_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_1/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_large_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_2/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_large_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_2/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_large_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_2/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_large_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_2/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_large_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_3/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_large_3/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_3/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_large_3/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_3/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_large_3/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_3/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_large_3/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_4/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_large_4/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_4/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_large_4/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_4/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_large_4/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_4/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_large_4/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_5/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_large_5/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_5/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_large_5/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_5/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_large_5/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_5/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_large_5/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_6/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_large_6/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_6/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_large_6/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_6/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_large_6/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_large_6/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_large_6/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_1/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_1/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_1/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_1/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_10/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_10/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_10/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_10/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_10/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_10/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_10/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_10/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_11/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_11/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_11/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_11/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_11/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_11/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_11/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_11/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_2/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_2/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_2/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_2/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_3/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_3/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_3/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_3/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_3/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_3/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_3/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_3/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_4/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_4/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_4/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_4/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_4/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_4/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_4/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_4/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_5/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_5/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_5/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_5/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_5/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_5/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_5/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_5/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_6/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_6/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_6/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_6/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_6/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_6/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_6/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_6/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_7/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_7/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_7/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_7/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_7/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_7/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_7/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_7/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_8/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_8/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_8/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_8/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_8/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_8/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_8/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_8/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_9/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_9/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_9/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_9/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_9/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_medium_9/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_medium_9/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_medium_9/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_1/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_small_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_1/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_1/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_small_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_1/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_2/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_small_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_2/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_2/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_small_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_2/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_3/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_small_3/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_3/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_3/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_3/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_small_3/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_3/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_3/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_4/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_small_4/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_4/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_4/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_4/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_small_4/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_4/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_4/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_5/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_small_5/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_5/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_5/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_5/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_small_5/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_5/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_5/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_6/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_small_6/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_6/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_6/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_6/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_small_6/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_6/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_6/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_7/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_small_7/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_7/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_7/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_7/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_small_7/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_7/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_7/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_8/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_small_8/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_8/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_8/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_8/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_small_8/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_8/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_8/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_9/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/generic_small_9/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_9/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_9/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_9/config.json b/gameserver/src/main/resources/prefabs/dungeons/generic_small_9/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/generic_small_9/config.json rename to gameserver/src/main/resources/prefabs/dungeons/generic_small_9/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/hell_small_1/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/hell_small_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/hell_small_1/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/hell_small_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/hell_small_1/config.json b/gameserver/src/main/resources/prefabs/dungeons/hell_small_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/hell_small_1/config.json rename to gameserver/src/main/resources/prefabs/dungeons/hell_small_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/hell_surface_1/blocks.dat b/gameserver/src/main/resources/prefabs/dungeons/hell_surface_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/hell_surface_1/blocks.dat rename to gameserver/src/main/resources/prefabs/dungeons/hell_surface_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/dungeons/hell_surface_1/config.json b/gameserver/src/main/resources/prefabs/dungeons/hell_surface_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/dungeons/hell_surface_1/config.json rename to gameserver/src/main/resources/prefabs/dungeons/hell_surface_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/bucket_shrine/blocks.dat b/gameserver/src/main/resources/prefabs/misc/bucket_shrine/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/bucket_shrine/blocks.dat rename to gameserver/src/main/resources/prefabs/misc/bucket_shrine/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/bucket_shrine/config.json b/gameserver/src/main/resources/prefabs/misc/bucket_shrine/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/bucket_shrine/config.json rename to gameserver/src/main/resources/prefabs/misc/bucket_shrine/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/gift_bunker_brass/blocks.dat b/gameserver/src/main/resources/prefabs/misc/gift_bunker_brass/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/gift_bunker_brass/blocks.dat rename to gameserver/src/main/resources/prefabs/misc/gift_bunker_brass/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/gift_bunker_brass/config.json b/gameserver/src/main/resources/prefabs/misc/gift_bunker_brass/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/gift_bunker_brass/config.json rename to gameserver/src/main/resources/prefabs/misc/gift_bunker_brass/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/gift_bunker_iron/blocks.dat b/gameserver/src/main/resources/prefabs/misc/gift_bunker_iron/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/gift_bunker_iron/blocks.dat rename to gameserver/src/main/resources/prefabs/misc/gift_bunker_iron/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/gift_bunker_iron/config.json b/gameserver/src/main/resources/prefabs/misc/gift_bunker_iron/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/gift_bunker_iron/config.json rename to gameserver/src/main/resources/prefabs/misc/gift_bunker_iron/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/gift_bunker_wood/blocks.dat b/gameserver/src/main/resources/prefabs/misc/gift_bunker_wood/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/gift_bunker_wood/blocks.dat rename to gameserver/src/main/resources/prefabs/misc/gift_bunker_wood/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/gift_bunker_wood/config.json b/gameserver/src/main/resources/prefabs/misc/gift_bunker_wood/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/gift_bunker_wood/config.json rename to gameserver/src/main/resources/prefabs/misc/gift_bunker_wood/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/head_bunker/blocks.dat b/gameserver/src/main/resources/prefabs/misc/head_bunker/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/head_bunker/blocks.dat rename to gameserver/src/main/resources/prefabs/misc/head_bunker/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/head_bunker/config.json b/gameserver/src/main/resources/prefabs/misc/head_bunker/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/head_bunker/config.json rename to gameserver/src/main/resources/prefabs/misc/head_bunker/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/luxury_bunker/blocks.dat b/gameserver/src/main/resources/prefabs/misc/luxury_bunker/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/luxury_bunker/blocks.dat rename to gameserver/src/main/resources/prefabs/misc/luxury_bunker/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/luxury_bunker/config.json b/gameserver/src/main/resources/prefabs/misc/luxury_bunker/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/luxury_bunker/config.json rename to gameserver/src/main/resources/prefabs/misc/luxury_bunker/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/machine_bunker/blocks.dat b/gameserver/src/main/resources/prefabs/misc/machine_bunker/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/machine_bunker/blocks.dat rename to gameserver/src/main/resources/prefabs/misc/machine_bunker/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/machine_bunker/config.json b/gameserver/src/main/resources/prefabs/misc/machine_bunker/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/machine_bunker/config.json rename to gameserver/src/main/resources/prefabs/misc/machine_bunker/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/music_bunker/blocks.dat b/gameserver/src/main/resources/prefabs/misc/music_bunker/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/music_bunker/blocks.dat rename to gameserver/src/main/resources/prefabs/misc/music_bunker/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/music_bunker/config.json b/gameserver/src/main/resources/prefabs/misc/music_bunker/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/music_bunker/config.json rename to gameserver/src/main/resources/prefabs/misc/music_bunker/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/painting_bunker/blocks.dat b/gameserver/src/main/resources/prefabs/misc/painting_bunker/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/painting_bunker/blocks.dat rename to gameserver/src/main/resources/prefabs/misc/painting_bunker/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/painting_bunker/config.json b/gameserver/src/main/resources/prefabs/misc/painting_bunker/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/painting_bunker/config.json rename to gameserver/src/main/resources/prefabs/misc/painting_bunker/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/sculpture_bunker_double/blocks.dat b/gameserver/src/main/resources/prefabs/misc/sculpture_bunker_double/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/sculpture_bunker_double/blocks.dat rename to gameserver/src/main/resources/prefabs/misc/sculpture_bunker_double/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/sculpture_bunker_double/config.json b/gameserver/src/main/resources/prefabs/misc/sculpture_bunker_double/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/sculpture_bunker_double/config.json rename to gameserver/src/main/resources/prefabs/misc/sculpture_bunker_double/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/sculpture_bunker_single/blocks.dat b/gameserver/src/main/resources/prefabs/misc/sculpture_bunker_single/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/sculpture_bunker_single/blocks.dat rename to gameserver/src/main/resources/prefabs/misc/sculpture_bunker_single/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/misc/sculpture_bunker_single/config.json b/gameserver/src/main/resources/prefabs/misc/sculpture_bunker_single/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/misc/sculpture_bunker_single/config.json rename to gameserver/src/main/resources/prefabs/misc/sculpture_bunker_single/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_1/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_1/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_1/config.json b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_1/config.json rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_2/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_2/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_2/config.json b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_2/config.json rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_3/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_3/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_3/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_3/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_3/config.json b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_3/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_3/config.json rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_3/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_4/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_4/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_4/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_4/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_4/config.json b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_4/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_4/config.json rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_4/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_5/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_5/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_5/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_5/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_5/config.json b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_5/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_5/config.json rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_5/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_6/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_6/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_6/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_6/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_6/config.json b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_6/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_6/config.json rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_6/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_7/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_7/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_7/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_7/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_7/config.json b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_7/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_7/config.json rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_7/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_8/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_8/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_8/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_8/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_8/config.json b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_8/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_8/config.json rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_8/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_9/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_9/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_9/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_9/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_9/config.json b/gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_9/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/arctic_surface_building_9/config.json rename to gameserver/src/main/resources/prefabs/ruins/arctic_surface_building_9/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_1/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/desert_surface_building_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_1/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/desert_surface_building_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_1/config.json b/gameserver/src/main/resources/prefabs/ruins/desert_surface_building_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_1/config.json rename to gameserver/src/main/resources/prefabs/ruins/desert_surface_building_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_2/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/desert_surface_building_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_2/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/desert_surface_building_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_2/config.json b/gameserver/src/main/resources/prefabs/ruins/desert_surface_building_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_2/config.json rename to gameserver/src/main/resources/prefabs/ruins/desert_surface_building_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_3/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/desert_surface_building_3/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_3/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/desert_surface_building_3/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_3/config.json b/gameserver/src/main/resources/prefabs/ruins/desert_surface_building_3/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_3/config.json rename to gameserver/src/main/resources/prefabs/ruins/desert_surface_building_3/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_4/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/desert_surface_building_4/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_4/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/desert_surface_building_4/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_4/config.json b/gameserver/src/main/resources/prefabs/ruins/desert_surface_building_4/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/desert_surface_building_4/config.json rename to gameserver/src/main/resources/prefabs/ruins/desert_surface_building_4/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_buried_house_1/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/generic_buried_house_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_buried_house_1/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/generic_buried_house_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_buried_house_1/config.json b/gameserver/src/main/resources/prefabs/ruins/generic_buried_house_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_buried_house_1/config.json rename to gameserver/src/main/resources/prefabs/ruins/generic_buried_house_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_buried_house_2/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/generic_buried_house_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_buried_house_2/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/generic_buried_house_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_buried_house_2/config.json b/gameserver/src/main/resources/prefabs/ruins/generic_buried_house_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_buried_house_2/config.json rename to gameserver/src/main/resources/prefabs/ruins/generic_buried_house_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_buried_house_3/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/generic_buried_house_3/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_buried_house_3/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/generic_buried_house_3/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_buried_house_3/config.json b/gameserver/src/main/resources/prefabs/ruins/generic_buried_house_3/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_buried_house_3/config.json rename to gameserver/src/main/resources/prefabs/ruins/generic_buried_house_3/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_1/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/generic_metal_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_1/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/generic_metal_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_1/config.json b/gameserver/src/main/resources/prefabs/ruins/generic_metal_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_1/config.json rename to gameserver/src/main/resources/prefabs/ruins/generic_metal_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_2/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/generic_metal_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_2/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/generic_metal_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_2/config.json b/gameserver/src/main/resources/prefabs/ruins/generic_metal_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_2/config.json rename to gameserver/src/main/resources/prefabs/ruins/generic_metal_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_3/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/generic_metal_3/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_3/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/generic_metal_3/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_3/config.json b/gameserver/src/main/resources/prefabs/ruins/generic_metal_3/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_3/config.json rename to gameserver/src/main/resources/prefabs/ruins/generic_metal_3/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_4/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/generic_metal_4/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_4/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/generic_metal_4/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_4/config.json b/gameserver/src/main/resources/prefabs/ruins/generic_metal_4/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_metal_4/config.json rename to gameserver/src/main/resources/prefabs/ruins/generic_metal_4/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_stone_1/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/generic_stone_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_stone_1/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/generic_stone_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_stone_1/config.json b/gameserver/src/main/resources/prefabs/ruins/generic_stone_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_stone_1/config.json rename to gameserver/src/main/resources/prefabs/ruins/generic_stone_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_1/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/generic_wood_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_1/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/generic_wood_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_1/config.json b/gameserver/src/main/resources/prefabs/ruins/generic_wood_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_1/config.json rename to gameserver/src/main/resources/prefabs/ruins/generic_wood_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_2/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/generic_wood_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_2/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/generic_wood_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_2/config.json b/gameserver/src/main/resources/prefabs/ruins/generic_wood_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_2/config.json rename to gameserver/src/main/resources/prefabs/ruins/generic_wood_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_3/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/generic_wood_3/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_3/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/generic_wood_3/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_3/config.json b/gameserver/src/main/resources/prefabs/ruins/generic_wood_3/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_3/config.json rename to gameserver/src/main/resources/prefabs/ruins/generic_wood_3/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_4/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/generic_wood_4/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_4/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/generic_wood_4/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_4/config.json b/gameserver/src/main/resources/prefabs/ruins/generic_wood_4/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_4/config.json rename to gameserver/src/main/resources/prefabs/ruins/generic_wood_4/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_5/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/generic_wood_5/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_5/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/generic_wood_5/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_5/config.json b/gameserver/src/main/resources/prefabs/ruins/generic_wood_5/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/generic_wood_5/config.json rename to gameserver/src/main/resources/prefabs/ruins/generic_wood_5/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_1/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_1/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_1/config.json b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_1/config.json rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_2/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_2/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_2/config.json b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_2/config.json rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_3/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_3/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_3/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_3/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_3/config.json b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_3/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_3/config.json rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_3/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_4/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_4/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_4/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_4/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_4/config.json b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_4/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_4/config.json rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_4/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_5/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_5/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_5/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_5/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_5/config.json b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_5/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_5/config.json rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_5/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_6/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_6/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_6/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_6/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_6/config.json b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_6/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_6/config.json rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_6/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_7/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_7/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_7/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_7/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_7/config.json b/gameserver/src/main/resources/prefabs/ruins/hell_surface_building_7/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/hell_surface_building_7/config.json rename to gameserver/src/main/resources/prefabs/ruins/hell_surface_building_7/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_1/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_1/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_1/config.json b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_1/config.json rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_10/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_10/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_10/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_10/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_10/config.json b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_10/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_10/config.json rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_10/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_2/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_2/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_2/config.json b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_2/config.json rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_3/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_3/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_3/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_3/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_3/config.json b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_3/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_3/config.json rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_3/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_4/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_4/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_4/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_4/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_4/config.json b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_4/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_4/config.json rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_4/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_5/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_5/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_5/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_5/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_5/config.json b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_5/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_5/config.json rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_5/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_6/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_6/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_6/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_6/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_6/config.json b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_6/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_6/config.json rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_6/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_7/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_7/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_7/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_7/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_7/config.json b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_7/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_7/config.json rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_7/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_8/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_8/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_8/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_8/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_8/config.json b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_8/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_8/config.json rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_8/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_9/blocks.dat b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_9/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_9/blocks.dat rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_9/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_9/config.json b/gameserver/src/main/resources/prefabs/ruins/plain_surface_building_9/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/ruins/plain_surface_building_9/config.json rename to gameserver/src/main/resources/prefabs/ruins/plain_surface_building_9/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/arctic_1/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/arctic_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/arctic_1/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/arctic_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/arctic_1/config.json b/gameserver/src/main/resources/prefabs/spawns/arctic_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/arctic_1/config.json rename to gameserver/src/main/resources/prefabs/spawns/arctic_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/arctic_2/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/arctic_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/arctic_2/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/arctic_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/arctic_2/config.json b/gameserver/src/main/resources/prefabs/spawns/arctic_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/arctic_2/config.json rename to gameserver/src/main/resources/prefabs/spawns/arctic_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/arctic_3/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/arctic_3/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/arctic_3/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/arctic_3/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/arctic_3/config.json b/gameserver/src/main/resources/prefabs/spawns/arctic_3/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/arctic_3/config.json rename to gameserver/src/main/resources/prefabs/spawns/arctic_3/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/brain_1/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/brain_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/brain_1/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/brain_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/brain_1/config.json b/gameserver/src/main/resources/prefabs/spawns/brain_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/brain_1/config.json rename to gameserver/src/main/resources/prefabs/spawns/brain_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/deep_1/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/deep_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/deep_1/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/deep_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/deep_1/config.json b/gameserver/src/main/resources/prefabs/spawns/deep_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/deep_1/config.json rename to gameserver/src/main/resources/prefabs/spawns/deep_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/desert_1/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/desert_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/desert_1/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/desert_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/desert_1/config.json b/gameserver/src/main/resources/prefabs/spawns/desert_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/desert_1/config.json rename to gameserver/src/main/resources/prefabs/spawns/desert_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/desert_2/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/desert_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/desert_2/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/desert_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/desert_2/config.json b/gameserver/src/main/resources/prefabs/spawns/desert_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/desert_2/config.json rename to gameserver/src/main/resources/prefabs/spawns/desert_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/hell_1/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/hell_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/hell_1/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/hell_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/hell_1/config.json b/gameserver/src/main/resources/prefabs/spawns/hell_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/hell_1/config.json rename to gameserver/src/main/resources/prefabs/spawns/hell_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/hell_2/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/hell_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/hell_2/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/hell_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/hell_2/config.json b/gameserver/src/main/resources/prefabs/spawns/hell_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/hell_2/config.json rename to gameserver/src/main/resources/prefabs/spawns/hell_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/hell_3/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/hell_3/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/hell_3/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/hell_3/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/hell_3/config.json b/gameserver/src/main/resources/prefabs/spawns/hell_3/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/hell_3/config.json rename to gameserver/src/main/resources/prefabs/spawns/hell_3/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/plain_1/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/plain_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/plain_1/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/plain_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/plain_1/config.json b/gameserver/src/main/resources/prefabs/spawns/plain_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/plain_1/config.json rename to gameserver/src/main/resources/prefabs/spawns/plain_1/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/plain_2/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/plain_2/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/plain_2/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/plain_2/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/plain_2/config.json b/gameserver/src/main/resources/prefabs/spawns/plain_2/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/plain_2/config.json rename to gameserver/src/main/resources/prefabs/spawns/plain_2/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/plain_3/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/plain_3/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/plain_3/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/plain_3/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/plain_3/config.json b/gameserver/src/main/resources/prefabs/spawns/plain_3/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/plain_3/config.json rename to gameserver/src/main/resources/prefabs/spawns/plain_3/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/plain_4/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/plain_4/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/plain_4/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/plain_4/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/plain_4/config.json b/gameserver/src/main/resources/prefabs/spawns/plain_4/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/plain_4/config.json rename to gameserver/src/main/resources/prefabs/spawns/plain_4/config.json diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/space_1/blocks.dat b/gameserver/src/main/resources/prefabs/spawns/space_1/blocks.dat similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/space_1/blocks.dat rename to gameserver/src/main/resources/prefabs/spawns/space_1/blocks.dat diff --git a/gameserver/src/main/resources/defaults/prefabs/spawns/space_1/config.json b/gameserver/src/main/resources/prefabs/spawns/space_1/config.json similarity index 100% rename from gameserver/src/main/resources/defaults/prefabs/spawns/space_1/config.json rename to gameserver/src/main/resources/prefabs/spawns/space_1/config.json diff --git a/gameserver/src/main/resources/shop.json b/gameserver/src/main/resources/shop.json new file mode 100644 index 00000000..de163030 --- /dev/null +++ b/gameserver/src/main/resources/shop.json @@ -0,0 +1,460 @@ +{ + "sections": { + "utilities": { + "name": "Utilities", + "icon": "icon-profile-equipment", + "products": [ + "brain-wine", + "skill-book", + "skill-reset", + "name-change" + ] + }, + "protectors": { + "name": "Protectors", + "icon": "icon-skill-automata", + "products": [ + "giga-protector", + "mega-protector", + "large-protector", + "small-protector", + "micro-protector-pack" + ] + }, + "worlds": { + "name": "Private Worlds", + "icon": "icon-world", + "products": [ + "private-world-small", + "private-world", + "private-world-large", + "private-world-hell-small", + "private-world-hell", + "private-world-hell-large", + "private-world-arctic-small", + "private-world-arctic", + "private-world-arctic-large", + "private-world-desert-small", + "private-world-desert", + "private-world-desert-large", + "private-world-deep-small", + "private-world-deep", + "private-world-deep-large", + "private-world-brain-small", + "private-world-brain", + "private-world-brain-large", + "private-world-space-small", + "private-world-space", + "private-world-space-large" + ] + } + }, + "products": { + "brain-wine": { + "type": "item", + "name": "Brain Wine", + "cost": 75, + "description": "Upgrade any skill by one point with this carefully brewed essence of brain! Note: each skill can only be upgraded once via brain wine, and only to Level 10.", + "image": [ + { "sprite": "inventory/consumables/brain-wine" }, + { "sprite": "shop/banner", "color": "AAFF66" }, + { "sprite": "shop/banner-text-sale" } + ], + "items": { + "consumables/brain-wine": 1 + } + }, + "skill-book": { + "type": "item", + "name": "Skill Book", + "cost": 75, + "description": "Upgrade any skill by one point with this lovely tome of knowledge! Note: each skill can only be upgraded once via books, and only to Level 10.", + "image": [ + { "sprite": "inventory/consumables/book-skill" }, + { "sprite": "shop/banner", "color": "AAFF66" }, + { "sprite": "shop/banner-text-sale" } + ], + "items": { + "consumables/book-skill": 1 + } + }, + "skill-reset": { + "type": "item", + "name": "Skill Reset", + "cost": 100, + "description": "Go back in time a little with this handy skill reset! Reclaim all of your skill points and re-apply them however you like.", + "image": "inventory/consumables/skill-reset", + "items": { + "consumables/skill-reset": 1 + } + }, + "name-change": { + "type": "item", + "name": "Name Change", + "cost": 125, + "description": "Want to change your player name? Grab this handy name tag and get creative!", + "image": "inventory/consumables/name-change", + "items": { + "consumables/name-change": 1 + } + }, + "giga-protector": { + "type": "item", + "name": "Giga Protector", + "cost": 225, + "description": "Protect your creations and valuables from being mined by other players with this giga protector! When placed, it will emit a protective field that protects blocks within a 120 block radius.", + "image": "inventory/mechanical/dish-giga", + "items": { + "mechanical/dish-giga": 1 + } + }, + "mega-protector": { + "type": "item", + "name": "Mega Protector", + "cost": 125, + "description": "Protect your creations and valuables from being mined by other players with this mega protector! When placed, it will emit a protective field that protects blocks within a 60 block radius.", + "image": "inventory/mechanical/dish-mega", + "items": { + "mechanical/dish-mega": 1 + } + }, + "large-protector": { + "type": "item", + "name": "Large Protector", + "cost": 50, + "description": "Protect your creations and valuables from being mined by other players with this large protector! When placed, it will emit a protective field that protects blocks within a 30 block radius.", + "image": "inventory/mechanical/dish-large", + "items": { + "mechanical/dish-large": 1 + } + }, + "small-protector": { + "type": "item", + "name": "Small Protector", + "cost": 20, + "description": "Protect your creations and valuables from being mined by other players with this small protector! When placed, it will emit a protective field that protects blocks within a 15 block radius.", + "image": "inventory/mechanical/dish", + "items": { + "mechanical/dish": 1 + } + }, + "micro-protector-pack": { + "type": "item", + "name": "Micro Protector Pack", + "cost": 20, + "description": "Protect your creations and valuables from being mined by other players with these micro protectors! When placed, each dish will emit a protective field that protects blocks within a 5 block radius.", + "image": "inventory/mechanical/dish-micro", + "items": { + "mechanical/dish-micro": 4 + } + }, + "private-world-small": { + "type": "zone", + "name": "Private Mini World", + "cost": 250, + "description": "Claim your own untouched world! Get all the resources, loot, and exploration you'll ever need. You can even invite friends to come visit and settle with you... or just keep it all to yourself!", + "image": [ + { "sprite": "shop/biome-normal" }, + { "sprite": "shop/banner", "color": "AAFF66" }, + { "sprite": "shop/banner-text-mini" } + ], + "zone": { + "biome": "plain", + "width": 800, + "height": 400 + } + }, + "private-world": { + "type": "zone", + "name": "Private World", + "cost": 500, + "description": "Claim your own untouched world! Get all the resources, loot, and exploration you'll ever need. You can even invite friends to come visit and settle with you... or just keep it all to yourself!", + "image": "shop/biome-normal", + "zone": { + "biome": "plain", + "width": 2000, + "height": 600 + } + }, + "private-world-large": { + "type": "zone", + "name": "Private Large World", + "cost": 1000, + "description": "Claim your own untouched world! Get all the resources, loot, and exploration you'll ever need. You can even invite friends to come visit and settle with you... or just keep it all to yourself!", + "image": [ + { "sprite": "shop/biome-normal" }, + { "sprite": "shop/banner", "color": "AAFF66" }, + { "sprite": "shop/banner-text-xl" } + ], + "zone": { + "biome": "plain", + "width": 3000, + "height": 800 + } + }, + "private-world-hell-small": { + "type": "zone", + "name": "Private Mini Hell World", + "cost": 250, + "description": "Claim your own untouched Hell world! Get your own expiator, plus a fresh bevy of fire salt, bloodstone, and a never-ending supply of ghostly ectoplasm.", + "image": [ + { "sprite": "shop/biome-hell" }, + { "sprite": "shop/banner", "color": "FF3311" }, + { "sprite": "shop/banner-text-mini" } + ], + "zone": { + "biome": "hell", + "width": 800, + "height": 400 + } + }, + "private-world-hell": { + "type": "zone", + "name": "Private Hell World", + "cost": 500, + "description": "Claim your own untouched Hell world! Get your own expiator, plus a fresh bevy of fire salt, bloodstone, and a never-ending supply of ghostly ectoplasm.", + "image": "shop/biome-hell", + "zone": { + "biome": "hell", + "width": 2000, + "height": 600 + } + }, + "private-world-hell-large": { + "type": "zone", + "name": "Private Large Hell World", + "cost": 1000, + "description": "Claim your own untouched Hell world! Get your own expiator, plus a fresh bevy of fire salt, bloodstone, and a never-ending supply of ghostly ectoplasm.", + "image": [ + { "sprite": "shop/biome-hell" }, + { "sprite": "shop/banner", "color": "FF3311" }, + { "sprite": "shop/banner-text-xl" } + ], + "zone": { + "biome": "hell", + "width": 3000, + "height": 800 + } + }, + "private-world-arctic-small": { + "type": "zone", + "name": "Private Mini Arctic World", + "cost": 250, + "description": "Claim your own untouched Arctic world! Discover sparkly diamonds, ice sculptures, and maybe even the rare jackalope head. Also includes all the snow and ice you'll ever need!", + "image": [ + { "sprite": "shop/biome-arctic" }, + { "sprite": "shop/banner", "color": "66AAFF" }, + { "sprite": "shop/banner-text-mini" } + ], + "zone": { + "biome": "arctic", + "width": 800, + "height": 400 + } + }, + "private-world-arctic": { + "type": "zone", + "name": "Private Arctic World", + "cost": 500, + "description": "Claim your own untouched Arctic world! Discover sparkly diamonds, ice sculptures, and maybe even the rare jackalope head. Also includes all the snow and ice you'll ever need!", + "image": "shop/biome-arctic", + "zone": { + "biome": "arctic", + "width": 2000, + "height": 600 + } + }, + "private-world-arctic-large": { + "type": "zone", + "name": "Private Large Arctic World", + "cost": 1000, + "description": "Claim your own untouched Arctic world! Discover sparkly diamonds, ice sculptures, and maybe even the rare jackalope head. Also includes all the snow and ice you'll ever need!", + "image": [ + { "sprite": "shop/biome-arctic" }, + { "sprite": "shop/banner", "color": "66AAFF" }, + { "sprite": "shop/banner-text-xl" } + ], + "zone": { + "biome": "arctic", + "width": 3000, + "height": 800 + } + }, + "private-world-desert-small": { + "type": "zone", + "name": "Private Mini Desert World", + "cost": 250, + "description": "Claim your own untouched Desert world! Get a fresh supply of beryllium ore, tumbleweeds, and cacti. You'll also find a steady supply of armadillos, scorpions, and sandworms to farm!", + "image": [ + { "sprite": "shop/biome-desert" }, + { "sprite": "shop/banner", "color": "FFDD77" }, + { "sprite": "shop/banner-text-mini" } + ], + "zone": { + "biome": "desert", + "width": 800, + "height": 400 + } + }, + "private-world-desert": { + "type": "zone", + "name": "Private Desert World", + "cost": 500, + "description": "Claim your own untouched Desert world! Get a fresh supply of beryllium ore, tumbleweeds, and cacti. You'll also find a steady supply of armadillos, scorpions, and sandworms to farm!", + "image": "shop/biome-desert", + "zone": { + "biome": "desert", + "width": 2000, + "height": 600 + } + }, + "private-world-desert-large": { + "type": "zone", + "name": "Private Large Desert World", + "cost": 1000, + "description": "Claim your own untouched Desert world! Get a fresh supply of beryllium ore, tumbleweeds, and cacti. You'll also find a steady supply of armadillos, scorpions, and sandworms to farm!", + "image": [ + { "sprite": "shop/biome-desert" }, + { "sprite": "shop/banner", "color": "FFDD77" }, + { "sprite": "shop/banner-text-xl" } + ], + "zone": { + "biome": "desert", + "width": 3000, + "height": 800 + } + }, + "private-world-deep-small": { + "type": "zone", + "name": "Private Mini Deep World", + "cost": 250, + "description": "Claim your own untouched Deep world! Find the elusive orange crystal along with an above-average amount of dungeons to raid! Just watch out for those explosive acid pipes!", + "image": [ + { "sprite": "shop/biome-deep" }, + { "sprite": "shop/banner", "color": "44334A" }, + { "sprite": "shop/banner-text-mini" } + ], + "zone": { + "biome": "deep", + "width": 600, + "height": 500 + } + }, + "private-world-deep": { + "type": "zone", + "name": "Private Deep World", + "cost": 500, + "description": "Claim your own untouched Deep world! Find the elusive orange crystal along with an above-average amount of dungeons to raid! Just watch out for those explosive acid pipes!", + "image": "shop/biome-deep", + "zone": { + "biome": "deep", + "width": 1200, + "height": 1000 + } + }, + "private-world-deep-large": { + "type": "zone", + "name": "Private Large Deep World", + "cost": 1000, + "description": "Claim your own untouched Deep world! Find the elusive orange crystal along with an above-average amount of dungeons to raid! Just watch out for those explosive acid pipes!", + "image": [ + { "sprite": "shop/biome-deep" }, + { "sprite": "shop/banner", "color": "44334A" }, + { "sprite": "shop/banner-text-xl" } + ], + "zone": { + "biome": "deep", + "width": 1800, + "height": 1500 + } + }, + "private-world-brain-small": { + "type": "zone", + "name": "Private Mini Brain World", + "cost": 250, + "description": "Claim your own untouched Brain world! Perfect for the thrill seeker, you'll find an insane amount of brains, green crystals, and earn that coveted Insurrectionist achievement!", + "image": [ + { "sprite": "shop/biome-brain" }, + { "sprite": "shop/banner", "color": "FF3311" }, + { "sprite": "shop/banner-text-mini" } + ], + "zone": { + "biome": "brain", + "width": 800, + "height": 400 + } + }, + "private-world-brain": { + "type": "zone", + "name": "Private Brain World", + "cost": 500, + "description": "Claim your own untouched Brain world! Perfect for the thrill seeker, you'll find an insane amount of brains, green crystals, and earn that coveted Insurrectionist achievement!", + "image": "shop/biome-brain", + "zone": { + "biome": "brain", + "width": 2000, + "height": 600 + } + }, + "private-world-brain-large": { + "type": "zone", + "name": "Private Large Brain World", + "cost": 1000, + "description": "Claim your own untouched Brain world! Perfect for the thrill seeker, you'll find an insane amount of brains, green crystals, and earn that coveted Insurrectionist achievement!", + "image": [ + { "sprite": "shop/biome-brain" }, + { "sprite": "shop/banner", "color": "FF3311" }, + { "sprite": "shop/banner-text-xl" } + ], + "zone": { + "biome": "brain", + "width": 3000, + "height": 800 + } + }, + "private-world-space-small": { + "type": "zone", + "name": "Private Mini Space World", + "cost": 250, + "description": "Claim your own untouched SPACE world! This brand new biome has it all - rare platinum, white crystals, custom loots, supernovas, low gravity, and more!", + "image": [ + { "sprite": "shop/biome-space" }, + { "sprite": "shop/banner", "color": "1A1A1A" }, + { "sprite": "shop/banner-text-mini" } + ], + "zone": { + "biome": "space", + "width": 800, + "height": 400 + } + }, + "private-world-space": { + "type": "zone", + "name": "Private Space World", + "cost": 500, + "description": "Claim your own untouched SPACE world! This brand new biome has it all - rare platinum, white crystals, custom loots, supernovas, low gravity, and more!", + "image": "shop/biome-space", + "zone": { + "biome": "space", + "width": 2000, + "height": 600 + } + }, + "private-world-space-large": { + "type": "zone", + "name": "Private Large Space World", + "cost": 1000, + "description": "Claim your own untouched SPACE world! This brand new biome has it all - rare platinum, white crystals, custom loots, supernovas, low gravity, and more!", + "image": [ + { "sprite": "shop/biome-space" }, + { "sprite": "shop/banner", "color": "1A1A1A" }, + { "sprite": "shop/banner-text-xl" } + ], + "zone": { + "biome": "space", + "width": 3000, + "height": 800 + } + } + } +} diff --git a/gameserver/src/main/resources/defaults/spawning.json b/gameserver/src/main/resources/spawning.json similarity index 98% rename from gameserver/src/main/resources/defaults/spawning.json rename to gameserver/src/main/resources/spawning.json index 32c72601..71c2e71f 100644 --- a/gameserver/src/main/resources/defaults/spawning.json +++ b/gameserver/src/main/resources/spawning.json @@ -29,55 +29,55 @@ { "entity": "creatures/bluejay", "locale": "sky", - "purification": 1.0, + "max_acidity": 0.05, "frequency": 3 }, { "entity": "creatures/cardinal", "locale": "sky", - "purification": 1.0, + "max_acidity": 0.05, "frequency": 1 }, { "entity": "creatures/seagull", "locale": "sky", - "purification": 1.0, + "max_acidity": 0.05, "frequency": 2 }, { "entity": "creatures/butterfly-monarch", "locale": "sky", - "purification": 1.0, + "max_acidity": 0.05, "frequency":23 }, { "entity": "creatures/papilio-ulysses", "locale": "sky", - "purification": 1.0, + "max_acidity": 0.05, "frequency": 0.5 }, { "entity": "creatures/butterfly-swallowtail", "locale": "sky", - "purification": 1.0, + "max_acidity": 0.05, "frequency": 1 }, { "entity": "creatures/butterfly-moth", "locale": "sky", - "purification": 1.0, + "max_acidity": 0.05, "frequency": 5 }, { "entity": "creatures/butterfly-owl", "locale": "sky", - "purification": 1.0, + "max_acidity": 0.05, "frequency": 0.1 }, { "entity": "creatures/butterfly-paper-kite", "locale": "sky", - "purification": 1.0, + "max_acidity": 0.05, "frequency": 0.01 }, { diff --git a/settings.gradle b/settings.gradle index 38583d44..566f3e28 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,6 @@ rootProject.name = 'brainwine' +includeBuild 'build-logic' include('api', 'gameserver', 'shared') +project(":api").name = 'brainwine-api' +project(':gameserver').name = 'brainwine-gameserver' +project(':shared').name = 'brainwine-shared' diff --git a/shared/build.gradle b/shared/build.gradle index 451764c6..0782595a 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -15,7 +15,3 @@ dependencies { api 'commons-validator:commons-validator:1.7' api 'org.apache.commons:commons-text:1.9' } - -jar { - archiveBaseName = 'brainwine-shared' -} diff --git a/shared/src/main/java/brainwine/shared/JsonHelper.java b/shared/src/main/java/brainwine/shared/JsonHelper.java index 864c1ce5..334c08ea 100644 --- a/shared/src/main/java/brainwine/shared/JsonHelper.java +++ b/shared/src/main/java/brainwine/shared/JsonHelper.java @@ -2,6 +2,8 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.net.URL; import java.util.List; import com.fasterxml.jackson.core.JsonGenerationException; @@ -17,11 +19,14 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; public class JsonHelper { public static final ObjectMapper MAPPER = JsonMapper.builder() .findAndAddModules() + .addModule(new SimpleModule() + .addSerializer(OffsetDateTimeSerializer.INSTANCE)) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE) .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) @@ -34,10 +39,18 @@ public static T readValue(String string, Class type) throws JsonMappingEx return MAPPER.readValue(string, type); } - public static T readValue(File file, Class type) throws JsonParseException, JsonMappingException, IOException { + public static T readValue(File file, Class type) throws IOException { return MAPPER.readValue(file, type); } + public static T readValue(URL url, Class type) throws IOException { + return MAPPER.readValue(url, type); + } + + public static T readValue(InputStream inputStream, Class type) throws IOException { + return MAPPER.readValue(inputStream, type); + } + public static T readValue(Object object, Class type) throws JsonProcessingException { return readValue(writeValueAsString(object), type); } @@ -50,6 +63,14 @@ public static T readValue(File file, TypeReference type) throws IOExcepti return MAPPER.readValue(file, type); } + public static T readValue(URL url, TypeReference type) throws IOException { + return MAPPER.readValue(url, type); + } + + public static T readValue(InputStream inputStream, TypeReference type) throws IOException { + return MAPPER.readValue(inputStream, type); + } + public static T readValue(Object object, TypeReference type) throws JsonProcessingException { return readValue(writeValueAsString(object), type); } diff --git a/shared/src/main/java/brainwine/shared/OffsetDateTimeSerializer.java b/shared/src/main/java/brainwine/shared/OffsetDateTimeSerializer.java new file mode 100644 index 00000000..35080eb5 --- /dev/null +++ b/shared/src/main/java/brainwine/shared/OffsetDateTimeSerializer.java @@ -0,0 +1,31 @@ +package brainwine.shared; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +public class OffsetDateTimeSerializer extends StdSerializer { + + public static final OffsetDateTimeSerializer INSTANCE = new OffsetDateTimeSerializer(); + private static final long serialVersionUID = 7309329981624380784L; + private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd") + .appendLiteral('T') + .appendPattern("HH:mm:ss.SSS") + .appendOffsetId() + .toFormatter(); + + protected OffsetDateTimeSerializer() { + super(OffsetDateTime.class); + } + + @Override + public void serialize(OffsetDateTime dateTime, JsonGenerator generator, SerializerProvider provider) throws IOException { + generator.writeString(formatter.format(dateTime)); + } +} diff --git a/shared/src/main/java/brainwine/shared/TokenGenerator.java b/shared/src/main/java/brainwine/shared/TokenGenerator.java new file mode 100644 index 00000000..e666c9c9 --- /dev/null +++ b/shared/src/main/java/brainwine/shared/TokenGenerator.java @@ -0,0 +1,47 @@ +package brainwine.shared; + +import java.security.SecureRandom; +import java.util.function.Function; + +/** + * Utility for generating alphanumeric tokens. + */ +public class TokenGenerator { + + public static final String CHARTABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; + public static final int MAX_ATTEMPTS = 100; + private static final SecureRandom secureRandom = new SecureRandom(); + + /** + * Attempts to generate a unique token by testing for duplicates with the caller-specified function. + * If no unique token is generated within 100 attempts, the function gives up and returns {@code null} instead. + */ + public static String generateToken(int size, Function dupCheck) { + int attempts = MAX_ATTEMPTS; + + while(attempts > 0) { + String token = generateToken(size); + + if(!dupCheck.apply(token)) { + return token; + } + + attempts--; + } + + return null; + } + + /** + * Securely generates a token of the specified size. + */ + public static String generateToken(int size) { + char[] chars = new char[size]; + + for(int i = 0; i < size; i++) { + chars[i] = CHARTABLE.charAt(secureRandom.nextInt(CHARTABLE.length())); + } + + return new String(chars); + } +} diff --git a/src/main/java/brainwine/DirectDataFetcher.java b/src/main/java/brainwine/DirectDataFetcher.java index 76a8bdb7..5b089878 100644 --- a/src/main/java/brainwine/DirectDataFetcher.java +++ b/src/main/java/brainwine/DirectDataFetcher.java @@ -3,10 +3,13 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; import brainwine.api.DataFetcher; import brainwine.api.models.ZoneInfo; -import brainwine.gameserver.entity.player.PlayerManager; +import brainwine.gameserver.player.Player; +import brainwine.gameserver.player.PlayerManager; import brainwine.gameserver.zone.Zone; import brainwine.gameserver.zone.ZoneManager; @@ -34,6 +37,18 @@ public String registerPlayer(String name) { public String login(String name, String password) { return playerManager.login(name, password); } + + @Override + public String fetchPlayerName(String name) { + Player player = playerManager.getPlayer(name); + return player == null ? null : player.getName(); + } + + @Override + public String fetchPlayerId(String apiToken) { + Player player = playerManager.getPlayerByApiToken(apiToken); + return player == null ? null : player.getDocumentId(); + } @Override public boolean verifyAuthToken(String name, String token) { @@ -41,27 +56,65 @@ public boolean verifyAuthToken(String name, String token) { } @Override - public boolean verifyApiToken(String apiToken) { - return true; // TODO + public ZoneInfo getZoneInfo(String nameOrId) { + Zone zone = zoneManager.getZoneByName(nameOrId); + + if(zone == null) { + zone = zoneManager.getZone(nameOrId); + } + + return zone == null ? null : createZoneInfo(zone); } + /** + * TODO this will probably be slow if there is a large number of zones + */ @Override public Collection fetchZoneInfo() { List zoneInfo = new ArrayList<>(); Collection zones = zoneManager.getZones(); for(Zone zone : zones) { - zoneInfo.add(new ZoneInfo(zone.getName(), - zone.getBiome().getId(), - null, - false, - false, - false, - zone.getPlayers().size(), - zone.getExplorationProgress(), - zone.getCreationDate())); + zoneInfo.add(createZoneInfo(zone)); } return zoneInfo; } + + @Override + public Collection fetchRecentZoneInfo(String apiToken) { + Player player = playerManager.getPlayerByApiToken(apiToken); + return player == null ? new ArrayList<>() : createZoneInfo(player.getRecentZones()); + } + + @Override + public Collection fetchBookmarkedZoneInfo(String apiToken) { + Player player = playerManager.getPlayerByApiToken(apiToken); + return player == null ? new ArrayList<>() : createZoneInfo(player.getBookmarkedZones()); + } + + private List createZoneInfo(Collection zoneIds) { + return zoneIds.stream().map(zoneManager::getZone) + .filter(Objects::nonNull) + .map(DirectDataFetcher::createZoneInfo) + .collect(Collectors.toCollection(ArrayList::new)); + } + + private static ZoneInfo createZoneInfo(Zone zone) { + return new ZoneInfo(zone.getName(), + zone.getBiome().getId(), + null, + zone.isPvp(), + false, + zone.isPrivate(), + zone.isProtected(), + zone.getPlayers().size(), + zone.getWidth(), + zone.getHeight(), + zone.getSurface(), + zone.getExplorationProgress(), + zone.getCreationDate(), + zone.getOwner(), + zone.getMembers()); + } } diff --git a/src/main/java/brainwine/Bootstrap.java b/src/main/java/brainwine/Main.java similarity index 83% rename from src/main/java/brainwine/Bootstrap.java rename to src/main/java/brainwine/Main.java index bb1969fa..f48ab7ee 100644 --- a/src/main/java/brainwine/Bootstrap.java +++ b/src/main/java/brainwine/Main.java @@ -6,6 +6,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; import javax.swing.ImageIcon; import javax.swing.JOptionPane; @@ -17,17 +19,18 @@ import org.apache.logging.log4j.Logger; import brainwine.gui.GuiPreferences; -import brainwine.gui.MainView; import brainwine.gui.theme.ThemeManager; +import brainwine.gui.view.MainView; +import brainwine.util.OperatingSystem; import brainwine.util.SwingUtils; -public class Bootstrap { +public class Main { private static Logger logger = LogManager.getLogger(); private static boolean disableGui = false; private static boolean forceGui = false; + private final List listeners = new ArrayList<>(); private ServerThread serverThread; - private MainView mainView; private boolean closeRequested; public static void main(String[] args) { @@ -41,10 +44,10 @@ public static void main(String[] args) { } } - new Bootstrap(); + new Main(); } - public Bootstrap() { + public Main() { // Create gui or directly start server if gui is disabled or not supported if(!disableGui && (Desktop.isDesktopSupported() || forceGui)) { try { @@ -102,12 +105,11 @@ private void createMainView() throws InvocationTargetException, InterruptedExcep UIManager.put("Brainwine.settingsIcon", new ImageIcon(getClass().getResource("/settingsIcon16x.png"))); UIManager.put("Brainwine.communityIcon", new ImageIcon(getClass().getResource("/communityIcon16x.png"))); UIManager.put("Brainwine.powerIcon", new ImageIcon(getClass().getResource("/powerIcon16x.png"))); - UIManager.put("Brainwine.consoleFont", new Font("Consolas", Font.PLAIN, 12)); + UIManager.put("Brainwine.consoleFont", new Font(OperatingSystem.isMacOS() ? "Andale Mono" : "Consolas", Font.PLAIN, 12)); UIManager.put("Spinner.editorAlignment", JTextField.LEFT); UIManager.put("TitlePane.unifiedBackground", false); UIManager.put("Button.foreground", UIManager.get("MenuBar.foreground")); - SwingUtils.setDefaultFontSize(Math.min(28, Math.max(10, GuiPreferences.getInt(GuiPreferences.FONT_SIZE_KEY, 15)))); - SwingUtils.setMenuBarEmbedded(GuiPreferences.getBoolean(GuiPreferences.EMBED_MENU_BAR_KEY, true)); + SwingUtils.setDefaultFontSize(Math.min(28, Math.max(10, GuiPreferences.getFontSize()))); // Check read/write permissions if(!checkReadWritePermissions()) { @@ -117,7 +119,7 @@ private void createMainView() throws InvocationTargetException, InterruptedExcep } // Create view - mainView = new MainView(this); + new MainView(this); }); } @@ -126,8 +128,17 @@ private boolean checkReadWritePermissions() { return Files.isReadable(path) && Files.isWritable(path); } + public void toggleServer() { + if(isServerRunning()) { + stopServer(); + } else { + startServer(); + } + } + public void startServer() { if(!isServerRunning()) { + listeners.forEach(ServerStatusListener::onServerStarting); serverThread = new ServerThread(this); serverThread.start(); } @@ -135,25 +146,28 @@ public void startServer() { public void stopServer() { if(isServerRunning()) { + listeners.forEach(ServerStatusListener::onServerStopping); serverThread.stopGracefully(); } } public void onServerStarted() { - if(mainView != null) { - SwingUtilities.invokeLater(mainView::enableServerButton); - } + listeners.forEach(ServerStatusListener::onServerStarted); } public void onServerStopped() { + listeners.forEach(ServerStatusListener::onServerStopped); + if(closeRequested) { System.exit(0); - } else if(mainView != null) { - SwingUtilities.invokeLater(mainView::enableServerButton); } } public boolean isServerRunning() { return serverThread != null && serverThread.isRunning(); } + + public void addServerStatusListener(ServerStatusListener listener) { + listeners.add(listener); + } } diff --git a/src/main/java/brainwine/ServerStatusListener.java b/src/main/java/brainwine/ServerStatusListener.java new file mode 100644 index 00000000..f03c831e --- /dev/null +++ b/src/main/java/brainwine/ServerStatusListener.java @@ -0,0 +1,9 @@ +package brainwine; + +public interface ServerStatusListener { + + public void onServerStarting(); + public void onServerStopping(); + public void onServerStarted(); + public void onServerStopped(); +} diff --git a/src/main/java/brainwine/ServerThread.java b/src/main/java/brainwine/ServerThread.java index 8ca022f8..3b7279b0 100644 --- a/src/main/java/brainwine/ServerThread.java +++ b/src/main/java/brainwine/ServerThread.java @@ -13,14 +13,14 @@ public class ServerThread extends Thread { private static Logger logger = LogManager.getLogger(); - private final Bootstrap bootstrap; + private final Main main; private GameServer gameServer; private Api api; private boolean running; - public ServerThread(Bootstrap bootstrap) { + public ServerThread(Main main) { super("server"); - this.bootstrap = bootstrap; + this.main = main; } @Override @@ -36,7 +36,7 @@ public void run() { logger.info(SERVER_MARKER, "Server has started"); running = true; - bootstrap.onServerStarted(); + main.onServerStarted(); while(!gameServer.shouldStop()) { tickLoop.update(); @@ -80,7 +80,7 @@ private void stopUnsafe() { logger.error(SERVER_MARKER, "An unexpected exception occured whilst shutting down", e); } finally { running = false; - bootstrap.onServerStopped(); + main.onServerStopped(); } } diff --git a/src/main/java/brainwine/gui/GameLauncher.java b/src/main/java/brainwine/gui/GameLauncher.java new file mode 100644 index 00000000..23c623dc --- /dev/null +++ b/src/main/java/brainwine/gui/GameLauncher.java @@ -0,0 +1,233 @@ +package brainwine.gui; + +import static brainwine.gui.GuiConstants.DEEPWORLD_ASSEMBLY_PATH; +import static brainwine.gui.GuiConstants.DEEPWORLD_PLAYERPREFS; +import static brainwine.gui.GuiConstants.HTTP_STEAM_DOWNLOAD_URL; +import static brainwine.gui.GuiConstants.STEAM_REGISTRY_LOCATION; +import static brainwine.gui.GuiConstants.STEAM_RUN_GAME_URL; + +import java.awt.Desktop; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; + +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; + +import brainwine.gui.task.FileDownloadTask; +import brainwine.gui.task.ZipExtractTask; +import brainwine.gui.view.ProgressView; +import brainwine.patch.PatchFile; +import brainwine.util.DesktopUtils; +import brainwine.util.ProcessResult; +import brainwine.util.RegistryKey; +import brainwine.util.RegistryUtils; +import brainwine.util.SwingUtils; + +public abstract class GameLauncher { + + protected final JComponent owner; + + public GameLauncher(JComponent owner) { + this.owner = owner; + } + + public abstract void startGame(); + + /** + * Windows game launcher + */ + public static class Windows extends GameLauncher { + + public Windows(JComponent owner) { + super(owner); + } + + @Override + public void startGame() { + // Show option to download Steam if it is not installed + if(!isSteamInstalled()) { + if(JOptionPane.showConfirmDialog(owner, "You need the Steam desktop application to play Deepworld on Windows.\n" + + "Would you like to go to the download page?", "Attention", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { + DesktopUtils.browseUrl(HTTP_STEAM_DOWNLOAD_URL); + } + + return; + } + + // Update registry keys + String serverAddress = GameSettings.getServerAddress(); + String gatewayPort = String.format(":%s", GameSettings.getGatewayPort()); + String apiPort = String.format(":%s", GameSettings.getApiPort()); + boolean appendPort = !serverAddress.equals("local"); + ProcessResult addGatewayResult = RegistryUtils.add(DEEPWORLD_PLAYERPREFS, "gateway", String.format("%s%s", serverAddress, appendPort ? gatewayPort : "")); + ProcessResult addApiResult = RegistryUtils.add(DEEPWORLD_PLAYERPREFS, "api", String.format("%s%s", serverAddress, appendPort ? apiPort : "")); + + if(!addGatewayResult.wasSuccessful() || !addApiResult.wasSuccessful()) { + JOptionPane.showMessageDialog(owner, "Couldn't update gateway/API host settings in the registry." + + " You may have to do it manually.", "Error", JOptionPane.ERROR_MESSAGE); + } + + // Check if the game is patched + if(!serverAddress.equals("local") && !isGamePatched()) { + JOptionPane.showMessageDialog(owner, + "It appears that the game has not been patched." + + " The world search function will likely not work if this is the case." + + " To patch the game, please follow the instructions on the GitHub repository.", + "Attention", JOptionPane.WARNING_MESSAGE); + } + + // Start the game! + DesktopUtils.browseUrl(STEAM_RUN_GAME_URL); + } + + private boolean isSteamInstalled() { + RegistryKey steamKey = RegistryUtils.getFirstQueryResult(RegistryUtils.query(STEAM_REGISTRY_LOCATION)); + return steamKey != null; + } + + private boolean isGamePatched() { + RegistryKey steamPathKey = RegistryUtils.getFirstQueryResult( + RegistryUtils.query(STEAM_REGISTRY_LOCATION, "SteamPath")); + + if(steamPathKey != null) { + String assemblyPath = steamPathKey.getValue() + DEEPWORLD_ASSEMBLY_PATH; + File file = new File(assemblyPath); + + // Won't always be 100% accurate but it does the job. + if(file.length() == 3916800) { + return false; + } + } + + return true; + } + } + + /** + * MacOS game launcher + */ + public static class Mac extends GameLauncher { + + private final JButton startButton; + private SwingWorker currentTask; + + public Mac(JComponent owner, JButton startButton) { + super(owner); + this.startButton = startButton; + } + + @Override + public void startGame() { + String serverAddress = GameSettings.getServerAddress(); + String version = GameSettings.getGameVersion(); + startGame(serverAddress, version); + } + + private void startGame(String serverAddress, String version) { + File clientDirectory = new File("clients", String.format("v%s", version)); + File applicationFile = new File(clientDirectory, "Deepworld.app"); + File binaryFile = new File(applicationFile, "Contents/MacOS/Deepworld"); + + // Download game if necessary + if(!binaryFile.exists()) { + if(JOptionPane.showConfirmDialog(owner, String.format("Couldn't find game client for Deepworld v%s\nDo you want to download it?", version), + "Attention", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) { + return; + } + + // The callback will not run if download or extraction fails + downloadGameClient(version, clientDirectory, () -> startGame(serverAddress, version)); + return; + } + + // chmod +x + binaryFile.setExecutable(true); + + // Patch the game binary + try { + if(!patchGameBinary(binaryFile, serverAddress, version)) { + return; + } + } catch(IOException e) { + SwingUtils.showExceptionInfo(owner, "Couldn't patch game binary.", e); + return; + } + + // Launch the application + try { + Desktop.getDesktop().open(applicationFile); + } catch(IOException e) { + SwingUtils.showExceptionInfo(owner, "Couldn't launch application.", e); + } + } + + private boolean patchGameBinary(File binaryFile, String serverAddress, String version) throws IOException { + PatchFile patchFile = null; + + // Load patch file + try(InputStream inputStream = getClass().getResourceAsStream(String.format("/patches/deepworld-%s.patch", version))) { + patchFile = new PatchFile(inputStream); + } + + try(RandomAccessFile file = new RandomAccessFile(binaryFile, "rw")) { + serverAddress = (serverAddress.isEmpty() || serverAddress.equals("local")) ? "http://127.0.0.1:5001" : serverAddress; + byte[] addressBytes = serverAddress.getBytes(); + + // Check server address length + if(addressBytes.length > 33) { + JOptionPane.showMessageDialog(owner, "Server address may not exceed 33 characters in length.", "Attention", JOptionPane.WARNING_MESSAGE); + return false; + } + + // Apply patches + patchFile.apply(file, patch -> { + patch.setBytes("gateway_host", addressBytes); + patch.setInt("gateway_host_strlen", addressBytes.length); + }); + } + + return true; + } + + private void downloadGameClient(String version, File outputDirectory, Runnable callback) { + ProgressView progressView = new ProgressView(owner, "Downloading..."); + progressView.addCloseListener(() -> { + if(currentTask != null && !currentTask.isDone()) { + currentTask.cancel(true); + } + }); + + startButton.setEnabled(false); + String url = String.format("https://github.com/kuroppoi/deepworld-binaries/releases/download/MacOS/deepworld-%s.zip", version); + FileDownloadTask downloadTask = new FileDownloadTask(url, file -> { + if(file == null) { + startButton.setEnabled(true); + progressView.dispose(); + return; + } + + ZipExtractTask extractTask = new ZipExtractTask(file, outputDirectory, success -> { + startButton.setEnabled(true); + progressView.dispose(); + + if(success) { + callback.run(); + } + }); + + progressView.setText("Extracting..."); + currentTask = extractTask; + extractTask.addPropertyChangeListener(event -> progressView.setProgress(extractTask.getProgress())); + extractTask.execute(); + }); + + currentTask = downloadTask; + downloadTask.addPropertyChangeListener(event -> progressView.setProgress(downloadTask.getProgress())); + downloadTask.execute(); + } + } +} diff --git a/src/main/java/brainwine/gui/GamePanel.java b/src/main/java/brainwine/gui/GamePanel.java deleted file mode 100644 index 9c164ea7..00000000 --- a/src/main/java/brainwine/gui/GamePanel.java +++ /dev/null @@ -1,160 +0,0 @@ -package brainwine.gui; - -import static brainwine.gui.GuiConstants.DEEPWORLD_ASSEMBLY_PATH; -import static brainwine.gui.GuiConstants.DEEPWORLD_PLAYERPREFS; -import static brainwine.gui.GuiConstants.HTTP_COMMUNITY_HUB_URL; -import static brainwine.gui.GuiConstants.HTTP_STEAM_DOWNLOAD_URL; -import static brainwine.gui.GuiConstants.STEAM_COMMUNITY_HUB_URL; -import static brainwine.gui.GuiConstants.STEAM_REGISTRY_LOCATION; -import static brainwine.gui.GuiConstants.STEAM_RUN_GAME_URL; -import static brainwine.shared.LogMarkers.GUI_MARKER; - -import java.awt.Color; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.GridBagLayout; -import java.awt.GridLayout; -import java.awt.LinearGradientPaint; -import java.io.File; -import java.io.IOException; - -import javax.imageio.ImageIO; -import javax.swing.JButton; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.UIManager; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import brainwine.gui.component.ImagePanel; -import brainwine.util.DesktopUtils; -import brainwine.util.ProcessResult; -import brainwine.util.RegistryKey; -import brainwine.util.RegistryUtils; -import brainwine.util.SwingUtils; - -@SuppressWarnings("serial") -public class GamePanel extends ImagePanel { - - private static final Logger logger = LogManager.getLogger(); - private final LinearGradientPaint gradientPaint = new LinearGradientPaint(0, 0, 0, 15, - new float[] {0.0F, 1.0F}, new Color[] {Color.BLACK, new Color(0, 0, 0, 0)}); - private final JButton startGameButton; - private final JButton communityHubButton; - - public GamePanel(MainView mainView) { - setLayout(new GridBagLayout()); - - // Host settings button - JButton hostSettingsButton = new JButton("Host Settings", UIManager.getIcon("Brainwine.settingsIcon")); - hostSettingsButton.addActionListener(event -> mainView.showHostSettings()); - - // Community hub button - communityHubButton = new JButton("Community Hub", UIManager.getIcon("Brainwine.communityIcon")); - communityHubButton.addActionListener(event -> openCommunityHub()); - - // Start game button - startGameButton = new JButton("Start Deepworld", UIManager.getIcon("Brainwine.playIcon")); - startGameButton.addActionListener(event -> startGame()); - - // Button panel - JPanel buttonPanel = new JPanel(new GridBagLayout()); - buttonPanel.setOpaque(false); - - JPanel topPanel = new JPanel(new GridLayout(1, 2)); - topPanel.setOpaque(false); - topPanel.add(hostSettingsButton); - topPanel.add(communityHubButton); - buttonPanel.add(topPanel, SwingUtils.createConstraints(0, 0)); - buttonPanel.add(startGameButton, SwingUtils.createConstraints(0, 1, 2, 1)); - add(buttonPanel); - - // Load & set background image - try { - setImage(ImageIO.read(getClass().getResourceAsStream("/background.jpg"))); - } catch (IllegalArgumentException | IOException e) { - logger.error(GUI_MARKER, "Could not load background image", e); - } - } - - @Override - public void paintComponent(Graphics graphics) { - super.paintComponent(graphics); - - // Draw shadow gradient below the title bar if this panel has a background image - if(getImage() != null) { - Graphics2D g2d = (Graphics2D)graphics; - g2d.setPaint(gradientPaint); - g2d.fillRect(0, 0, getWidth(), 15); - } - } - - private void startGame() { - // Show option to download Steam if it is not installed - if(!isSteamInstalled()) { - if(JOptionPane.showConfirmDialog(getRootPane(), "You need the Steam desktop application to play Deepworld on Windows.\n" - + "Would you like to go to the download page?", "Attention", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { - DesktopUtils.browseUrl(HTTP_STEAM_DOWNLOAD_URL); - } - - return; - } - - // Update registry keys - String gatewayHost = GuiPreferences.getString(GuiPreferences.GATEWAY_HOST_KEY, "local"); - String apiHost = GuiPreferences.getString(GuiPreferences.API_HOST_KEY, "local"); - ProcessResult addGatewayResult = RegistryUtils.add(DEEPWORLD_PLAYERPREFS, "gateway", gatewayHost.isEmpty() ? "local" : gatewayHost); - ProcessResult addApiResult = RegistryUtils.add(DEEPWORLD_PLAYERPREFS, "api", apiHost.isEmpty() ? "local" : apiHost); - - if(!addGatewayResult.wasSuccessful() || !addApiResult.wasSuccessful()) { - JOptionPane.showMessageDialog(getRootPane(), "Couldn't update gateway/api host settings in the registry." - + " You may have to do it manually.", "Error", JOptionPane.ERROR_MESSAGE); - } - - // Check if the game is patched - if(!isGamePatched()) { - JOptionPane.showMessageDialog(getRootPane(), - "It appears that the game has not been patched." - + " API features such as the zone searcher will likely not work if this is the case." - + " To patch the game, please follow the instructions on the GitHub repository.", - "Attention", JOptionPane.WARNING_MESSAGE); - } - - // Start the game! - DesktopUtils.browseUrl(STEAM_RUN_GAME_URL); - } - - private void openCommunityHub() { - // If Steam is not installed, open the community hub in a web browser - if(!isSteamInstalled()) { - DesktopUtils.browseUrl(HTTP_COMMUNITY_HUB_URL); - return; - } - - // Otherwise, open it through Steam! - DesktopUtils.browseUrl(STEAM_COMMUNITY_HUB_URL); - } - - private boolean isSteamInstalled() { - RegistryKey steamKey = RegistryUtils.getFirstQueryResult(RegistryUtils.query(STEAM_REGISTRY_LOCATION)); - return steamKey != null; - } - - private boolean isGamePatched() { - RegistryKey steamPathKey = RegistryUtils.getFirstQueryResult( - RegistryUtils.query(STEAM_REGISTRY_LOCATION, "SteamPath")); - - if(steamPathKey != null) { - String assemblyPath = steamPathKey.getValue() + DEEPWORLD_ASSEMBLY_PATH; - File file = new File(assemblyPath); - - // Won't always be 100% accurate but it does the job. - if(file.length() == 3916800) { - return false; - } - } - - return true; - } -} diff --git a/src/main/java/brainwine/gui/GameSettings.java b/src/main/java/brainwine/gui/GameSettings.java new file mode 100644 index 00000000..74856fbc --- /dev/null +++ b/src/main/java/brainwine/gui/GameSettings.java @@ -0,0 +1,62 @@ +package brainwine.gui; + +import java.util.prefs.Preferences; + +import brainwine.gui.component.GamePanel; +import brainwine.util.OperatingSystem; + +public class GameSettings { + + // Keys + public static final String GAME_VERSION_KEY = "gameVersion"; + public static final String SERVER_ADDRESS_KEY = "serverAddress"; + public static final String GATEWAY_PORT_KEY = "gatewayPort"; + public static final String API_PORT_KEY = "apiPort"; + + // Defaults + public static final String GAME_VERSION_DEFAULT = GamePanel.MAC_GAME_VERSIONS[0]; + public static final String SERVER_ADDRESS_DEFAULT = OperatingSystem.isWindows() ? "local" : "http://127.0.0.1:5001"; + public static final int GATEWAY_PORT_DEFAULT = 5001; + public static final int API_PORT_DEFAULT = 5003; + + private static Preferences preferences = Preferences.userRoot().node(GameSettings.class.getName()); + + public static void resetToDefaults() { + setGameVersion(GAME_VERSION_DEFAULT); + setServerAddress(SERVER_ADDRESS_DEFAULT); + setGatewayPort(GATEWAY_PORT_DEFAULT); + setApiPort(API_PORT_DEFAULT); + } + + public static void setGameVersion(String value) { + preferences.put(GAME_VERSION_KEY, value); + } + + public static String getGameVersion() { + return preferences.get(GAME_VERSION_KEY, GAME_VERSION_DEFAULT); + } + + public static void setServerAddress(String value) { + preferences.put(SERVER_ADDRESS_KEY, value); + } + + public static String getServerAddress() { + return preferences.get(SERVER_ADDRESS_KEY, SERVER_ADDRESS_DEFAULT); + } + + public static void setGatewayPort(int value) { + preferences.putInt(GATEWAY_PORT_KEY, value); + } + + public static int getGatewayPort() { + return preferences.getInt(GATEWAY_PORT_KEY, GATEWAY_PORT_DEFAULT); + } + + public static void setApiPort(int value) { + preferences.putInt(API_PORT_KEY, value); + } + + public static int getApiPort() { + return preferences.getInt(API_PORT_KEY, API_PORT_DEFAULT); + } +} diff --git a/src/main/java/brainwine/gui/GuiPreferences.java b/src/main/java/brainwine/gui/GuiPreferences.java index 92d416cb..077ec735 100644 --- a/src/main/java/brainwine/gui/GuiPreferences.java +++ b/src/main/java/brainwine/gui/GuiPreferences.java @@ -1,87 +1,50 @@ package brainwine.gui; -import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; +import com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDarkerIJTheme; + public class GuiPreferences { - - public static final String ROOT_KEY = "brainwine"; + + // Keys public static final String THEME_KEY = "theme"; public static final String TAB_PLACEMENT_KEY = "tabPlacement"; public static final String FONT_SIZE_KEY = "fontSize"; - public static final String EMBED_MENU_BAR_KEY = "embedMenuBar"; - public static final String GATEWAY_HOST_KEY = "gatewayHost"; - public static final String API_HOST_KEY = "apiHost"; - private static Preferences preferences; - - public static Preferences get() { - if(preferences == null) { - preferences = Preferences.userRoot().node(ROOT_KEY); - } - - return preferences; - } - - public static void setString(String key, String value) { - get().put(key, value); - } - - public static String getString(String key, String def) { - return get().get(key, def); - } - public static void setBoolean(String key, boolean value) { - get().putBoolean(key, value); - } - - public static boolean getBoolean(String key, boolean def) { - return get().getBoolean(key, def); - } - - public static void setInt(String key, int value) { - get().putInt(key, value); - } + // Defaults + public static final String THEME_DEFAULT = FlatMaterialDarkerIJTheme.class.getName(); + public static final int TAB_PLACEMENT_DEFAULT = 1; + public static final int FONT_SIZE_DEFAULT = 16; - public static int getInt(String key, int def) { - return get().getInt(key, def); - } - - public static void setFloat(String key, float value) { - get().putFloat(key, value); - } - - public static float getFloat(String key, float def) { - return get().getFloat(key, def); - } + private static Preferences preferences = Preferences.userRoot().node(GuiPreferences.class.getName()); - public static void setLong(String key, long value) { - get().putLong(key, value); + public static void resetToDefaults() { + setTheme(THEME_DEFAULT); + setTabPlacement(TAB_PLACEMENT_DEFAULT); + setFontSize(FONT_SIZE_DEFAULT); } - public static long getLong(String key, long def) { - return get().getLong(key, def); + public static void setTheme(String value) { + preferences.put(THEME_KEY, value); } - public static void setDouble(String key, double value) { - get().putDouble(key, value); + public static String getTheme() { + return preferences.get(THEME_KEY, THEME_DEFAULT); } - public static double getDouble(String key, double def) { - return get().getDouble(key, def); + public static void setTabPlacement(int value) { + preferences.putInt(TAB_PLACEMENT_KEY, value); } - public static void setByteArray(String key, byte[] value) { - get().putByteArray(key, value); + public static int getTabPlacement() { + return preferences.getInt(TAB_PLACEMENT_KEY, TAB_PLACEMENT_DEFAULT); } - public static byte[] getByteArray(String key, byte[] def) { - return get().getByteArray(key, def); + public static void setFontSize(int value) { + preferences.putInt(FONT_SIZE_KEY, value); } - public static void clear() throws BackingStoreException { - if(preferences != null) { - preferences.removeNode(); - preferences = null; - } + public static int getFontSize() { + return preferences.getInt(FONT_SIZE_KEY, FONT_SIZE_DEFAULT); } } diff --git a/src/main/java/brainwine/gui/SettingsPanel.java b/src/main/java/brainwine/gui/SettingsPanel.java deleted file mode 100644 index ca124e59..00000000 --- a/src/main/java/brainwine/gui/SettingsPanel.java +++ /dev/null @@ -1,231 +0,0 @@ -package brainwine.gui; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.GridBagLayout; -import java.awt.GridLayout; -import java.util.prefs.BackingStoreException; - -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JSpinner; -import javax.swing.SpinnerNumberModel; -import javax.swing.SwingUtilities; -import javax.swing.border.Border; -import javax.swing.border.TitledBorder; -import javax.swing.event.DocumentEvent; - -import com.formdev.flatlaf.extras.components.FlatScrollPane; -import com.formdev.flatlaf.extras.components.FlatTextField; -import com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDarkerIJTheme; - -import brainwine.gui.event.DocumentChangeListener; -import brainwine.gui.theme.Theme; -import brainwine.gui.theme.ThemeManager; -import brainwine.util.OperatingSystem; -import brainwine.util.SwingUtils; - -@SuppressWarnings("serial") -public class SettingsPanel extends JPanel { - - private final MainView mainView; - private JComboBox themeBox; - private JComboBox tabPlacementBox; - private JSpinner fontSizeSpinner; - private JCheckBox embedMenuBarCheckbox; - private FlatTextField gatewayHostField; - private FlatTextField apiHostField; - - public SettingsPanel(MainView mainView) { - this.mainView = mainView; - - // Reset Button - JButton resetButton = new JButton("Reset to Defaults"); - resetButton.addActionListener(event -> resetSettings(true)); - - // Clear Button - JButton clearButton = new JButton("Clear Settings"); - clearButton.addActionListener(event -> clearSettings()); - - // Button Panel - JPanel buttonPanel = new JPanel(new GridLayout(1, 2)); - buttonPanel.setBorder(createCategoryBorder("Reset Settings")); - buttonPanel.add(resetButton); - buttonPanel.add(clearButton); - - // Main panel - JPanel settingsPanel = new JPanel(new GridBagLayout()); - settingsPanel.add(createVisualSettingsPanel(), SwingUtils.createConstraints(0, 0)); - - if(OperatingSystem.isWindows()) { - settingsPanel.add(createGameSettingsPanel(), SwingUtils.createConstraints(0, 1)); - } - - settingsPanel.add(buttonPanel, SwingUtils.createConstraints(0, 2)); - - // Scroll pane (TODO doesn't actually scroll) - FlatScrollPane scrollPane = new FlatScrollPane(); - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - scrollPane.setViewportView(settingsPanel); - scrollPane.setShowButtons(true); - scrollPane.setFocusable(false); - add(scrollPane); - } - - private JPanel createVisualSettingsPanel() { - // Theme selector box - themeBox = new JComboBox<>(); - ThemeManager.getThemes().forEach(themeBox::addItem); - themeBox.setSelectedItem(ThemeManager.getCurrentTheme()); - themeBox.addActionListener(event -> setThemePreference((Theme)themeBox.getSelectedItem())); - - // Tabbed pane orientation box - tabPlacementBox = new JComboBox<>(new String[] {"Top", "Left", "Bottom", "Right"}); - tabPlacementBox.setSelectedIndex(mainView.getTabPlacement() - 1); - tabPlacementBox.addActionListener(event -> setTabPlacementPreference(tabPlacementBox.getSelectedIndex() + 1)); - - // Font size changer - fontSizeSpinner = new JSpinner(new SpinnerNumberModel(SwingUtils.getDefaultFontSize(), 10, 28, 1)); - fontSizeSpinner.addChangeListener(event -> setFontSizePreference((int)fontSizeSpinner.getValue())); - - // Menu bar embed checkbox - embedMenuBarCheckbox = new JCheckBox(); - embedMenuBarCheckbox.setSelected(SwingUtils.isMenuBarEmbedded()); - embedMenuBarCheckbox.addChangeListener(event -> setEmbedMenuBarPreference(embedMenuBarCheckbox.isSelected())); - - // Panel - JPanel panel = new JPanel(new GridBagLayout()); - panel.setBorder(createCategoryBorder("Visual Settings")); - panel.add(new JLabel("Theme"), SwingUtils.createConstraints(0, 0)); - panel.add(themeBox, SwingUtils.createConstraints(1, 0)); - panel.add(new JLabel("Tab Placement"), SwingUtils.createConstraints(0, 1)); - panel.add(tabPlacementBox, SwingUtils.createConstraints(1, 1)); - panel.add(new JLabel("Font Size"), SwingUtils.createConstraints(0, 2)); - panel.add(fontSizeSpinner, SwingUtils.createConstraints(1, 2)); - panel.add(new JLabel("Embed Menu Bar"), SwingUtils.createConstraints(0, 3)); - panel.add(embedMenuBarCheckbox, SwingUtils.createConstraints(1, 3)); - return panel; - } - - private JPanel createGameSettingsPanel() { - // Gateway host field - gatewayHostField = new FlatTextField() { - @Override - public Dimension getPreferredSize() { - return themeBox.getPreferredSize(); - } - }; - gatewayHostField.setText(GuiPreferences.getString(GuiPreferences.GATEWAY_HOST_KEY, "local")); - gatewayHostField.setPlaceholderText("127.0.0.1:5001"); - gatewayHostField.getDocument().addDocumentListener(new DocumentChangeListener() { - @Override - public void changedUpdate(DocumentEvent event) { - GuiPreferences.setString(GuiPreferences.GATEWAY_HOST_KEY, gatewayHostField.getText()); - } - }); - - // API host field - apiHostField = new FlatTextField() { - @Override - public Dimension getPreferredSize() { - return themeBox.getPreferredSize(); - } - }; - apiHostField.setText(GuiPreferences.getString(GuiPreferences.API_HOST_KEY, "local")); - apiHostField.setPlaceholderText("127.0.0.1:5003"); - apiHostField.getDocument().addDocumentListener(new DocumentChangeListener() { - @Override - public void changedUpdate(DocumentEvent event) { - GuiPreferences.setString(GuiPreferences.API_HOST_KEY, apiHostField.getText()); - } - }); - - // Panel - JPanel panel = new JPanel(new GridBagLayout()); - panel.setBorder(createCategoryBorder("Game Settings")); - panel.add(new JLabel("Gateway Host"), SwingUtils.createConstraints(0, 0)); - panel.add(gatewayHostField, SwingUtils.createConstraints(1, 0, 1, 1, 0, 0)); - panel.add(new JLabel("API Host"), SwingUtils.createConstraints(0, 1)); - panel.add(apiHostField, SwingUtils.createConstraints(1, 1, 1, 1, 0, 0)); - return panel; - } - - public void focusHostSettings() { - gatewayHostField.requestFocus(); - } - - private void setThemePreference(Theme theme) { - SwingUtilities.invokeLater(() -> { - ThemeManager.setTheme(theme); - }); - - GuiPreferences.setString(GuiPreferences.THEME_KEY, theme.getClassName()); - } - - private void setTabPlacementPreference(int tabPlacement) { - SwingUtilities.invokeLater(() -> { - mainView.setTabPlacement(tabPlacement); - }); - - GuiPreferences.setInt(GuiPreferences.TAB_PLACEMENT_KEY, tabPlacement); - } - - private void setFontSizePreference(int fontSize) { - SwingUtils.setDefaultFontSize(fontSize); - GuiPreferences.setInt(GuiPreferences.FONT_SIZE_KEY, fontSize); - } - - private void setEmbedMenuBarPreference(boolean embedMenuBar) { - SwingUtils.setMenuBarEmbedded(embedMenuBar); - GuiPreferences.setBoolean(GuiPreferences.EMBED_MENU_BAR_KEY, embedMenuBar); - } - - private void resetSettings(boolean showPrompt) { - if(!showPrompt || JOptionPane.showConfirmDialog(getRootPane(), - "Are you sure you want to reset all settings to their default values?", - "Confirmation", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { - themeBox.setSelectedItem(ThemeManager.getTheme(FlatMaterialDarkerIJTheme.class)); - tabPlacementBox.setSelectedIndex(0); - fontSizeSpinner.setValue(15); - embedMenuBarCheckbox.setSelected(true); - - if(OperatingSystem.isWindows()) { - gatewayHostField.setText("local"); - apiHostField.setText("local"); - } - - if(showPrompt) { - JOptionPane.showMessageDialog(getRootPane(), "Settings reset successfully."); - } - } - } - - private void clearSettings() { - if(JOptionPane.showConfirmDialog(getRootPane(), - "This will reset all settings to their default values and remove them from this user's preferences" - + " until you change them again or restart the application. Are you sure you want to continue?", - "Confirmation", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { - resetSettings(false); - - // Invoke later because settings are not reset immediately. - SwingUtilities.invokeLater(() -> { - try { - GuiPreferences.clear(); - JOptionPane.showMessageDialog(getRootPane(), "Settings cleared successfully."); - } catch(BackingStoreException e) { - JOptionPane.showMessageDialog(getRootPane(), "Could not clear preferences: " + e.getMessage(), - "Error", JOptionPane.ERROR_MESSAGE); - } - }); - } - } - - private Border createCategoryBorder(String category) { - return BorderFactory.createTitledBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Color.LIGHT_GRAY), category, TitledBorder.CENTER, TitledBorder.CENTER); - } -} diff --git a/src/main/java/brainwine/gui/component/GamePanel.java b/src/main/java/brainwine/gui/component/GamePanel.java new file mode 100644 index 00000000..844bd905 --- /dev/null +++ b/src/main/java/brainwine/gui/component/GamePanel.java @@ -0,0 +1,215 @@ +package brainwine.gui.component; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.io.IOException; + +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +import com.formdev.flatlaf.extras.components.FlatTextField; + +import brainwine.Main; +import brainwine.ServerStatusListener; +import brainwine.gui.GameLauncher; +import brainwine.gui.GameSettings; +import brainwine.gui.GuiConstants; +import brainwine.util.DesktopUtils; +import brainwine.util.OperatingSystem; +import brainwine.util.SwingUtils; + +@SuppressWarnings("serial") +public class GamePanel extends JPanel { + + public static final String[] MAC_GAME_VERSIONS = { "2.11.1", "1.13.3" }; + private final Main main; + private final GameLauncher gameLauncher; + private JButton serverButton; + private JButton startGameButton; + private FlatTextField serverAddressField; + private FlatTextField gatewayPortField; // Windows only + private FlatTextField apiPortField; // Windows only + private JComboBox gameVersionBox; // Mac only + + public GamePanel(Main main) { + this.main = main; + setLayout(new BorderLayout()); + + // Create image panel + ImagePanel imagePanel = new ImagePanel() { + @Override + public void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + Graphics2D g2d = (Graphics2D)graphics; + Color gradientColor = new Color(8, 8, 8, 255); + Color fadeColor = new Color(8, 8, 8, 0); + g2d.setPaint(new GradientPaint(0, 0, gradientColor, 0, 20, fadeColor)); + g2d.fillRect(0, 0, getWidth(), 20); + g2d.setPaint(new GradientPaint(0, getHeight() - 20, fadeColor, 0, getHeight(), gradientColor)); + g2d.fillRect(0, getHeight() - 20, getWidth(), 20); + } + }; + + // Load background image + try { + imagePanel.setImage(ImageIO.read(getClass().getResourceAsStream("/background.jpg"))); + } catch (IllegalArgumentException | IOException e) { + SwingUtils.showExceptionInfo(this, "Could not load background image.", e); + } + + // Add components + add(imagePanel); + add(createLauncherPanel(), BorderLayout.SOUTH); // Game launcher relies on this so do not move! + + // Create game launcher + gameLauncher = OperatingSystem.isWindows() ? new GameLauncher.Windows(this) : new GameLauncher.Mac(this, startGameButton); + + // Create server status listener + main.addServerStatusListener(new ServerStatusListener() { + @Override + public void onServerStarting() { + serverButton.setEnabled(false); + } + + @Override + public void onServerStopping() { + serverButton.setEnabled(false); + } + + @Override + public void onServerStarted() { + SwingUtilities.invokeLater(() -> { + serverButton.setText("Stop Server"); + serverButton.setEnabled(true); + }); + } + + @Override + public void onServerStopped() { + SwingUtilities.invokeLater(() -> { + serverButton.setText("Start Server"); + serverButton.setEnabled(true); + }); + } + }); + } + + private void openCommunityHub() { + // Try opening with Steam and open with browser if it fails + if(!DesktopUtils.browseUrl(GuiConstants.STEAM_COMMUNITY_HUB_URL)) { + DesktopUtils.browseUrl(GuiConstants.HTTP_COMMUNITY_HUB_URL); + } + } + + private JPanel createLauncherPanel() { + // Start button + startGameButton = new JButton("Start Deepworld", UIManager.getIcon("Brainwine.playIcon")); + startGameButton.addActionListener(event -> { + // Update preferences + GameSettings.setServerAddress(SwingUtils.getTextFieldValue(serverAddressField)); + + if(OperatingSystem.isWindows()) { + try { + GameSettings.setGatewayPort(Integer.parseInt(SwingUtils.getTextFieldValue(gatewayPortField))); + GameSettings.setApiPort(Integer.parseInt(SwingUtils.getTextFieldValue(apiPortField))); + } catch(NumberFormatException e) { + // Show warning & discard exception silently + JOptionPane.showMessageDialog(this, "Server ports must be numerical.", "Attention", JOptionPane.WARNING_MESSAGE); + return; + } + } else { + GameSettings.setGameVersion((String)gameVersionBox.getSelectedItem()); + } + + // Launch the game + gameLauncher.startGame(); + }); + + // Server button + serverButton = new JButton("Start Server", UIManager.getIcon("Brainwine.serverIcon")); + serverButton.addActionListener(event -> main.toggleServer()); + + // Community hub button + JButton communityHubButton = new JButton("Community Hub", UIManager.getIcon("Brainwine.communityIcon")); + communityHubButton.addActionListener(event -> openCommunityHub()); + + // Create top button panel + JPanel topButtonPanel = new JPanel(new GridLayout(1, 2)); + topButtonPanel.add(serverButton); + topButtonPanel.add(communityHubButton); + + // Create button panel + JPanel buttonPanel = new JPanel(new GridLayout(2, 1)); + buttonPanel.add(topButtonPanel); + buttonPanel.add(startGameButton); + + // Create panel + JPanel panel = new JPanel(new GridLayout(1, 2)); + panel.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 5)); + panel.add(OperatingSystem.isWindows() ? createWindowsGameSettingsPanel() : createMacGameSettingsPanel()); + panel.add(buttonPanel); + return panel; + } + + private JPanel createWindowsGameSettingsPanel() { + // Create server address field + serverAddressField = new FlatTextField(); + serverAddressField.setText(GameSettings.getServerAddress()); + serverAddressField.setPlaceholderText("local"); + serverAddressField.setShowClearButton(true); + + // Create gateway port field + gatewayPortField = new FlatTextField(); + gatewayPortField.setText(String.valueOf(GameSettings.getGatewayPort())); + gatewayPortField.setPlaceholderText("5001"); + gatewayPortField.setShowClearButton(true); + + // Create API port field + apiPortField = new FlatTextField(); + apiPortField.setText(String.valueOf(GameSettings.getApiPort())); + apiPortField.setPlaceholderText("5003"); + apiPortField.setShowClearButton(true); + + // Create panel + JPanel panel = new JPanel(new GridBagLayout()); + panel.add(new JLabel("Server Address"), SwingUtils.createConstraints(0, 0, 1, 1, 0, 1)); + panel.add(serverAddressField, SwingUtils.createConstraints(1, 0, 3, 1)); + panel.add(new JLabel("Gateway Port"), SwingUtils.createConstraints(0, 1)); + panel.add(gatewayPortField, SwingUtils.createConstraints(1, 1, 1, 1, 1, 0)); + panel.add(new JLabel("API Port", JLabel.CENTER), SwingUtils.createConstraints(2, 1)); + panel.add(apiPortField, SwingUtils.createConstraints(3, 1, 1, 1, 1, 0)); + return panel; + } + + private JPanel createMacGameSettingsPanel() { + // Create server address field + serverAddressField = new FlatTextField(); + serverAddressField.setText(GameSettings.getServerAddress()); + serverAddressField.setPlaceholderText("http://127.0.0.1:5001"); + serverAddressField.setShowClearButton(true); + + // Create game version selector + gameVersionBox = new JComboBox<>(MAC_GAME_VERSIONS); + gameVersionBox.setSelectedItem(GameSettings.getGameVersion()); + + // Create panel + JPanel panel = new JPanel(new GridBagLayout()); + panel.add(new JLabel("Server Address"), SwingUtils.createConstraints(0, 0, 1, 1, 0, 1)); + panel.add(serverAddressField, SwingUtils.createConstraints(1, 0, 1, 1)); + panel.add(new JLabel("Game Version"), SwingUtils.createConstraints(0, 1, 1, 1, 0, 1)); + panel.add(gameVersionBox, SwingUtils.createConstraints(1, 1, 1, 1)); + return panel; + } +} diff --git a/src/main/java/brainwine/gui/ServerPanel.java b/src/main/java/brainwine/gui/component/ServerPanel.java similarity index 75% rename from src/main/java/brainwine/gui/ServerPanel.java rename to src/main/java/brainwine/gui/component/ServerPanel.java index 50a48df4..2416ff5a 100644 --- a/src/main/java/brainwine/gui/ServerPanel.java +++ b/src/main/java/brainwine/gui/component/ServerPanel.java @@ -1,4 +1,4 @@ -package brainwine.gui; +package brainwine.gui.component; import static brainwine.gui.GuiConstants.ERROR_COLOR; import static brainwine.gui.GuiConstants.INFO_COLOR; @@ -14,6 +14,7 @@ import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextPane; +import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; @@ -27,20 +28,21 @@ import com.formdev.flatlaf.extras.components.FlatScrollPane; import com.formdev.flatlaf.extras.components.FlatTextField; -import brainwine.Bootstrap; import brainwine.ListenableAppender; +import brainwine.Main; +import brainwine.ServerStatusListener; import brainwine.gui.event.AutoScrollAdjustmentListener; @SuppressWarnings("serial") public class ServerPanel extends JPanel { - private final Bootstrap bootstrap; + private final Main main; private final JTextPane consoleOutput; private final FlatTextField consoleInput; private final JButton serverButton; - public ServerPanel(Bootstrap bootstrap) { - this.bootstrap = bootstrap; + public ServerPanel(Main main) { + this.main = main; setLayout(new BorderLayout()); setBorder(BorderFactory.createEmptyBorder(0, 3, 3, 3)); @@ -93,8 +95,41 @@ public Font getFont() { // Server Toggle Button serverButton = new JButton("Start Server", UIManager.getIcon("Brainwine.powerIcon")); - serverButton.addActionListener(event -> toggleServer()); + serverButton.addActionListener(event -> main.toggleServer()); bottomPanel.add(serverButton, BorderLayout.LINE_END); + + // Create server status listener + main.addServerStatusListener(new ServerStatusListener() { + @Override + public void onServerStarting() { + serverButton.setEnabled(false); + consoleOutput.setText(null); + } + + @Override + public void onServerStopping() { + serverButton.setEnabled(false); + consoleInput.setEditable(false); + consoleInput.setText(null); + } + + @Override + public void onServerStarted() { + SwingUtilities.invokeLater(() -> { + serverButton.setText("Stop Server"); + serverButton.setEnabled(true); + consoleInput.setEditable(true); + }); + } + + @Override + public void onServerStopped() { + SwingUtilities.invokeLater(() -> { + serverButton.setText("Start Server"); + serverButton.setEnabled(true); + }); + } + }); } private void appendConsoleOutput(String text, Color color) { @@ -114,32 +149,8 @@ private void processConsoleInput() { if(!commandLine.isEmpty()) { appendConsoleOutput(String.format("> %s\n", commandLine), Color.GRAY); - bootstrap.executeCommand(commandLine); - consoleInput.setText(null); - } - } - - private void toggleServer() { - if(bootstrap.isServerRunning()) { - serverButton.setEnabled(false); - consoleInput.setEditable(false); + main.executeCommand(commandLine); consoleInput.setText(null); - bootstrap.stopServer(); - } else { - serverButton.setEnabled(false); - consoleInput.setEditable(true); - consoleOutput.setText(null); - bootstrap.startServer(); } } - - public void enableServerButton() { - if(!bootstrap.isServerRunning()) { - consoleInput.setEditable(false); - consoleInput.setText(null); - } - - serverButton.setText(bootstrap.isServerRunning() ? "Stop Server" : "Start Server"); - serverButton.setEnabled(true); - } } diff --git a/src/main/java/brainwine/gui/component/SettingsPanel.java b/src/main/java/brainwine/gui/component/SettingsPanel.java new file mode 100644 index 00000000..8cd0d9d6 --- /dev/null +++ b/src/main/java/brainwine/gui/component/SettingsPanel.java @@ -0,0 +1,111 @@ +package brainwine.gui.component; + +import java.awt.GridBagLayout; +import java.awt.GridLayout; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; + +import com.formdev.flatlaf.extras.components.FlatScrollPane; + +import brainwine.gui.GuiPreferences; +import brainwine.gui.theme.Theme; +import brainwine.gui.theme.ThemeManager; +import brainwine.gui.view.MainView; +import brainwine.util.SwingUtils; + +@SuppressWarnings("serial") +public class SettingsPanel extends JPanel { + + private final MainView mainView; + private JComboBox themeBox; + private JComboBox tabPlacementBox; + private JSpinner fontSizeSpinner; + + public SettingsPanel(MainView mainView) { + this.mainView = mainView; + + // Save button + JButton saveButton = new JButton("Save Settings"); + saveButton.addActionListener(event -> saveSettings()); + + // Reset Button + JButton resetButton = new JButton("Reset to Defaults"); + resetButton.addActionListener(event -> resetSettings(true)); + + // Button Panel + JPanel buttonPanel = new JPanel(new GridLayout(1, 2)); + buttonPanel.add(saveButton); + buttonPanel.add(resetButton); + + // Main panel + JPanel settingsPanel = new JPanel(new GridBagLayout()); + settingsPanel.add(createVisualSettingsPanel(), SwingUtils.createConstraints(0, 0)); + settingsPanel.add(buttonPanel, SwingUtils.createConstraints(0, 1)); + + // Scroll pane (TODO doesn't actually scroll) + FlatScrollPane scrollPane = new FlatScrollPane(); + scrollPane.setBorder(BorderFactory.createEmptyBorder()); + scrollPane.setViewportView(settingsPanel); + scrollPane.setShowButtons(true); + scrollPane.setFocusable(false); + add(scrollPane); + } + + private JPanel createVisualSettingsPanel() { + // Theme selector box + themeBox = new JComboBox<>(); + ThemeManager.getThemes().forEach(themeBox::addItem); + themeBox.setSelectedItem(ThemeManager.getCurrentTheme()); + themeBox.addActionListener(event -> ThemeManager.setTheme((Theme)themeBox.getSelectedItem())); + + // Tabbed pane orientation box + tabPlacementBox = new JComboBox<>(new String[] {"Top", "Left", "Bottom", "Right"}); + tabPlacementBox.setSelectedIndex(mainView.getTabPlacement() - 1); + tabPlacementBox.addActionListener(event -> mainView.setTabPlacement(tabPlacementBox.getSelectedIndex() + 1)); + + // Font size changer + fontSizeSpinner = new JSpinner(new SpinnerNumberModel(SwingUtils.getDefaultFontSize(), 10, 28, 1)); + fontSizeSpinner.addChangeListener(event -> SwingUtils.setDefaultFontSize((int)fontSizeSpinner.getValue())); + + // Panel + JPanel panel = new JPanel(new GridBagLayout()); + panel.add(new JLabel("Theme"), SwingUtils.createConstraints(0, 0)); + panel.add(themeBox, SwingUtils.createConstraints(1, 0)); + panel.add(new JLabel("Tab Placement"), SwingUtils.createConstraints(0, 1)); + panel.add(tabPlacementBox, SwingUtils.createConstraints(1, 1)); + panel.add(new JLabel("Font Size"), SwingUtils.createConstraints(0, 2)); + panel.add(fontSizeSpinner, SwingUtils.createConstraints(1, 2)); + return panel; + } + + private void saveSettings() { + GuiPreferences.setTheme(((Theme)themeBox.getSelectedItem()).getClassName()); + GuiPreferences.setTabPlacement(tabPlacementBox.getSelectedIndex() + 1); + GuiPreferences.setFontSize((int)fontSizeSpinner.getValue()); + JOptionPane.showMessageDialog(getRootPane(), "Settings have been saved."); + } + + private void resetSettings(boolean showPrompt) { + if(!showPrompt || JOptionPane.showConfirmDialog(getRootPane(), + "Are you sure you want to reset all settings to their default values?", + "Confirmation", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { + GuiPreferences.resetToDefaults(); + + // Update components + themeBox.setSelectedItem(ThemeManager.getTheme(GuiPreferences.getTheme())); + tabPlacementBox.setSelectedIndex(GuiPreferences.getTabPlacement() - 1); + fontSizeSpinner.setValue(GuiPreferences.getFontSize()); + + if(showPrompt) { + JOptionPane.showMessageDialog(getRootPane(), "Settings reset successfully."); + } + } + } +} diff --git a/src/main/java/brainwine/gui/task/FileDownloadTask.java b/src/main/java/brainwine/gui/task/FileDownloadTask.java new file mode 100644 index 00000000..6a62c14b --- /dev/null +++ b/src/main/java/brainwine/gui/task/FileDownloadTask.java @@ -0,0 +1,79 @@ +package brainwine.gui.task; + +import static brainwine.shared.LogMarkers.GUI_MARKER; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +import javax.swing.SwingWorker; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import brainwine.util.SwingUtils; + +public class FileDownloadTask extends SwingWorker { + + private static final Logger logger = LogManager.getLogger(); + private final String urlString; + private final Consumer callback; + + public FileDownloadTask(String urlString, Consumer callback) { + this.urlString = urlString; + this.callback = callback; + } + + @Override + protected File doInBackground() throws Exception { + URL url = new URL(urlString); + URLConnection connection = url.openConnection(); + long totalLength = connection.getContentLengthLong(); + int length = 0; + long downloaded = 0; + byte[] buffer = new byte[1024]; + File file = File.createTempFile("download", ".tmp"); + file.deleteOnExit(); + BufferedOutputStream outputStream = null; + + try(BufferedInputStream inputStream = new BufferedInputStream(connection.getInputStream())) { + outputStream = new BufferedOutputStream(new FileOutputStream(file)); + + while((length = inputStream.read(buffer)) != -1) { + downloaded += length; + int progress = (int)(downloaded / (double)totalLength * 100); + setProgress(progress); + outputStream.write(buffer, 0, length); + } + } finally { + if(outputStream != null) { + outputStream.close(); + } + } + + return file; + } + + @Override + protected void done() { + File file = null; + + try { + file = get(); + } catch(ExecutionException e) { + String message = "Could not download file."; + logger.error(GUI_MARKER, message, e.getCause()); + SwingUtils.showExceptionInfo(null, message, e.getCause()); + } catch(CancellationException | InterruptedException e) { + // Discard silently + } + + callback.accept(file); + } +} diff --git a/src/main/java/brainwine/gui/task/ZipExtractTask.java b/src/main/java/brainwine/gui/task/ZipExtractTask.java new file mode 100644 index 00000000..9d79a21c --- /dev/null +++ b/src/main/java/brainwine/gui/task/ZipExtractTask.java @@ -0,0 +1,84 @@ +package brainwine.gui.task; + +import static brainwine.shared.LogMarkers.GUI_MARKER; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.swing.SwingWorker; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import brainwine.util.SwingUtils; + +public class ZipExtractTask extends SwingWorker { + + private static final Logger logger = LogManager.getLogger(); + private final File zipFile; + private final File outputDirectory; + private final Consumer callback; + + public ZipExtractTask(File zipFile, File outputDirectory, Consumer callback) { + this.zipFile = zipFile; + this.outputDirectory = outputDirectory; + this.callback = callback; + } + + @Override + protected Void doInBackground() throws Exception { + byte[] buffer = new byte[1024]; + long totalLength = zipFile.length(); + long extracted = 0; + + try(ZipInputStream inputStream = new ZipInputStream(new FileInputStream(zipFile))) { + ZipEntry entry = null; + + while((entry = inputStream.getNextEntry()) != null) { + if(entry.isDirectory()) { + continue; + } + + File file = new File(outputDirectory, entry.getName()); + file.getParentFile().mkdirs(); + + try(FileOutputStream outputStream = new FileOutputStream(file)) { + int length = 0; + + while((length = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, length); + } + } + + extracted += entry.getCompressedSize(); + inputStream.closeEntry(); + setProgress((int)(extracted / (double)totalLength * 100)); + } + } + + return null; + } + + @Override + protected void done() { + try { + get(); + callback.accept(true); + return; + } catch(ExecutionException e) { + String message = "Could not extract archive."; + logger.error(GUI_MARKER, message, e.getCause()); + SwingUtils.showExceptionInfo(null, message, e.getCause()); + } catch(CancellationException | InterruptedException e) { + // Discard silently + } + + callback.accept(false); + } +} diff --git a/src/main/java/brainwine/gui/theme/ThemeManager.java b/src/main/java/brainwine/gui/theme/ThemeManager.java index ab914513..7a49307c 100644 --- a/src/main/java/brainwine/gui/theme/ThemeManager.java +++ b/src/main/java/brainwine/gui/theme/ThemeManager.java @@ -19,7 +19,6 @@ import com.formdev.flatlaf.extras.FlatAnimatedLafChange; import com.formdev.flatlaf.intellijthemes.FlatAllIJThemes; import com.formdev.flatlaf.intellijthemes.FlatAllIJThemes.FlatIJLookAndFeelInfo; -import com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDarkerIJTheme; import brainwine.gui.GuiPreferences; @@ -43,7 +42,7 @@ public static void init() { } // Set saved or default theme - setTheme(GuiPreferences.getString(GuiPreferences.THEME_KEY, FlatMaterialDarkerIJTheme.class.getName()), false); + setTheme(GuiPreferences.getTheme(), false); } public static void registerTheme(Theme theme) { diff --git a/src/main/java/brainwine/gui/MainView.java b/src/main/java/brainwine/gui/view/MainView.java similarity index 79% rename from src/main/java/brainwine/gui/MainView.java rename to src/main/java/brainwine/gui/view/MainView.java index 85899024..280f8d61 100644 --- a/src/main/java/brainwine/gui/MainView.java +++ b/src/main/java/brainwine/gui/view/MainView.java @@ -1,4 +1,4 @@ -package brainwine.gui; +package brainwine.gui.view; import static brainwine.gui.GuiConstants.DEEPWORLD_PLAYERPREFS; import static brainwine.gui.GuiConstants.GITHUB_REPOSITORY_URL; @@ -25,10 +25,15 @@ import com.formdev.flatlaf.extras.components.FlatTabbedPane; import com.formdev.flatlaf.extras.components.FlatTabbedPane.TabAlignment; -import brainwine.Bootstrap; +import brainwine.Main; +import brainwine.gui.GuiPreferences; +import brainwine.gui.component.GamePanel; +import brainwine.gui.component.ServerPanel; +import brainwine.gui.component.SettingsPanel; import brainwine.util.DesktopUtils; import brainwine.util.OperatingSystem; import brainwine.util.ProcessResult; +import brainwine.util.ProcessUtils; import brainwine.util.RegistryKey; import brainwine.util.RegistryUtils; import brainwine.util.SwingUtils; @@ -39,10 +44,8 @@ public class MainView { private final JFrame frame; private final JPanel panel; private final FlatTabbedPane tabbedPane; - private final ServerPanel serverPanel; - private final SettingsPanel settingsPanel; - public MainView(Bootstrap bootstrap) { + public MainView(Main main) { logger.info(GUI_MARKER, "Creating main view ..."); // Panel @@ -52,24 +55,20 @@ public MainView(Bootstrap bootstrap) { tabbedPane = new FlatTabbedPane(); tabbedPane.setShowContentSeparators(true); tabbedPane.setTabAlignment(TabAlignment.leading); - setTabPlacement(GuiPreferences.getInt(GuiPreferences.TAB_PLACEMENT_KEY, 1), false); + setTabPlacement(GuiPreferences.getTabPlacement(), false); - if(OperatingSystem.isWindows()) { - tabbedPane.addTab("Play Game", UIManager.getIcon("Brainwine.playIcon"), new GamePanel(this)); + if(OperatingSystem.isWindows() || OperatingSystem.isMacOS()) { + tabbedPane.addTab("Play Game", UIManager.getIcon("Brainwine.playIcon"), new GamePanel(main)); } - tabbedPane.addTab("Server", UIManager.getIcon("Brainwine.serverIcon"), serverPanel = new ServerPanel(bootstrap)); - tabbedPane.addTab("Settings", UIManager.getIcon("Brainwine.settingsIcon"), settingsPanel = new SettingsPanel(this)); + tabbedPane.addTab("Server", UIManager.getIcon("Brainwine.serverIcon"), new ServerPanel(main)); + tabbedPane.addTab("Settings", UIManager.getIcon("Brainwine.settingsIcon"), new SettingsPanel(this)); panel.add(tabbedPane); // Menu JMenuBar menuBar = new JMenuBar(); JMenu helpMenu = new JMenu("Help"); - - if(OperatingSystem.isWindows()) { - helpMenu.add(SwingUtils.createAction("Clear Account Lock", this::showAccountLockPrompt)); - } - + helpMenu.add(SwingUtils.createAction("Clear Account Lock", this::showAccountLockPrompt)); helpMenu.add(SwingUtils.createAction("GitHub", () -> DesktopUtils.browseUrl(GITHUB_REPOSITORY_URL))); menuBar.add(helpMenu); @@ -82,7 +81,7 @@ public MainView(Bootstrap bootstrap) { frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent event) { - bootstrap.closeApplication(); + main.closeApplication(); } }); frame.setJMenuBar(menuBar); @@ -115,15 +114,6 @@ public int getTabPlacement() { return tabbedPane.getTabPlacement(); } - public void showHostSettings() { - tabbedPane.setSelectedComponent(settingsPanel); - settingsPanel.focusHostSettings(); - } - - public void enableServerButton() { - serverPanel.enableServerButton(); - } - private void showAccountLockPrompt() { int result = JOptionPane.showConfirmDialog(frame, "If you've found yourself in a situation where you are unable to log into an unregistered account" @@ -135,12 +125,16 @@ private void showAccountLockPrompt() { if(clearAccountLock()) { JOptionPane.showMessageDialog(frame, "Account lock removed. Register your account next time."); } else { - JOptionPane.showMessageDialog(frame, "Failed to remove account lock.", "Error", JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(frame, "Could not remove account lock.\nEither there is no account lock, or an error has occured.", "Error", JOptionPane.ERROR_MESSAGE); } } } private boolean clearAccountLock() { + return OperatingSystem.isWindows() ? clearAccountLockWindows() : OperatingSystem.isMacOS() ? clearAccountLockMacOS() : false; + } + + private boolean clearAccountLockWindows() { ProcessResult queryResult = RegistryUtils.query(DEEPWORLD_PLAYERPREFS, "playerLock*"); if(queryResult.wasSuccessful()) { @@ -151,11 +145,15 @@ private boolean clearAccountLock() { ProcessResult deleteResult = RegistryUtils.delete(DEEPWORLD_PLAYERPREFS, name); return deleteResult.wasSuccessful(); } else { - // Might as well. - return true; + return false; } } return false; } + + // A bit simpler but it should to the trick just fine. + private boolean clearAccountLockMacOS() { + return ProcessUtils.executeCommand("defaults delete com.bytebin.deepworld playerLocked").wasSuccessful(); + } } diff --git a/src/main/java/brainwine/gui/view/ProgressView.java b/src/main/java/brainwine/gui/view/ProgressView.java new file mode 100644 index 00000000..713e13c6 --- /dev/null +++ b/src/main/java/brainwine/gui/view/ProgressView.java @@ -0,0 +1,70 @@ +package brainwine.gui.view; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; + +public class ProgressView { + + private final List closeListeners = new ArrayList<>(); + private final JDialog dialog; + private final JLabel label; + private final JProgressBar progressBar; + + public ProgressView(JComponent owner, String text) { + // Create progress bar + progressBar = new JProgressBar(); + progressBar.setBorder(BorderFactory.createEmptyBorder(8, 0, 0, 0)); + progressBar.setMinimumSize(new Dimension(300, 40)); + progressBar.setPreferredSize(progressBar.getMinimumSize()); + progressBar.setStringPainted(true); + + // Create main panel + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + panel.add(label = new JLabel(text), BorderLayout.PAGE_START); + panel.add(progressBar, BorderLayout.CENTER); + + // Create dialog + JFrame frame = (JFrame)owner.getTopLevelAncestor(); + dialog = new JDialog(frame, "Please wait..."); + dialog.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent event) { + closeListeners.forEach(Runnable::run); + } + }); + dialog.setResizable(false); + dialog.add(panel); + dialog.pack(); + dialog.setLocationRelativeTo(frame); + dialog.setVisible(true); + } + + public void addCloseListener(Runnable listener) { + closeListeners.add(listener); + } + + public void setText(String text) { + label.setText(text); + } + + public void setProgress(int progress) { + progressBar.setValue(progress); + } + + public void dispose() { + dialog.dispose(); + } +} diff --git a/src/main/java/brainwine/patch/BytePatch.java b/src/main/java/brainwine/patch/BytePatch.java new file mode 100644 index 00000000..e97da46d --- /dev/null +++ b/src/main/java/brainwine/patch/BytePatch.java @@ -0,0 +1,41 @@ +package brainwine.patch; + +import java.io.IOException; +import java.io.RandomAccessFile; + +public class BytePatch { + + private final long address; + private final byte from; + private final byte to; + + public BytePatch(long address, int from, int to) { + this.address = address; + this.from = (byte)from; + this.to = (byte)to; + } + + public void apply(RandomAccessFile file) throws IOException { + file.seek(address); + + if(file.readByte() != to) { + file.seek(address); + file.writeByte(to); + } + } + + public void validate(RandomAccessFile file) throws IOException { + if(address < 0 || address >= file.length()) { + throw new IOException(String.format("Address %08X exceeds file range %08X", address, file.length() - 1)); + } + + if(address >= 0 && address < file.length()) { + file.seek(address); + byte b = file.readByte(); + + if(b != from && b != to) { + throw new IOException(String.format("Byte at %08X failed to validate: expected %02X or %02X, got %02X", address, from, to, b)); + } + } + } +} diff --git a/src/main/java/brainwine/patch/DynamicPatch.java b/src/main/java/brainwine/patch/DynamicPatch.java new file mode 100644 index 00000000..c6be3844 --- /dev/null +++ b/src/main/java/brainwine/patch/DynamicPatch.java @@ -0,0 +1,47 @@ +package brainwine.patch; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class DynamicPatch { + + private final Map data = new HashMap<>(); + private final PatchFile patchFile; + + protected DynamicPatch(PatchFile patchFile) { + this.patchFile = patchFile; + } + + protected void apply(RandomAccessFile file) throws IOException { + for(long address : data.keySet()) { + byte[] bytes = data.get(address); + byte[] original = new byte[bytes.length]; + file.seek(address); + file.read(original); + + if(!Arrays.equals(bytes, original)) { + file.seek(address); + file.write(bytes); + } + } + } + + public void setBytes(String name, byte[] bytes) { + long address = patchFile.getAddress(name); + + if(address != -1) { + data.put(address, bytes); + } + } + + public void setInt(String name, int value) { + setBytes(name, new byte[] { + (byte)(value), + (byte)(value >> 8), + (byte)(value >> 16), + (byte)(value >> 24)}); + } +} diff --git a/src/main/java/brainwine/patch/PatchFile.java b/src/main/java/brainwine/patch/PatchFile.java new file mode 100644 index 00000000..e6d53957 --- /dev/null +++ b/src/main/java/brainwine/patch/PatchFile.java @@ -0,0 +1,104 @@ +package brainwine.patch; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class PatchFile { + + private final Map addresses = new HashMap<>(); + private final List bytePatches = new ArrayList<>(); + private long binarySize; + + public PatchFile(InputStream inputStream) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + List lines = reader.lines().collect(Collectors.toList()); + + for(String line : lines) { + parseLine(line); + } + } + + private void parseLine(String line) throws IOException { + // Remove excessive whitespace + line = line.trim().replaceAll(" +", " "); + + // Ignore empty lines and comments + if(line.isEmpty() || line.startsWith("#")) { + return; + } + + String[] entry = line.split(" ", 2); + + // Throw exception if entry is just a key with no value + if(entry.length == 1) { + throw new IOException("Entry must be a key followed by a value"); + } + + String key = entry[0]; + String value = entry[1]; + + // Process value based on key + switch(key) { + case "binary_name": + break; + case "binary_size": + binarySize = Long.decode(entry[1]); + break; + case "location": + String[] segments = value.split(" "); + + if(segments.length != 2) { + throw new IOException("Location must be a name followed by an address"); + } + + addresses.put(segments[0], Long.decode(segments[1])); + break; + case "byte_patch": + segments = value.split(" "); + + if(segments.length != 3) { + throw new IOException("Byte patch must be an address followed by the expected and target bytes"); + } + + bytePatches.add(new BytePatch(Long.decode(segments[0]), Integer.decode(segments[1]), Integer.decode(segments[2]))); + break; + default: + throw new IOException(String.format("Unknown key: %s", key)); + } + } + + public void apply(RandomAccessFile file, Consumer propertySetter) throws IOException { + // Validate binary info + if(file.length() != binarySize) { + throw new IOException(String.format("Binary size doesn't match: got %08X, expected %08X", file.length(), binarySize)); + } + + // Validate byte patches + for(BytePatch patch : bytePatches) { + patch.validate(file); + } + + // Apply byte patches + for(BytePatch patch : bytePatches) { + patch.apply(file); + } + + // Apply dynamic patches + DynamicPatch patch = new DynamicPatch(this); + propertySetter.accept(patch); + patch.apply(file); + } + + public long getAddress(String name) { + return addresses.getOrDefault(name, -1L); + } +} diff --git a/src/main/java/brainwine/util/DesktopUtils.java b/src/main/java/brainwine/util/DesktopUtils.java index 364c1b5f..bd01f36e 100644 --- a/src/main/java/brainwine/util/DesktopUtils.java +++ b/src/main/java/brainwine/util/DesktopUtils.java @@ -8,16 +8,19 @@ public class DesktopUtils { - public static void browseUrl(String url) { + public static boolean browseUrl(String url) { Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null; if(desktop != null && desktop.isSupported(Action.BROWSE)) { try { URI uri = new URI(url); desktop.browse(uri); + return true; } catch(URISyntaxException | IOException e) { // TODO log this somewhere } } + + return false; } } diff --git a/src/main/java/brainwine/util/SwingUtils.java b/src/main/java/brainwine/util/SwingUtils.java index cd8d0089..527b13f8 100644 --- a/src/main/java/brainwine/util/SwingUtils.java +++ b/src/main/java/brainwine/util/SwingUtils.java @@ -1,14 +1,26 @@ package brainwine.util; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.event.ActionEvent; +import java.io.PrintWriter; +import java.io.StringWriter; import javax.swing.AbstractAction; import javax.swing.Action; +import javax.swing.BorderFactory; import javax.swing.Icon; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; import javax.swing.UIManager; import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.extras.components.FlatTextField; public class SwingUtils { @@ -59,13 +71,8 @@ public static GridBagConstraints createConstraints(int x, int y, int width, int return constraints; } - public static void setMenuBarEmbedded(boolean flag) { - UIManager.put("TitlePane.menuBarEmbedded", flag); - FlatLaf.updateUI(); - } - - public static boolean isMenuBarEmbedded() { - return UIManager.getBoolean("TitlePane.menuBarEmbedded"); + public static String getTextFieldValue(FlatTextField textField) { + return textField.getText().isEmpty() ? textField.getPlaceholderText() : textField.getText(); } public static void setDefaultFontSize(int size) { @@ -77,4 +84,29 @@ public static void setDefaultFontSize(int size) { public static int getDefaultFontSize() { return UIManager.getFont("defaultFont").getSize(); } + + public static void showExceptionInfo(Component parent, String message, Throwable throwable) { + // Create stacktrace string + StringWriter writer = new StringWriter(); + throwable.printStackTrace(new PrintWriter(writer)); + + // Create text area + JTextArea area = new JTextArea(writer.toString()); + area.setBorder(BorderFactory.createEmptyBorder(8, 8, 0, 0)); + area.setFont(UIManager.getFont("Brainwine.consoleFont")); + area.setEditable(false); + + // Create scroll pane + JScrollPane scrollPane = new JScrollPane(area); + scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + scrollPane.setPreferredSize(new Dimension(600, 200)); + scrollPane.setMaximumSize(scrollPane.getPreferredSize()); + + // Create dialog + String label = String.format("%s
Exception details:

", message); + JPanel panel = new JPanel(new BorderLayout()); + panel.add(new JLabel(label), BorderLayout.PAGE_START); + panel.add(scrollPane); + JOptionPane.showMessageDialog(parent, panel, "An error has occured", JOptionPane.ERROR_MESSAGE); + } } diff --git a/src/main/resources/patches/deepworld-0.5.5.patch b/src/main/resources/patches/deepworld-0.5.5.patch new file mode 100644 index 00000000..c19787b9 --- /dev/null +++ b/src/main/resources/patches/deepworld-0.5.5.patch @@ -0,0 +1,7 @@ +# Binary info +binary_name Deepworld +binary_size 0x33E394 + +# Addresses +location gateway_host 0x0012B7E5 +location gateway_host_strlen 0x001E3EC0 diff --git a/src/main/resources/patches/deepworld-0.7.4.patch b/src/main/resources/patches/deepworld-0.7.4.patch new file mode 100644 index 00000000..11405afd --- /dev/null +++ b/src/main/resources/patches/deepworld-0.7.4.patch @@ -0,0 +1,7 @@ +# Binary info +binary_name Deepworld +binary_size 0x367DA0 + +# Addresses +location gateway_host 0x0013EB0B +location gateway_host_strlen 0x001FF3C8 diff --git a/src/main/resources/patches/deepworld-0.7.5.patch b/src/main/resources/patches/deepworld-0.7.5.patch new file mode 100644 index 00000000..de546f73 --- /dev/null +++ b/src/main/resources/patches/deepworld-0.7.5.patch @@ -0,0 +1,7 @@ +# Binary info +binary_name Deepworld +binary_size 0x36CC68 + +# Addresses +location gateway_host 0x0013FD46 +location gateway_host_strlen 0x002017F0 diff --git a/src/main/resources/patches/deepworld-1.13.3.patch b/src/main/resources/patches/deepworld-1.13.3.patch new file mode 100644 index 00000000..1cce3d48 --- /dev/null +++ b/src/main/resources/patches/deepworld-1.13.3.patch @@ -0,0 +1,28 @@ +# Binary info +binary_name Deepworld +binary_size 0x3A4470 + +# Addresses +location gateway_host 0x001A960E +location gateway_host_strlen 0x002BCEF8 + +# Make gateway reachable +byte_patch 0x0002A211 0x74 0xEB + +# Remove code signature +byte_patch 0x00000010 0x22 0x21 +byte_patch 0x00000014 0x78 0x68 +byte_patch 0x00001488 0x1D 0x00 +byte_patch 0x0000148C 0x10 0x00 +byte_patch 0x00001490 0xC0 0x00 +byte_patch 0x00001491 0xD5 0x00 +byte_patch 0x00001492 0x39 0x00 +byte_patch 0x00001494 0xB0 0x00 +byte_patch 0x00001495 0x6E 0x00 +byte_patch 0x0039D5C0 0xFA 0x00 +byte_patch 0x0039D5C1 0xDE 0x00 +byte_patch 0x0039D5C2 0x0C 0x00 +byte_patch 0x0039D5C3 0xC0 0x00 +byte_patch 0x0039D5C6 0x5C 0x00 +byte_patch 0x0039D5C7 0x60 0x00 +byte_patch 0x0039D5CB 0x04 0x00 diff --git a/src/main/resources/patches/deepworld-1.3.10.patch b/src/main/resources/patches/deepworld-1.3.10.patch new file mode 100644 index 00000000..272b8069 --- /dev/null +++ b/src/main/resources/patches/deepworld-1.3.10.patch @@ -0,0 +1,10 @@ +# Binary info +binary_name Deepworld +binary_size 0x3B3FA0 + +# Addresses +location gateway_host 0x0014FC3D +location gateway_host_strlen 0x00228760 + +# Make gateway reachable +byte_patch 0x0002968F 0x74 0xEB diff --git a/src/main/resources/patches/deepworld-2.11.1.patch b/src/main/resources/patches/deepworld-2.11.1.patch new file mode 100644 index 00000000..d85eef12 --- /dev/null +++ b/src/main/resources/patches/deepworld-2.11.1.patch @@ -0,0 +1,29 @@ +# Binary info +binary_name Deepworld +binary_size 0x350010 + +# Addresses +location gateway_host 0x001DAB8D +location gateway_host_strlen 0x00284C60 + +# Make gateway reachable +byte_patch 0x000373AD 0x74 0xEB + +# Remove code signature +byte_patch 0x00000010 0x22 0x21 +byte_patch 0x00000014 0x08 0xF8 +byte_patch 0x00000015 0x14 0x13 +byte_patch 0x00001418 0x1D 0x00 +byte_patch 0x0000141C 0x10 0x00 +byte_patch 0x00001420 0x30 0x00 +byte_patch 0x00001421 0x2E 0x00 +byte_patch 0x00001422 0x34 0x00 +byte_patch 0x00001424 0xE0 0x00 +byte_patch 0x00001425 0xD1 0x00 +byte_patch 0x00342E30 0xFA 0x00 +byte_patch 0x00342E31 0xDE 0x00 +byte_patch 0x00342E32 0x0C 0x00 +byte_patch 0x00342E33 0xC0 0x00 +byte_patch 0x00342E36 0xC0 0x00 +byte_patch 0x00342E37 0xA3 0x00 +byte_patch 0x00342E3B 0x05 0x00