diff --git a/.AddCompLib/global.json b/.AddCompLib/global.json deleted file mode 100644 index d17a160..0000000 --- a/.AddCompLib/global.json +++ /dev/null @@ -1 +0,0 @@ -{"behSimpleFiles":[],"resSimpleFiles":[],"maxSimple":5} \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2a7f671..40c6851 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,12 +1,6 @@ -name: Maven Build with GraalVM +name: Sculk Server -on: - push: - branches: - - main - pull_request: - branches: - - main +on: [ push, pull_request ] jobs: build: @@ -14,23 +8,25 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 - + uses: actions/checkout@v4 - name: Set up GraalVM with Java 21 uses: graalvm/setup-graalvm@v1 with: - version: '21.3.0' # Utilisez la version de GraalVM souhaitée - java-version: '21' - components: native-image # Installe également le composant Native Image de GraalVM + java-version: '21' # See 'Options' section below for all supported versions + distribution: 'graalvm' # See 'Options' section below for all available distributions + native-image-job-reports: 'true' + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Maven - uses: actions/setup-java@v3 + uses: graalvm/setup-graalvm@v1 with: - distribution: 'graalvm' # Spécifie que nous utilisons GraalVM - java-version: '21' + java-version: '21' # See 'Options' section below for all supported versions + distribution: 'graalvm' # See 'Options' section below for all available distributions + native-image-job-reports: 'true' + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Cache Maven packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} @@ -40,8 +36,10 @@ jobs: - name: Build with Maven run: mvn clean package - # Optionnel : Générer une image native avec GraalVM - - name: Build Native Image - run: | - mvn package -Pnative - if: success() # N'exécute cette étape que si la construction précédente a réussi + - name: Upload release artifacts + uses: actions/upload-artifact@v4 + with: + name: release_artifacts + path: | + ${{ github.workspace }}/target/Sculk-1.0-SNAPSHOT.jar + ${{ github.workspace }}/target/Sculk-1.0-SNAPSHOT-jar-with-dependencies.jar diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..2f9de52 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,31 @@ +name: Maven Package + +on: + release: + types: [created] + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + server-id: github # Value of the distributionManagement/repository/id field of the pom.xml + settings-path: ${{ github.workspace }} # location for the settings.xml file + + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Publish to GitHub Packages Apache Maven + run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.gitignore b/.gitignore index 030240e..c7a5bef 100644 --- a/.gitignore +++ b/.gitignore @@ -25,8 +25,21 @@ plugins/ resource_packs/ logs/ worlds/ -server.json -whitelist.json -op.json -banned-ips.json -banned-players.json \ No newline at end of file +whitelist.txt +op.txt +banned-ip.txt +banned-players.txt +server.properties +.run/sculk.yml +.run/logs/ +.run/players +.run/plugin_data +.run/plugins +.run/resource_packs +.run/worlds +.run/banned-ip.txt +.run/banned-players.txt +.run/op.txt +.run/sculk.yml +.run/server.properties +.run/whitelist.txt diff --git a/.idea/fileTemplates/includes/File Header.java b/.idea/fileTemplates/includes/File Header.java index a35fbac..a5f8734 100644 --- a/.idea/fileTemplates/includes/File Header.java +++ b/.idea/fileTemplates/includes/File Header.java @@ -1,10 +1,10 @@ /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by diff --git a/.run/Sculk.run.xml b/.run/Sculk.run.xml new file mode 100644 index 0000000..485aacb --- /dev/null +++ b/.run/Sculk.run.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index e0f15db..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.configuration.updateBuildConfiguration": "automatic" -} \ No newline at end of file diff --git a/README.md b/README.md index 1953bc8..be95406 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ -![Header](https://capsule-render.vercel.app/api?type=Waving&color=timeGradient&height=200&animation=fadeIn§ion=header&text=Sculk-MP&fontSize=70)
-

Open source server software for Minecraft: Bedrock Edition written in Java

+Logo Sculk +

Open source server software for Minecraft: Bedrock Edition written in Java

+[![SculkVersion](https://img.shields.io/badge/version-soon-14191E.svg?cacheSeconds=2592000)]() +[![MinecraftVersion](https://img.shields.io/badge/minecraft-v1.21.21%20(Bedrock)-17272F)]() +[![ProtocolVersion](https://img.shields.io/badge/protocol-712-38D3DF)]() [![Github Download](https://img.shields.io/github/downloads/sculkmp/Sculk/total?label=downloads%40total)]() [![License](https://img.shields.io/badge/License-LGPL--3-yellow.svg)]() [![JitPack](https://jitpack.io/v/sculkmp/Sculk.svg)]() -[![MinecraftVersion](https://img.shields.io/badge/minecraft-v1.21.1%20(Bedrock)-56383E)]() -[![SculkVersion](https://img.shields.io/badge/version-1.0.0-blue.svg?cacheSeconds=2592000)]()
@@ -18,7 +19,7 @@ Sculk is open source server software for Minecraft: Bedrock Edition, It has a fe * We provided a high-level friendly API akin PocketMine plugin developers. Save yourself the hassle of dealing with the dot-and-cross of the low-level system API and hooks, we've done the difficult part for you! ## ✨ Creating plugins -Add SculkMP to your dependencies *(it is hosted by JitPack, so you need to specify a custom repository)*. +Add Sculk to your dependencies *(it is hosted by JitPack, so you need to specify a custom repository)*. For maven: ```xml @@ -49,15 +50,17 @@ dependencies { | Milestone | Status | |------------------------------------------|--------| -| **⚒️ Construction of the server tree** | ✅ | -| **🛜 Join server** | ⏳ | +| **⚒️ Construction of the server tree** | ✅ | +| **👓 Visible server** | ✅ | +| **🛜 Join server** | ✅ | | **🎍 World loader** | 🚧 | -| **🔌Plugin loader** | 🚧 | +| **🔌Plugin loader** | ⏳ | | **⌨️ Command System** | 🚧 | | **🔐 Permission System** | 🚧 | -| **🎈 Event System** | 🚧 | -| **🖼 Form & Scoreboard API** | 🚧 | -| **👤 Player & Actor API** | 🚧 | +| **🎈 Event System** | ⏳ | +| **🖼 Scoreboard API** | 🚧 | +| **🖼 Form API** | ✅ | +| **👤 Player & Actor API** | ⏳ | | **🔩 Item API** | 🚧 | | **🧱 Block API** | 🚧 | | **📦 Inventory API** | 🚧 | @@ -95,5 +98,5 @@ Please ensure your code follows our coding standards and include tests where pos This project is licensed under LGPL-3.0. Please see the [LICENSE](/LICENSE) file for details. `sculkmp/Sculk` are not affiliated with Mojang. -All brands and trademarks belong to their respective owners. Sculk-MP is not a Mojang-approved software, +All brands and trademarks belong to their respective owners. Sculk is not a Mojang-approved software, nor is it associated with Mojang. \ No newline at end of file diff --git a/Sculk.iml b/Sculk.iml new file mode 100644 index 0000000..2baae8a --- /dev/null +++ b/Sculk.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index f5e2266..e8bf360 100644 --- a/pom.xml +++ b/pom.xml @@ -110,6 +110,16 @@ + + com.google.inject + guice + 7.0.0 + + + com.google.guava + guava-gwt + 33.2.1-jre + com.bugsnag [3.0,4.0) @@ -135,7 +145,7 @@ it.unimi.dsi fastutil - 8.5.12 + 8.5.13 compile @@ -257,7 +267,7 @@ com.nimbusds nimbus-jose-jwt - 9.10.1 + 9.37.2 diff --git a/sculk.yml b/sculk.yml new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/co/aikar/timings/FullServerTickTiming.java b/src/main/java/co/aikar/timings/FullServerTickTiming.java new file mode 100644 index 0000000..66526bf --- /dev/null +++ b/src/main/java/co/aikar/timings/FullServerTickTiming.java @@ -0,0 +1,108 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import static co.aikar.timings.TimingIdentifier.DEFAULT_GROUP; +import static co.aikar.timings.TimingsManager.*; + +public class FullServerTickTiming extends Timing { + private static final TimingIdentifier IDENTIFIER = new TimingIdentifier(DEFAULT_GROUP.name, "Full Server Tick", null); + final TimingData minuteData; + double avgFreeMemory = -1D; + double avgUsedMemory = -1D; + + FullServerTickTiming() { + super(IDENTIFIER); + this.minuteData = new TimingData(this.id); + + TIMING_MAP.put(IDENTIFIER, this); + } + + @Override + public Timing startTiming() { + if (TimingsManager.needsFullReset) { + TimingsManager.resetTimings(); + } else if (TimingsManager.needsRecheckEnabled) { + TimingsManager.recheckEnabled(); + } + super.startTiming(); + return this; + } + + @Override + public void stopTiming() { + super.stopTiming(); + if (!this.enabled) { + return; + } + + if (TimingsHistory.timedTicks % 20 == 0) { + final Runtime runtime = Runtime.getRuntime(); + double usedMemory = runtime.totalMemory() - runtime.freeMemory(); + double freeMemory = runtime.maxMemory() - usedMemory; + + if (this.avgFreeMemory == -1) { + this.avgFreeMemory = freeMemory; + } else { + this.avgFreeMemory = (this.avgFreeMemory * (59 / 60D)) + (freeMemory * (1 / 60D)); + } + + if (this.avgUsedMemory == -1) { + this.avgUsedMemory = usedMemory; + } else { + this.avgUsedMemory = (this.avgUsedMemory * (59 / 60D)) + (usedMemory * (1 / 60D)); + } + } + + long start = System.nanoTime(); + TimingsManager.tick(); + long diff = System.nanoTime() - start; + + CURRENT = Timings.timingsTickTimer; + Timings.timingsTickTimer.addDiff(diff); + //addDiff for timingsTickTimer incremented this, bring it back down to 1 per tick. + this.record.curTickCount--; + this.minuteData.curTickTotal = this.record.curTickTotal; + this.minuteData.curTickCount = 1; + boolean violated = isViolated(); + this.minuteData.tick(violated); + Timings.timingsTickTimer.tick(violated); + tick(violated); + + if (TimingsHistory.timedTicks % 1200 == 0) { + MINUTE_REPORTS.add(new TimingsHistory.MinuteReport()); + TimingsHistory.resetTicks(false); + this.minuteData.reset(); + } + + if (TimingsHistory.timedTicks % Timings.getHistoryInterval() == 0) { + TimingsManager.HISTORY.add(new TimingsHistory()); + TimingsManager.resetTimings(); + } + } + + boolean isViolated() { + return this.record.curTickTotal > 50000000; + } +} diff --git a/src/main/java/co/aikar/timings/Timing.java b/src/main/java/co/aikar/timings/Timing.java new file mode 100644 index 0000000..b9430d2 --- /dev/null +++ b/src/main/java/co/aikar/timings/Timing.java @@ -0,0 +1,169 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import java.util.HashMap; +import java.util.Map; + +public class Timing implements AutoCloseable { + private static int idPool = 1; + final int id = idPool++; + + final String name; + private final boolean verbose; + + final Map children = new HashMap<>(); + private Timing parent; + + private final Timing groupTiming; + final TimingData record; + + private long start = 0; + private int timingDepth = 0; + private boolean added; + boolean timed; + boolean enabled; + + Timing(TimingIdentifier id) { + if (id.name.startsWith("##")) { + this.verbose = true; + this.name = id.name.substring(3); + } else { + this.name = id.name; + this.verbose = false; + } + + this.record = new TimingData(this.id); + this.groupTiming = id.groupTiming; + + TimingIdentifier.getGroup(id.group).timings.add(this); + this.checkEnabled(); + } + + final void checkEnabled() { + this.enabled = Timings.isTimingsEnabled() && (!this.verbose || Timings.isVerboseEnabled()); + } + + void tick(boolean violated) { + if (this.timingDepth != 0 || this.record.curTickCount == 0) { + this.timingDepth = 0; + this.start = 0; + return; + } + + this.record.tick(violated); + for (TimingData data : this.children.values()) { + data.tick(violated); + } + } + + public Timing startTiming() { + if (!this.enabled) { + return this; + } + + if (++this.timingDepth == 1) { + this.start = System.nanoTime(); + this.parent = TimingsManager.CURRENT; + TimingsManager.CURRENT = this; + } + + return this; + } + + public void stopTiming() { + if (!this.enabled) { + return; + } + + if (--this.timingDepth == 0 && this.start != 0) { + this.addDiff(System.nanoTime() - this.start); + this.start = 0; + } + } + + public void abort() { + if (this.enabled && this.timingDepth > 0) { + this.start = 0; + } + } + + void addDiff(long diff) { + if (TimingsManager.CURRENT == this) { + TimingsManager.CURRENT = this.parent; + if (this.parent != null) { + if (!this.parent.children.containsKey(this.id)) + this.parent.children.put(this.id, new TimingData(this.id)); + this.parent.children.get(this.id).add(diff); + } + } + + this.record.add(diff); + if (!this.added) { + this.added = true; + this.timed = true; + TimingsManager.TIMINGS.add(this); + } + + if (this.groupTiming != null) { + this.groupTiming.addDiff(diff); + + if (!this.groupTiming.children.containsKey(this.id)) + this.groupTiming.children.put(this.id, new TimingData(this.id)); + this.groupTiming.children.get(this.id).add(diff); + } + } + + void reset(boolean full) { + this.record.reset(); + if (full) { + this.timed = false; + } + this.start = 0; + this.timingDepth = 0; + this.added = false; + this.children.clear(); + this.checkEnabled(); + } + + @Override + public boolean equals(Object o) { + return (o instanceof Timing && this == o); + } + + @Override + public int hashCode() { + return this.id; + } + + //For try-with-resources + @Override + public void close() { + this.stopTiming(); + } + + boolean isSpecial() { + return this == Timings.fullServerTickTimer || this == Timings.timingsTickTimer; + } +} diff --git a/src/main/java/co/aikar/timings/TimingData.java b/src/main/java/co/aikar/timings/TimingData.java new file mode 100644 index 0000000..1601ed1 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingData.java @@ -0,0 +1,90 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.sculk.timings.JsonUtil; + +class TimingData { + private int id; + int count = 0; + private int lagCount = 0; + long totalTime = 0; + private long lagTotalTime = 0; + + int curTickCount = 0; + int curTickTotal = 0; + + TimingData(int id) { + this.id = id; + } + + TimingData(TimingData data) { + this.id = data.id; + this.count = data.count; + this.lagCount = data.lagCount; + this.totalTime = data.totalTime; + this.lagTotalTime = data.lagTotalTime; + } + + void add(long diff) { + ++this.curTickCount; + this.curTickTotal += diff; + } + + void tick(boolean violated) { + this.count += this.curTickCount; + this.totalTime += this.curTickTotal; + + if (violated) { + this.lagCount += this.curTickCount; + this.lagTotalTime += this.curTickTotal; + } + + this.curTickCount = 0; + this.curTickTotal = 0; + } + + void reset() { + this.count = 0; + this.lagCount = 0; + this.totalTime = 0; + this.lagTotalTime = 0; + this.curTickCount = 0; + this.curTickTotal = 0; + } + + protected TimingData clone() { + return new TimingData(this); + } + + ArrayNode export() { + ArrayNode array = JsonUtil.toArray(this.id, this.count, this.totalTime); + if (this.lagCount > 0) { + array.add(this.lagCount); + array.add(this.lagTotalTime); + } + return array; + } +} diff --git a/src/main/java/co/aikar/timings/TimingIdentifier.java b/src/main/java/co/aikar/timings/TimingIdentifier.java new file mode 100644 index 0000000..fcefc06 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingIdentifier.java @@ -0,0 +1,82 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import java.util.ArrayDeque; +import java.util.IdentityHashMap; +import java.util.Map; + +class TimingIdentifier { + static final Map GROUP_MAP = new IdentityHashMap<>(64); + static final TimingGroup DEFAULT_GROUP = getGroup("Cloudburst"); + + final String group; + final String name; + final Timing groupTiming; + private final int hashCode; + + TimingIdentifier(String group, String name, Timing groupTiming) { + this.group = group != null ? group.intern() : DEFAULT_GROUP.name; + this.name = name.intern(); + this.groupTiming = groupTiming; + this.hashCode = (31 * this.group.hashCode()) + this.name.hashCode(); + } + + static TimingGroup getGroup(String name) { + if (name == null) { + return DEFAULT_GROUP; + } + + return GROUP_MAP.computeIfAbsent(name, k -> new TimingGroup(name)); + } + + @Override + @SuppressWarnings("all") + public boolean equals(Object o) { + if (o == null || !(o instanceof TimingIdentifier)) { + return false; + } + + TimingIdentifier that = (TimingIdentifier) o; + //Using intern() method on strings makes possible faster string comparison with == + return this.group == that.group && this.name == that.name; + } + + @Override + public int hashCode() { + return this.hashCode; + } + + static class TimingGroup { + private static int idPool = 1; + final int id = idPool++; + + final String name; + ArrayDeque timings = new ArrayDeque<>(64); + + TimingGroup(String name) { + this.name = name.intern(); + } + } +} diff --git a/src/main/java/co/aikar/timings/Timings.java b/src/main/java/co/aikar/timings/Timings.java new file mode 100644 index 0000000..4e6c3d0 --- /dev/null +++ b/src/main/java/co/aikar/timings/Timings.java @@ -0,0 +1,263 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.*; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; +import org.sculk.event.Event; +import org.sculk.scheduler.TaskHandler; + +import java.lang.reflect.Method; +import java.util.*; + +import static co.aikar.timings.TimingIdentifier.DEFAULT_GROUP; + +public final class Timings { + private static final Logger log = LogManager.getLogger(Timings.class); + private static boolean timingsEnabled = false; + private static boolean verboseEnabled = false; + private static boolean privacy = false; + private static Set ignoredConfigSections = new HashSet<>(); + + private static final int MAX_HISTORY_FRAMES = 12; + private static int historyInterval = -1; + private static int historyLength = -1; + + public static final FullServerTickTiming fullServerTickTimer; + public static final Timing timingsTickTimer; + public static final Timing pluginEventTimer; + + public static final Timing connectionTimer; + public static final Timing schedulerTimer; + public static final Timing schedulerAsyncTimer; + public static final Timing schedulerSyncTimer; + public static final Timing commandTimer; + public static final Timing serverCommandTimer; + public static final Timing levelSaveTimer; + + public static final Timing playerNetworkSendTimer; + public static final Timing playerNetworkReceiveTimer; + public static final Timing playerChunkOrderTimer; + public static final Timing playerChunkSendTimer; + public static final Timing playerCommandTimer; + public static final Timing playerEntityLookingAtTimer; + public static final Timing playerEntityAtPositionTimer; + + public static final Timing tickEntityTimer; + public static final Timing tickBlockEntityTimer; + public static final Timing entityMoveTimer; + public static final Timing entityBaseTickTimer; + public static final Timing livingEntityBaseTickTimer; + + public static final Timing generationTimer; + public static final Timing populationTimer; + public static final Timing generationCallbackTimer; + + public static final Timing permissibleCalculationTimer; + public static final Timing permissionDefaultTimer; + + @Data + @Setter(AccessLevel.PRIVATE) + @Builder + @Getter + @AllArgsConstructor + @NoArgsConstructor + @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) + public static class timingz { + @Builder.Default + private boolean enabled = false; + @Builder.Default + private boolean verbose = false; + @Builder.Default + private boolean privacy = false; + @Builder.Default + private int historyInterval = 6000; + @Builder.Default + private int historyLength = 72000; + @Builder.Default + private List ignore = Collections.emptyList(); + @Builder.Default + private boolean bypassMax = false; + } + + private static Timings.timingz timingz = new Timings.timingz(); + + public static Timings.timingz getTimings() { + return timingz; + } + + static { + setTimingsEnabled(true); + setVerboseEnabled(true); + setHistoryInterval(20); + setHistoryLength(20); + + privacy = false; + ignoredConfigSections.addAll(timingz.getIgnore()); + + log.debug("Timings: \n" + + "Enabled - " + isTimingsEnabled() + "\n" + + "Verbose - " + isVerboseEnabled() + "\n" + + "History Interval - " + getHistoryInterval() + "\n" + + "History Length - " + getHistoryLength()); + + fullServerTickTimer = new FullServerTickTiming(); + timingsTickTimer = TimingsManager.getTiming(DEFAULT_GROUP.name, "Timings Tick", fullServerTickTimer); + pluginEventTimer = TimingsManager.getTiming("Plugin Events"); + + connectionTimer = TimingsManager.getTiming("Connection Handler"); + schedulerTimer = TimingsManager.getTiming("Scheduler"); + schedulerAsyncTimer = TimingsManager.getTiming("## Scheduler - Async Tasks"); + schedulerSyncTimer = TimingsManager.getTiming("## Scheduler - Sync Tasks"); + commandTimer = TimingsManager.getTiming("Commands"); + serverCommandTimer = TimingsManager.getTiming("Server Command"); + levelSaveTimer = TimingsManager.getTiming("Level Save"); + + playerNetworkSendTimer = TimingsManager.getTiming("Player Network Send"); + playerNetworkReceiveTimer = TimingsManager.getTiming("Player Network Receive"); + playerChunkOrderTimer = TimingsManager.getTiming("Player Order Chunks"); + playerChunkSendTimer = TimingsManager.getTiming("Player Send Chunks"); + playerCommandTimer = TimingsManager.getTiming("Player Command"); + playerEntityLookingAtTimer = TimingsManager.getTiming("## Player: Entity Looking At"); + playerEntityAtPositionTimer = TimingsManager.getTiming(DEFAULT_GROUP.name, "## Player: Entity At Position", playerEntityLookingAtTimer); + + tickEntityTimer = TimingsManager.getTiming("## Entity Tick"); + tickBlockEntityTimer = TimingsManager.getTiming("## BlockEntity Tick"); + entityMoveTimer = TimingsManager.getTiming("## Entity Move"); + entityBaseTickTimer = TimingsManager.getTiming("## Entity Base Tick"); + livingEntityBaseTickTimer = TimingsManager.getTiming("## LivingEntity Base Tick"); + + generationTimer = TimingsManager.getTiming("Level Generation"); + populationTimer = TimingsManager.getTiming("Level Population"); + generationCallbackTimer = TimingsManager.getTiming("Level Generation Callback"); + + permissibleCalculationTimer = TimingsManager.getTiming("Permissible Calculation"); + permissionDefaultTimer = TimingsManager.getTiming("Default Permission Calculation"); + } + + public static boolean isTimingsEnabled() { + return timingsEnabled; + } + + public static void setTimingsEnabled(boolean enabled) { + timingsEnabled = enabled; + TimingsManager.reset(); + } + + public static boolean isVerboseEnabled() { + return verboseEnabled; + } + + public static void setVerboseEnabled(boolean enabled) { + verboseEnabled = enabled; + TimingsManager.needsRecheckEnabled = true; + } + + public static boolean isPrivacy() { + return privacy; + } + + public static Set getIgnoredConfigSections() { + return ignoredConfigSections; + } + + public static int getHistoryInterval() { + return historyInterval; + } + + public static void setHistoryInterval(int interval) { + historyInterval = Math.max(20 * 60, interval); + //Recheck the history length with the new Interval + if (historyLength != -1) { + setHistoryLength(historyLength); + } + } + + public static int getHistoryLength() { + return historyLength; + } + + public static void setHistoryLength(int length) { + //Cap at 12 History Frames, 1 hour at 5 minute frames. + int maxLength = Integer.MAX_VALUE; + + historyLength = Math.max(Math.min(maxLength, length), historyInterval); + + Queue oldQueue = TimingsManager.HISTORY; + int frames = (getHistoryLength() / getHistoryInterval()); + if (length > maxLength) { + log.warn("Timings Length too high. Requested " + length + ", max is " + maxLength + + ". To get longer history, you must increase your interval. Set Interval to " + + Math.ceil((float) length / MAX_HISTORY_FRAMES) + + " to achieve this length."); + } + + TimingsManager.HISTORY = new TimingsManager.BoundedQueue<>(frames); + TimingsManager.HISTORY.addAll(oldQueue); + } + + public static void reset() { + TimingsManager.reset(); + } + + public static Timing getTaskTiming(TaskHandler handler, long period) { + String repeating = " "; + if (period > 0) { + repeating += "(interval:" + period + ")"; + } else { + repeating += "(Single)"; + } + + if (!handler.isAsynchronous()) { + return TimingsManager.getTiming(DEFAULT_GROUP.name, "Task: " + handler.getTaskId() + repeating, schedulerSyncTimer); + } else { + return null; + } + } + + public static Timing getPluginEventTiming(Class event, Object listener, Method method) { + Timing group = TimingsManager.getTiming("", "Combined Total", pluginEventTimer); + + return TimingsManager.getTiming("", "Event: " + listener.getClass().getName() + "." + + (method.getName()) + + " (" + event.getSimpleName() + ")", group); + } + + public static Timing getReceiveDataPacketTiming(BedrockPacket pk) { + return TimingsManager.getTiming(DEFAULT_GROUP.name, "## Receive Packet: " + pk.getClass().getSimpleName(), playerNetworkReceiveTimer); + } + + public static Timing getSendDataPacketTiming(BedrockPacket pk) { + return TimingsManager.getTiming(DEFAULT_GROUP.name, "## Send Packet: " + pk.getClass().getSimpleName(), playerNetworkSendTimer); + } + + public static void stopServer() { + setTimingsEnabled(false); + TimingsManager.recheckEnabled(); + } +} diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java new file mode 100644 index 0000000..78cd8d8 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsExport.java @@ -0,0 +1,149 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.log4j.Log4j2; +import org.apache.logging.log4j.Level; +import org.sculk.Sculk; +import org.sculk.Server; +import org.sculk.timings.JsonUtil; + +import java.io.*; +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.zip.GZIPOutputStream; + +import static co.aikar.timings.TimingsManager.HISTORY; + +@Log4j2 +public class TimingsExport extends Thread { + + private final ObjectNode out; + private final TimingsHistory[] history; + + private TimingsExport(ObjectNode out, TimingsHistory[] history) { + super("Timings paste thread"); + this.out = out; + this.history = history; + } + + public static void reportTimings() { + ObjectNode out = Sculk.JSON_MAPPER.createObjectNode(); + out.put("version", Server.getInstance().getVersion()); + out.put("maxplayers", Server.getInstance().getMaxPlayers()); + out.put("start", TimingsManager.timingStart / 1000); + out.put("end", System.currentTimeMillis() / 1000); + out.put("sampletime", (System.currentTimeMillis() - TimingsManager.timingStart) / 1000); + + if (!Timings.isPrivacy()) { + out.put("server", Server.getInstance().getMotd()); + out.put("motd", Server.getInstance().getMotd()); + out.put("online-mode", Server.getInstance().isXboxAuth()); + out.put("icon", ""); //"data:image/png;base64," + } + + final Runtime runtime = Runtime.getRuntime(); + RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean(); + + ObjectNode system = Sculk.JSON_MAPPER.createObjectNode(); + system.put("timingcost", getCost()); + system.put("name", System.getProperty("os.name")); + system.put("version", System.getProperty("os.version")); + system.put("jvmversion", System.getProperty("java.version")); + system.put("arch", System.getProperty("os.arch")); + system.put("maxmem", runtime.maxMemory()); + system.put("cpu", runtime.availableProcessors()); + system.put("runtime", ManagementFactory.getRuntimeMXBean().getUptime()); + system.put("flags", String.join(" ", runtimeBean.getInputArguments())); + system.set("gc", JsonUtil.mapToObject(ManagementFactory.getGarbageCollectorMXBeans(), (input) -> + new JsonUtil.JSONPair(input.getName(), JsonUtil.toArray(input.getCollectionCount(), input.getCollectionTime())))); + out.set("system", system); + + TimingsHistory[] history = HISTORY.toArray(new TimingsHistory[HISTORY.size() + 1]); + history[HISTORY.size()] = new TimingsHistory(); //Current snapshot + + ObjectNode timings = Sculk.JSON_MAPPER.createObjectNode(); + for (TimingIdentifier.TimingGroup group : TimingIdentifier.GROUP_MAP.values()) { + for (Timing id : group.timings) { + if (!id.timed && !id.isSpecial()) { + continue; + } + + timings.set(String.valueOf(id.id), JsonUtil.toArray(group.id, id.name)); + } + } + + new TimingsExport(out, history).start(); + } + + private static long getCost() { + int passes = 200; + Timing SAMPLER1 = TimingsManager.getTiming(null, "Timings sampler 1", null); + Timing SAMPLER2 = TimingsManager.getTiming(null, "Timings sampler 2", null); + Timing SAMPLER3 = TimingsManager.getTiming(null, "Timings sampler 3", null); + Timing SAMPLER4 = TimingsManager.getTiming(null, "Timings sampler 4", null); + Timing SAMPLER5 = TimingsManager.getTiming(null, "Timings sampler 5", null); + Timing SAMPLER6 = TimingsManager.getTiming(null, "Timings sampler 6", null); + + long start = System.nanoTime(); + for (int i = 0; i < passes; i++) { + SAMPLER1.startTiming(); + SAMPLER2.startTiming(); + SAMPLER3.startTiming(); + SAMPLER4.startTiming(); + SAMPLER5.startTiming(); + SAMPLER6.startTiming(); + SAMPLER6.stopTiming(); + SAMPLER5.stopTiming(); + SAMPLER4.stopTiming(); + SAMPLER3.stopTiming(); + SAMPLER2.stopTiming(); + SAMPLER1.stopTiming(); + } + + long timingsCost = (System.nanoTime() - start) / passes / 6; + + SAMPLER1.reset(true); + SAMPLER2.reset(true); + SAMPLER3.reset(true); + SAMPLER4.reset(true); + SAMPLER5.reset(true); + SAMPLER6.reset(true); + + return timingsCost; + } + + @Override + public void run() {} + + private String getResponse(HttpURLConnection con) { + return null; + } +} diff --git a/src/main/java/co/aikar/timings/TimingsHistory.java b/src/main/java/co/aikar/timings/TimingsHistory.java new file mode 100644 index 0000000..681278e --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsHistory.java @@ -0,0 +1,179 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.checkerframework.checker.signature.qual.Identifier; +import org.sculk.player.Player; +import org.sculk.Sculk; +import org.sculk.Server; +import org.sculk.timings.JsonUtil; + +import java.lang.management.ManagementFactory; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import static co.aikar.timings.Timings.fullServerTickTimer; +import static co.aikar.timings.TimingsManager.MINUTE_REPORTS; + +public class TimingsHistory { + public static long lastMinuteTime; + public static long timedTicks; + public static long playerTicks; + public static long entityTicks; + public static long tileEntityTicks; + public static long activatedEntityTicks; + + private static int levelIdPool = 1; + static Map levelMap = new HashMap<>(); + static Map entityMap = new HashMap<>(); + static Map blockEntityMap = new HashMap<>(); + + private final long endTime; + private final long startTime; + private final long totalTicks; + // Represents all time spent running the server this history + private final long totalTime; + private final MinuteReport[] minuteReports; + + private final TimingsHistoryEntry[] entries; + private final ObjectNode levels = Sculk.JSON_MAPPER.createObjectNode(); + + TimingsHistory() { + this.endTime = System.currentTimeMillis() / 1000; + this.startTime = TimingsManager.historyStart / 1000; + + if (timedTicks % 1200 != 0 || MINUTE_REPORTS.isEmpty()) { + this.minuteReports = MINUTE_REPORTS.toArray(new MinuteReport[MINUTE_REPORTS.size() + 1]); + this.minuteReports[this.minuteReports.length - 1] = new MinuteReport(); + } else { + this.minuteReports = MINUTE_REPORTS.toArray(new MinuteReport[0]); + } + + long ticks = 0; + for (MinuteReport mr : this.minuteReports) { + ticks += mr.ticksRecord.timed; + } + + this.totalTicks = ticks; + this.totalTime = fullServerTickTimer.record.totalTime; + this.entries = new TimingsHistoryEntry[TimingsManager.TIMINGS.size()]; + + int i = 0; + for (Timing timing : TimingsManager.TIMINGS) { + this.entries[i++] = new TimingsHistoryEntry(timing); + } + + final Map entityCounts = new HashMap<>(); + final Map blockEntityCounts = new HashMap<>(); + } + + static void resetTicks(boolean fullReset) { + if (fullReset) { + timedTicks = 0; + } + lastMinuteTime = System.nanoTime(); + playerTicks = 0; + tileEntityTicks = 0; + entityTicks = 0; + activatedEntityTicks = 0; + } + + ObjectNode export() { + ObjectNode json = Sculk.JSON_MAPPER.createObjectNode(); + json.put("s", this.startTime); + json.put("e", this.endTime); + json.put("tk", this.totalTicks); + json.put("tm", this.totalTime); + json.set("w", this.levels); + json.set("h", JsonUtil.mapToArray(this.entries, (entry) -> { + if (entry.data.count == 0) { + return null; + } + return entry.export(); + })); + json.set("mp", JsonUtil.mapToArray(this.minuteReports, MinuteReport::export)); + return json; + } + + static class MinuteReport { + final long time = System.currentTimeMillis() / 1000; + + final TicksRecord ticksRecord = new TicksRecord(); + final PingRecord pingRecord = new PingRecord(); + final TimingData fst = Timings.fullServerTickTimer.minuteData.clone(); + final double tps = 1E9 / (System.nanoTime() - lastMinuteTime) * this.ticksRecord.timed; + final double usedMemory = Timings.fullServerTickTimer.avgUsedMemory; + final double freeMemory = Timings.fullServerTickTimer.avgFreeMemory; + final double loadAvg = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage(); + + ArrayNode export() { + return JsonUtil.toArray(this.time, + Math.round(this.tps * 100D) / 100D, + Math.round(this.pingRecord.avg * 100D) / 100D, + this.fst.export(), + JsonUtil.toArray(this.ticksRecord.timed, + this.ticksRecord.player, + this.ticksRecord.entity, + this.ticksRecord.activatedEntity, + this.ticksRecord.tileEntity), + this.usedMemory, + this.freeMemory, + this.loadAvg); + } + } + + private static class TicksRecord { + final long timed; + final long player; + final long entity; + final long activatedEntity; + final long tileEntity; + + TicksRecord() { + this.timed = timedTicks - (TimingsManager.MINUTE_REPORTS.size() * 1200); + this.player = playerTicks; + this.entity = entityTicks; + this.activatedEntity = activatedEntityTicks; + this.tileEntity = tileEntityTicks; + } + } + + private static class PingRecord { + final double avg; + + PingRecord() { + final Collection onlinePlayers = Server.getInstance().getOnlinePlayers().values(); + int totalPing = 0; + for (Player player : onlinePlayers) { + totalPing += player.getPing(); + } + + this.avg = onlinePlayers.isEmpty() ? 0 : (float) totalPing / onlinePlayers.size(); + } + } +} diff --git a/src/main/java/co/aikar/timings/TimingsHistoryEntry.java b/src/main/java/co/aikar/timings/TimingsHistoryEntry.java new file mode 100644 index 0000000..d2cf03f --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsHistoryEntry.java @@ -0,0 +1,48 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.sculk.timings.JsonUtil; + +class TimingsHistoryEntry { + final TimingData data; + final TimingData[] children; + + TimingsHistoryEntry(Timing timing) { + this.data = timing.record.clone(); + this.children = new TimingData[timing.children.size()]; + + int i = 0; + for (TimingData child : timing.children.values()) { + this.children[i++] = child.clone(); + } + } + + ArrayNode export() { + ArrayNode json = this.data.export(); + if (this.children.length > 0) json.addAll(JsonUtil.mapToArray(this.children, TimingData::export)); + return json; + } +} diff --git a/src/main/java/co/aikar/timings/TimingsManager.java b/src/main/java/co/aikar/timings/TimingsManager.java new file mode 100644 index 0000000..9006fb5 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsManager.java @@ -0,0 +1,138 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import org.sculk.Server; + +import java.util.*; + +public class TimingsManager { + static final Map TIMING_MAP = Collections.synchronizedMap(new HashMap<>(256, 0.5f)); + + static final Queue TIMINGS = new ArrayDeque<>(); + static final ArrayDeque MINUTE_REPORTS = new ArrayDeque<>(); + + static Queue HISTORY = new BoundedQueue<>(12); + + static Timing CURRENT; + + static long timingStart = 0; + static long historyStart = 0; + static boolean needsFullReset = false; + static boolean needsRecheckEnabled = false; + + static void reset() { + needsFullReset = true; + } + + /** + * Called every tick to count the number of times a timer caused TPS loss. + */ + static void tick() { + if (Timings.isTimingsEnabled()) { + boolean violated = Timings.fullServerTickTimer.isViolated(); + + synchronized (TIMINGS) { + for (Timing timing : TIMINGS) { + if (timing.isSpecial()) { + // Called manually + continue; + } + + timing.tick(violated); + } + } + + TimingsHistory.playerTicks += Server.getInstance().getOnlinePlayers().size(); + TimingsHistory.timedTicks++; + } + } + + static void recheckEnabled() { + synchronized (TIMING_MAP) { + TIMING_MAP.values().forEach(Timing::checkEnabled); + } + + needsRecheckEnabled = false; + } + + static void resetTimings() { + if (needsFullReset) { + // Full resets need to re-check every handlers enabled state + // Timing map can be modified from async so we must sync on it. + synchronized (TIMING_MAP) { + for (Timing timing : TIMING_MAP.values()) { + timing.reset(true); + } + } + + HISTORY.clear(); + needsFullReset = false; + needsRecheckEnabled = false; + timingStart = System.currentTimeMillis(); + } else { + // Soft resets only need to act on timings that have done something + // Handlers can only be modified on main thread. + for (Timing timing : TIMINGS) { + timing.reset(false); + } + } + + TIMINGS.clear(); + MINUTE_REPORTS.clear(); + + TimingsHistory.resetTicks(true); + historyStart = System.currentTimeMillis(); + } + + public static Timing getTiming(String name) { + return getTiming(null, name, null); + } + + static Timing getTiming(String group, String name, Timing groupTiming) { + TimingIdentifier id = new TimingIdentifier(group, name, groupTiming); + return TIMING_MAP.computeIfAbsent(id, k -> new Timing(id)); + } + + static final class BoundedQueue extends LinkedList { + final int maxSize; + + BoundedQueue(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize must be greater than zero"); + } + + this.maxSize = maxSize; + } + + @Override + public boolean add(E e) { + if (this.size() == maxSize) { + this.remove(); + } + + return super.add(e); + } + } +} diff --git a/src/main/java/org/sculk/Sculk.java b/src/main/java/org/sculk/Sculk.java index 784cdbf..de24b50 100644 --- a/src/main/java/org/sculk/Sculk.java +++ b/src/main/java/org/sculk/Sculk.java @@ -1,5 +1,6 @@ package org.sculk; +import com.fasterxml.jackson.databind.json.JsonMapper; import lombok.extern.log4j.Log4j2; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -7,11 +8,11 @@ import org.sculk.utils.TextFormat; /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -26,32 +27,30 @@ public class Sculk { public static final long START_TIME = System.currentTimeMillis(); - - public final static String MINECRAFT_VERSION = ProtocolInfo.MINECRAFT_VERSION; - public final static String MINECRAFT_VERSION_NETWORK = ProtocolInfo.MINECRAFT_VERSION_NETWORK; - public final static String CODE_NAME = "Sculk-MP"; - public final static String CODE_VERSION = "v1.0.0"; - - public final static String DATA_PATH = System.getProperty("user.dir") + "/"; + public static final String MINECRAFT_VERSION = ProtocolInfo.MINECRAFT_VERSION; + public static final String MINECRAFT_VERSION_NETWORK = ProtocolInfo.MINECRAFT_VERSION_NETWORK; + public static final String CODE_NAME = "Sculk"; + public static final String CODE_VERSION = "v1.0.0"; + public static final JsonMapper JSON_MAPPER = JsonMapper.builder().build(); + public static final String DATA_PATH = System.getProperty("user.dir") + "/"; public static void main(String[] args) { Thread.currentThread().setName("sculkmp-main"); System.setProperty("log4j.skipJansi", "false"); - System.out.println("Starting SculkMP..."); - Logger logger = LogManager.getLogger(Sculk.class); - logger.info("{}Starting SculkMP software", TextFormat.WHITE); + Logger log = LogManager.getLogger(Sculk.class); + log.info("Starting {} software", CODE_NAME); int javaVersion = getJavaVersion(); - if(javaVersion < 21) { - logger.error("{}Using unsupported Java version! Minimum supported version is Java 21, found java {}", TextFormat.RED, javaVersion); + if (javaVersion < 21) { + log.error("{}Using unsupported Java version! Minimum supported version is Java 21, found java {}", TextFormat.RED, javaVersion); LogManager.shutdown(); return; } try { - new Server(logger, DATA_PATH); - } catch(Exception e) { + new Server(log, DATA_PATH); + } catch (Exception e) { log.throwing(e); shutdown(); } @@ -65,5 +64,4 @@ protected static void shutdown() { private static int getJavaVersion() { return Runtime.version().feature(); } - -} +} \ No newline at end of file diff --git a/src/main/java/org/sculk/SculkModule.java b/src/main/java/org/sculk/SculkModule.java new file mode 100644 index 0000000..d4b5819 --- /dev/null +++ b/src/main/java/org/sculk/SculkModule.java @@ -0,0 +1,41 @@ +package org.sculk; + + +import com.google.inject.PrivateModule; +import com.google.inject.binder.AnnotatedBindingBuilder; +import lombok.RequiredArgsConstructor; +import org.sculk.event.EventManager; +import org.sculk.event.EventManagerInterface; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@RequiredArgsConstructor +public class SculkModule extends PrivateModule { + + private final Server server; + + private AnnotatedBindingBuilder bindAndExpose(final Class type) { + this.expose(type); + return this.bind(type); + } + + @Override + protected void configure() { + this.bindAndExpose(Server.class).toInstance(this.server); + this.bindAndExpose(EventManagerInterface.class).to(EventManager.class); + } + +} diff --git a/src/main/java/org/sculk/Server.java b/src/main/java/org/sculk/Server.java index 3d9381d..55c2f58 100644 --- a/src/main/java/org/sculk/Server.java +++ b/src/main/java/org/sculk/Server.java @@ -1,34 +1,53 @@ package org.sculk; +import co.aikar.timings.Timing; +import co.aikar.timings.Timings; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Stage; import lombok.SneakyThrows; -import lombok.extern.log4j.Log4j; import lombok.extern.log4j.Log4j2; import org.apache.logging.log4j.Logger; +import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket; +import org.sculk.command.CommandSender; +import org.sculk.command.SimpleCommandMap; import org.sculk.config.Config; +import org.sculk.config.ServerProperties; +import org.sculk.config.ServerPropertiesKeys; import org.sculk.console.TerminalConsole; -import org.sculk.network.EventLoops; +import org.sculk.event.EventManager; +import org.sculk.event.command.CommandEvent; +import org.sculk.event.player.PlayerCreationEvent; +import org.sculk.network.BedrockInterface; import org.sculk.network.Network; -import org.sculk.thread.ThreadFactoryBuilder; +import org.sculk.network.SourceInterface; +import org.sculk.network.protocol.ProtocolInfo; +import org.sculk.network.session.SculkServerSession; +import org.sculk.player.Player; +import org.sculk.player.client.ClientChainData; +import org.sculk.plugin.PluginManager; +import org.sculk.scheduler.Scheduler; import org.sculk.utils.TextFormat; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.epoll.Epoll; - +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.CompletableFuture; /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -45,15 +64,19 @@ public class Server { private final Logger logger; private final TerminalConsole console; + private final EventManager eventManager; + private final PluginManager pluginManager; + private final Injector injector; + + private Scheduler scheduler; + private SimpleCommandMap simpleCommandMap; private Network network; - private final EventLoopGroup bossEventLoopGroup; - private final EventLoopGroup workerEventLoopGroup; private final Path dataPath; private final Path pluginDataPath; - private final Config properties; + private final ServerProperties properties; private final Config config; private final Config operators; private final Config whitelist; @@ -61,10 +84,15 @@ public class Server { private final Config banByIp; private final Map playerList = new HashMap<>(); + private final Map players = new HashMap<>(); private String motd; + private String submotd; private int maxPlayers; private String defaultGamemode; + private UUID serverId; + private long nextTick; + private int tickCounter; public volatile boolean shutdown = false; @@ -86,54 +114,29 @@ public Server(Logger logger, String dataPath) { if(!playerPath.toFile().exists()) playerPath.toFile().mkdirs(); logger.info("Loading {}...", TextFormat.AQUA + "sculk.yml" + TextFormat.WHITE); - this.config = new Config(this.dataPath + "sculk.yml"); + this.config = new Config(this.dataPath + "/sculk.yml"); logger.info("Loading {}...", TextFormat.AQUA + "server.properties" + TextFormat.WHITE); - this.properties = new Config(this.dataPath.resolve("server.properties").toString(), Config.PROPERTIES); - if(!this.properties.exists("server-port")) { - this.properties.set("language", "English"); - this.properties.set("motd", "A Sculk Server Software"); - this.properties.set("server-port", 19132); - this.properties.set("server-ip", "0.0.0.0"); - this.properties.set("white-list", false); - this.properties.set("max-players", 20); - this.properties.set("gamemode", "Survival"); - this.properties.set("pvp", true); - this.properties.set("difficulty", 1); - this.properties.set("level-name", "world"); - this.properties.set("level-seed", ""); - this.properties.set("level-type", "DEFAULT"); - this.properties.set("auto-save", true); - this.properties.set("xbox-auth", true); - this.properties.save(); - } - this.motd = this.properties.getString("motd"); + this.properties = new ServerProperties(this.dataPath); + this.motd = this.properties.get(ServerPropertiesKeys.MOTD, "A Sculk Server Software"); + this.submotd = this.properties.get(ServerPropertiesKeys.SUB_MOTD, "Powered by Sculk"); + + this.injector = Guice.createInjector(Stage.PRODUCTION, new SculkModule(this)); + this.eventManager = injector.getInstance(EventManager.class); + this.scheduler = injector.getInstance(Scheduler.class); + this.pluginManager = new PluginManager(this); + + logger.info("Loading commands..."); + this.simpleCommandMap = new SimpleCommandMap(this); this.operators = new Config(this.dataPath.resolve("op.txt").toString(), Config.ENUM); this.whitelist = new Config(this.dataPath.resolve("whitelist.txt").toString(), Config.ENUM); this.banByName = new Config(this.dataPath.resolve("banned-players.txt").toString(), Config.ENUM); this.banByIp = new Config(this.dataPath.resolve("banned-ip.txt").toString(), Config.ENUM); - logger.info("Selected {} as the base language", this.properties.getString("language")); + logger.info("Selected {} as the base language", this.properties.get(ServerPropertiesKeys.LANGUAGE, "English")); logger.info("Starting Minecraft: Bedrock Edition server version {}", TextFormat.AQUA + Sculk.MINECRAFT_VERSION + TextFormat.WHITE); - EventLoops.ChannelType channelType = EventLoops.getChannelType(); - this.logger.info("Using " + channelType.name() + " channel implementation as default!"); - for (EventLoops.ChannelType type : EventLoops.ChannelType.values()) { - this.logger.debug("Supported " + type.name() + " channels: " + type.isAvailable()); - } - - ThreadFactoryBuilder workerFactory = ThreadFactoryBuilder.builder() - .format("Bedrock Listener - #%d") - .daemon(true) - .build(); - ThreadFactoryBuilder bossFactory = ThreadFactoryBuilder.builder() - .format("RakNet Listener - #%d") - .daemon(true) - .build(); - this.workerEventLoopGroup = channelType.newEventLoopGroup(0, workerFactory); - this.bossEventLoopGroup = channelType.newEventLoopGroup(0, bossFactory); - this.console = new TerminalConsole(this); this.start(); } @@ -141,34 +144,44 @@ public Server(Logger logger, String dataPath) { public void start() { this.console.getConsoleThread().start(); - //String serverIP = this.properties.getString("server-ip", "0.0.0.0"); - //int port = this.properties.getInt("server-port", 19132); - - //getLogger().info(serverIP); - //getLogger().info(port); - - InetSocketAddress bindAddress = new InetSocketAddress("0.0.0.0", 19132); - this.bindChannels(bindAddress); - // TODO: Load server raknet - getLogger().info("Minecraft network interface running on {}", bindAddress); - - - if(this.properties.getBoolean("xbox-auth")) { + logger.info("Loading all plugins..."); + pluginManager.loadAllPlugins(); + logger.info("All plugins loaded successfully"); + + InetSocketAddress bindAddress = new InetSocketAddress(this.getProperties().get(ServerPropertiesKeys.SERVER_IP, "0.0.0.0"), this.getProperties().get(ServerPropertiesKeys.SERVER_PORT, 19132)); + this.serverId = UUID.randomUUID(); + this.network = new Network(this); + this.network.setName(this.motd); + try { + this.network.registerInterface(new BedrockInterface(this)); + getLogger().info("Minecraft network interface running on {}", bindAddress); + } catch(Exception e) { + logger.fatal("**** FAILED TO BIND TO " + bindAddress); + logger.fatal("Peahaps a server s already running on that port?"); + shutdown(); + } + + this.tickCounter = 0; + + if(this.properties.get(ServerPropertiesKeys.XBOX_AUTH, true)) { logger.info("Online mode is enable. The server will verify that players are authenticated to XboxLive."); } else { logger.info("{}Online mode is not enabled. The server no longer checks if players are authenticated to XboxLive.", TextFormat.RED); } logger.info("This server is running on version {}",TextFormat.AQUA + Sculk.CODE_VERSION); - logger.info("Sculk-MP is distributed undex the {}",TextFormat.AQUA + "GNU GENERAL PUBLIC LICENSE"); + logger.info("Sculk is distributed undex the {}",TextFormat.AQUA + "GNU GENERAL PUBLIC LICENSE"); - - Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown)); - getLogger().info("Done ("+ (double) (System.currentTimeMillis() - Sculk.START_TIME) / 1000 +"s)! For help, type \"help\" or \"?"); - } + logger.info("Enable all plugins..."); + pluginManager.enableAllPlugins(); + logger.info("All plugins enabled successfully"); - private void bindChannels(InetSocketAddress address) { - boolean allowEpool = Epoll.isAvailable(); - + getLogger().info("Done ({}s)! For help, type \"help\" or \"?", (double) (System.currentTimeMillis() - Sculk.START_TIME) / 1000); + + this.getScheduler().scheduleDelayedTask(() -> { + System.out.println("5ms"); + }, 5, false); + + this.tickProcessor(); } public void shutdown() { @@ -178,10 +191,57 @@ public void shutdown() { this.logger.info("Stopping the server"); this.shutdown = true; + logger.info("Disabling all plugins..."); + pluginManager.disableAllPlugins(); + logger.info("Disabled all plugins"); + + Sculk.shutdown(); + + this.logger.info("Stopping network interfaces"); + for(SourceInterface sourceInterface : this.network.getInterfaces()) { + sourceInterface.shutdown(); + this.network.unregisterInterface(sourceInterface); + } + + this.logger.info("Closing console"); this.console.getConsoleThread().interrupt(); + + this.logger.info("Stopping other threads"); } + public CompletableFuture createPlayer(SculkServerSession session, ClientChainData info, boolean authenticated){ + return CompletableFuture.supplyAsync(() -> { + PlayerCreationEvent event = new PlayerCreationEvent(session); + event.call(); + Class clazz = event.getPlayerClass(); + Player player; + + try { + Constructor constructor = clazz.getConstructor(SculkServerSession.class, ClientChainData.class); + player = constructor.newInstance(session, info); + this.addPlayer(session.getSocketAddress(), player); + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { + this.getLogger().warn("Failed to create player", e); + throw new RuntimeException(e); // Propager l'exception dans le futur + } + + return player; + }); + } + + public EventManager getEventManager() { + return eventManager; + } + + public PluginManager getPluginManager() { + return pluginManager; + } + + public Injector getInjector() { + return injector; + } + public boolean isRunning() { return !this.shutdown; } @@ -206,7 +266,7 @@ public Map getOnlinePlayers() { return Collections.unmodifiableMap(playerList); } - public Config getProperties() { + public ServerProperties getProperties() { return properties; } @@ -221,4 +281,120 @@ public int getMaxPlayers() { public String getDefaultGamemode() { return defaultGamemode; } + + public String getMotd() { + return motd; + } + + public String getSubMotd() { + return submotd; + } + + public UUID getServerId() { + return serverId; + } + + public void addPlayer(SocketAddress socketAddress, Player player) { + this.players.put(socketAddress, player); + } + + public void addOnlinePlayer(Player player) { + this.playerList.put(player.getServerId(), player); + } + + public void onPlayerCompleteLogin(Player player) { + this.sendFullPlayerList(player); + } + + public void sendFullPlayerList(Player player) { + PlayerListPacket packet = new PlayerListPacket(); + packet.setAction(PlayerListPacket.Action.ADD); + packet.getEntries().addAll(this.playerList.values().stream().map(p -> { + PlayerListPacket.Entry entry = new PlayerListPacket.Entry(p.getServerId()); + entry.setEntityId(p.getUniqueId()); + entry.setName(p.getName()); + entry.setSkin(p.getSerializedSkin()); + entry.setPlatformChatId(""); + return entry; + }).toList()); + player.sendDataPacket(packet); + } + + public Scheduler getScheduler() { + return scheduler; + } + + public String getVersion() { + return ProtocolInfo.MINECRAFT_VERSION; + } + + public boolean isXboxAuth() { + return true; // TODO default true for test + } + + public void tickProcessor() { + this.nextTick = System.currentTimeMillis(); + try { + while(this.isRunning()) { + try { + this.tick(); + long next = this.nextTick; + long current = System.currentTimeMillis(); + if(next - 0.1 > current) { + long allocated = next - current - 1; + if(allocated > 0) { + Thread.sleep(allocated, 900000); + } + } + } catch(RuntimeException exception) { + log.error("Error whilst ticking server", exception); + } + } + } catch(Throwable throwable) { + log.fatal("Exception happened while ticking server", throwable); + } + } + + private boolean tick() { + long tickTime = System.currentTimeMillis(); + long time = tickTime - this.nextTick; + if(time < -25) { + try { + Thread.sleep(Math.max(5, -time - 25)); + } catch(InterruptedException exception) { + log.error("Server interrupted whilst sleeping", exception); + } + } + long tickTimeNano = System.nanoTime(); + if((tickTimeNano - this.nextTick) < -25) { + return false; + } + try(Timing ignored = Timings.fullServerTickTimer.startTiming()) { + ++this.tickCounter; + try(Timing timing1 = Timings.fullServerTickTimer.startTiming()) { + this.network.processInterfaces(); + } + try(Timing timing1 = Timings.schedulerTimer.startTiming()) { + this.scheduler.mainThread(this.tickCounter); + } + } + return true; + } + + public SimpleCommandMap getCommandMap() { + return this.simpleCommandMap; + } + + public void dispatchCommand(CommandSender sender, String commandLine, boolean internal) { + if(!internal) { + CommandEvent commandEvent = new CommandEvent(sender, commandLine); + commandEvent.call(); + if(commandEvent.isCancelled()) { + return; + } + commandLine = commandEvent.getCommand(); + } + simpleCommandMap.dispatch(sender, commandLine); + } + } diff --git a/src/main/java/org/sculk/command/Command.java b/src/main/java/org/sculk/command/Command.java new file mode 100644 index 0000000..903b958 --- /dev/null +++ b/src/main/java/org/sculk/command/Command.java @@ -0,0 +1,135 @@ +package org.sculk.command; + + +import lombok.Getter; +import lombok.NonNull; +import org.sculk.command.data.CommandData; +import org.sculk.command.data.CommandParameter; +import org.sculk.exception.CommandException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public abstract class Command { + + private String name; + private String nextLabel; + private String label; + @Getter + private String description; + private String usageMessage; + + @Getter + private List aliases = new ArrayList<>(); + private List activeAliases = new ArrayList<>(); + + private List permissions = new ArrayList<>(); + private String permissionMessage = null; + + private CommandMap commandMap = null; + @Getter + private CommandData commandData; + @Getter + private List parameters = new ArrayList<>(); + + public Command(String name, String description, String usageMessage, List aliases) { + this.name = name; + this.setLabel(name); + this.setDescription(description); + this.usageMessage = usageMessage != null ? usageMessage : "/" + name; + this.setAliases(aliases); + } + + public void registerParameter(@NonNull CommandParameter paramSet) { + List parameters1 = new ArrayList<>(); + parameters1.add(new CommandParameter[]{paramSet}); + this.parameters.addAll(parameters1); + } + + public abstract void execute(CommandSender sender, String commandLabel, List args) throws CommandException; + + public String getName() { + return name; + } + + public List getPermissions() { + return permissions; + } + + public void setPermissions(List permissions) { + this.permissions.addAll(permissions); + } + + public void setPermission(String permission) { + setPermissions(permission == null ? new ArrayList<>() : List.of(permission.split(";"))); + } + + public void setDescription(String description) { + this.description = description; + } + + public void setUsage(String usage) { + this.usageMessage = usage; + } + + public void setAliases(List aliases) { + this.aliases = aliases != null ? aliases : new ArrayList<>(); + } + + public String getLabel() { + return this.label; + } + + public boolean setLabel(String name) { + this.nextLabel = name; + if(!isRegistered()) { + this.label = name; + return true; + } + return false; + } + + public boolean register(CommandMap commandMap) { + return false; + } + + public boolean unregister(CommandMap commandMap) { + return false; + } + + private boolean allowChangeFrom(CommandMap commandMap) { + return this.commandMap == null || this.commandMap == commandMap; + } + + public boolean isRegistered() { + return this.commandMap != null; + } + + + public final void buildCommand() { + this.commandData = CommandData.builder(name) + .setDescription(description) + .setUsageMessage(usageMessage) + .setAliases(aliases) + .setPermissionMessage(this.permissionMessage == null ? "" : this.permissionMessage) + .setParameters(this.getParameters()) + .setPermissions(this.getPermissions()) + .build(); + } +} diff --git a/src/main/java/org/sculk/command/CommandMap.java b/src/main/java/org/sculk/command/CommandMap.java new file mode 100644 index 0000000..f9e9f18 --- /dev/null +++ b/src/main/java/org/sculk/command/CommandMap.java @@ -0,0 +1,29 @@ +package org.sculk.command; + + +import java.util.List; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface CommandMap { + + void registerAll(String fallbackPrefix, List commands); + void register(String fallbackPrefix, Command command, String label); + boolean dispatch(CommandSender sender, String cmdLine); + void clearCommands(); + Command getCommand(String name); + +} diff --git a/src/main/java/org/sculk/command/CommandSender.java b/src/main/java/org/sculk/command/CommandSender.java new file mode 100644 index 0000000..975a49d --- /dev/null +++ b/src/main/java/org/sculk/command/CommandSender.java @@ -0,0 +1,30 @@ +package org.sculk.command; + + +import org.sculk.Server; +import org.sculk.permission.Permissible; +import org.sculk.player.text.RawTextBuilder; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface CommandSender extends Permissible { + + void sendMessage(String message); + void sendMessage(RawTextBuilder textBuilder); + Server getServer(); + String getName(); + +} diff --git a/src/main/java/org/sculk/command/SimpleCommandMap.java b/src/main/java/org/sculk/command/SimpleCommandMap.java new file mode 100644 index 0000000..8c4d753 --- /dev/null +++ b/src/main/java/org/sculk/command/SimpleCommandMap.java @@ -0,0 +1,134 @@ +package org.sculk.command; + + +import org.sculk.Server; +import org.sculk.command.defaults.HelpCommand; +import org.sculk.command.defaults.VersionCommand; +import org.sculk.command.utils.CommandStringHelper; + +import java.util.*; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class SimpleCommandMap implements CommandMap { + + protected Map knownCommands = new HashMap<>(); + private final Server server; + + public SimpleCommandMap(Server server) { + this.server = server; + this.setDefaultCommands(); + } + + private void setDefaultCommands() { + registerAll("sculk", List.of( + new VersionCommand(), new HelpCommand() + )); + } + + public void registerAll(String fallbackPrefix, List commands) { + for(Command command : commands) { + command.buildCommand(); + register(fallbackPrefix, command); + } + } + + public void register(String fallbackPrefix, Command command) { + command.buildCommand(); + register(fallbackPrefix, command, null); + } + + public void register(String fallbackPrefix, Command command, String label) { + if(command.getPermissions().isEmpty()) { + throw new IllegalArgumentException("Commands must have a permission set"); + } + if(label == null) { + label = command.getLabel(); + } + label = label.trim(); + fallbackPrefix = fallbackPrefix.toLowerCase().trim(); + + boolean registered = this.registerAlias(command, false, fallbackPrefix, label); + + List aliases = command.getAliases(); + for(int index = 0; index < aliases.size(); index++) { + String alias = aliases.get(index); + if (!registerAlias(command, true, fallbackPrefix, alias)) { + aliases.remove(index); + } + } + command.setAliases(aliases); + + if(!registered) { + command.setAliases(Collections.singletonList(fallbackPrefix + ":" + label)); + } + command.register(this); + } + + public boolean unregister(Command command) { + for(String label : knownCommands.keySet()) { + if(knownCommands.get(label) == command) { + knownCommands.remove(label); + } + } + command.unregister(this); + return true; + } + + private boolean registerAlias(Command command, boolean isAliases, String fallbackPrefix, String label) { + this.knownCommands.put(fallbackPrefix + ":" + label, command); + if(!isAliases) { + command.setLabel(label); + } + this.knownCommands.put(label, command); + return true; + } + + public boolean dispatch(CommandSender sender, String commandLine) { + String[] args = CommandStringHelper.parseQuoteAware(commandLine); + if(args.length == 0) { + sender.sendMessage("§cUnknown command: §4" + commandLine + "§c. Use /help for a list available commands."); + return false; + } + String sendCommandLabel = args[0]; + args = Arrays.copyOfRange(args, 1, args.length); + + Command target = this.getCommand(sendCommandLabel); + if(target != null) { + target.execute(sender, sendCommandLabel, List.of(args)); + return true; + } + sender.sendMessage("§cUnknown command: §4" + commandLine + "§c. Use /help for a list available commands."); + return false; + } + + public void clearCommands() { + + } + + public Command getCommand(String name) { + return knownCommands.get(name); + } + + public Map getCommands() { + return knownCommands; + } + + public void registerServerAliases() { + + } + +} diff --git a/src/main/java/org/sculk/command/data/CommandData.java b/src/main/java/org/sculk/command/data/CommandData.java new file mode 100644 index 0000000..12cc423 --- /dev/null +++ b/src/main/java/org/sculk/command/data/CommandData.java @@ -0,0 +1,208 @@ +package org.sculk.command.data; + +import lombok.NonNull; +import lombok.ToString; +import org.cloudburstmc.protocol.bedrock.data.command.CommandOverloadData; +import org.cloudburstmc.protocol.bedrock.data.command.CommandParamData; +import org.cloudburstmc.protocol.bedrock.data.command.CommandParamType; +import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@ToString +public class CommandData { + private final String cmdName; + private final String description; + private final String usage; + private final String permMsg; + private final List permissions; + private final CommandEnum aliases; + private final List overloads; + private String registeredName; + + private CommandData(String cmdName, String description, String usage, String permissionMessage, List permissions, CommandEnum aliases, List overloads) { + this.cmdName = cmdName; + this.description = description; + this.usage = usage; + this.permMsg = permissionMessage; + this.permissions = permissions; + this.aliases = aliases; + this.overloads = overloads; + } + + public static Builder builder(@NonNull String commandName) { + return new Builder(commandName); + } + + public String getRegisteredName() { + return this.registeredName == null ? cmdName : registeredName; + } + + public void setRegisteredName(String name) { + this.registeredName = name; + } + + public String getDescription() { + return this.description; + } + + public List getPermissions() { + return this.permissions; + } + + public List getAliases() { + return this.aliases.getValues(); + } + + public void removeAlias(String alias) { + this.aliases.getValues().remove(alias); + } + + public org.cloudburstmc.protocol.bedrock.data.command.CommandData toNetwork() { + String description = this.description; + + CommandOverloadData[] overloadData = new CommandOverloadData[this.overloads.size()]; + + + for (int i = 0; i < overloadData.length; i++) { + CommandParameter[] parameters = this.overloads.get(i); + CommandParamData[] params = new CommandParamData[parameters.length]; + for (int i2 = 0; i2 < parameters.length; i2++) { + params[i2] = parameters[i2].toNetwork(); + } + overloadData[i] = new CommandOverloadData(false, params); + } + + return new org.cloudburstmc.protocol.bedrock.data.command.CommandData(this.getRegisteredName(), description, Collections.emptySet(), + CommandPermission.ANY, this.aliases.toNetwork(), Collections.emptyList(), overloadData); + } + + public List getOverloads() { + return this.overloads; + } + + public String getPermissionMessage() { + return this.permMsg; + } + + public String getUsage() { + return this.usage; + } + + public static class Builder { + private final String name; + private String desc = ""; + private String usage = ""; + private String permMsg = ""; + private List perms = new ArrayList<>(); + private List aliases = new ArrayList<>(); + private List overloads = new ArrayList<>(); + + public Builder(@NonNull String name) { + this.name = name.toLowerCase(); + } + + public CommandData build() { + return new CommandData(name, desc, usage, permMsg, perms, new CommandEnum(name, aliases), overloads); + } + + public Builder setDescription(@NonNull String description) { + this.desc = description; + return this; + } + + public Builder setUsageMessage(@NonNull String usage) { + this.usage = usage; + return this; + } + + public Builder setPermissionMessage(@NonNull String message) { + this.permMsg = message; + return this; + } + + public Builder setPermissions(@NonNull String... permissions) { + this.perms = Arrays.asList(permissions); + return this; + } + + public Builder setPermissions(@NonNull List permissions) { + this.perms = new ArrayList(permissions); + return this; + } + + public Builder addPermission(@NonNull String permission) { + this.perms.add(permission); + return this; + } + + public Builder addPermissions(@NonNull String... permissions) { + this.perms.addAll(Arrays.asList(permissions)); + return this; + } + + public Builder addPermissions(@NonNull List permissions) { + this.perms.addAll(permissions); + return this; + } + + public Builder setAliases(@NonNull String... aliases) { + this.aliases = Arrays.asList(aliases); + return this; + } + + public Builder setAliases(@NonNull List aliases) { + this.aliases = new ArrayList(aliases); + return this; + } + + public Builder addAlias(@NonNull String alias) { + this.aliases.add(alias); + return this; + } + + public Builder addAliases(@NonNull String... aliases) { + this.aliases.addAll(Arrays.asList(aliases)); + return this; + } + public Builder addAliases(@NonNull List aliases) { + this.aliases.addAll(aliases); + return this; + } + + public Builder setParameters(@NonNull CommandParameter[]... paramSet) { + this.overloads = Arrays.asList(paramSet); + return this; + } + + public Builder setParameters(@NonNull List parameters) { + this.overloads = parameters; + return this; + } + + public Builder addParameters(@NonNull CommandParameter[]... paramSet) { + this.overloads.addAll(Arrays.asList(paramSet)); + return this; + } + } + +} diff --git a/src/main/java/org/sculk/command/data/CommandEnum.java b/src/main/java/org/sculk/command/data/CommandEnum.java new file mode 100644 index 0000000..41e2bbe --- /dev/null +++ b/src/main/java/org/sculk/command/data/CommandEnum.java @@ -0,0 +1,66 @@ +package org.sculk.command.data; + +import lombok.ToString; +import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumConstraint; +import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumData; + +import java.util.*; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@ToString +public class CommandEnum { + + private final String name; + private final List values; + + public CommandEnum(String name, List values) { + this.name = name; + this.values = values; + } + + public String getName() { + return name; + } + + public List getValues() { + return values; + } + + public int hashCode() { + return name.hashCode(); + } + + public CommandEnumData toNetwork() { + String[] aliases; + if (!values.isEmpty()) { + List aliasList = new ArrayList<>(values); + aliasList.add(this.name); + aliases = aliasList.toArray(new String[0]); + } else { + aliases = new String[]{this.name}; + } + return new CommandEnumData(this.name + "Aliases", toNetwork(aliases), false); + } + + private static LinkedHashMap> toNetwork(String[] values) { + LinkedHashMap> map = new LinkedHashMap<>(); + for (String value : values) { + map.put(value, Collections.emptySet()); + } + return map; + } +} diff --git a/src/main/java/org/sculk/command/data/CommandParameter.java b/src/main/java/org/sculk/command/data/CommandParameter.java new file mode 100644 index 0000000..e78416c --- /dev/null +++ b/src/main/java/org/sculk/command/data/CommandParameter.java @@ -0,0 +1,120 @@ +package org.sculk.command.data; + +import com.google.common.collect.ImmutableMap; +import lombok.ToString; +import org.cloudburstmc.protocol.bedrock.data.command.*; + +import java.util.*; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@ToString +public class CommandParameter { + + private static final ImmutableMap PARAM_MAPPINGS = ImmutableMap.builder() + .put(CommandParamType.INT, CommandParam.INT) + .put(CommandParamType.FLOAT, CommandParam.FLOAT) + .put(CommandParamType.VALUE, CommandParam.VALUE) + .put(CommandParamType.WILDCARD_INT, CommandParam.WILDCARD_INT) + .put(CommandParamType.OPERATOR, CommandParam.OPERATOR) + .put(CommandParamType.TARGET, CommandParam.TARGET) + .put(CommandParamType.WILDCARD_TARGET, CommandParam.WILDCARD_TARGET) + .put(CommandParamType.FILE_PATH, CommandParam.FILE_PATH) + .put(CommandParamType.INT_RANGE, CommandParam.INT_RANGE) + .put(CommandParamType.STRING, CommandParam.STRING) + .put(CommandParamType.POSITION, CommandParam.POSITION) + .put(CommandParamType.BLOCK_POSITION, CommandParam.BLOCK_POSITION) + .put(CommandParamType.MESSAGE, CommandParam.MESSAGE) + .put(CommandParamType.TEXT, CommandParam.TEXT) + .put(CommandParamType.JSON, CommandParam.JSON) + .put(CommandParamType.COMMAND, CommandParam.COMMAND) + .build(); + + public String name; + public CommandParamType type; + public boolean optional; + public byte options = 0; + + public CommandEnum enumData; + public String postFix; + + public CommandParameter(String name, CommandParamType type, boolean optional) { + this.name = name; + this.type = type; + this.optional = optional; + } + + public CommandParameter(String name, boolean optional) { + this(name, CommandParamType.TEXT, optional); + } + + public CommandParameter(String name) { + this(name, false); + } + + public CommandParameter(String name, boolean optional, String enumType) { + this.name = name; + this.type = CommandParamType.TEXT; + this.optional = optional; + this.enumData = new CommandEnum(enumType, new ArrayList<>()); + } + + public CommandParameter(String name, boolean optional, String[] enumValues) { + this.name = name; + this.type = CommandParamType.TEXT; + this.optional = optional; + this.enumData = new CommandEnum(name + "Enums", Arrays.asList(enumValues)); + } + + public CommandParameter(String name, String enumType) { + this(name, false, enumType); + } + + public CommandParameter(String name, String[] enumValues) { + this(name, false, enumValues); + } + + protected CommandParamData toNetwork() { + CommandParamData data = new CommandParamData(); + data.setName(this.name); + data.setOptional(this.optional); + data.setEnumData(this.enumData != null ? new CommandEnumData(this.name, toNetwork(this.enumData.getValues()), false) : null); + data.setType(PARAM_MAPPINGS.get(this.type)); + data.setPostfix(this.postFix); + + return data; + } + + private static LinkedHashMap> toNetwork(List values) { + LinkedHashMap> map = new LinkedHashMap<>(); + for (String value : values) { + map.put(value, Collections.emptySet()); + } + return map; + } + + protected static CommandParamType fromString(String param) { + return switch (param) { + case "string", "stringenum" -> CommandParamType.STRING; + case "target" -> CommandParamType.TARGET; + case "blockpos" -> CommandParamType.POSITION; + case "rawtext" -> CommandParamType.TEXT; + case "int" -> CommandParamType.INT; + default -> CommandParamType.TEXT; + }; + + } +} diff --git a/src/main/java/org/sculk/command/defaults/HelpCommand.java b/src/main/java/org/sculk/command/defaults/HelpCommand.java new file mode 100644 index 0000000..d772994 --- /dev/null +++ b/src/main/java/org/sculk/command/defaults/HelpCommand.java @@ -0,0 +1,62 @@ +package org.sculk.command.defaults; + +import com.google.gson.Gson; +import org.sculk.Server; +import org.sculk.command.Command; +import org.sculk.command.CommandSender; +import org.sculk.exception.CommandException; +import org.sculk.permission.DefaultPermissionNames; +import org.sculk.player.text.RawTextBuilder; +import org.sculk.player.text.TextBuilder; +import org.sculk.player.text.TranslaterBuilder; + +import java.util.ArrayList; +import java.util.List; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class HelpCommand extends Command { + + public HelpCommand() { + super("help", "Show the help menu", "/help [page|command name]", List.of("?")); + this.setPermission(DefaultPermissionNames.COMMAND_HELP); + } + + @Override + public void execute(CommandSender sender, String commandLabel, List args) throws CommandException { + StringBuilder builder = new StringBuilder(); + TranslaterBuilder translaterBuilder = new TranslaterBuilder(); + List commandSending = new ArrayList<>(); + translaterBuilder.setTranslate("§6-------------- §fHelp - %%s command(s) §6--------------\n%%s"); + Server.getInstance().getCommandMap().getCommands().forEach((s, command) -> { + String commandName = s.contains(":") ? s.substring(s.indexOf(':') + 1) : s; + if(!commandSending.contains(commandName)) { + builder.append("§6/").append(commandName).append(":§f ").append(command.getDescription()).append("\n"); + commandSending.add(commandName); + } + }); + + translaterBuilder.setWith(new RawTextBuilder() + .add(new TextBuilder() + .setText(Integer.toString(commandSending.size()))) + .add(new TextBuilder() + .setText(builder.substring(0, builder.length() - 2))) + ); + sender.sendMessage(new RawTextBuilder().add(translaterBuilder)); + + } + +} diff --git a/src/main/java/org/sculk/command/defaults/VersionCommand.java b/src/main/java/org/sculk/command/defaults/VersionCommand.java new file mode 100644 index 0000000..a72aac8 --- /dev/null +++ b/src/main/java/org/sculk/command/defaults/VersionCommand.java @@ -0,0 +1,56 @@ +package org.sculk.command.defaults; + + +import org.cloudburstmc.protocol.bedrock.data.command.CommandParamType; +import org.sculk.Sculk; +import org.sculk.command.Command; +import org.sculk.command.CommandSender; +import org.sculk.command.data.CommandParameter; +import org.sculk.exception.CommandException; +import org.sculk.network.protocol.ProtocolInfo; +import org.sculk.permission.DefaultPermissionNames; +import org.sculk.player.text.RawTextBuilder; +import org.sculk.player.text.TextBuilder; +import org.sculk.player.text.TranslaterBuilder; + +import java.util.List; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class VersionCommand extends Command { + + public VersionCommand() { + super("version", "Gets the version of this server including any plugins in use", "/version [plugin name]", List.of("ver", "about")); + this.setPermission(DefaultPermissionNames.COMMAND_VERSION); + this.registerParameter(new CommandParameter("player", CommandParamType.TARGET, true)); + } + + @Override + public void execute(CommandSender sender, String commandLabel, List args) throws CommandException { + sender.sendMessage(new RawTextBuilder().add( + new TranslaterBuilder() + .setTranslate("§fThis server is running §a%%s\n§fServer version: §a%%s\n§fCompatible Minecraft version: §a%%s §f(protocol version: §a%%s§f)\nOperating system: §a%%s") + .setWith(new RawTextBuilder() + .add(new TextBuilder().setText(Sculk.CODE_NAME)) // software name + .add(new TextBuilder().setText(Sculk.CODE_VERSION)) // software version + .add(new TextBuilder().setText(ProtocolInfo.MINECRAFT_VERSION)) // Minecraft version + .add(new TextBuilder().setText(String.valueOf(ProtocolInfo.CURRENT_PROTOCOL))) // software protocol + .add(new TextBuilder().setText(System.getProperty("os.name").toLowerCase())) // system + )) + ); + } + +} diff --git a/src/main/java/org/sculk/command/utils/CommandStringHelper.java b/src/main/java/org/sculk/command/utils/CommandStringHelper.java new file mode 100644 index 0000000..d5cbdfc --- /dev/null +++ b/src/main/java/org/sculk/command/utils/CommandStringHelper.java @@ -0,0 +1,45 @@ +package org.sculk.command.utils; + + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public final class CommandStringHelper { + + public static String[] parseQuoteAware(String commandLine) { + ArrayList args = new ArrayList<>(); + Pattern pattern = Pattern.compile("\"((?:\\\\.|[^\\\\\"])*)\"|(\\S+)"); + Matcher matcher = pattern.matcher(commandLine); + + while (matcher.find()) { + String match = null; + if (matcher.group(1) != null) { + match = matcher.group(1).replaceAll("\\\\([\\\\\"])", "$1"); + } else if (matcher.group(2) != null) { + match = matcher.group(2); + } + + if (match != null) { + args.add(match); + } + } + return args.toArray(new String[0]); + } + +} diff --git a/src/main/java/org/sculk/config/Config.java b/src/main/java/org/sculk/config/Config.java index 2da168d..a2f9575 100644 --- a/src/main/java/org/sculk/config/Config.java +++ b/src/main/java/org/sculk/config/Config.java @@ -20,11 +20,11 @@ import java.util.regex.Pattern; /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by diff --git a/src/main/java/org/sculk/config/ConfigSection.java b/src/main/java/org/sculk/config/ConfigSection.java index 1ec2a6d..b904447 100644 --- a/src/main/java/org/sculk/config/ConfigSection.java +++ b/src/main/java/org/sculk/config/ConfigSection.java @@ -3,11 +3,11 @@ import java.util.*; /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by diff --git a/src/main/java/org/sculk/config/ServerProperties.java b/src/main/java/org/sculk/config/ServerProperties.java new file mode 100644 index 0000000..4ab9020 --- /dev/null +++ b/src/main/java/org/sculk/config/ServerProperties.java @@ -0,0 +1,124 @@ +package org.sculk.config; + +import java.io.File; +import java.nio.file.Path; + +public class ServerProperties { + private final Config properties; + + public ServerProperties(Path dataPath) { + File file = new File(dataPath + "/server.properties"); + if (!file.exists()) { + ConfigSection defaults = getDefaultValues(); + new Config(file.getPath(), Config.PROPERTIES, defaults).save(); + } + this.properties = new Config(dataPath + "/server.properties", Config.PROPERTIES, getDefaultValues()); + } + + private ConfigSection getDefaultValues() { + ConfigSection defaults = new ConfigSection(); + defaults.put(ServerPropertiesKeys.LANGUAGE.toString(), "English"); + defaults.put(ServerPropertiesKeys.MOTD.toString(), "A Sculk Server Software"); + defaults.put(ServerPropertiesKeys.SUB_MOTD.toString(), "Powered by Sculk"); + defaults.put(ServerPropertiesKeys.SERVER_IP.toString(), "0.0.0.0"); + defaults.put(ServerPropertiesKeys.SERVER_PORT.toString(), 19132); + defaults.put(ServerPropertiesKeys.WHITELIST.toString(), "off"); + defaults.put(ServerPropertiesKeys.MAX_PLAYERS.toString(), 20); + defaults.put(ServerPropertiesKeys.GAMEMODE.toString(), 0); + defaults.put(ServerPropertiesKeys.PVP.toString(), "on"); + defaults.put(ServerPropertiesKeys.DIFFICULTY.toString(), 1); + defaults.put(ServerPropertiesKeys.LEVEL_NAME.toString(), "world"); + defaults.put(ServerPropertiesKeys.LEVEL_SEED.toString(), ""); + defaults.put(ServerPropertiesKeys.LEVEL_TYPE.toString(), "DEFAULT"); + defaults.put(ServerPropertiesKeys.SPAWN_ANIMALS.toString(), "on"); + defaults.put(ServerPropertiesKeys.SPAWN_MONSTERS.toString(), "on"); + defaults.put(ServerPropertiesKeys.AUTO_SAVE.toString(), "on"); + defaults.put(ServerPropertiesKeys.XBOX_AUTH.toString(), "on"); + return defaults; + } + + public ConfigSection getProperties() { + return this.properties.getRootSection(); + } + + public Integer get(ServerPropertiesKeys key, Integer defaultValue) { + Object value = this.properties.get(key.toString()); + if (value instanceof String) { + try { + return Integer.parseInt((String) value); + } catch (NumberFormatException e) { + return defaultValue; + } + } else if (value instanceof Integer) { + return (Integer) value; + } else { + return defaultValue; + } + } + + public String get(ServerPropertiesKeys key, String defaultValue) { + Object value = this.properties.get(key.toString()); + if (value instanceof String) { + return (String) value; + } else { + return defaultValue; + } + } + + public Boolean get(ServerPropertiesKeys key, Boolean defaultValue) { + Object value = this.properties.get(key.toString()); + if (value instanceof String) { + String stringValue = ((String) value).toLowerCase(); + if (stringValue.equals("on")) { + return true; + } else if (stringValue.equals("off")) { + return false; + } + } else if (value instanceof Boolean) { + return (Boolean) value; + } + return defaultValue; + } + + public Long get(ServerPropertiesKeys key, Long defaultValue) { + Object value = this.properties.get(key.toString()); + if (value instanceof String stringValue) { + if (!stringValue.isEmpty()) { + try { + return Long.parseLong(stringValue); + } catch (NumberFormatException e) { + return defaultValue; + } + } else { + return defaultValue; + } + } else if (value instanceof Long) { + return (Long) value; + } else { + return defaultValue; + } + } + + public void set(String key, Object value) { + if (value instanceof Boolean) { + value = (Boolean) value ? "on" : "off"; + } + this.properties.set(key, value); + } + + public void remove(String key) { + this.properties.remove(key); + } + + public boolean exists(String key) { + return this.properties.exists(key); + } + + public void save() { + this.properties.save(); + } + + public void reload() { + this.properties.reload(); + } +} \ No newline at end of file diff --git a/src/main/java/org/sculk/config/ServerPropertiesKeys.java b/src/main/java/org/sculk/config/ServerPropertiesKeys.java new file mode 100644 index 0000000..234a391 --- /dev/null +++ b/src/main/java/org/sculk/config/ServerPropertiesKeys.java @@ -0,0 +1,32 @@ +package org.sculk.config; + +public enum ServerPropertiesKeys { + LANGUAGE("language"), + MOTD("motd"), + SUB_MOTD("sub-motd"), + SERVER_PORT("server-port"), + SERVER_IP("server-ip"), + WHITELIST("white-list"), + MAX_PLAYERS("max-players"), + GAMEMODE("gamemode"), + PVP("pvp"), + DIFFICULTY("difficulty"), + LEVEL_NAME("level-name"), + LEVEL_SEED("level-seed"), + LEVEL_TYPE("level-type"), + SPAWN_ANIMALS("spawn-animals"), + SPAWN_MONSTERS("spawn-monsters"), + AUTO_SAVE("auto-save"), + XBOX_AUTH("xbox-auth"); + + private final String key; + + ServerPropertiesKeys(String key) { + this.key = key; + } + + @Override + public String toString() { + return key; + } +} diff --git a/src/main/java/org/sculk/console/ConsoleThread.java b/src/main/java/org/sculk/console/ConsoleThread.java index 6b838ed..87b54d1 100644 --- a/src/main/java/org/sculk/console/ConsoleThread.java +++ b/src/main/java/org/sculk/console/ConsoleThread.java @@ -3,11 +3,11 @@ import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by diff --git a/src/main/java/org/sculk/console/TerminalConsole.java b/src/main/java/org/sculk/console/TerminalConsole.java index d87eafc..6cf730f 100644 --- a/src/main/java/org/sculk/console/TerminalConsole.java +++ b/src/main/java/org/sculk/console/TerminalConsole.java @@ -1,16 +1,19 @@ package org.sculk.console; +import lombok.Getter; import net.minecrell.terminalconsole.SimpleTerminalConsole; import org.jline.reader.LineReader; import org.jline.reader.LineReaderBuilder; import org.sculk.Server; +import org.sculk.command.CommandSender; +import org.sculk.player.text.RawTextBuilder; /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -20,8 +23,9 @@ * @author: SculkTeams * @link: http://www.sculkmp.org/ */ -public class TerminalConsole extends SimpleTerminalConsole { +public class TerminalConsole extends SimpleTerminalConsole implements CommandSender { + @Getter private final Server server; private final ConsoleThread consoleThread; @@ -35,9 +39,26 @@ protected boolean isRunning() { return this.server.isRunning(); } + + @Override + public String getName() { + return "Console"; + } + + @Override + public void sendMessage(String message) { + this.server.getLogger().info(message); + + } + + @Override + public void sendMessage(RawTextBuilder textBuilder) { + this.server.getLogger().info(textBuilder.toString()); + } + @Override protected void runCommand(String s) { - // TODO: Soon + this.getServer().dispatchCommand(this, s, true); } @Override diff --git a/src/main/java/org/sculk/entity/Attribute.java b/src/main/java/org/sculk/entity/Attribute.java new file mode 100644 index 0000000..adf9ef6 --- /dev/null +++ b/src/main/java/org/sculk/entity/Attribute.java @@ -0,0 +1,132 @@ +package org.sculk.entity; + + +import lombok.Getter; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class Attribute { + + public static final String MC_PREFIX = "minecraft:"; + public static final String ABSORPTION = MC_PREFIX + "absorption"; + public static final String SATURATION = MC_PREFIX + "player.saturation"; + public static final String EXHAUSTION = MC_PREFIX + "player.exhaustion"; + public static final String KNOCKBACK_RESISTANCE = MC_PREFIX + "knockback_resistance"; + public static final String HEALTH = MC_PREFIX + "health"; + public static final String MOVEMENT_SPEED = MC_PREFIX + "movement"; + public static final String FOLLOW_RANGE = MC_PREFIX + "follow_range"; + public static final String HUNGER = MC_PREFIX + "player.hunger"; + public static final String FOOD = HUNGER; + public static final String ATTACK_DAMAGE = MC_PREFIX + "attack_damage"; + public static final String EXPERIENCE_LEVEL = MC_PREFIX + "player.level"; + public static final String EXPERIENCE = MC_PREFIX + "player.experience"; + public static final String UNDERWATER_MOVEMENT = MC_PREFIX + "underwater_movement"; + public static final String LUCK = MC_PREFIX + "luck"; + public static final String FALL_DAMAGE = MC_PREFIX + "fall_damage"; + public static final String HORSE_JUMP_STRENGTH = MC_PREFIX + "horse.jump_strength"; + public static final String ZOMBIE_SPAWN_REINFORCEMENTS = MC_PREFIX + "zombie.spawn_reinforcements"; + public static final String LAVA_MOVEMENT = MC_PREFIX + "lava_movement"; + + @Getter + protected String id; + protected boolean shouldSend; + protected boolean desynchronized = true; + @Getter + protected float minValue; + @Getter + protected float maxValue; + @Getter + protected float defaultValue; + @Getter + protected float currentValue; + + public Attribute(String id, float minValue, float maxValue, float defaultValue, boolean shouldSend) { + if(minValue > maxValue || defaultValue > maxValue || defaultValue < minValue) { + throw new IllegalArgumentException("Invalid ranges: min value: " + minValue + ", max value: " + maxValue + ", " + defaultValue + ": " + defaultValue); + } + this.id = id; + this.shouldSend = true; + this.minValue = minValue; + this.maxValue = maxValue; + this.defaultValue = defaultValue; + this.currentValue = this.defaultValue; + } + + public Attribute(Attribute attribute) { + this.id = attribute.id; + this.shouldSend = attribute.shouldSend; + this.minValue = attribute.minValue; + this.maxValue = attribute.maxValue; + this.defaultValue = attribute.defaultValue; + this.currentValue = attribute.currentValue; + } + + public boolean isSyncable() { + return this.shouldSend; + } + + public boolean isDesynchronized() { + return this.shouldSend && this.desynchronized; + } + + public void markSynchronized(boolean synced) { + this.desynchronized = !synced; + } + + public Attribute setMaxValue(float maxValue) { + float min = this.getMinValue(); + if(maxValue < min) { + throw new IllegalArgumentException("Maximum " + maxValue + " is less than the minimum " + min); + } + if(this.maxValue != maxValue) { + this.desynchronized = true; + this.maxValue = maxValue; + } + return this; + } + + public void resetToDefault() { + setValue(getDefaultValue(), true, true); + } + + public Attribute setDefaultValue(float defaultValue) { + if (defaultValue > getMaxValue() || defaultValue < getMinValue()) { + throw new IllegalArgumentException("Default " + defaultValue + " is outside the range " + getMinValue() + " - " + getMaxValue()); + } + if (this.defaultValue != defaultValue) { + this.desynchronized = true; + this.defaultValue = defaultValue; + } + return this; + } + + public Attribute setValue(float value, boolean fit, boolean forceSend) { + if (value > getMaxValue() || value < getMinValue()) { + if (!fit) { + throw new IllegalArgumentException("Value " + value + " is outside the range " + getMinValue() + " - " + getMaxValue()); + } + value = Math.min(Math.max(value, getMinValue()), getMaxValue()); + } + if (this.currentValue != value) { + this.desynchronized = true; + this.currentValue = value; + } else if (forceSend) { + this.desynchronized = true; + } + return this; + } + +} diff --git a/src/main/java/org/sculk/entity/AttributeFactory.java b/src/main/java/org/sculk/entity/AttributeFactory.java new file mode 100644 index 0000000..da6359e --- /dev/null +++ b/src/main/java/org/sculk/entity/AttributeFactory.java @@ -0,0 +1,74 @@ +package org.sculk.entity; + + +import lombok.Getter; +import org.cloudburstmc.protocol.bedrock.data.AttributeData; + +import java.util.HashMap; +import java.util.Map; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public final class AttributeFactory { + + @Getter + private static final AttributeFactory INSTANCE = new AttributeFactory(); + private final Map attributeMap = new HashMap<>(); + + private AttributeFactory() { + register(Attribute.ABSORPTION, 0.00f, 340282346638528859811704183484516925440.00f, 0.00f); + register(Attribute.SATURATION, 0.00f, 20.00f, 20.00f); + register(Attribute.EXHAUSTION, 0.00f, 5.00f, 0.0f, false); + register(Attribute.KNOCKBACK_RESISTANCE, 0.00f, 1.00f, 0.00f); + register(Attribute.HEALTH, 0.00f, 20.00f, 20.00f); + register(Attribute.MOVEMENT_SPEED, 0.00f, 340282346638528859811704183484516925440.00f, 0.10f); + register(Attribute.FOLLOW_RANGE, 0.00f, 2048.00f, 16.00f, false); + register(Attribute.HUNGER, 0.00f, 20.00f, 20.00f); + register(Attribute.ATTACK_DAMAGE, 0.00f, 340282346638528859811704183484516925440.00f, 1.00f, false); + register(Attribute.EXPERIENCE_LEVEL, 0.00f, 24791.00f, 0.00f); + register(Attribute.EXPERIENCE, 0.00f, 1.00f, 0.00f); + register(Attribute.UNDERWATER_MOVEMENT, 0.0f, 340282346638528859811704183484516925440.00f, 0.02f); + register(Attribute.LUCK, -1024.0f, 1024.0f, 0.0f); + register(Attribute.FALL_DAMAGE, 0.0f, 340282346638528859811704183484516925440.00f, 1.0f); + register(Attribute.HORSE_JUMP_STRENGTH, 0.0f, 2.0f, 0.7f); + register(Attribute.ZOMBIE_SPAWN_REINFORCEMENTS, 0.0f, 1.0f, 0.0f); + register(Attribute.LAVA_MOVEMENT, 0.0f, 340282346638528859811704183484516925440.00f, 0.02f); + } + + public Attribute get(String id) { + Attribute attribute = attributeMap.get(id); + return attribute != null ? new Attribute(attribute) : null; + } + + public Attribute mustGet(String id) { + Attribute result = get(id); + if (result == null) { + throw new IllegalArgumentException("Attribute " + id + " is not registered"); + } + return result; + } + + public Attribute register(String id, float minValue, float maxValue, float defaultValue) { + return register(id, minValue, maxValue, defaultValue, true); + } + + public Attribute register(String id, float minValue, float maxValue, float defaultValue, boolean shouldSend) { + Attribute attribute = new Attribute(id, minValue, maxValue, defaultValue, shouldSend); + attributeMap.put(id, attribute); + return attribute; + } + +} diff --git a/src/main/java/org/sculk/entity/AttributeMap.java b/src/main/java/org/sculk/entity/AttributeMap.java new file mode 100644 index 0000000..1ff9866 --- /dev/null +++ b/src/main/java/org/sculk/entity/AttributeMap.java @@ -0,0 +1,45 @@ +package org.sculk.entity; + + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class AttributeMap { + + private final Map attributeMap = new HashMap<>(); + + public void add(Attribute attribute) { + attributeMap.put(attribute.getId(), attribute); + } + + public Attribute get(String id) { + return attributeMap.getOrDefault(id, null); + } + + public Map getAll() { + return attributeMap; + } + + public Map needSend() { + return attributeMap.entrySet().stream() + .filter(stringAttributeEntry -> stringAttributeEntry.getValue().isSyncable() && stringAttributeEntry.getValue().isDesynchronized()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + +} diff --git a/src/main/java/org/sculk/Player.java b/src/main/java/org/sculk/entity/Entity.java similarity index 52% rename from src/main/java/org/sculk/Player.java rename to src/main/java/org/sculk/entity/Entity.java index 6b26cbc..0fbfb10 100644 --- a/src/main/java/org/sculk/Player.java +++ b/src/main/java/org/sculk/entity/Entity.java @@ -1,11 +1,12 @@ -package org.sculk; +package org.sculk.entity; + /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -15,5 +16,10 @@ * @author: SculkTeams * @link: http://www.sculkmp.org/ */ -public class Player { +public abstract class Entity { + + public void initEntity() {} + + public void onUpdate() {} + } diff --git a/src/main/java/org/sculk/entity/HumanEntity.java b/src/main/java/org/sculk/entity/HumanEntity.java new file mode 100644 index 0000000..c0d7034 --- /dev/null +++ b/src/main/java/org/sculk/entity/HumanEntity.java @@ -0,0 +1,47 @@ +package org.sculk.entity; + + +import org.sculk.entity.manager.ExperienceManager; +import org.sculk.entity.manager.HungerManager; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class HumanEntity extends Living { + + protected HungerManager hungerManager; + protected ExperienceManager experienceManager; + + @Override + public void initEntity() { + super.initEntity(); + this.hungerManager = new HungerManager(this); + this.experienceManager = new ExperienceManager(this); + } + + @Override + public void onUpdate() { + super.onUpdate(); + } + + public HungerManager getHungerManager() { + return hungerManager; + } + + public ExperienceManager getExperienceManager() { + return experienceManager; + } + +} diff --git a/src/main/java/org/sculk/entity/Living.java b/src/main/java/org/sculk/entity/Living.java new file mode 100644 index 0000000..4328de5 --- /dev/null +++ b/src/main/java/org/sculk/entity/Living.java @@ -0,0 +1,32 @@ +package org.sculk.entity; + + +import org.sculk.entity.manager.HungerManager; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public abstract class Living extends Entity { + + @Override + public void initEntity() { + super.initEntity(); + } + + @Override + public void onUpdate() { + super.onUpdate(); + } +} diff --git a/src/main/java/org/sculk/entity/data/SyncedEntityData.java b/src/main/java/org/sculk/entity/data/SyncedEntityData.java new file mode 100644 index 0000000..e15e02a --- /dev/null +++ b/src/main/java/org/sculk/entity/data/SyncedEntityData.java @@ -0,0 +1,58 @@ +package org.sculk.entity.data; + + +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataMap; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket; +import org.sculk.player.Player; + +import java.util.EnumSet; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class SyncedEntityData { + + private final EnumSet flags = EnumSet.noneOf(EntityFlag.class); + private final EntityDataMap entityDataMap = new EntityDataMap(); + + private final Player player; + + public SyncedEntityData(Player player) { + this.player = player; + } + + public void updateFlag() { + SetEntityDataPacket setEntityDataPacket = new SetEntityDataPacket(); + setEntityDataPacket.getMetadata().putFlags(this.flags); + this.player.sendDataPacket(setEntityDataPacket); + } + + public boolean getFlag(EntityFlag entityFlag) { + return flags.contains(entityFlag); + } + + public void setFlags(EntityFlag flags, boolean value) { + if(this.flags.contains(flags) != value) { + if(value) { + this.flags.add(flags); + } else { + this.flags.remove(flags); + } + this.entityDataMap.putFlags(this.flags); + } + } + +} diff --git a/src/main/java/org/sculk/entity/manager/ExperienceManager.java b/src/main/java/org/sculk/entity/manager/ExperienceManager.java new file mode 100644 index 0000000..adf2277 --- /dev/null +++ b/src/main/java/org/sculk/entity/manager/ExperienceManager.java @@ -0,0 +1,175 @@ +package org.sculk.entity.manager; + + +import org.sculk.entity.Attribute; +import org.sculk.entity.AttributeFactory; +import org.sculk.entity.Entity; +import org.sculk.entity.HumanEntity; +import org.sculk.event.player.PlayerExperienceChangeEvent; +import org.sculk.utils.ExperienceUtils; + +import java.util.Map; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class ExperienceManager { + + private Attribute levelAttribute; + private Attribute progressAttribute; + + private int totalXp = 0; + private boolean canAttractXpOrbs = true; + private int xpCooldown = 0; + + private HumanEntity humanEntity; + + public ExperienceManager(HumanEntity humanEntity) { + this.humanEntity = humanEntity; + this.levelAttribute = fetchAttribute(humanEntity, Attribute.EXPERIENCE_LEVEL); + this.progressAttribute = fetchAttribute(humanEntity, Attribute.EXPERIENCE); + } + + private static Attribute fetchAttribute(Entity entity, String attributeId) { + Attribute attribute = AttributeFactory.getINSTANCE().mustGet(attributeId); + // TODO next step add attribute to entity + return attribute; + } + + public boolean setXpAndProgress(Integer level, Float progress) { + PlayerExperienceChangeEvent playerExperienceChangeEvent = new PlayerExperienceChangeEvent(this.humanEntity, getXpLevel(), getXpProgress(), level, progress); + playerExperienceChangeEvent.call(); + + if(playerExperienceChangeEvent.isCancelled()) { + return false; + } + level = playerExperienceChangeEvent.getNewLevel(); + progress = playerExperienceChangeEvent.getNewProgress(); + + if(level != null) { + this.levelAttribute.setValue(level, true, true); + } + if(progress != null) { + this.progressAttribute.setValue(progress, true, true); + } + return true; + } + + public int getXpLevel() { + return (int) this.levelAttribute.getCurrentValue(); + } + + public boolean setXpLevel(int level) { + return this.setXpAndProgress(level, null); + } + + public boolean addXpLevels(int amount) { + int oldLevel = this.getXpLevel(); + return this.setXpLevel(oldLevel + amount); + } + + public boolean substractXpLevels(int amount) { + return this.addXpLevels(-amount); + } + + public float getXpProgress() { + return this.progressAttribute.getCurrentValue(); + } + + public boolean setXpProgress(float progress) { + return this.setXpAndProgress(null, progress); + } + + public int getRemainderXp() { + return (int) (ExperienceUtils.getXpToCompleteLevel(this.getXpLevel()) * this.getXpProgress()); + } + + public int getCurrentTotalXp() { + return ExperienceUtils.getXpToReachLevel(this.getXpLevel()) + this.getRemainderXp(); + } + + public boolean setCurrentTotalXp(int amount) { + float newLevel = ExperienceUtils.getLevelFromXp(amount); + int xpLevel = (int) (newLevel - (int) newLevel); + float xpProgress = (int) (newLevel - (int) newLevel); + return setXpAndProgress(xpLevel, xpProgress); + } + + public boolean addXp(int amount) { + amount = Math.min(amount, Integer.MAX_VALUE - this.totalXp); + int oldLevel = this.getXpLevel(); + int oldTotal = this.getCurrentTotalXp(); + if(this.setCurrentTotalXp(oldTotal + amount)) { + if(amount > 0) { + this.totalXp += amount; + } + return true; + } + return false; + } + + public boolean subtractXp(int amount) { + return this.addXp(-amount); + } + + public void setXpAndProgressNoEvent(int level, float progress) { + this.levelAttribute.setValue(level, true, true); + this.progressAttribute.setValue(progress, true, true); + } + + public int getLifetimeTotalXp() { + return this.totalXp; + } + + public void setLifetimeTotalXp(int amount) { + if(amount < 0 || amount > Integer.MAX_VALUE) { + throw new IllegalArgumentException("XP must be greater than 0 and less than " + Integer.MAX_VALUE); + } + this.totalXp = amount; + } + + public boolean canPickupXp() { + return this.xpCooldown == 0; + } + + public void onPickupXp(int xpValue) { + int mainHandIndex = -1; + int offHandIndex = -2; + + // TODO: Logic for repair item + + this.addXp(xpValue); + this.resetXpCooldown(); + } + + public void resetXpCooldown() { + this.xpCooldown = 2; + } + + public void tick(int tickDiff) { + if(this.xpCooldown > 0) { + this.xpCooldown = Math.max(0, this.xpCooldown - tickDiff); + } + } + + public boolean canAttractXpOrbs() { + return this.canAttractXpOrbs; + } + + public void setCanAttractXpOrbs(boolean canAttractXpOrbs) { + this.canAttractXpOrbs = canAttractXpOrbs; + } + +} diff --git a/src/main/java/org/sculk/entity/manager/HungerManager.java b/src/main/java/org/sculk/entity/manager/HungerManager.java new file mode 100644 index 0000000..4958439 --- /dev/null +++ b/src/main/java/org/sculk/entity/manager/HungerManager.java @@ -0,0 +1,163 @@ +package org.sculk.entity.manager; + + +import lombok.Getter; +import org.sculk.entity.Attribute; +import org.sculk.entity.AttributeFactory; +import org.sculk.entity.Entity; +import org.sculk.entity.HumanEntity; +import org.sculk.event.player.PlayerExhaustEvent; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class HungerManager { + + private Attribute hungerAttribute; + private Attribute saturationAttribute; + private Attribute exhaustionAttrbute; + + @Getter + private int foodTickTimer = 0; + @Getter + private boolean enabled = true; + private HumanEntity humanEntity; + + public HungerManager(HumanEntity humanEntity) { + this.humanEntity = humanEntity; + this.hungerAttribute = fetchAttribute(humanEntity, Attribute.HUNGER); + this.saturationAttribute = fetchAttribute(humanEntity, Attribute.SATURATION); + this.exhaustionAttrbute = fetchAttribute(humanEntity, Attribute.EXHAUSTION); + } + + private static Attribute fetchAttribute(Entity entity, String attributeId) { + Attribute attribute = AttributeFactory.getINSTANCE().mustGet(attributeId); + // TODO next step add attribute to entity + return attribute; + } + + public float getFood() { + return this.hungerAttribute.getCurrentValue(); + } + + public void setFood(float newFood) { + float oldFood = this.hungerAttribute.getCurrentValue(); + this.hungerAttribute.setValue(newFood, true, true); + // Ranges 18-20 (regen), 7-17 (none) 1-6 (no sprint), 0 (health depletion) + for(int bound : new int[]{17, 6, 0}) { + if((oldFood > bound) != (newFood > bound)) { + foodTickTimer = 0; + break; + } + } + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public float getMaxFood() { + return this.hungerAttribute.getMaxValue(); + } + + public void addFood(float amount) { + float newAmount = Math.max(Math.min(amount + this.hungerAttribute.getCurrentValue(), this.hungerAttribute.getMaxValue()), this.hungerAttribute.getMinValue()); + setFood(newAmount); + } + + public boolean isHungry() { + return this.getFood() < this.getMaxFood(); + } + + public float getSaturation() { + return this.saturationAttribute.getCurrentValue(); + } + + public void setSaturation(float saturation) { + this.saturationAttribute.setValue(saturation, true, true); + } + + public void addSaturation(float amount) { + this.saturationAttribute.setValue(this.saturationAttribute.getCurrentValue() + amount, true, true); + } + + public float getExhaustion() { + return this.exhaustionAttrbute.getCurrentValue(); + } + + public void setExhaustion(float exhaustion) { + this.exhaustionAttrbute.setValue(exhaustion, true, true); + } + + public float exhaust(float amount, int cause) { + if(!this.enabled) { + return 0; + } + float eventAmount = amount; + PlayerExhaustEvent playerExhaustEvent = new PlayerExhaustEvent(this.humanEntity, amount, cause); + playerExhaustEvent.call(); + if(playerExhaustEvent.isCancelled()) { + return 0.0f; + } + eventAmount = playerExhaustEvent.getAmount(); + + float exhaustion = this.getExhaustion() + eventAmount; + while(exhaustion >= 4.0f) { + exhaustion -= 4.0f; + float saturation = this.getSaturation(); + if(saturation > 0) { + saturation = Math.max(0, saturation - 1.0f); + this.setSaturation(saturation); + } else { + float food = this.getFood(); + if(food > 0) { + food--; + this.setFood(Math.max(food, 0)); + } + } + } + this.setExhaustion(exhaustion); + return eventAmount; + } + + public void setFoodTickTimer(int foodTickTimer) { + if(foodTickTimer < 0) { + throw new IllegalArgumentException("Expected a non-negative value"); + } + this.foodTickTimer = foodTickTimer; + } + + // TODO To be put in the Player::onUpdate() + public void tick(int tickDiff) { + float food = this.getFood(); + + foodTickTimer += tickDiff; + if(foodTickTimer >= 80) { + foodTickTimer = 0; + } + if(foodTickTimer == 0) { + if(food >= 18) { + // TODO Next step: heal entity + exhaust(6.0f, PlayerExhaustEvent.CAUSE_HEALTH_REGEN); + } else if(food <= 0) { + // TODO soon + } + } + if(food <= 6) { + // TODO Disable sprinting + } + } + +} diff --git a/src/main/java/org/sculk/event/Cancellable.java b/src/main/java/org/sculk/event/Cancellable.java new file mode 100644 index 0000000..3fc1b21 --- /dev/null +++ b/src/main/java/org/sculk/event/Cancellable.java @@ -0,0 +1,25 @@ +package org.sculk.event; + + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface Cancellable { + + boolean isCancelled(); + void setCancelled(); + void setCancelled(boolean cancelled); + +} diff --git a/src/main/java/org/sculk/event/EntityEvent.java b/src/main/java/org/sculk/event/EntityEvent.java new file mode 100644 index 0000000..3bb876b --- /dev/null +++ b/src/main/java/org/sculk/event/EntityEvent.java @@ -0,0 +1,28 @@ +package org.sculk.event; + + +import org.sculk.entity.Entity; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public abstract class EntityEvent extends Event { + + protected Entity entity; + + public Entity getEntity() { + return entity; + } +} diff --git a/src/main/java/org/sculk/event/Event.java b/src/main/java/org/sculk/event/Event.java new file mode 100644 index 0000000..0ffdcaf --- /dev/null +++ b/src/main/java/org/sculk/event/Event.java @@ -0,0 +1,48 @@ +package org.sculk.event; + + +import org.sculk.Server; +import org.sculk.exception.EventException; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public abstract class Event { + + private boolean isCancelled = false; + + public boolean isCancelled() { + if(!(this instanceof Cancellable)) { + throw new EventException("Event is not Cancellable"); + } + return isCancelled; + } + + public void setCancelled() { + setCancelled(true); + } + + public void setCancelled(boolean cancelled) { + if (!(this instanceof Cancellable)) { + throw new EventException("Event is not Cancellable"); + } + isCancelled = cancelled; + } + + public final void call() { + Server.getInstance().getEventManager().call(this); + } + +} diff --git a/src/main/java/org/sculk/event/EventCallHandler.java b/src/main/java/org/sculk/event/EventCallHandler.java new file mode 100644 index 0000000..2e80cb1 --- /dev/null +++ b/src/main/java/org/sculk/event/EventCallHandler.java @@ -0,0 +1,33 @@ +package org.sculk.event; + + +import java.lang.reflect.Method; +import java.util.List; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface EventCallHandler { + + interface ListenerMethod extends Comparable { + Object getListener(); + Method getMethods(); + void run(Event event) throws Exception; + } + + void call(Event event); + List getMethods(); + +} diff --git a/src/main/java/org/sculk/event/EventManager.java b/src/main/java/org/sculk/event/EventManager.java new file mode 100644 index 0000000..07fa8bf --- /dev/null +++ b/src/main/java/org/sculk/event/EventManager.java @@ -0,0 +1,37 @@ +package org.sculk.event; + + +import java.util.Collections; +import java.util.Map; + +import static org.cloudburstmc.protocol.common.util.Preconditions.checkNotNull; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class EventManager implements EventManagerInterface { + + private volatile Map, EventCallHandler> eventHandlers = Collections.emptyMap(); + + @Override + public void call(Event event) { + checkNotNull(event, "event"); + EventCallHandler handler = eventHandlers.get(event.getClass()); + if(handler != null) { + handler.call(event); + } + } + +} diff --git a/src/main/java/org/sculk/event/EventManagerInterface.java b/src/main/java/org/sculk/event/EventManagerInterface.java new file mode 100644 index 0000000..2fbf56e --- /dev/null +++ b/src/main/java/org/sculk/event/EventManagerInterface.java @@ -0,0 +1,23 @@ +package org.sculk.event; + + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface EventManagerInterface { + + void call(Event event); + +} diff --git a/src/main/java/org/sculk/event/EventPriority.java b/src/main/java/org/sculk/event/EventPriority.java new file mode 100644 index 0000000..48b21ea --- /dev/null +++ b/src/main/java/org/sculk/event/EventPriority.java @@ -0,0 +1,28 @@ +package org.sculk.event; + + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public enum EventPriority { + + LOWEST, + LOW, + NORMAL, + HIGH, + HIGHEST, + MONITOR + +} diff --git a/src/main/java/org/sculk/event/Listener.java b/src/main/java/org/sculk/event/Listener.java new file mode 100644 index 0000000..bcb977d --- /dev/null +++ b/src/main/java/org/sculk/event/Listener.java @@ -0,0 +1,30 @@ +package org.sculk.event; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Listener { + + boolean ignoreCancelled() default false; + +} diff --git a/src/main/java/org/sculk/event/ServerEvent.java b/src/main/java/org/sculk/event/ServerEvent.java new file mode 100644 index 0000000..f58326c --- /dev/null +++ b/src/main/java/org/sculk/event/ServerEvent.java @@ -0,0 +1,19 @@ +package org.sculk.event; + + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public abstract class ServerEvent extends Event {} diff --git a/src/main/java/org/sculk/event/command/CommandEvent.java b/src/main/java/org/sculk/event/command/CommandEvent.java new file mode 100644 index 0000000..37fd8a2 --- /dev/null +++ b/src/main/java/org/sculk/event/command/CommandEvent.java @@ -0,0 +1,39 @@ +package org.sculk.event.command; + + +import lombok.Getter; +import lombok.Setter; +import org.sculk.command.CommandSender; +import org.sculk.event.Cancellable; +import org.sculk.event.ServerEvent; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class CommandEvent extends ServerEvent implements Cancellable { + + @Getter + @Setter + protected CommandSender sender; + @Getter + @Setter + protected String command; + + public CommandEvent(CommandSender sender, String command) { + this.sender = sender; + this.command = command; + } + +} diff --git a/src/main/java/org/sculk/event/player/PlayerAsyncPreLoginEvent.java b/src/main/java/org/sculk/event/player/PlayerAsyncPreLoginEvent.java new file mode 100644 index 0000000..502695f --- /dev/null +++ b/src/main/java/org/sculk/event/player/PlayerAsyncPreLoginEvent.java @@ -0,0 +1,87 @@ +package org.sculk.event.player; + + +import org.sculk.player.Player; +import org.sculk.network.session.SculkServerSession; +import org.sculk.player.client.LoginChainData; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public final class PlayerAsyncPreLoginEvent extends PlayerEvent { + + private final LoginChainData loginChainData; + + public enum LoginResult { + SUCCESS, + KICK + } + private LoginResult loginResult = LoginResult.SUCCESS; + private String kickMessage = "Sculk server"; + + private final List> scheduledActions = new ArrayList<>(); + + public PlayerAsyncPreLoginEvent(LoginChainData loginChainData) { + super(null); + this.loginChainData = loginChainData; + } + + public LoginChainData getLoginChainData() { + return loginChainData; + } + + public LoginResult getLoginResult() { + return loginResult; + } + + public void setLoginResult(LoginResult loginResult) { + this.loginResult = loginResult; + } + + public String getKickMessage() { + return kickMessage; + } + + public void setKickMessage(String kickMessage) { + this.kickMessage = kickMessage; + } + + public List> getScheduledActions() { + return new ArrayList<>(scheduledActions); + } + + @Override + public Player getPlayer() { + throw new UnsupportedOperationException("No player instance provided in async event"); + } + + public void disAllow(String message) { + this.loginResult = LoginResult.KICK; + this.kickMessage = message; + } + + public void allow() { + this.loginResult = LoginResult.SUCCESS; + } + + public void scheduleSyncAction(Consumer action) { + this.scheduledActions.add(action); + } + +} diff --git a/src/main/java/org/sculk/event/player/PlayerChatEvent.java b/src/main/java/org/sculk/event/player/PlayerChatEvent.java new file mode 100644 index 0000000..4d54147 --- /dev/null +++ b/src/main/java/org/sculk/event/player/PlayerChatEvent.java @@ -0,0 +1,49 @@ +package org.sculk.event.player; + + +import org.sculk.player.Player; +import org.sculk.event.Cancellable; +import org.sculk.player.chat.ChatFormatter; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class PlayerChatEvent extends PlayerEvent implements Cancellable { + + protected String message; + protected ChatFormatter chatFormatter; + + public PlayerChatEvent(Player player, String message, ChatFormatter chatFormatter) { + super(player); + this.chatFormatter = chatFormatter; + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public ChatFormatter getChatFormatter() { + return chatFormatter; + } + + public void setChatFormatter(ChatFormatter chatFormatter) { + this.chatFormatter = chatFormatter; + } +} diff --git a/src/main/java/org/sculk/event/player/PlayerCreationEvent.java b/src/main/java/org/sculk/event/player/PlayerCreationEvent.java new file mode 100644 index 0000000..bfca1fc --- /dev/null +++ b/src/main/java/org/sculk/event/player/PlayerCreationEvent.java @@ -0,0 +1,42 @@ +package org.sculk.event.player; + + +import lombok.Getter; +import lombok.Setter; +import org.sculk.player.Player; +import org.sculk.event.Event; +import org.sculk.network.session.SculkServerSession; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class PlayerCreationEvent extends Event { + + @Getter + private final SculkServerSession session; + @Getter + @Setter + private Class baseClass; + @Getter + @Setter + private Class playerClass; + + public PlayerCreationEvent(SculkServerSession session) { + this.session = session; + this.baseClass = Player.class; + this.playerClass = Player.class; + } + +} diff --git a/src/main/java/org/sculk/event/player/PlayerEvent.java b/src/main/java/org/sculk/event/player/PlayerEvent.java new file mode 100644 index 0000000..0258a6e --- /dev/null +++ b/src/main/java/org/sculk/event/player/PlayerEvent.java @@ -0,0 +1,34 @@ +package org.sculk.event.player; + + +import org.sculk.player.Player; +import org.sculk.event.Event; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public abstract class PlayerEvent extends Event { + + private final Player player; + + public PlayerEvent(Player player) { + this.player = player; + } + + public Player getPlayer() { + return player; + } + +} diff --git a/src/main/java/org/sculk/event/player/PlayerExhaustEvent.java b/src/main/java/org/sculk/event/player/PlayerExhaustEvent.java new file mode 100644 index 0000000..7bcd7e0 --- /dev/null +++ b/src/main/java/org/sculk/event/player/PlayerExhaustEvent.java @@ -0,0 +1,69 @@ +package org.sculk.event.player; + + +import lombok.Getter; +import org.sculk.entity.Entity; +import org.sculk.entity.HumanEntity; +import org.sculk.event.Cancellable; +import org.sculk.event.EntityEvent; +import org.sculk.event.Event; +import org.sculk.player.client.ClientChainData; + +import java.net.SocketAddress; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class PlayerExhaustEvent extends EntityEvent implements Cancellable { + + protected HumanEntity humanEntity; + protected float amount; + protected int cause; + + public static final int CAUSE_ATTACK = 1; + public static final int CAUSE_DAMAGE = 2; + public static final int CAUSE_MINING = 3; + public static final int CAUSE_HEALTH_REGEN = 4; + public static final int CAUSE_POTION = 5; + public static final int CAUSE_WALKING = 6; + public static final int CAUSE_SPRINTING = 7; + public static final int CAUSE_SWIMMING = 8; + public static final int CAUSE_JUMPING = 9; + public static final int CAUSE_SPRINT_JUMPING = 10; + public static final int CAUSE_CUSTOM = 11; + + public PlayerExhaustEvent(HumanEntity humanEntity, float amount, int cause) { + this.humanEntity = humanEntity; + this.amount = amount; + this.cause = cause; + } + + public Entity getPlayer() { + return this.humanEntity; + } + + public float getAmount() { + return amount; + } + + public void setAmount(float amount) { + this.amount = amount; + } + + public int getCause() { + return cause; + } + +} diff --git a/src/main/java/org/sculk/event/player/PlayerExperienceChangeEvent.java b/src/main/java/org/sculk/event/player/PlayerExperienceChangeEvent.java new file mode 100644 index 0000000..da9ffcf --- /dev/null +++ b/src/main/java/org/sculk/event/player/PlayerExperienceChangeEvent.java @@ -0,0 +1,65 @@ +package org.sculk.event.player; + + +import org.sculk.entity.HumanEntity; +import org.sculk.event.Cancellable; +import org.sculk.event.EntityEvent; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class PlayerExperienceChangeEvent extends EntityEvent implements Cancellable { + + protected HumanEntity humanEntity; + protected int oldLevel; + protected float oldProgress; + protected int newLevel; + protected float newProgress; + + public PlayerExperienceChangeEvent(HumanEntity humanEntity, int oldLevel, float oldProgress, int newLevel, float newProgress) { + this.humanEntity = humanEntity; + this.oldLevel = oldLevel; + this.oldProgress = oldProgress; + this.newLevel = newLevel; + this.newProgress = newProgress; + } + + public int getOldLevel() { + return oldLevel; + } + + public float getOldProgress() { + return oldProgress; + } + + public int getNewLevel() { + return newLevel; + } + + public float getNewProgress() { + return newProgress; + } + + public void setNewLevel(int newLevel) { + this.newLevel = newLevel; + } + + public void setNewProgress(float newProgress) { + if(newProgress < 0.0 || newProgress > 1.0) { + throw new IllegalArgumentException("XP progress must be range 0-1"); + } + this.newProgress = newProgress; + } +} diff --git a/src/main/java/org/sculk/event/player/PlayerFormRespondedEvent.java b/src/main/java/org/sculk/event/player/PlayerFormRespondedEvent.java new file mode 100644 index 0000000..13ac99d --- /dev/null +++ b/src/main/java/org/sculk/event/player/PlayerFormRespondedEvent.java @@ -0,0 +1,37 @@ +package org.sculk.event.player; + +import lombok.Getter; +import org.sculk.player.Player; +import org.sculk.form.Form; +import org.sculk.form.response.Response; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +public class PlayerFormRespondedEvent extends PlayerEvent { + protected final int formId; + protected final Form form; + protected final Response response; + + public PlayerFormRespondedEvent(Player player, int formId, Form form, Response response) { + super(player); + + this.formId = formId; + this.form = form; + this.response = response; + } +} \ No newline at end of file diff --git a/src/main/java/org/sculk/event/player/PlayerPreLoginEvent.java b/src/main/java/org/sculk/event/player/PlayerPreLoginEvent.java new file mode 100644 index 0000000..2652b6a --- /dev/null +++ b/src/main/java/org/sculk/event/player/PlayerPreLoginEvent.java @@ -0,0 +1,49 @@ +package org.sculk.event.player; + + +import lombok.Getter; +import org.sculk.event.Cancellable; +import org.sculk.event.Event; +import org.sculk.player.PlayerLoginData; +import org.sculk.player.client.ClientChainData; + +import java.net.SocketAddress; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class PlayerPreLoginEvent extends Event implements Cancellable { + + @Getter + protected ClientChainData loginData; + @Getter + protected SocketAddress address; + protected String kickMessage; + + public PlayerPreLoginEvent(ClientChainData loginData, SocketAddress address, String kickMessage) { + this.loginData = loginData; + this.address = address; + this.kickMessage = kickMessage; + } + + public void setKickMessage(String kickMessage) { + this.kickMessage = kickMessage; + } + + public String getKickMessage() { + return kickMessage; + } + +} diff --git a/src/main/java/org/sculk/exception/CommandException.java b/src/main/java/org/sculk/exception/CommandException.java new file mode 100644 index 0000000..70abadc --- /dev/null +++ b/src/main/java/org/sculk/exception/CommandException.java @@ -0,0 +1,19 @@ +package org.sculk.exception; + + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class CommandException extends RuntimeException { } diff --git a/src/main/java/org/sculk/exception/EventException.java b/src/main/java/org/sculk/exception/EventException.java new file mode 100644 index 0000000..a9899fc --- /dev/null +++ b/src/main/java/org/sculk/exception/EventException.java @@ -0,0 +1,46 @@ +package org.sculk.exception; + + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class EventException extends RuntimeException { + + private final Throwable cause; + + public EventException(Throwable throwable) { + cause = throwable; + } + + public EventException() { + cause = null; + } + + public EventException(Throwable cause, String message) { + super(message); + this.cause = cause; + } + + public EventException(String message) { + super(message); + cause = null; + } + + @Override + public Throwable getCause() { + return cause; + } + +} diff --git a/src/main/java/org/sculk/exception/TaskException.java b/src/main/java/org/sculk/exception/TaskException.java new file mode 100644 index 0000000..00bac63 --- /dev/null +++ b/src/main/java/org/sculk/exception/TaskException.java @@ -0,0 +1,46 @@ +package org.sculk.exception; + + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class TaskException extends RuntimeException { + + private final Throwable cause; + + public TaskException(Throwable throwable) { + cause = throwable; + } + + public TaskException() { + cause = null; + } + + public TaskException(Throwable cause, String message) { + super(message); + this.cause = cause; + } + + public TaskException(String message) { + super(message); + cause = null; + } + + @Override + public Throwable getCause() { + return cause; + } + +} diff --git a/src/main/java/org/sculk/form/Form.java b/src/main/java/org/sculk/form/Form.java new file mode 100644 index 0000000..ea32b47 --- /dev/null +++ b/src/main/java/org/sculk/form/Form.java @@ -0,0 +1,53 @@ +package org.sculk.form; + +import org.cloudburstmc.protocol.bedrock.packet.ModalFormResponsePacket; +import org.cloudburstmc.protocol.common.PacketSignal; +import org.sculk.player.Player; +import org.sculk.Server; +import org.sculk.event.player.PlayerFormRespondedEvent; +import org.sculk.form.response.Response; +import org.sculk.utils.json.Serializable; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public abstract class Form implements Serializable { + public abstract Response processResponse(Player player, ModalFormResponsePacket packet); + + /** + * + * Method used within a {@link org.cloudburstmc.protocol.bedrock.packet.BedrockPacketHandler} to handle a client's response + * to a form sent by the server. + * + * @param player The player which sent the packet + * @param packet The packet + * @return {@link PacketSignal} to HANDLED to ensure a one-line implementation + */ + public static PacketSignal handleIncomingPacket(Player player, ModalFormResponsePacket packet) { + int formId = packet.getFormId(); + Form form = player.getForm(formId); + + if (form == null) { + return PacketSignal.HANDLED; + } + + Response response = form.processResponse(player, packet); + + PlayerFormRespondedEvent event = new PlayerFormRespondedEvent(player, formId, form, response); + Server.getInstance().getEventManager().call(event); + + return PacketSignal.HANDLED; + } +} diff --git a/src/main/java/org/sculk/form/defaults/CustomForm.java b/src/main/java/org/sculk/form/defaults/CustomForm.java new file mode 100644 index 0000000..85a80fb --- /dev/null +++ b/src/main/java/org/sculk/form/defaults/CustomForm.java @@ -0,0 +1,141 @@ +package org.sculk.form.defaults; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import lombok.*; +import lombok.experimental.Accessors; +import org.cloudburstmc.protocol.bedrock.packet.ModalFormResponsePacket; +import org.sculk.player.Player; +import org.sculk.form.Form; +import org.sculk.form.element.*; +import org.sculk.form.response.CustomResponse; +import org.sculk.form.response.ElementResponse; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@Setter +@Accessors(chain = true) +@RequiredArgsConstructor +public class CustomForm extends Form { + private static Type LIST_STRING_TYPE = new TypeToken>(){}.getType(); + + @NonNull protected String title; + @NonNull protected ObjectArrayList elements; + + protected Consumer closed = player -> {}; + protected BiConsumer submitted = (player, response) -> {}; + + public CustomForm() { + this(""); + } + + public CustomForm(String title) { + this(title, new ObjectArrayList<>()); + } + + public CustomForm addElement(Element element) { + this.elements.add(element); + return this; + } + + /** + * + * Forms need an identifier so that the Minecraft client knows what type of form to open. ('type' => 'custom_form') + * The json data of a custom form contains a title and content (array of elements). + * + * @return A json object containing data used by the Minecraft client to construct a custom form + */ + @Override + public JsonObject toJson() { + JsonObject object = new JsonObject(); + object.addProperty("type", "custom_form"); // DO NOT CHANGE: Required to find out which form should be created client-side + object.addProperty("title", this.getTitle()); + + JsonArray elementArray = new JsonArray(); + this.getElements().forEach(element -> elementArray.add(element.toJson())); + + object.add("content", elementArray); + return object; + } + + /** + * + * The client sends us an array of responses, which are in the same order as the elements within the form + * The value will be 'null' if the player closes the form + * We retrieve the corresponding element from our array and set the responses into a new {@link CustomResponse}. + * + * @param packet The packet sent to the server by the client + * @return A response object + */ + @Override + public CustomResponse processResponse(Player player, ModalFormResponsePacket packet) { + CustomResponse response = new CustomResponse(); + + String data = packet.getFormData().trim(); + if (data.equals("null")) { + this.closed.accept(player); + return response.setClosed(true); + } + + List parsedResponse = GsonHolder.GSON.fromJson(data, LIST_STRING_TYPE); + + for (int i = 0, responseSize = parsedResponse.size(); i < responseSize; i++) { + if (i >= this.elements.size()) { + break; + } + + String responseData = parsedResponse.get(i); + Element element = this.elements.get(i); + + switch (element) { + case ElementDropdown dropdown -> { + int index = Integer.parseInt(responseData); + String option = dropdown.getOptions().get(index); + response.setDropdownResponse(i, new ElementResponse(index, option)); + } + case ElementInput input -> response.setInputResponse(i, responseData); + case ElementLabel label -> response.setLabelResponse(i, label.getText()); + case ElementSlider slider -> { + float answer = Float.parseFloat(responseData); + response.setSliderResponse(i, answer); + } + case ElementStepSlider stepSlider -> { + int index = Integer.parseInt(responseData); + String step = stepSlider.getSteps().get(index); + response.setStepSliderResponse(i, new ElementResponse(index, step)); + } + case ElementToggle toggle -> { + boolean value = Boolean.parseBoolean(responseData); + response.setToggleResponse(i, value); + } + + default -> {} + } + } + + this.submitted.accept(player, response); + return response; + } +} diff --git a/src/main/java/org/sculk/form/defaults/ModalForm.java b/src/main/java/org/sculk/form/defaults/ModalForm.java new file mode 100644 index 0000000..c723ef8 --- /dev/null +++ b/src/main/java/org/sculk/form/defaults/ModalForm.java @@ -0,0 +1,104 @@ +package org.sculk.form.defaults; + +import com.google.gson.JsonObject; +import lombok.*; +import lombok.experimental.Accessors; +import org.cloudburstmc.protocol.bedrock.packet.ModalFormResponsePacket; +import org.sculk.player.Player; +import org.sculk.form.Form; +import org.sculk.form.response.ModalResponse; + +import java.util.function.Consumer; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@Setter +@Accessors(chain = true) +@RequiredArgsConstructor +public class ModalForm extends Form { + @NonNull protected String title; + @NonNull protected String content; + + @NonNull protected String yes; + @NonNull protected String no; + + protected Consumer onYes = player -> {}; + protected Consumer onNo = player -> {}; + + public ModalForm() { + this(""); + } + + public ModalForm(String title) { + this(title, ""); + } + + public ModalForm(String title, String content) { + this(title, content, "", ""); + } + + public ModalForm setText(String yes, String no) { + return this.setYes(yes).setNo(no); + } + + /** + * + * Forms need an identifier so that the Minecraft client knows what type of form to open. ('type' => 'modal') + * The json data of a modal form contains a title, content and two button texts. + * + * @return A json object containing data used by the Minecraft client to construct a modal form + */ + @Override + public JsonObject toJson() { + JsonObject object = new JsonObject(); + object.addProperty("type", "modal"); // DO NOT CHANGE: Required to find out which form should be created client-side + object.addProperty("title", this.title); + object.addProperty("content", this.content); + object.addProperty("button1", this.yes); + object.addProperty("button2", this.no); + return object; + } + + /** + * + * The client sends us a boolean value that we can use to determine whether the player clicked 'yes' or 'no'. + * The value will be 'null' if the player closes the form. + * + * @param packet The packet sent to the server by the client + * @return A response object + */ + @Override + public ModalResponse processResponse(Player player, ModalFormResponsePacket packet) { + ModalResponse response = new ModalResponse(); + + String data = packet.getFormData().trim(); + if (data.equals("null")) { + return response.setClosed(true); + } + + boolean clickedYes = data.equals("true"); + if (clickedYes) { + this.onYes.accept(player); + response.setButtonId(0).setText(this.yes); + } else { + this.onNo.accept(player); + response.setButtonId(1).setText(this.no); + } + return response; + } +} diff --git a/src/main/java/org/sculk/form/defaults/SimpleForm.java b/src/main/java/org/sculk/form/defaults/SimpleForm.java new file mode 100644 index 0000000..7687f4d --- /dev/null +++ b/src/main/java/org/sculk/form/defaults/SimpleForm.java @@ -0,0 +1,139 @@ +package org.sculk.form.defaults; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import lombok.*; +import lombok.experimental.Accessors; +import org.cloudburstmc.protocol.bedrock.packet.ModalFormResponsePacket; +import org.cloudburstmc.protocol.common.util.Preconditions; +import org.sculk.player.Player; +import org.sculk.form.Form; +import org.sculk.form.element.button.ElementButton; +import org.sculk.form.element.button.Image; +import org.sculk.form.response.SimpleResponse; + +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@Setter +@Accessors(chain = true) +@RequiredArgsConstructor +public class SimpleForm extends Form { + private static final ElementButton[] EMPTY_ARRAY = new ElementButton[0]; + + @NonNull protected String title; + @NonNull protected String content; + @NonNull protected Object2ObjectArrayMap> elements; // No OpenHashMap here because it messes with the entry order + + protected Consumer closed = player -> {}; + protected BiConsumer submitted = (player, response) -> {}; + + public SimpleForm() { + this(""); + } + + public SimpleForm(String title) { + this(title, ""); + } + + public SimpleForm(String title, String content) { + this(title, content, new Object2ObjectArrayMap<>()); + } + + public SimpleForm addButton(String text) { + return this.addButton(text, null); + } + + public SimpleForm addButton(String text, Image image) { + return this.addButton(new ElementButton(text, image)); + } + + public SimpleForm addButton(ElementButton button) { + return this.addButton(button, null); + } + + public SimpleForm addButton(ElementButton button, Consumer callback) { + this.elements.put(button, callback); + return this; + } + + /** + * + * Forms need an identifier so that the Minecraft client knows what type of form to open. ('type' => 'form') + * The json data of a simple form contains a title, content and an array of buttons (text + optional image). + * + * @return A json object containing data used by the Minecraft client to construct a simple form + */ + @Override + public JsonObject toJson() { + JsonObject object = new JsonObject(); + object.addProperty("type", "form"); // DO NOT CHANGE: Required to find out which form should be created client-side + object.addProperty("title", this.getTitle()); + object.addProperty("content", this.getContent()); + + JsonArray buttons = new JsonArray(); + this.getElements().keySet() + .stream() + .map(ElementButton::toJson) + .forEach(buttons::add); + object.add("buttons", buttons); + + return object; + } + + /** + * + * The client sends us a buttonId corresponding to the button's position within the form, which we use to determine the button clicked in our elements array + * If the response does not contain an integer ('null'), the form has been closed. + * + * @param packet The packet sent to the server by the client + * @return A response object + */ + @Override + public SimpleResponse processResponse(Player player, ModalFormResponsePacket packet) { + SimpleResponse response = new SimpleResponse(); + + String data = packet.getFormData().trim(); + int buttonId; + try { + buttonId = Integer.parseInt(data); + } catch (Exception e) { + this.closed.accept(player); + return response.setClosed(true); // If the player closes the form, the data will be 'null' + } + + Preconditions.checkArgument(buttonId < this.elements.size(), "buttonId out of range"); + + ElementButton button = this.elements.keySet().toArray(EMPTY_ARRAY)[buttonId]; + if (button != null) { + Optional.ofNullable(this.elements.get(button)) // Only accept consumer if present + .ifPresent(callback -> callback.accept(player)); + } + + response.setButtonId(buttonId) + .setButton(button); + + this.submitted.accept(player, response); + + return response; + } +} diff --git a/src/main/java/org/sculk/form/element/Element.java b/src/main/java/org/sculk/form/element/Element.java new file mode 100644 index 0000000..0b79fae --- /dev/null +++ b/src/main/java/org/sculk/form/element/Element.java @@ -0,0 +1,32 @@ +package org.sculk.form.element; + +import org.sculk.utils.json.Serializable; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface Element extends Serializable { + + Type getType(); + + enum Type { + DROPDOWN, + INPUT, + LABEL, + SLIDER, + STEP_SLIDER, + TOGGLE + } +} diff --git a/src/main/java/org/sculk/form/element/ElementDropdown.java b/src/main/java/org/sculk/form/element/ElementDropdown.java new file mode 100644 index 0000000..98f7a46 --- /dev/null +++ b/src/main/java/org/sculk/form/element/ElementDropdown.java @@ -0,0 +1,71 @@ +package org.sculk.form.element; + +import com.google.common.base.Preconditions; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.ArrayList; +import java.util.List; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@Setter +@Accessors(chain = true) +@AllArgsConstructor +public class ElementDropdown implements Element { + protected String text; + protected List options; + protected int defaultOption; + + public ElementDropdown() { + this(""); + } + + public ElementDropdown(String text) { + this(text, new ArrayList<>()); + } + + public ElementDropdown(String text, List options) { + this(text, options, 0); + } + + @Override + public Type getType() { + return Type.DROPDOWN; + } + + @Override + public JsonObject toJson() { + Preconditions.checkArgument(this.defaultOption > -1 && this.defaultOption < this.options.size(), "Default option not an index"); + + JsonObject object = new JsonObject(); + object.addProperty("type", "dropdown"); + object.addProperty("text", this.text); + object.addProperty("default", this.defaultOption); + + JsonArray optionsArray = new JsonArray(); + this.options.forEach(optionsArray::add); + + object.add("options", optionsArray); + return object; + } +} diff --git a/src/main/java/org/sculk/form/element/ElementInput.java b/src/main/java/org/sculk/form/element/ElementInput.java new file mode 100644 index 0000000..3c28a61 --- /dev/null +++ b/src/main/java/org/sculk/form/element/ElementInput.java @@ -0,0 +1,60 @@ +package org.sculk.form.element; + +import com.google.gson.JsonObject; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@Setter +@Accessors(chain = true) +@AllArgsConstructor +public class ElementInput implements Element { + protected String text; + protected String placeholder; + protected String defaultText; + + public ElementInput() { + this(""); + } + + public ElementInput(String text) { + this(text, ""); + } + + public ElementInput(String text, String placeholder) { + this(text, placeholder, ""); + } + + @Override + public Type getType() { + return Type.INPUT; + } + + @Override + public JsonObject toJson() { + JsonObject object = new JsonObject(); + object.addProperty("type", "input"); + object.addProperty("text", this.text); + object.addProperty("placeholder", this.placeholder); + object.addProperty("default", this.defaultText); + return object; + } +} diff --git a/src/main/java/org/sculk/form/element/ElementLabel.java b/src/main/java/org/sculk/form/element/ElementLabel.java new file mode 100644 index 0000000..c1ca80c --- /dev/null +++ b/src/main/java/org/sculk/form/element/ElementLabel.java @@ -0,0 +1,46 @@ +package org.sculk.form.element; + +import com.google.gson.JsonObject; +import lombok.*; +import lombok.experimental.Accessors; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@Setter +@Accessors(chain = true) +@AllArgsConstructor +public class ElementLabel implements Element { + protected String text; + + public ElementLabel() { + this(""); + } + + @Override + public Type getType() { + return Type.LABEL; + } + + @Override + public JsonObject toJson() { + JsonObject object = new JsonObject(); + object.addProperty("type", "label"); + object.addProperty("text", this.text); + return object; + } +} diff --git a/src/main/java/org/sculk/form/element/ElementSlider.java b/src/main/java/org/sculk/form/element/ElementSlider.java new file mode 100644 index 0000000..cb66cad --- /dev/null +++ b/src/main/java/org/sculk/form/element/ElementSlider.java @@ -0,0 +1,76 @@ +package org.sculk.form.element; + +import com.google.common.base.Preconditions; +import com.google.gson.JsonObject; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@Setter +@Accessors(chain = true) +@AllArgsConstructor +public class ElementSlider implements Element { + protected String text; + protected float min; + protected float max; + protected int step; + protected float defaultValue; + + public ElementSlider() { + this(""); + } + + public ElementSlider(String text) { + this(text, 1); + } + + public ElementSlider(String text, float min) { + this(text, min, Math.max(min, 100)); + } + + public ElementSlider(String text, float min, float max) { + this(text, min, max, 1); + } + + public ElementSlider(String text, float min, float max, int step) { + this(text, min, max, step, 1); + } + + @Override + public Type getType() { + return Type.SLIDER; + } + + @Override + public JsonObject toJson() { + Preconditions.checkArgument(this.min < this.max, "Maximum slider value must exceed the minimum value"); + Preconditions.checkArgument(this.defaultValue >= this.min && this.defaultValue <= this.max, "Default value out of range"); + + JsonObject object = new JsonObject(); + object.addProperty("type", "slider"); + object.addProperty("text", this.text); + object.addProperty("min", this.min); + object.addProperty("max", this.max); + object.addProperty("step", this.step); + object.addProperty("default", this.defaultValue); + return object; + } +} diff --git a/src/main/java/org/sculk/form/element/ElementStepSlider.java b/src/main/java/org/sculk/form/element/ElementStepSlider.java new file mode 100644 index 0000000..3d9d4c1 --- /dev/null +++ b/src/main/java/org/sculk/form/element/ElementStepSlider.java @@ -0,0 +1,71 @@ +package org.sculk.form.element; + +import com.google.common.base.Preconditions; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.ArrayList; +import java.util.List; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@Setter +@Accessors(chain = true) +@AllArgsConstructor +public class ElementStepSlider implements Element { + protected String text; + protected List steps; + protected int defaultStep; + + public ElementStepSlider() { + this(""); + } + + public ElementStepSlider(String text) { + this(text, new ArrayList<>()); + } + + public ElementStepSlider(String text, List steps) { + this(text, steps, 0); + } + + @Override + public Type getType() { + return Type.STEP_SLIDER; + } + + @Override + public JsonObject toJson() { + Preconditions.checkArgument(this.defaultStep > -1 && this.defaultStep < this.steps.size(), "Default option not within range"); + + JsonObject object = new JsonObject(); + object.addProperty("type", "step_slider"); + object.addProperty("text", this.text); + object.addProperty("default", this.defaultStep); + + JsonArray optionsArray = new JsonArray(); + this.steps.forEach(optionsArray::add); + + object.add("steps", optionsArray); + return object; + } +} diff --git a/src/main/java/org/sculk/form/element/ElementToggle.java b/src/main/java/org/sculk/form/element/ElementToggle.java new file mode 100644 index 0000000..e6541a2 --- /dev/null +++ b/src/main/java/org/sculk/form/element/ElementToggle.java @@ -0,0 +1,54 @@ +package org.sculk.form.element; + +import com.google.gson.JsonObject; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@Setter +@Accessors(chain = true) +@AllArgsConstructor +public class ElementToggle implements Element { + protected String text; + protected boolean defaultValue; + + public ElementToggle() { + this(""); + } + + public ElementToggle(String text) { + this(text, false); + } + + @Override + public Type getType() { + return Type.TOGGLE; + } + + @Override + public JsonObject toJson() { + JsonObject object = new JsonObject(); + object.addProperty("type", "toggle"); + object.addProperty("text", this.text); + object.addProperty("default", this.defaultValue); + return object; + } +} diff --git a/src/main/java/org/sculk/form/element/button/ElementButton.java b/src/main/java/org/sculk/form/element/button/ElementButton.java new file mode 100644 index 0000000..0a7d7d3 --- /dev/null +++ b/src/main/java/org/sculk/form/element/button/ElementButton.java @@ -0,0 +1,51 @@ +package org.sculk.form.element.button; + +import com.google.gson.JsonObject; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.sculk.utils.json.Serializable; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@Setter +@Accessors(chain = true) +@AllArgsConstructor +public class ElementButton implements Serializable { + protected String text; + protected Image image; + + public ElementButton(String text) { + this(text, null); + } + + @Override + public JsonObject toJson() { + JsonObject object = new JsonObject(); + object.addProperty("text", this.text); + + if (this.image != null) { + JsonObject imageObject = new JsonObject(); + imageObject.addProperty("type", this.image.getType().name().toLowerCase()); + imageObject.addProperty("path", this.image.getPath()); + object.add("image", imageObject); + } + return object; + } +} diff --git a/src/main/java/org/sculk/form/element/button/Image.java b/src/main/java/org/sculk/form/element/button/Image.java new file mode 100644 index 0000000..cc7be29 --- /dev/null +++ b/src/main/java/org/sculk/form/element/button/Image.java @@ -0,0 +1,36 @@ +package org.sculk.form.element.button; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@RequiredArgsConstructor +public class Image { + protected final Type type; + protected final String path; + + public enum Type { + PATH, + URL; + + public Image of(String path) { + return new Image(this, path); + } + } +} diff --git a/src/main/java/org/sculk/form/response/CustomResponse.java b/src/main/java/org/sculk/form/response/CustomResponse.java new file mode 100644 index 0000000..67cc94c --- /dev/null +++ b/src/main/java/org/sculk/form/response/CustomResponse.java @@ -0,0 +1,98 @@ +package org.sculk.form.response; + +import it.unimi.dsi.fastutil.ints.Int2BooleanOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@Setter +@Accessors(chain = true) +public class CustomResponse implements Response { + protected boolean closed = false; + + protected final Int2ObjectOpenHashMap responses = new Int2ObjectOpenHashMap<>(); + + protected final Int2ObjectOpenHashMap dropdownResponses = new Int2ObjectOpenHashMap<>(); + protected final Int2ObjectOpenHashMap inputResponses = new Int2ObjectOpenHashMap<>(); + protected final Int2ObjectOpenHashMap labelResponses = new Int2ObjectOpenHashMap<>(); + protected final Int2FloatOpenHashMap sliderResponses = new Int2FloatOpenHashMap(); + protected final Int2ObjectOpenHashMap stepSliderResponses = new Int2ObjectOpenHashMap<>(); + protected final Int2BooleanOpenHashMap toggleResponses = new Int2BooleanOpenHashMap(); + + public void setDropdownResponse(int index, ElementResponse response) { + this.responses.put(index, response); + this.dropdownResponses.put(index, response); + } + + public void setInputResponse(int index, String response) { + this.responses.put(index, response); + this.inputResponses.put(index, response); + } + + public void setLabelResponse(int index, String response) { + this.responses.put(index, response); + this.labelResponses.put(index, response); + } + + public void setSliderResponse(int index, float response) { + this.responses.put(index, (Float) response); + this.sliderResponses.put(index, response); + } + + public void setStepSliderResponse(int index, ElementResponse response) { + this.responses.put(index, response); + this.stepSliderResponses.put(index, response); + } + + public void setToggleResponse(int index, boolean response) { + this.responses.put(index, (Boolean) response); + this.toggleResponses.put(index, response); + } + + public Object getResponse(int index) { + return this.responses.get(index); + } + + public ElementResponse getDropdownResponse(int index) { + return this.dropdownResponses.get(index); + } + + public String getInputResponse(int index) { + return this.inputResponses.get(index); + } + + public String getLabelResponse(int index) { + return this.labelResponses.get(index); + } + + public float getSliderResponse(int index) { + return this.sliderResponses.get(index); + } + + public ElementResponse getStepSliderResponse(int index) { + return this.stepSliderResponses.get(index); + } + + public boolean getToggleResponse(int index) { + return this.toggleResponses.get(index); + } +} diff --git a/src/main/java/org/sculk/form/response/ElementResponse.java b/src/main/java/org/sculk/form/response/ElementResponse.java new file mode 100644 index 0000000..22f8fcc --- /dev/null +++ b/src/main/java/org/sculk/form/response/ElementResponse.java @@ -0,0 +1,27 @@ +package org.sculk.form.response; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@RequiredArgsConstructor +public class ElementResponse { + protected final int elementId; + protected final String elementText; +} diff --git a/src/main/java/org/sculk/form/response/ModalResponse.java b/src/main/java/org/sculk/form/response/ModalResponse.java new file mode 100644 index 0000000..cb9fb85 --- /dev/null +++ b/src/main/java/org/sculk/form/response/ModalResponse.java @@ -0,0 +1,30 @@ +package org.sculk.form.response; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@Setter +@Accessors(chain = true) +public class ModalResponse implements Response { + protected boolean closed = false; + protected int buttonId = -1; + protected String text = ""; +} diff --git a/src/main/java/org/sculk/form/response/Response.java b/src/main/java/org/sculk/form/response/Response.java new file mode 100644 index 0000000..d7ab7cc --- /dev/null +++ b/src/main/java/org/sculk/form/response/Response.java @@ -0,0 +1,19 @@ +package org.sculk.form.response; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface Response { +} diff --git a/src/main/java/org/sculk/form/response/SimpleResponse.java b/src/main/java/org/sculk/form/response/SimpleResponse.java new file mode 100644 index 0000000..f943ae7 --- /dev/null +++ b/src/main/java/org/sculk/form/response/SimpleResponse.java @@ -0,0 +1,31 @@ +package org.sculk.form.response; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.sculk.form.element.button.ElementButton; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ + +@Getter +@Setter +@Accessors(chain = true) +public class SimpleResponse implements Response { + protected boolean closed = false; + protected int buttonId = -1; + protected ElementButton button = null; +} diff --git a/src/main/java/org/sculk/network/AdvancedSourceInterface.java b/src/main/java/org/sculk/network/AdvancedSourceInterface.java new file mode 100644 index 0000000..678ddf8 --- /dev/null +++ b/src/main/java/org/sculk/network/AdvancedSourceInterface.java @@ -0,0 +1,32 @@ +package org.sculk.network; + +import io.netty.buffer.ByteBuf; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface AdvancedSourceInterface extends SourceInterface { + + void blockAddress(InetAddress address); + void blockAddress(InetAddress address, long timeout, TimeUnit unit); + void unblockAddress(InetAddress address); + void setNetwork(Network network); + void sendRawPacket(InetSocketAddress socketAddress, ByteBuf payload); + +} diff --git a/src/main/java/org/sculk/network/BedrockInterface.java b/src/main/java/org/sculk/network/BedrockInterface.java new file mode 100644 index 0000000..f80aafb --- /dev/null +++ b/src/main/java/org/sculk/network/BedrockInterface.java @@ -0,0 +1,130 @@ +package org.sculk.network; + + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import org.cloudburstmc.netty.channel.raknet.RakChannelFactory; +import org.cloudburstmc.netty.channel.raknet.config.RakChannelOption; +import org.cloudburstmc.protocol.bedrock.BedrockPeer; +import org.cloudburstmc.protocol.bedrock.BedrockPong; +import org.cloudburstmc.protocol.bedrock.BedrockServerSession; +import org.cloudburstmc.protocol.bedrock.netty.initializer.BedrockServerInitializer; +import org.sculk.Server; +import org.sculk.config.ServerPropertiesKeys; +import org.sculk.network.handler.SessionStartPacketHandler; +import org.sculk.network.protocol.ProtocolInfo; +import org.sculk.network.session.SculkServerSession; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class BedrockInterface implements AdvancedSourceInterface { + + private final Server server; + private final List channels = new ArrayList<>(); + private final BedrockPong bedrockPong = new BedrockPong(); + + public BedrockInterface(Server server) throws Exception { + this.server = server; + ServerBootstrap bootstrap = new ServerBootstrap() + .channelFactory(RakChannelFactory.server(NioDatagramChannel.class)) + .group(new NioEventLoopGroup()) + .childHandler(new BedrockServerInitializer() { + @Override + protected void initSession(BedrockServerSession bedrockServerSession) { + bedrockServerSession.setCodec(ProtocolInfo.CODEC); + bedrockServerSession.setLogging(false); + } + + @Override + public BedrockServerSession createSession0(BedrockPeer peer, int subClientId) { + return new SculkServerSession(BedrockInterface.this, server, peer, subClientId); + } + }) + .localAddress(this.server.getProperties().get(ServerPropertiesKeys.SERVER_IP, "0.0.0.0"), this.server.getProperties().get(ServerPropertiesKeys.SERVER_PORT, 19132)); + this.channels.add(bootstrap.bind().awaitUninterruptibly().channel()); + } + + @Override + public void blockAddress(InetAddress address) { + + } + + @Override + public void blockAddress(InetAddress address, long timeout, TimeUnit unit) { + + } + + @Override + public void unblockAddress(InetAddress address) { + + } + + @Override + public void setNetwork(Network network) { + + } + + @Override + public void sendRawPacket(InetSocketAddress socketAddress, ByteBuf payload) { + + } + + @Override + public void setName(String name) { + this.bedrockPong.edition("MCPE") + .motd(this.server.getMotd()) + .subMotd(this.server.getMotd()) + .playerCount(this.server.getOnlinePlayers().size()) + .serverId(1) + .maximumPlayerCount(20) + .version("1") + .protocolVersion(ProtocolInfo.CURRENT_PROTOCOL) + .gameType("Survival") + .nintendoLimited(false) + .ipv4Port(19132) + .ipv6Port(19132); + + for (Channel channel : this.channels) { + channel.config().setOption(RakChannelOption.RAK_ADVERTISEMENT, this.bedrockPong.toByteBuf()); + } + } + + @Override + public boolean process() { + return true; + } + + @Override + public void shutdown() { + for(Channel channel : this.channels) { + channel.closeFuture().awaitUninterruptibly(); + } + } + + @Override + public void emergencyShutdown() { + this.shutdown(); + } +} diff --git a/src/main/java/org/sculk/network/EventLoops.java b/src/main/java/org/sculk/network/EventLoops.java index d056bb7..5fa5d05 100644 --- a/src/main/java/org/sculk/network/EventLoops.java +++ b/src/main/java/org/sculk/network/EventLoops.java @@ -17,11 +17,11 @@ import java.util.function.BiFunction; /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by diff --git a/src/main/java/org/sculk/network/Network.java b/src/main/java/org/sculk/network/Network.java index c8300b9..415d512 100644 --- a/src/main/java/org/sculk/network/Network.java +++ b/src/main/java/org/sculk/network/Network.java @@ -3,12 +3,15 @@ import lombok.extern.log4j.Log4j2; import org.sculk.Server; +import java.util.HashSet; +import java.util.Set; + /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -22,6 +25,8 @@ public class Network { private final Server server; + private final Set interfaces = new HashSet<>(); + private final Set advancedInterfaces = new HashSet(); private String name; private int maxPlayers; @@ -56,4 +61,36 @@ public String getName() { return name; } + public Set getInterfaces() { + return interfaces; + } + + public void processInterfaces() { + for(SourceInterface sourceInterface : this.interfaces) { + try { + sourceInterface.process(); + } catch(Exception exception) { + sourceInterface.emergencyShutdown(); + this.unregisterInterface(sourceInterface); + log.fatal("Unregister source interface: " + sourceInterface.getClass().getName()); + } + } + } + + public void registerInterface(SourceInterface interfaz) { + this.interfaces.add(interfaz); + if (interfaz instanceof AdvancedSourceInterface) { + this.advancedInterfaces.add((AdvancedSourceInterface) interfaz); + ((AdvancedSourceInterface) interfaz).setNetwork(this); + } + interfaz.setName(this.name + "!@#" + this.getName()); + } + + public void unregisterInterface(SourceInterface sourceInterface) { + this.interfaces.remove(sourceInterface); + if (sourceInterface instanceof AdvancedSourceInterface) { + this.advancedInterfaces.remove(sourceInterface); + } + } + } diff --git a/src/main/java/org/sculk/network/SourceInterface.java b/src/main/java/org/sculk/network/SourceInterface.java new file mode 100644 index 0000000..67a940e --- /dev/null +++ b/src/main/java/org/sculk/network/SourceInterface.java @@ -0,0 +1,26 @@ +package org.sculk.network; + + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface SourceInterface { + + void setName(String name); + boolean process(); + void shutdown(); + void emergencyShutdown(); + +} diff --git a/src/main/java/org/sculk/network/handler/HandshakePacketHandler.java b/src/main/java/org/sculk/network/handler/HandshakePacketHandler.java new file mode 100644 index 0000000..eb314f7 --- /dev/null +++ b/src/main/java/org/sculk/network/handler/HandshakePacketHandler.java @@ -0,0 +1,38 @@ +package org.sculk.network.handler; + + +import org.cloudburstmc.protocol.bedrock.packet.ClientToServerHandshakePacket; +import org.cloudburstmc.protocol.common.PacketSignal; +import org.sculk.network.session.SculkServerSession; + +import java.util.function.Consumer; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class HandshakePacketHandler extends SculkPacketHandler { + + private Consumer onHandshakeCompleted; + + public HandshakePacketHandler(SculkServerSession session, Consumer onHandshakeCompleted) { + super(session); + this.onHandshakeCompleted = onHandshakeCompleted; + } + + @Override + public PacketSignal handle(ClientToServerHandshakePacket packet) { + return PacketSignal.HANDLED; + } +} diff --git a/src/main/java/org/sculk/network/handler/InGamePacketHandler.java b/src/main/java/org/sculk/network/handler/InGamePacketHandler.java new file mode 100644 index 0000000..1287802 --- /dev/null +++ b/src/main/java/org/sculk/network/handler/InGamePacketHandler.java @@ -0,0 +1,58 @@ +package org.sculk.network.handler; + + +import lombok.NonNull; +import org.cloudburstmc.protocol.bedrock.packet.CommandRequestPacket; +import org.cloudburstmc.protocol.bedrock.packet.EmotePacket; +import org.cloudburstmc.protocol.bedrock.packet.TextPacket; +import org.cloudburstmc.protocol.common.PacketSignal; +import org.sculk.player.Player; +import org.sculk.network.session.SculkServerSession; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class InGamePacketHandler extends SculkPacketHandler { + + private final @NonNull Player player; + + public InGamePacketHandler(Player player, SculkServerSession session) { + super(session); + this.player = player; + } + + @Override + public PacketSignal handle(TextPacket packet) { + switch(packet.getType()) { + case TextPacket.Type.CHAT -> { + String chatMessage = packet.getMessage(); + int breakLine = chatMessage.indexOf("\n"); + if(breakLine != -1) { + chatMessage = chatMessage.substring(0, breakLine); + } + this.player.onChat(chatMessage); + } + } + return PacketSignal.HANDLED; + } + + @Override + public PacketSignal handle(CommandRequestPacket packet) { + if(packet.getCommand().startsWith("/")) { + this.player.onChat(packet.getCommand()); + } + return PacketSignal.HANDLED; + } +} diff --git a/src/main/java/org/sculk/network/handler/LoginPacketHandler.java b/src/main/java/org/sculk/network/handler/LoginPacketHandler.java new file mode 100644 index 0000000..419e573 --- /dev/null +++ b/src/main/java/org/sculk/network/handler/LoginPacketHandler.java @@ -0,0 +1,134 @@ +package org.sculk.network.handler; + +import lombok.SneakyThrows; +import org.cloudburstmc.protocol.bedrock.packet.*; +import org.cloudburstmc.protocol.common.PacketSignal; +import org.cloudburstmc.protocol.common.util.QuadConsumer; +import org.sculk.Server; +import org.sculk.event.player.PlayerAsyncPreLoginEvent; +import org.sculk.event.player.PlayerPreLoginEvent; +import org.sculk.network.session.SculkServerSession; +import org.sculk.player.PlayerLoginData; +import org.sculk.player.client.ClientChainData; +import org.sculk.scheduler.AsyncTask; + +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class LoginPacketHandler extends SculkPacketHandler { + + private final Consumer playerInfo; + private final PlayerLoginData loginData; + + private static final Pattern NAME_PATTERN = Pattern.compile("^[aA-zZ\\s\\d_]{3,16}+$"); + + public LoginPacketHandler(SculkServerSession session, Consumer playerInfo, QuadConsumer authCallback) { + super(session); + this.playerInfo = playerInfo; + this.loginData = new PlayerLoginData(session, authCallback); + } + + @SneakyThrows + @Override + public PacketSignal handle(LoginPacket packet) { + ClientChainData clientChainData = ClientChainData.read(packet); + if(clientChainData.isXboxAuthed()) { + session.disconnect("disconnectionScreen.notAuthenticated"); + return PacketSignal.HANDLED; + } + + String username = clientChainData.getUsername(); + Matcher matcher = NAME_PATTERN.matcher(username); + if(!matcher.matches() || username.equalsIgnoreCase("rcon") || username.equalsIgnoreCase("console")) { + session.disconnect("disconnectionScreen.invalidName"); + return PacketSignal.HANDLED; + } + + if(!clientChainData.getSerializedSkin().isValid()) { + session.disconnect("disconnectionScreen.invalidSkin"); + return PacketSignal.HANDLED; + } + + PlayerPreLoginEvent playerPreLoginEvent = new PlayerPreLoginEvent(clientChainData, session.getSocketAddress(),"Sculk server"); + playerPreLoginEvent.call(); + if(playerPreLoginEvent.isCancelled()) { + session.disconnect(playerPreLoginEvent.getKickMessage()); + return PacketSignal.HANDLED; + } + + //session.setPacketHandler(new ResourcePackHandler(session, server, loginData)); + + this.playerInfo.accept(clientChainData); + loginData.setPreLoginEventTask(new AsyncTask() { + + private PlayerAsyncPreLoginEvent playerAsyncPreLoginEvent; + + @Override + public void onRun() { + playerAsyncPreLoginEvent = new PlayerAsyncPreLoginEvent(session.getPlayerInfo()); + playerAsyncPreLoginEvent.call(); + } + + @Override + public void onCompletion(Server server) { + if(loginData.getSession().getPeer().isConnected()) { + loginData.setShouldLogin(true); + if(playerAsyncPreLoginEvent.getLoginResult() == PlayerAsyncPreLoginEvent.LoginResult.KICK) { + loginData.getSession().disconnect(playerAsyncPreLoginEvent.getKickMessage()); + } else if(loginData.isShouldLogin()) { + Exception error = null; + try { + for(Consumer action : playerAsyncPreLoginEvent.getScheduledActions()) { + action.accept(LoginPacketHandler.this.session); + } + } catch(Exception e) { + error = e; + } + loginData.getAuthCallback().accept(clientChainData.isXboxAuthed(), false, error, clientChainData.getIdentityPublicKey()); + } else { + loginData.setLoginTasks(playerAsyncPreLoginEvent.getScheduledActions()); + } + } else { + session.disconnect("Already connected"); + } + } + }); + Server.getInstance().getScheduler().scheduleAsyncTask(loginData.getPreLoginEventTask()); + /* + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); + keyPairGenerator.initialize(Curve.P_384.toECParameterSpec()); + + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + byte[] token = EncryptionUtils.generateRandomToken(); + + ServerToClientndshakePacket serverToClientHandshakePacket = new ServerToClientHandshakePacket(); + serverToClientHandshakePacket.handle(this); + serverToClientHandshakePacket.setJwt(EncryptionUtils.createHandshakeJwt(keyPair, token)); + System.out.println(session.getCodec().getProtocolVersion()); + session.sendPacket(serverToClientHandshakePacket);*/ + + //packet.setProtocolVersion(712); + //chain.removeLast(Ha); + //packet.getChain().add(); + + // TODO: View Login in log + return PacketSignal.HANDLED; + } + +} diff --git a/src/main/java/org/sculk/network/handler/PreSpawnPacketHandler.java b/src/main/java/org/sculk/network/handler/PreSpawnPacketHandler.java new file mode 100644 index 0000000..cf427a5 --- /dev/null +++ b/src/main/java/org/sculk/network/handler/PreSpawnPacketHandler.java @@ -0,0 +1,152 @@ +package org.sculk.network.handler; + + +import org.cloudburstmc.math.vector.Vector2f; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.math.vector.Vector3i; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.*; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.cloudburstmc.protocol.bedrock.packet.*; +import org.cloudburstmc.protocol.common.PacketSignal; +import org.cloudburstmc.protocol.common.util.OptionalBoolean; +import org.sculk.player.Player; +import org.sculk.Server; +import org.sculk.network.session.SculkServerSession; + +import java.util.List; +import java.util.UUID; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class PreSpawnPacketHandler extends SculkPacketHandler { + + private final Player player; + public PreSpawnPacketHandler(SculkServerSession session, Player player) { + super(session); + this.player = player; + } + + @Override + public void setUp() { + StartGamePacket startGamePacket = new StartGamePacket(); + startGamePacket.setUniqueEntityId(0); + startGamePacket.setRuntimeEntityId(0); + startGamePacket.setPlayerGameType(GameType.DEFAULT); + startGamePacket.setPlayerPosition(Vector3f.from(0, 69, 0)); + startGamePacket.setRotation(Vector2f.from(1, 1)); + + startGamePacket.setSeed(-1L); + startGamePacket.setDimensionId(0); + startGamePacket.setGeneratorId(1); + startGamePacket.setLevelGameType(GameType.SURVIVAL); + startGamePacket.setDifficulty(1); + startGamePacket.setDefaultSpawn(Vector3i.ZERO); + startGamePacket.setAchievementsDisabled(true); + startGamePacket.setCurrentTick(-1); + startGamePacket.setEduEditionOffers(0); + startGamePacket.setEduFeaturesEnabled(false); + startGamePacket.setRainLevel(0); + startGamePacket.setLightningLevel(0); + startGamePacket.setMultiplayerGame(true); + startGamePacket.setBroadcastingToLan(true); + startGamePacket.setPlatformBroadcastMode(GamePublishSetting.PUBLIC); + startGamePacket.setXblBroadcastMode(GamePublishSetting.PUBLIC); + startGamePacket.setCommandsEnabled(true); + startGamePacket.setTexturePacksRequired(false); + startGamePacket.setBonusChestEnabled(false); + startGamePacket.setStartingWithMap(false); + startGamePacket.setTrustingPlayers(true); + startGamePacket.setDefaultPlayerPermission(PlayerPermission.MEMBER); + startGamePacket.setServerChunkTickRange(4); + startGamePacket.setBehaviorPackLocked(false); + startGamePacket.setResourcePackLocked(false); + startGamePacket.setFromLockedWorldTemplate(false); + startGamePacket.setUsingMsaGamertagsOnly(false); + startGamePacket.setFromWorldTemplate(false); + startGamePacket.setWorldTemplateOptionLocked(false); + startGamePacket.setSpawnBiomeType(SpawnBiomeType.DEFAULT); + startGamePacket.setCustomBiomeName(""); + startGamePacket.setEducationProductionId(""); + startGamePacket.setForceExperimentalGameplay(OptionalBoolean.empty()); + + String serverName = session.getServer().getSubMotd(); + startGamePacket.setLevelId(serverName); + startGamePacket.setLevelName(serverName); + + startGamePacket.setPremiumWorldTemplateId("00000000-0000-0000-0000-000000000000"); + // startGamePacket.setCurrentTick(0); + startGamePacket.setEnchantmentSeed(0); + startGamePacket.setMultiplayerCorrelationId(""); + + startGamePacket.getItemDefinitions().addAll(List.of()); + + // Needed for custom block mappings and custom skulls system + startGamePacket.getBlockProperties().addAll(List.of()); + +/* THANK GEYSER + // See https://learn.microsoft.com/en-us/minecraft/creator/documents/experimentalfeaturestoggle for info on each experiment + // data_driven_items (Holiday Creator Features) is needed for blocks and items + startGamePacket.getExperiments().add(new ExperimentData("data_driven_items", true)); + // Needed for block properties for states + startGamePacket.getExperiments().add(new ExperimentData("upcoming_creator_features", true)); + // Needed for certain molang queries used in blocks and items + startGamePacket.getExperiments().add(new ExperimentData("experimental_molang_features", true)); + // Required for experimental 1.21 features + startGamePacket.getExperiments().add(new ExperimentData("updateAnnouncedLive2023", true));*/ + + startGamePacket.setVanillaVersion("*"); + startGamePacket.setInventoriesServerAuthoritative(true); + startGamePacket.setServerEngine(""); // Do we want to fill this in? + + startGamePacket.setPlayerPropertyData(NbtMap.EMPTY); + startGamePacket.setWorldTemplateId(UUID.randomUUID()); + + startGamePacket.setChatRestrictionLevel(ChatRestrictionLevel.NONE); + + startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.SERVER); + startGamePacket.setRewindHistorySize(0); + startGamePacket.setServerAuthoritativeBlockBreaking(false); + + startGamePacket.setServerId(""); + startGamePacket.setWorldId(""); + startGamePacket.setScenarioId(""); + + session.sendPacket(startGamePacket); + session.getPlayer().sendAttributes(); + session.sendPacket(new CreativeContentPacket()); + session.sendPacket(new BiomeDefinitionListPacket()); + session.sendPacket(new AvailableEntityIdentifiersPacket()); + SetEntityDataPacket setEntityDataPacket = new SetEntityDataPacket(); + setEntityDataPacket.setRuntimeEntityId(0); + setEntityDataPacket.getMetadata().put(EntityDataTypes.PLAYER_FLAGS, (byte) 2); + session.sendPacket(setEntityDataPacket); + session.getPlayer().updateFlags(); + + SetTimePacket setTimePacket = new SetTimePacket(); + setTimePacket.setTime(0); + session.sendPacket(setTimePacket); + + Server.getInstance().getLogger().info("Player: §b" + session.getPlayer().getName()); + } + + @Override + public PacketSignal handlePacket(BedrockPacket packet) { + System.out.println(packet); + return super.handlePacket(packet); + } + +} diff --git a/src/main/java/org/sculk/network/handler/ResourcePackHandler.java b/src/main/java/org/sculk/network/handler/ResourcePackHandler.java new file mode 100644 index 0000000..7869821 --- /dev/null +++ b/src/main/java/org/sculk/network/handler/ResourcePackHandler.java @@ -0,0 +1,71 @@ +package org.sculk.network.handler; + +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackClientResponsePacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePackStackPacket; +import org.cloudburstmc.protocol.bedrock.packet.ResourcePacksInfoPacket; +import org.cloudburstmc.protocol.common.PacketSignal; +import org.sculk.network.session.SculkServerSession; + +import java.util.ArrayList; +import java.util.function.Consumer; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class ResourcePackHandler extends SculkPacketHandler { + + private final Consumer completeClosure; + public ResourcePackHandler(SculkServerSession session, Consumer completeClosure) { + super(session); + this.completeClosure = completeClosure; + } + + @Override + public void setUp() { + ResourcePacksInfoPacket resourcePacksInfoPacket = new ResourcePacksInfoPacket(); + session.sendPacket(resourcePacksInfoPacket); + } + + + public PacketSignal handle(ResourcePackClientResponsePacket packet) { + switch (packet.getStatus()) { + case REFUSED -> { + session.disconnect("You must accept resource packs to join this server", false); + break; + } + case SEND_PACKS -> { + break; + } + case HAVE_ALL_PACKS -> { + ArrayList entries = new ArrayList<>(); + + ResourcePackStackPacket stackPacket = new ResourcePackStackPacket(); + //todo: getForceRessource Pack in server.properties + stackPacket.setForcedToAccept(false); + stackPacket.setExperimentsPreviouslyToggled(false); + stackPacket.setGameVersion("*"); + //todo: create ResourcePackManager return packs + session.sendPacket(stackPacket); + break; + } + case COMPLETED -> { + this.completeClosure.accept(null); + break; + } + } + return PacketSignal.HANDLED; + } + +} diff --git a/src/main/java/org/sculk/network/handler/SculkPacketHandler.java b/src/main/java/org/sculk/network/handler/SculkPacketHandler.java new file mode 100644 index 0000000..f9787ff --- /dev/null +++ b/src/main/java/org/sculk/network/handler/SculkPacketHandler.java @@ -0,0 +1,32 @@ +package org.sculk.network.handler; + +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacketHandler; +import org.sculk.network.session.SculkServerSession; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class SculkPacketHandler implements BedrockPacketHandler { + + protected SculkServerSession session; + + public SculkPacketHandler(SculkServerSession session){ + this.session = session; + } + + public void setUp() { + + } +} diff --git a/src/main/java/org/sculk/network/handler/SessionStartPacketHandler.java b/src/main/java/org/sculk/network/handler/SessionStartPacketHandler.java new file mode 100644 index 0000000..ea3f1c3 --- /dev/null +++ b/src/main/java/org/sculk/network/handler/SessionStartPacketHandler.java @@ -0,0 +1,46 @@ +package org.sculk.network.handler; + +import org.cloudburstmc.protocol.bedrock.data.PacketCompressionAlgorithm; +import org.cloudburstmc.protocol.bedrock.packet.NetworkSettingsPacket; +import org.cloudburstmc.protocol.bedrock.packet.RequestNetworkSettingsPacket; +import org.cloudburstmc.protocol.common.PacketSignal; +import org.sculk.Server; +import org.sculk.network.session.SculkServerSession; + +import java.util.function.Consumer; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class SessionStartPacketHandler extends SculkPacketHandler { + + private final Consumer onSuccess; + public SessionStartPacketHandler(SculkServerSession session, Consumer onSuccess) { + super(session); + this.onSuccess = onSuccess; + } + + @Override + public PacketSignal handle(RequestNetworkSettingsPacket packet) { + NetworkSettingsPacket networkSettingsPacket = new NetworkSettingsPacket(); + networkSettingsPacket.setCompressionThreshold(1); + networkSettingsPacket.setCompressionAlgorithm(PacketCompressionAlgorithm.ZLIB); + session.sendPacketImmediately(networkSettingsPacket); + session.getPeer().setCompression(PacketCompressionAlgorithm.ZLIB); + this.onSuccess.accept(null); + return PacketSignal.HANDLED; + } + +} diff --git a/src/main/java/org/sculk/network/handler/SpawnResponsePacketHandler.java b/src/main/java/org/sculk/network/handler/SpawnResponsePacketHandler.java new file mode 100644 index 0000000..298f5f1 --- /dev/null +++ b/src/main/java/org/sculk/network/handler/SpawnResponsePacketHandler.java @@ -0,0 +1,51 @@ +package org.sculk.network.handler; + + +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; +import org.cloudburstmc.protocol.bedrock.packet.SetLocalPlayerAsInitializedPacket; +import org.cloudburstmc.protocol.bedrock.packet.TextPacket; +import org.cloudburstmc.protocol.common.PacketSignal; +import org.sculk.Server; +import org.sculk.network.session.SculkServerSession; +import org.sculk.utils.TextFormat; + +import java.util.function.Consumer; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class SpawnResponsePacketHandler extends SculkPacketHandler { + + private final Consumer responseCallback; + + public SpawnResponsePacketHandler(SculkServerSession session, Consumer responseCallback) { + super(session); + this.responseCallback = responseCallback; + } + + @Override + public PacketSignal handlePacket(BedrockPacket packet) { + System.out.println(packet); + return super.handlePacket(packet); + } + + @Override + public PacketSignal handle(SetLocalPlayerAsInitializedPacket packet) { + System.out.println(packet); + this.responseCallback.accept(null); + Server.getInstance().getLogger().info("§b" + session.getPlayer().getName() + "[/" + session.getPlayerInfo().getServerAddress() + "] logged in with entity id " + session.getPlayer().getUniqueId()); + return PacketSignal.HANDLED; + } +} diff --git a/src/main/java/org/sculk/network/protocol/ProtocolInfo.java b/src/main/java/org/sculk/network/protocol/ProtocolInfo.java index 246e385..c108dad 100644 --- a/src/main/java/org/sculk/network/protocol/ProtocolInfo.java +++ b/src/main/java/org/sculk/network/protocol/ProtocolInfo.java @@ -3,12 +3,16 @@ import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; import org.cloudburstmc.protocol.bedrock.codec.v712.Bedrock_v712; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import static com.google.common.base.Preconditions.checkNotNull; + /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -18,13 +22,28 @@ * @author: SculkTeams * @link: http://www.sculkmp.org/ */ -public interface ProtocolInfo { +public class ProtocolInfo { + + public static int CURRENT_PROTOCOL = Integer.parseInt("712"); + + public static BedrockCodec CODEC = Bedrock_v712.CODEC; + + private static final Set CODECS = ConcurrentHashMap.newKeySet(); - int CURRENT_PROTOCOL = Integer.parseInt("712"); + public static String MINECRAFT_VERSION_NETWORK = "1.21.21"; + public static String MINECRAFT_VERSION = "v" + MINECRAFT_VERSION_NETWORK; - BedrockCodec CODEC = Bedrock_v712.CODEC; + static { + CODECS.add(checkNotNull(CODEC, "packetCodec")); + } - String MINECRAFT_VERSION_NETWORK = "1.21.20"; - String MINECRAFT_VERSION = "v" + MINECRAFT_VERSION_NETWORK; + public static BedrockCodec getPacket(int protocol) { + for(BedrockCodec codec : CODECS) { + if(codec.getProtocolVersion() == protocol) { + return codec; + } + } + return null; + } } diff --git a/src/main/java/org/sculk/network/session/SculkServerSession.java b/src/main/java/org/sculk/network/session/SculkServerSession.java new file mode 100644 index 0000000..d3edd17 --- /dev/null +++ b/src/main/java/org/sculk/network/session/SculkServerSession.java @@ -0,0 +1,226 @@ +package org.sculk.network.session; + + +import com.google.gson.Gson; +import lombok.Getter; +import lombok.Setter; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.cloudburstmc.protocol.bedrock.BedrockPeer; +import org.cloudburstmc.protocol.bedrock.BedrockServerSession; +import org.cloudburstmc.protocol.bedrock.packet.BedrockPacketHandler; +import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; +import org.cloudburstmc.protocol.bedrock.packet.TextPacket; +import org.sculk.player.Player; +import org.sculk.Server; +import org.sculk.network.BedrockInterface; +import org.sculk.network.handler.*; +import org.sculk.player.client.ClientChainData; +import org.sculk.player.text.RawTextBuilder; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class SculkServerSession extends BedrockServerSession { + + @Getter + private @Nullable Player player; + @Getter + @Setter + private @Nullable ClientChainData playerInfo; + + @Getter + private final Server server; + @Getter + private final BedrockInterface bedrockInterface; + + public SculkServerSession(BedrockInterface bedrockInterface, Server server, BedrockPeer peer, int subClientId) { + super(peer, subClientId); + this.server = server; + this.bedrockInterface = bedrockInterface; + + this.setPacketHandler(new SessionStartPacketHandler(this, this::onSessionStartSuccess)); + } + + private void onSessionStartSuccess(Object e) { + this.setPacketHandler(new LoginPacketHandler( + this, + _playerInfo -> { + this.playerInfo = _playerInfo; + }, + this::setAuthenticationStatus + )); + } + + private void setAuthenticationStatus(boolean authenticated, boolean authRequired, Exception error, String clientPubKey) { + if(error == null){ + if(authenticated && Objects.requireNonNull(playerInfo).getXUID() == null){ + error = new Exception("Expected XUID but none found"); + }else if(clientPubKey == null){ + error = new Exception("Missing client public key"); //failsafe + } + } + if (error != null){ + this.disconnect(error.getMessage()); + return; + } + if(!authenticated){ + if(authRequired){ + this.disconnect("Not authenticated"); + return; + } + } + this.onServerLoginSuccess(); + } + + private void onServerLoginSuccess() { + this.setLogging(true); + PlayStatusPacket statusPacket = new PlayStatusPacket(); + statusPacket.setStatus(PlayStatusPacket.Status.LOGIN_SUCCESS); + this.sendPacket(statusPacket); + this.setPacketHandler(new ResourcePackHandler(this, this::createPlayer)); + } + + private void createPlayer(Object e) { + this.server.createPlayer(this, this.playerInfo, false).thenAccept(this::onPlayerCreated).exceptionally(ex -> { + server.getLogger().throwing(ex); + this.disconnect("Failed to create player."); + return null; + }); + } + + private void onPlayerCreated(Player player) { + this.player = player; + this.setPacketHandler(new PreSpawnPacketHandler(this, player)); + this.notifyTerrainReady(null); + } + + private void notifyTerrainReady(Object e) { + + PlayStatusPacket packet = new PlayStatusPacket(); + packet.setStatus(PlayStatusPacket.Status.PLAYER_SPAWN); + this.sendPacket(packet); + this.setPacketHandler(new SpawnResponsePacketHandler(this, this::onClientSpawnResponse)); + } + + private void onClientSpawnResponse(Object e) { + this.setPacketHandler(new InGamePacketHandler(this.getPlayer(),this)); + } + + @Override + public void setPacketHandler(@NonNull BedrockPacketHandler packetHandler) { + super.setPacketHandler(packetHandler); + if (packetHandler instanceof SculkPacketHandler _sculkHandler) + _sculkHandler.setUp(); + } + + public void onChatMessage(String message) { + TextPacket packet = new TextPacket(); + packet.setXuid(""); + packet.setType(TextPacket.Type.RAW); + packet.setMessage(message); + this.sendPacket(packet); + } + + public void onChatMessage(RawTextBuilder textBuilder) { + TextPacket packet = new TextPacket(); + packet.setXuid(""); + packet.setSourceName(""); + packet.setType(TextPacket.Type.JSON); + packet.setMessage(new Gson().toJson(textBuilder.build())); + this.sendPacket(packet); + } + + public void onJukeboxPopup(String message) { + TextPacket packet = new TextPacket(); + packet.setType(TextPacket.Type.JUKEBOX_POPUP); + packet.setMessage(message); + this.sendPacket(packet); + } + + public void onPopup(String message) { + TextPacket packet = new TextPacket(); + packet.setType(TextPacket.Type.POPUP); + packet.setXuid(""); + packet.setMessage(message); + this.sendPacket(packet); + } + + public void onTip(String message) { + TextPacket packet = new TextPacket(); + packet.setType(TextPacket.Type.TIP); + packet.setXuid(""); + packet.setMessage(message); + this.sendPacket(packet); + } + + public void onAnnouncement(String message) { + TextPacket packet = new TextPacket(); + packet.setType(TextPacket.Type.ANNOUNCEMENT); + packet.setXuid(""); + packet.setSourceName(""); + packet.setMessage(message); + this.sendPacket(packet); + } + + public void onAnnouncement(RawTextBuilder textBuilder) { + TextPacket packet = new TextPacket(); + packet.setType(TextPacket.Type.ANNOUNCEMENT_JSON); + packet.setXuid(""); + packet.setSourceName(""); + packet.setMessage(new Gson().toJson(textBuilder.build())); + this.sendPacket(packet); + } + + public void onWhisper(String message) { + TextPacket packet = new TextPacket(); + packet.setType(TextPacket.Type.WHISPER); + packet.setXuid(""); + packet.setSourceName(""); + packet.setMessage(message); + this.sendPacket(packet); + } + + public void onWhisper(RawTextBuilder textBuilder) { + TextPacket packet = new TextPacket(); + packet.setType(TextPacket.Type.WHISPER_JSON); + packet.setXuid(""); + packet.setSourceName(""); + packet.setMessage(new Gson().toJson(textBuilder.build())); + this.sendPacket(packet); + } + + public void onMessageTranslation(String translate, List param) { + TextPacket packet = new TextPacket(); + packet.setType(TextPacket.Type.TRANSLATION); + packet.setXuid(""); + packet.setSourceName(""); + packet.setMessage(translate); + packet.setParameters(param); + this.sendPacket(packet); + } + + public void onMessageSystem(String message) { + TextPacket packet = new TextPacket(); + packet.setType(TextPacket.Type.SYSTEM); + packet.setXuid(""); + packet.setSourceName(""); + packet.setMessage(message); + this.sendPacket(packet); + } +} diff --git a/src/main/java/org/sculk/permission/DefaultPermissionNames.java b/src/main/java/org/sculk/permission/DefaultPermissionNames.java new file mode 100644 index 0000000..f5c1e8b --- /dev/null +++ b/src/main/java/org/sculk/permission/DefaultPermissionNames.java @@ -0,0 +1,28 @@ +package org.sculk.permission; + + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class DefaultPermissionNames { + + public static final String COMMAND_VERSION = "sculk.command.version"; + public static final String COMMAND_HELP = "sculk.command.help"; + + public static final String GROUP_CONSOLE = "sculk.group.console"; + public static final String GROUP_OPERATOR = "sculk.group.operator"; + public static final String GROUP_USER = "sculk.group.user"; + +} diff --git a/src/main/java/org/sculk/permission/Permissible.java b/src/main/java/org/sculk/permission/Permissible.java new file mode 100644 index 0000000..c792c79 --- /dev/null +++ b/src/main/java/org/sculk/permission/Permissible.java @@ -0,0 +1,23 @@ +package org.sculk.permission; + + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface Permissible { + + + +} diff --git a/src/main/java/org/sculk/player/Player.java b/src/main/java/org/sculk/player/Player.java new file mode 100644 index 0000000..09658a7 --- /dev/null +++ b/src/main/java/org/sculk/player/Player.java @@ -0,0 +1,302 @@ +package org.sculk.player; + +import co.aikar.timings.Timings; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import lombok.Getter; +import org.cloudburstmc.protocol.bedrock.data.AttributeData; +import org.cloudburstmc.protocol.bedrock.data.command.*; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin; +import org.cloudburstmc.protocol.bedrock.packet.*; +import org.sculk.Server; +import org.sculk.command.Command; +import org.sculk.command.CommandSender; +import org.sculk.entity.Attribute; +import org.sculk.entity.AttributeFactory; +import org.sculk.entity.HumanEntity; +import org.sculk.entity.data.SyncedEntityData; +import org.sculk.event.player.PlayerChatEvent; +import org.sculk.form.Form; +import org.sculk.network.session.SculkServerSession; +import org.sculk.player.chat.StandardChatFormatter; +import org.sculk.player.client.ClientChainData; +import org.sculk.player.client.LoginChainData; +import org.sculk.player.text.RawTextBuilder; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class Player extends HumanEntity implements PlayerInterface, CommandSender { + + @Getter + private final SculkServerSession networkSession; + private final SyncedEntityData data = new SyncedEntityData(this); + private LoginChainData loginChainData; + + private AtomicInteger formId; + private Int2ObjectOpenHashMap
forms; + private List attributeMap; + + private String displayName; + private String username; + + protected int messageCounter = 2; + protected int MAX_CHAT_CHAR_LENGTH = 512; + protected int MAX_CHAT_BYTES_LENGTH = MAX_CHAT_CHAR_LENGTH * 2; + + public Player(SculkServerSession networkSession, ClientChainData data) { + this.networkSession = networkSession; + this.loginChainData = data; + + this.formId = new AtomicInteger(0); + this.forms = new Int2ObjectOpenHashMap<>(); + + this.displayName = data.getUsername(); + this.username = data.getUsername(); + + System.out.println(this.username); + + initEntity(); + } + + @Override + public void initEntity() { + super.initEntity(); + System.out.println("init Entity"); + sendCommandsData(); + } + + public void sendCommandsData() { + AvailableCommandsPacket availableCommandsPacket = new AvailableCommandsPacket(); + List commandData = availableCommandsPacket.getCommands(); + List commandSend = new ArrayList<>(); + for(Command command : this.getServer().getCommandMap().getCommands().values()) { + if (commandSend.contains(command.getLabel())){ + continue; + } + commandData.add(command.getCommandData().toNetwork()); + commandSend.add(command.getLabel()); + } + sendDataPacket(availableCommandsPacket); + } + + public void updateFlags() { + this.data.setFlags(EntityFlag.BREATHING, true); + this.data.updateFlag(); + } + + public void kick(String message) { + DisconnectPacket packet = new DisconnectPacket(); + packet.setKickMessage(message); + sendDataPacket(packet); + } + + public void processLogin() { + getServer().getLogger().info("process login call"); + + } + + public void completeLogin() { + ResourcePacksInfoPacket resourcePacksInfoPacket = new ResourcePacksInfoPacket(); + sendDataPacket(resourcePacksInfoPacket); + + ResourcePackClientResponsePacket resourcePackClientResponsePacket2 = new ResourcePackClientResponsePacket(); + resourcePackClientResponsePacket2.setStatus(ResourcePackClientResponsePacket.Status.HAVE_ALL_PACKS); + sendDataPacket(resourcePackClientResponsePacket2); + + ResourcePackClientResponsePacket resourcePackClientResponsePacket = new ResourcePackClientResponsePacket(); + resourcePackClientResponsePacket.setStatus(ResourcePackClientResponsePacket.Status.COMPLETED); + sendDataPacket(resourcePackClientResponsePacket); + Server.getInstance().getLogger().info("call pack stack"); + + ResourcePackStackPacket resourcePackStackPacket = new ResourcePackStackPacket(); + resourcePackStackPacket.setForcedToAccept(false); + resourcePackStackPacket.setGameVersion("*"); + sendDataPacket(resourcePackStackPacket); + Server.getInstance().getLogger().info("resourcePackStackPacket"); + + + //sendDataPacket(startGamePacket); + Server.getInstance().getLogger().info("call startgame"); + + this.getServer().addOnlinePlayer(this); + getServer().onPlayerCompleteLogin(this); + } + + public long getUniqueId() { + return UUID.randomUUID().getMostSignificantBits(); + } + + public long getRuntimeId() { + return UUID.randomUUID().getMostSignificantBits(); + } + + @Override + public String getName() { + return this.username; + } + + @Override + public UUID getServerId() { + return null; + } + + @Override + public Server getServer() { + return Server.getInstance(); + } + + public boolean sendDataPacket(BedrockPacket packet) { + sendPacketInternal(packet); + return true; + } + + public void sendPacketInternal(BedrockPacket packet) { + this.networkSession.sendPacket(packet); + } + + public SerializedSkin getSerializedSkin() { + return ((ClientChainData) this.loginChainData).getSerializedSkin(); + } + + public void sendAttributes() { + UpdateAttributesPacket updateAttributesPacket = new UpdateAttributesPacket(); + updateAttributesPacket.setRuntimeEntityId(this.getRuntimeId()); + List attributes = updateAttributesPacket.getAttributes(); + + Attribute hunger = AttributeFactory.getINSTANCE().mustGet(Attribute.HUNGER); + attributes.add(new AttributeData(hunger.getId(), hunger.getMinValue(), hunger.getMaxValue(), hunger.getCurrentValue(), hunger.getDefaultValue())); + + Attribute experienceLevel = AttributeFactory.getINSTANCE().mustGet(Attribute.EXPERIENCE_LEVEL); + attributes.add(new AttributeData(experienceLevel.getId(), experienceLevel.getMinValue(), experienceLevel.getMaxValue(), experienceLevel.getCurrentValue(), experienceLevel.getDefaultValue())); + + Attribute experience = AttributeFactory.getINSTANCE().mustGet(Attribute.EXPERIENCE); + attributes.add(new AttributeData(experience.getId(), experience.getMinValue(), experience.getMaxValue(), experience.getCurrentValue(), experience.getDefaultValue())); + + updateAttributesPacket.setAttributes(attributes); + sendDataPacket(updateAttributesPacket); + System.out.println(updateAttributesPacket); + } + + public long getPing() { + return -1; + } + + /** + * + * Used to send forms to the player + * + * @param form The form sent to the player + */ + public int openForm(Form form) { + int id = this.formId.getAndIncrement(); + this.forms.put(id, form); + + ModalFormRequestPacket packet = new ModalFormRequestPacket(); + packet.setFormId(id); + packet.setFormData(form.toJson().toString()); + + this.sendDataPacket(packet); + return id; + } + + /** + * + * Retrieve an already opened form from the map. + * The form will be deleted from the map upon retrieval. + * + * @param id The id given when opening the form + * @return {@link Form} + */ + public Form getForm(int id) { + return this.forms.remove(id); + } + + @Override + public void onUpdate() { + this.messageCounter = 2; + super.onUpdate(); + } + + public boolean onChat(String message) { + if(message.startsWith("./")) { + message = message.substring(1); + } + if(message.startsWith("/")) { + String command = message.substring(1); + Timings.playerCommandTimer.startTiming(); + this.getServer().dispatchCommand(this, command, false); + Timings.playerCommandTimer.stopTiming(); + } else { + PlayerChatEvent playerChatEvent = new PlayerChatEvent(this, message, new StandardChatFormatter()); + playerChatEvent.call(); + if(!playerChatEvent.isCancelled()) { + // TODO please change for use this.messageCount + String messageFormat = playerChatEvent.getChatFormatter().format(this.getName(), message); + this.getNetworkSession().onChatMessage(messageFormat); + this.getServer().getLogger().info(messageFormat); + } + } + return true; + } + + public void sendMessage(String message) { + this.getNetworkSession().onChatMessage(message); + } + + public void sendMessage(RawTextBuilder textBuilder) { + this.getNetworkSession().onChatMessage(textBuilder); + } + + public void sendJukeboxPopup(String message) { + this.getNetworkSession().onJukeboxPopup(message); + } + + public void sendPopup(String message) { + this.getNetworkSession().onPopup(message); + } + + public void sendTip(String message) { + this.getNetworkSession().onTip(message); + } + + public void sendAnnouncement(String message) { + this.getNetworkSession().onAnnouncement(message); + } + + public void sendAnnouncement(RawTextBuilder rawTextBuilder) { + this.getNetworkSession().onAnnouncement(rawTextBuilder); + } + + public void sendMessageSystem(String message) { + this.getNetworkSession().onMessageSystem(message); + } + + public void sendWhisper(String message) { + this.getNetworkSession().onWhisper(message); + } + + public void sendWhisper(RawTextBuilder rawTextBuilder) { + this.getNetworkSession().onWhisper(rawTextBuilder); + } + + public void sendMessageTranslation(String translate, List parameters) { + this.getNetworkSession().onMessageTranslation(translate, parameters); + } + +} diff --git a/src/main/java/org/sculk/player/PlayerInterface.java b/src/main/java/org/sculk/player/PlayerInterface.java new file mode 100644 index 0000000..fd8315c --- /dev/null +++ b/src/main/java/org/sculk/player/PlayerInterface.java @@ -0,0 +1,29 @@ +package org.sculk.player; + + +import org.sculk.Server; + +import java.util.UUID; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface PlayerInterface { + + String getName(); + UUID getServerId(); + Server getServer(); + +} diff --git a/src/main/java/org/sculk/player/PlayerLoginData.java b/src/main/java/org/sculk/player/PlayerLoginData.java new file mode 100644 index 0000000..b8615b8 --- /dev/null +++ b/src/main/java/org/sculk/player/PlayerLoginData.java @@ -0,0 +1,72 @@ +package org.sculk.player; + + +import lombok.Getter; +import org.cloudburstmc.protocol.bedrock.BedrockServerSession; +import org.cloudburstmc.protocol.common.util.QuadConsumer; +import org.sculk.network.session.SculkServerSession; +import org.sculk.scheduler.AsyncTask; + +import java.util.List; +import java.util.function.Consumer; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class PlayerLoginData { + + private final SculkServerSession session; + private boolean shouldLogin; + private AsyncTask preLoginEventTask; + private List> loginTasks; + @Getter + private final QuadConsumer authCallback; + + public PlayerLoginData(SculkServerSession serverSession, QuadConsumer authCallback) { + this.session = serverSession; + this.shouldLogin = false; + this.authCallback = authCallback; + } + + + public boolean isShouldLogin() { + return shouldLogin; + } + + public void setShouldLogin(boolean shouldLogin) { + this.shouldLogin = shouldLogin; + } + + public BedrockServerSession getSession() { + return session; + } + + public void setPreLoginEventTask(AsyncTask asyncTask) { + this.preLoginEventTask = asyncTask; + } + + public AsyncTask getPreLoginEventTask() { + return preLoginEventTask; + } + + public void setLoginTasks(List> loginTasks) { + this.loginTasks = loginTasks; + } + + public List> getLoginTasks() { + return loginTasks; + } + +} diff --git a/src/main/java/org/sculk/player/chat/ChatFormatter.java b/src/main/java/org/sculk/player/chat/ChatFormatter.java new file mode 100644 index 0000000..c834106 --- /dev/null +++ b/src/main/java/org/sculk/player/chat/ChatFormatter.java @@ -0,0 +1,23 @@ +package org.sculk.player.chat; + + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface ChatFormatter { + + String format(String username, String message); + +} diff --git a/src/main/java/org/sculk/player/chat/StandardChatFormatter.java b/src/main/java/org/sculk/player/chat/StandardChatFormatter.java new file mode 100644 index 0000000..7e7512d --- /dev/null +++ b/src/main/java/org/sculk/player/chat/StandardChatFormatter.java @@ -0,0 +1,26 @@ +package org.sculk.player.chat; + + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class StandardChatFormatter implements ChatFormatter { + + @Override + public String format(String username, String message) { + return "<" + username + "> " + message; + } + +} diff --git a/src/main/java/org/sculk/player/client/ClientChainData.java b/src/main/java/org/sculk/player/client/ClientChainData.java new file mode 100644 index 0000000..742ecb5 --- /dev/null +++ b/src/main/java/org/sculk/player/client/ClientChainData.java @@ -0,0 +1,329 @@ +package org.sculk.player.client; + + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.crypto.ECDSAVerifier; +import com.nimbusds.jwt.SignedJWT; +import io.netty.buffer.ByteBuf; +import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin; +import org.cloudburstmc.protocol.bedrock.packet.LoginPacket; +import org.sculk.Sculk; +import org.sculk.player.skin.Skin; +import org.sculk.utils.SkinUtils; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.ECPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.text.ParseException; +import java.util.*; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + * + */ +public class ClientChainData implements LoginChainData { + + private static final String MOJANG_PUBLIC_KEY_BASE64 = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8ELkixyLcwlZryUQcu1TvPOmI2B7vX83ndnWRUaXm74wFfa5f/lwQNTfrLVHa2PmenpGI6JhIMUJaWZrjmMj90NoKNFSNBuKdm8rYiXsfaz3K36x/1U26HpG0ZxK/V1V"; + private static final ECPublicKey MOJANG_PUBLIC_KEY; + + private static final TypeReference>> MAP_TYPE_REFERENCE = new TypeReference>>() {}; + + static { + try { + MOJANG_PUBLIC_KEY = generateKey(MOJANG_PUBLIC_KEY_BASE64); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + private final List chainData; + private final SignedJWT skinData; + private SerializedSkin serializedSkin; + private Skin skin; + + public final static int UI_PROFILE_CLASSIC = 0; + public final static int UI_PROFILE_POCKET = 1; + + private String username; + private UUID clientUUID; + private String xuid; + + private String identityPublicKey; + + private long clientId; + private String serverAddress; + private String deviceModel; + private int deviceOS; + private String deviceId; + private String gameVersion; + private int guiScale; + private String languageCode; + private int currentInputMode; + private int defaultInputMode; + + private int UIProfile; + + private ClientChainData(List chain, SignedJWT skinData) { + this.chainData = chain; + this.skinData = skinData; + decodeChainData(chainData); + decodeSkinData(skinData); + } + + @Override + public String getUsername() { + return username; + } + + @Override + public UUID getClientUUID() { + return clientUUID; + } + + @Override + public String getIdentityPublicKey() { + return identityPublicKey; + } + + @Override + public long getClientId() { + return clientId; + } + + @Override + public String getServerAddress() { + return serverAddress; + } + + @Override + public String getDeviceModel() { + return deviceModel; + } + + @Override + public int getDeviceOS() { + return deviceOS; + } + + @Override + public String getDeviceId() { + return deviceId; + } + + @Override + public String getGameVersion() { + return gameVersion; + } + + @Override + public int getGuiScale() { + return guiScale; + } + + @Override + public String getLanguageCode() { + return languageCode; + } + + @Override + public String getXUID() { + return xuid; + } + + private boolean xboxAuthed; + + @Override + public int getCurrentInputMode() { + return currentInputMode; + } + + @Override + public int getDefaultInputMode() { + return defaultInputMode; + } + + @Override + public int getUIProfile() { + return UIProfile; + } + + public static ClientChainData read(LoginPacket pk) { + return new ClientChainData(pk.getChain().stream().map(ClientChainData::parseJwt).toList(), parseJwt(pk.getExtra())); + } + + private static SignedJWT parseJwt(String jwt) { + try { + return SignedJWT.parse(jwt); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + private static ECPublicKey generateKey(String base64) throws NoSuchAlgorithmException, InvalidKeySpecException { + return (ECPublicKey) KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(base64))); + } + + private static String readString(ByteBuf buffer) { + int length = buffer.readIntLE(); + byte[] bytes = new byte[length]; + buffer.readBytes(bytes); + return new String(bytes, StandardCharsets.US_ASCII); // base 64 encoded. + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + ClientChainData that = (ClientChainData) obj; + return Objects.equals(this.skinData, that.skinData) && Objects.equals(this.chainData, that.chainData); + } + + @Override + public int hashCode() { + return Objects.hash(skinData, chainData); + } + + @Override + public boolean isXboxAuthed() { + return xboxAuthed; + } + + private boolean verify(ECPublicKey key, JWSObject object) throws JOSEException { + return object.verify(new ECDSAVerifier(key)); + } + + private JsonNode decodeToken(SignedJWT token) { + try { + return Sculk.JSON_MAPPER.readTree(token.getPayload().toBytes()); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid token JSON", e); + } + } + + private void decodeChainData(List chains) { + try { + xboxAuthed = verifyChain(chains); + } catch (Exception e) { + xboxAuthed = false; + } + + for (SignedJWT c : chains) { + JsonNode chainMap = decodeToken(c); + if (chainMap == null) continue; + if (chainMap.has("extraData")) { + JsonNode extra = chainMap.get("extraData"); + if (extra.has("displayName")) this.username = extra.get("displayName").textValue(); + if (extra.has("identity")) this.clientUUID = UUID.fromString(extra.get("identity").textValue()); + if (extra.has("XUID")) this.xuid = extra.get("XUID").textValue(); + } + if (chainMap.has("identityPublicKey")) + this.identityPublicKey = chainMap.get("identityPublicKey").textValue(); + } + + if (!xboxAuthed) { + xuid = null; + } + } + + private boolean verifyChain(List chains) throws Exception { + ECPublicKey lastKey = null; + boolean mojangKeyVerified = false; + Iterator iterator = chains.iterator(); + while (iterator.hasNext()) { + SignedJWT jwt = iterator.next(); + + URI x5u = jwt.getHeader().getX509CertURL(); + if (x5u == null) { + return false; + } + + ECPublicKey expectedKey = generateKey(x5u.toString()); + if (lastKey == null) { + lastKey = expectedKey; + } else if (!lastKey.equals(expectedKey)) { + return false; + } + + if (!verify(lastKey, jwt)) { + return false; + } + + if (mojangKeyVerified) { + return !iterator.hasNext(); + } + + if (lastKey.equals(MOJANG_PUBLIC_KEY)) { + mojangKeyVerified = true; + } + + Map payload = jwt.getPayload().toJSONObject(); + Object base64key = payload.get("identityPublicKey"); + if (!(base64key instanceof String)) { + throw new RuntimeException("No key found"); + } + lastKey = generateKey((String) base64key); + } + return mojangKeyVerified; + } + + @Override + public Skin getSkin() { + return skin; + } + + public SerializedSkin getSerializedSkin() { + if (this.serializedSkin == null) { + this.serializedSkin = SkinUtils.toSerialized(this.skin); + } + return this.serializedSkin; + } + + @Override + public void setSkin(Skin skin) { + this.skin = skin; + this.serializedSkin = SkinUtils.toSerialized(skin); + } + + public void setSkin(SerializedSkin skin) { + this.serializedSkin = skin; + this.skin = SkinUtils.fromSerialized(skin); + } + + private void decodeSkinData(SignedJWT skinData) { + JsonNode skinToken = decodeToken(skinData); + if (skinToken == null) return; + if (skinToken.has("ClientRandomId")) this.clientId = skinToken.get("ClientRandomId").longValue(); + if (skinToken.has("ServerAddress")) this.serverAddress = skinToken.get("ServerAddress").textValue(); + if (skinToken.has("DeviceModel")) this.deviceModel = skinToken.get("DeviceModel").textValue(); + if (skinToken.has("DeviceOS")) this.deviceOS = skinToken.get("DeviceOS").intValue(); + if (skinToken.has("DeviceId")) this.deviceId = skinToken.get("DeviceId").textValue(); + if (skinToken.has("GameVersion")) this.gameVersion = skinToken.get("GameVersion").textValue(); + if (skinToken.has("GuiScale")) this.guiScale = skinToken.get("GuiScale").intValue(); + if (skinToken.has("LanguageCode")) this.languageCode = skinToken.get("LanguageCode").textValue(); + if (skinToken.has("CurrentInputMode")) this.currentInputMode = skinToken.get("CurrentInputMode").intValue(); + if (skinToken.has("DefaultInputMode")) this.defaultInputMode = skinToken.get("DefaultInputMode").intValue(); + if (skinToken.has("UIProfile")) this.UIProfile = skinToken.get("UIProfile").intValue(); + this.skin = SkinUtils.fromToken(skinToken); + } + +} diff --git a/src/main/java/org/sculk/player/client/LoginChainData.java b/src/main/java/org/sculk/player/client/LoginChainData.java new file mode 100644 index 0000000..a3d93dc --- /dev/null +++ b/src/main/java/org/sculk/player/client/LoginChainData.java @@ -0,0 +1,43 @@ +package org.sculk.player.client; + +import org.sculk.player.skin.Skin; + +import java.util.UUID; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface LoginChainData { + + String getUsername(); + UUID getClientUUID(); + String getIdentityPublicKey(); + long getClientId(); + String getServerAddress(); + String getDeviceModel(); + int getDeviceOS(); + String getDeviceId(); + String getGameVersion(); + int getGuiScale(); + String getLanguageCode(); + String getXUID(); + boolean isXboxAuthed(); + int getCurrentInputMode(); + int getDefaultInputMode(); + Skin getSkin(); + void setSkin(Skin skin); + int getUIProfile(); + +} diff --git a/src/main/java/org/sculk/player/skin/Skin.java b/src/main/java/org/sculk/player/skin/Skin.java new file mode 100644 index 0000000..2e46887 --- /dev/null +++ b/src/main/java/org/sculk/player/skin/Skin.java @@ -0,0 +1,72 @@ +package org.sculk.player.skin; + + +import lombok.Data; +import lombok.ToString; +import org.sculk.player.skin.data.ImageData; +import org.sculk.player.skin.data.PersonaPiece; +import org.sculk.player.skin.data.PersonaPieceTint; +import org.sculk.player.skin.data.SkinAnimation; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@Data +@ToString(exclude = {"geometryData", "animationData"}) +public class Skin { + + private final String fullSkinId; + private String skinId; + private String playFabId; + private String skinResourcePatch = GEOMETRY_CUSTOM; + private ImageData skinData; + private List animations; + private final List personaPieces = new ArrayList<>(); + private final List tintColors = new ArrayList<>(); + private ImageData capeData; + private String geometryData; + private String animationData; + private boolean premium; + private boolean persona; + private boolean capeOnClassic; + private String capeId; + private String skinColor = "#0"; + private String armSize = "wide"; + private boolean trusted = false; + + public static final String GEOMETRY_CUSTOM = convertLegacyGeometryName("geometry.humanoid.custom"); + public static final String GEOMETRY_CUSTOM_SLIM = convertLegacyGeometryName("geometry.humanoid.customSlim"); + + private static String convertLegacyGeometryName(String geometryName) { + return "{\"geometry\" : {\"default\" : \"" + geometryName + "\"}}"; + } + + public Skin(String fullSkinId) { + this.fullSkinId = fullSkinId; + } + + public Skin() { + this(UUID.randomUUID().toString()); + } + + public boolean isValid() { + return skinId != null && !skinId.trim().isEmpty() && skinData != null && skinData.getWidth() >= 64 && skinData.getHeight() >= 32; + } + +} diff --git a/src/main/java/org/sculk/player/skin/data/ImageData.java b/src/main/java/org/sculk/player/skin/data/ImageData.java new file mode 100644 index 0000000..d3fae71 --- /dev/null +++ b/src/main/java/org/sculk/player/skin/data/ImageData.java @@ -0,0 +1,30 @@ +package org.sculk.player.skin.data; + + +import lombok.Data; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@Data +public class ImageData { + + private final int width; + private final int height; + private final byte[] image; + + public static final ImageData EMPTY = new ImageData(0, 0, new byte[0]); + +} diff --git a/src/main/java/org/sculk/player/skin/data/PersonaPiece.java b/src/main/java/org/sculk/player/skin/data/PersonaPiece.java new file mode 100644 index 0000000..fe82296 --- /dev/null +++ b/src/main/java/org/sculk/player/skin/data/PersonaPiece.java @@ -0,0 +1,30 @@ +package org.sculk.player.skin.data; + + +import lombok.Data; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@Data +public class PersonaPiece { + + private final String id; + private final String type; + private final String packId; + private final boolean isDefault; + private final String productId; + +} diff --git a/src/main/java/org/sculk/player/skin/data/PersonaPieceTint.java b/src/main/java/org/sculk/player/skin/data/PersonaPieceTint.java new file mode 100644 index 0000000..839475e --- /dev/null +++ b/src/main/java/org/sculk/player/skin/data/PersonaPieceTint.java @@ -0,0 +1,35 @@ +package org.sculk.player.skin.data; + + +import com.google.common.collect.ImmutableList; +import lombok.Data; + +import java.util.List; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@Data +public class PersonaPieceTint { + + private final String pieceType; + private final ImmutableList colors; + + public PersonaPieceTint(String pieceType, List colors) { + this.pieceType = pieceType; + this.colors = ImmutableList.copyOf(colors); + } + +} diff --git a/src/main/java/org/sculk/player/skin/data/SkinAnimation.java b/src/main/java/org/sculk/player/skin/data/SkinAnimation.java new file mode 100644 index 0000000..f3d1e38 --- /dev/null +++ b/src/main/java/org/sculk/player/skin/data/SkinAnimation.java @@ -0,0 +1,29 @@ +package org.sculk.player.skin.data; + + +import lombok.Data; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@Data +public class SkinAnimation { + + private final ImageData imageData; + private final int type; + private final float frames; + private final int expression; + +} diff --git a/src/main/java/org/sculk/player/text/IJsonText.java b/src/main/java/org/sculk/player/text/IJsonText.java new file mode 100644 index 0000000..6ca63c9 --- /dev/null +++ b/src/main/java/org/sculk/player/text/IJsonText.java @@ -0,0 +1,23 @@ +package org.sculk.player.text; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface IJsonText extends Cloneable{ + + String getName(); + + Object build(); +} \ No newline at end of file diff --git a/src/main/java/org/sculk/player/text/RawTextBuilder.java b/src/main/java/org/sculk/player/text/RawTextBuilder.java new file mode 100644 index 0000000..28dc80f --- /dev/null +++ b/src/main/java/org/sculk/player/text/RawTextBuilder.java @@ -0,0 +1,53 @@ +package org.sculk.player.text; + +import java.util.*; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class RawTextBuilder implements IJsonText{ + + public List build; + + public RawTextBuilder() { + build = new ArrayList<>(); + } + + @Override + public String getName() { + return "rawtext"; + } + + public RawTextBuilder add(IJsonText text) { + this.build.add(text); + return this; + } + + @Override + public Object build() { + HashMap> map = new HashMap<>(); + map.put(this.getName(), this.build.stream().map(IJsonText::build).toList()); + return map; + } + + @Override + public String toString() { + StringBuilder text = new StringBuilder(); + for (IJsonText jsonText: build){ + text.append(jsonText.toString()); + } + return text.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/org/sculk/player/text/ScoreBuilder.java b/src/main/java/org/sculk/player/text/ScoreBuilder.java new file mode 100644 index 0000000..48d7c78 --- /dev/null +++ b/src/main/java/org/sculk/player/text/ScoreBuilder.java @@ -0,0 +1,50 @@ +package org.sculk.player.text; + +import java.util.HashMap; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class ScoreBuilder implements IJsonText{ + + + private String name; + private String objective; + + @Override + public String getName() { + return "score"; + } + + public ScoreBuilder setName(String name) { + this.name = name; + return this; + } + + public ScoreBuilder setObjective(String objective) { + this.objective = objective; + return this; + } + + @Override + public Object build() { + HashMap> score = new HashMap<>(); + HashMap data = new HashMap<>(); + data.put("name", this.name); + data.put("objective", this.objective); + score.put("score", data); + return score; + } +} \ No newline at end of file diff --git a/src/main/java/org/sculk/player/text/SelectorBuilder.java b/src/main/java/org/sculk/player/text/SelectorBuilder.java new file mode 100644 index 0000000..c3e0c69 --- /dev/null +++ b/src/main/java/org/sculk/player/text/SelectorBuilder.java @@ -0,0 +1,29 @@ +package org.sculk.player.text; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class SelectorBuilder implements IJsonText{ + + @Override + public String getName() { + return ""; + } + + @Override + public Object build() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sculk/player/text/TextBuilder.java b/src/main/java/org/sculk/player/text/TextBuilder.java new file mode 100644 index 0000000..9becc1b --- /dev/null +++ b/src/main/java/org/sculk/player/text/TextBuilder.java @@ -0,0 +1,50 @@ +package org.sculk.player.text; + +import lombok.NonNull; + +import java.util.HashMap; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class TextBuilder implements IJsonText{ + + private @NonNull String text; + public TextBuilder() { + text = ""; + } + + public TextBuilder setText(@NonNull String text) { + this.text = text; + return this; + } + + @Override + public String getName() { + return "text"; + } + + @Override + public Object build() { + HashMap map = new HashMap<>(); + map.put(this.getName(), text); + return map; + } + + @Override + public String toString() { + return this.text; + } +} \ No newline at end of file diff --git a/src/main/java/org/sculk/player/text/TranslaterBuilder.java b/src/main/java/org/sculk/player/text/TranslaterBuilder.java new file mode 100644 index 0000000..2017fc1 --- /dev/null +++ b/src/main/java/org/sculk/player/text/TranslaterBuilder.java @@ -0,0 +1,153 @@ +package org.sculk.player.text; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class TranslaterBuilder implements IJsonText { + + public String translate; + private Object with; + private static final Pattern PATTERN_STRING = Pattern.compile("%%(s)"); + private static final Pattern PATTERN_INDEX = Pattern.compile("%%(\\d+)"); + + public TranslaterBuilder() { + with = null; + + } + + public TranslaterBuilder setTranslate(String translate) { + this.translate = translate; + return this; + } + + public TranslaterBuilder addWith(String data) { + if (this.with == null) + this.with = Arrays.asList(data); + else if (this.with instanceof ArrayList) { + ArrayList _with = (ArrayList) this.with; + _with.add(data); + } + return this; + } + + public TranslaterBuilder addWith(ArrayList data) { + if (this.with == null) + this.with = data; + else if (this.with instanceof ArrayList) { + ArrayList _with = (ArrayList) this.with; + _with.addAll(data); + } + return this; + } + + public TranslaterBuilder setWith(RawTextBuilder text) { + if (this.with == null) + this.with = text; + return this; + } + + @Override + public String getName() { + return "translate"; + } + + + @Override + public Object build() { + HashMap map = new HashMap<>(); + map.put(this.getName(), this.translate); + if (this.with != null) { + Object with = this.with; + + if (with instanceof RawTextBuilder rawTextBuilder) { + with = rawTextBuilder.build(); + } + map.put("with", with); + } + return map; + } + + @Override + public String toString() { + String data = translate; + if (this.with != null) { + List clone = getCloneOfWith(); + + if (clone != null) { + data = replaceStringPatterns(data, clone.iterator()); + data = replaceIndexedPatterns(data, clone); + } + } + return data; + } + + private List getCloneOfWith() { + if (this.with instanceof ArrayList) { + return new ArrayList<>((ArrayList) this.with); + } else if (this.with instanceof RawTextBuilder) { + return new ArrayList<>(((RawTextBuilder) this.with).build); + } + return null; + } + + private String replaceIndexedPatterns(String data, List clone) { + Matcher matcher = PATTERN_INDEX.matcher(data); + StringBuilder result = new StringBuilder(); + int lastEnd = 0; + + while (matcher.find()) { + result.append(data, lastEnd, matcher.start()); + String group = matcher.group(); + int index; + try { + index = Integer.parseInt(group.substring(2, 3)); + } catch (NumberFormatException e) { + continue; + } + String replacer = safeGetAtIndex(clone, index - 1); + result.append(replacer); + lastEnd = matcher.end(); + } + result.append(data.substring(lastEnd)); + return result.toString(); + } + + private String replaceStringPatterns(String data, Iterator clone) { + Matcher matcher = PATTERN_STRING.matcher(data); + StringBuilder result = new StringBuilder(); + int lastEnd = 0; + + while (matcher.find()) { + result.append(data, lastEnd, matcher.start()); + String replacer = clone.hasNext() ? clone.next().toString() : ""; + result.append(replacer); + lastEnd = matcher.end(); + } + result.append(data.substring(lastEnd)); + return result.toString(); + } + + private String safeGetAtIndex(List list, int index) { + if (!list.isEmpty()) { + Object removed = list.get(index); + return removed != null ? removed.toString() : ""; + } + return ""; + } +} \ No newline at end of file diff --git a/src/main/java/org/sculk/plugin/Plugin.java b/src/main/java/org/sculk/plugin/Plugin.java new file mode 100644 index 0000000..f006162 --- /dev/null +++ b/src/main/java/org/sculk/plugin/Plugin.java @@ -0,0 +1,76 @@ +package org.sculk.plugin; + +import com.google.common.base.Preconditions; +import lombok.Getter; +import org.apache.logging.log4j.Logger; +import org.sculk.Server; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +@Getter +public abstract class Plugin { + protected boolean enabled = false; + private PluginData description; + private Server server; + private Logger logger; + private File pluginFile; + private File dataFolder; + private boolean initialized = false; + + protected final void init(PluginData description, Server server, File pluginFile) { + Preconditions.checkArgument(!this.initialized, "Plugin has been already initialized!"); + this.initialized = true; + this.description = description; + this.server = server; + this.logger = server.getLogger(); + + this.pluginFile = pluginFile; + this.dataFolder = new File(Server.getInstance().getDataPath() + "/plugins/" + description.getName().toLowerCase() + "/"); + + if (this.dataFolder.mkdirs()) { + this.logger.info("Created plugin data folder"); + + } + } + + public void onLoad() { + } + + public abstract void onEnable(); + + public void onDisable() { + } + + public InputStream getResourceFile(String filename) { + try(JarFile jar = new JarFile(this.pluginFile)) { + JarEntry entry = jar.getJarEntry(filename); + return jar.getInputStream(entry); + } catch (IOException e) { + return null; + } + } + + public void setEnabled(boolean enabled) { + if (this.enabled != enabled) { + this.enabled = enabled; + + try { + if (enabled) { + this.onEnable(); + } else { + this.onDisable(); + } + } catch (Exception e) { + this.logger.error("Error while enabling/disabling plugin " + this.getName() + ": " + e); + } + } + } + + public String getName() { + return this.description.getName(); + } +} \ No newline at end of file diff --git a/src/main/java/org/sculk/plugin/PluginClassLoader.java b/src/main/java/org/sculk/plugin/PluginClassLoader.java new file mode 100644 index 0000000..8eeb5fe --- /dev/null +++ b/src/main/java/org/sculk/plugin/PluginClassLoader.java @@ -0,0 +1,43 @@ +package org.sculk.plugin; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; + +public class PluginClassLoader extends URLClassLoader { + + private final PluginManager pluginManager; + private final Object2ObjectOpenHashMap> classes = new Object2ObjectOpenHashMap<>(); + + public PluginClassLoader(PluginManager pluginManager, ClassLoader parent, File file) throws MalformedURLException { + super(new URL[]{file.toURI().toURL()}, parent); + this.pluginManager = pluginManager; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + return this.findClass(name, true); + } + + protected Class findClass(String name, boolean checkGlobal) throws ClassNotFoundException { + Class result = this.classes.get(name); + if (result != null) { + return result; + } + + if (checkGlobal) { + result = this.pluginManager.getClassFromCache(name); + } + + if (result == null && (result = super.findClass(name)) != null) { + this.pluginManager.cacheClass(name, result); + } + + this.classes.put(name, result); + return result; + } + +} \ No newline at end of file diff --git a/src/main/java/org/sculk/plugin/PluginData.java b/src/main/java/org/sculk/plugin/PluginData.java new file mode 100644 index 0000000..f52e331 --- /dev/null +++ b/src/main/java/org/sculk/plugin/PluginData.java @@ -0,0 +1,27 @@ +package org.sculk.plugin; + +import lombok.Getter; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.ArrayList; +import java.util.List; + +@Getter +@ToString +public class PluginData { + public String name; + public String version; + public String author; + public String main; + public String api; + public List apis; + public List depends; + + public List getApi() { + if(apis == null) apis = new ArrayList<>(); + if(api != null && !apis.contains(api)) apis.add(api); + + return apis; + } +} \ No newline at end of file diff --git a/src/main/java/org/sculk/plugin/PluginLoader.java b/src/main/java/org/sculk/plugin/PluginLoader.java new file mode 100644 index 0000000..db3e768 --- /dev/null +++ b/src/main/java/org/sculk/plugin/PluginLoader.java @@ -0,0 +1,80 @@ +package org.sculk.plugin; + +import lombok.extern.log4j.Log4j2; +import org.sculk.Sculk; + +import java.io.File; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.nio.file.Path; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +@Log4j2 +public class PluginLoader { + + private final PluginManager pluginManager; + + public PluginLoader(PluginManager pluginManager) { + this.pluginManager = pluginManager; + } + + protected static boolean isJarFile(Path file) { + return file.getFileName().toString().endsWith(".jar"); + } + + protected PluginClassLoader loadClassLoader(PluginData pluginConfig, File pluginJar) { + try { + return new PluginClassLoader(this.pluginManager, this.getClass().getClassLoader(), pluginJar); + } catch (MalformedURLException e) { + log.error("Error while creating class loader(plugin={})", pluginConfig.getName()); + } + return null; + } + + protected Plugin loadPluginJAR(PluginData pluginConfig, File pluginJar, PluginClassLoader loader) { + try { + Class mainClass = loader.loadClass(pluginConfig.getMain()); + if (!Plugin.class.isAssignableFrom(mainClass)) { + return null; + } + + Class castedMain = mainClass.asSubclass(Plugin.class); + Plugin plugin = castedMain.getDeclaredConstructor().newInstance(); + plugin.init(pluginConfig, this.pluginManager.getServer(), pluginJar); + return plugin; + } catch (Exception e) { + log.error("Error while loading plugin main class(main={}, plugin={})", pluginConfig.getMain(), pluginConfig.getName(), e); + } + return null; + } + + protected PluginData loadPluginData(File file) { + try (JarFile pluginJar = new JarFile(file)) { + JarEntry configEntry = pluginJar.getJarEntry("sculk.yml"); + + if (configEntry == null) { + configEntry = pluginJar.getJarEntry("plugin.yml"); + } + + if (configEntry == null) { + log.warn("Jar file " + file.getName() + " doesnt contain a sculk.yml or plugin.yml!"); + return null; + } + + try (InputStream fileStream = pluginJar.getInputStream(configEntry)) { + PluginData pluginConfig = PluginManager.yamlLoader.loadAs(fileStream, PluginData.class); + if (pluginConfig.getMain() != null && pluginConfig.getName() != null && pluginConfig.getApi().contains(Sculk.CODE_VERSION.replace("v", ""))) { + // Valid plugin.yml, main and name set + return pluginConfig; + } + } + + log.warn("Invalid plugin.yml for " + file.getName() + ": main and/or name property missing, incompactible api version"); + + } catch (Exception e) { + log.error("Can not load plugin files in " + file.getPath(), e); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sculk/plugin/PluginManager.java b/src/main/java/org/sculk/plugin/PluginManager.java new file mode 100644 index 0000000..12c97d1 --- /dev/null +++ b/src/main/java/org/sculk/plugin/PluginManager.java @@ -0,0 +1,258 @@ +package org.sculk.plugin; + +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair; +import lombok.Getter; + +import org.sculk.Server; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.CustomClassLoaderConstructor; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Stream; + +public class PluginManager { + public static final Yaml yamlLoader; + + static { + Representer representer = new Representer(new DumperOptions()); + representer.getPropertyUtils().setSkipMissingProperties(true); + yamlLoader = new Yaml(new CustomClassLoaderConstructor(PluginManager.class.getClassLoader(), new LoaderOptions()), representer); + } + + @Getter + private final Server server; + private final PluginLoader pluginLoader; + + protected final Object2ObjectMap pluginClassLoaders = new Object2ObjectArrayMap<>(); + private final Object2ObjectMap pluginMap = new Object2ObjectArrayMap<>(); + private final Object2ObjectMap> cachedClasses = new Object2ObjectArrayMap<>(); + + private final List> pluginsToLoad = new ObjectArrayList<>(); + + public PluginManager(Server server) { + this.server = server; + this.pluginLoader = new PluginLoader(this); + try { + this.loadPluginsInside(Paths.get(server.getDataPath() + "/plugins/")); + } catch (IOException e) { + server.getLogger().error("Error while filtering plugin files: " + e); + } + } + + private void loadPluginsInside(Path folderPath) throws IOException { + Comparator comparator = (o1, o2) -> { + if (o2.getName().equals(o1.getName())) { + return 0; + } + if (o2.getDepends() == null) { + return 1; + } + return o2.getDepends().contains(o1.getName()) ? -1 : 1; + }; + + Map plugins = new TreeMap<>(comparator); + try (Stream stream = Files.walk(folderPath)) { + stream.filter(Files::isRegularFile).filter(PluginLoader::isJarFile).forEach(jarPath -> { + PluginData config = this.loadPluginConfig(jarPath); + if (config != null) { + plugins.put(config, jarPath); + } + }); + } + plugins.forEach(this::registerClassLoader); + } + + private PluginData loadPluginConfig(Path path) { + if (!Files.isRegularFile(path) || !PluginLoader.isJarFile(path)) { + server.getLogger().warn("Cannot load plugin: Provided file is no jar file: " + path.getFileName()); + return null; + } + + File pluginFile = path.toFile(); + if (!pluginFile.exists()) { + return null; + } + + return this.pluginLoader.loadPluginData(pluginFile); + } + + private PluginClassLoader registerClassLoader(PluginData config, Path path) { + if (this.getPluginByName(config.getName()) != null) { + server.getLogger().warn("Plugin is already loaded: " + config.getName()); + return null; + } + + PluginClassLoader classLoader = this.pluginLoader.loadClassLoader(config, path.toFile()); + if (classLoader != null) { + this.pluginClassLoaders.put(config.getName(), classLoader); + this.pluginsToLoad.add(ObjectObjectImmutablePair.of(config, path)); + server.getLogger().debug("Loaded class loader from " + path.getFileName()); + } + return classLoader; + } + + public void loadAllPlugins() { + for (Pair pair : this.pluginsToLoad) { + this.loadPlugin(pair.key(), pair.value()); + } + this.pluginsToLoad.clear(); + } + + public Plugin loadPlugin(PluginData config, Path path) { + File pluginFile = path.toFile(); + if (this.getPluginByName(config.getName()) != null) { + server.getLogger().warn("Plugin is already loaded: " + config.getName()); + return null; + } + + PluginClassLoader classLoader = this.pluginClassLoaders.get(config.getName()); + if (classLoader == null) { + classLoader = this.registerClassLoader(config, path); + } + + if (classLoader == null) { + return null; + } + + Plugin plugin = this.pluginLoader.loadPluginJAR(config, pluginFile, classLoader); + if (plugin == null) { + return null; + } + + try { + plugin.onLoad(); + } catch (Exception e) { + server.getLogger().error("Failed to load plugin " + config.getName() + ": " + e); + return null; + } + + server.getLogger().info("Loaded plugin " + config.getName() + " successfully!"); + this.pluginMap.put(config.getName(), plugin); + return plugin; + } + + public void enableAllPlugins() { + LinkedList failed = new LinkedList<>(); + + for (Plugin plugin : this.pluginMap.values()) { + if (!this.enablePlugin(plugin, null)) { + failed.add(plugin); + } + } + + if (failed.isEmpty()) { + return; + } + + server.getLogger().warn("§cFailed to load plugins: §e" + String.join(", ", failed.stream() + .map(Plugin::getName) + .toList())); + } + + public boolean enablePlugin(Plugin plugin, String parent) { + if (plugin.isEnabled()) { + return true; + } + + if (plugin.getDescription().getDepends() != null && !this.checkDependencies(plugin, parent)) { + return false; + } + + try { + plugin.setEnabled(true); + } catch (Exception e) { + server.getLogger().error(e.getMessage()); + return false; + } + return true; + } + + private boolean checkDependencies(Plugin plugin, String parent) { + String pluginName = plugin.getName(); + if (plugin.getDescription().getDepends() != null) { + for (String depend : plugin.getDescription().getDepends()) { + if (depend.equals(parent)) { + server.getLogger().warn("§cCannot enable plugin " + pluginName + ", circular dependency " + parent + "!"); + return false; + } + + Plugin dependPlugin = this.getPluginByName(depend); + if (dependPlugin == null) { + server.getLogger().warn("§cCannot enable plugin " + pluginName + ", missing dependency " + depend + "!"); + + return false; + } + + if (!dependPlugin.isEnabled() && !this.enablePlugin(dependPlugin, pluginName)) { + return false; + } + } + } + + return true; + } + + public void disableAllPlugins() { + for (Plugin plugin : this.pluginMap.values()) { + server.getLogger().info("Disabling plugin " + plugin.getName() + "!"); + try { + plugin.setEnabled(false); + } catch (Exception e) { + server.getLogger().error(e.getMessage()); + } + } + } + + public Class getClassFromCache(String className) { + Class clazz = this.cachedClasses.get(className); + if (clazz != null) { + return clazz; + } + + for (PluginClassLoader loader : this.pluginClassLoaders.values()) { + try { + + clazz = loader.findClass(className, false); + if (clazz != null) { + this.cachedClasses.put(className, clazz); // Cache the found class + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + } + return null; + } + + protected void cacheClass(String className, Class clazz) { + this.cachedClasses.putIfAbsent(className, clazz); + } + + public Map getPluginMap() { + return Collections.unmodifiableMap(this.pluginMap); + } + + public Collection getPlugins() { + return Collections.unmodifiableCollection(this.pluginMap.values()); + } + + public Collection getPluginClassLoaders() { + return Collections.unmodifiableCollection(this.pluginClassLoaders.values()); + } + + public Plugin getPluginByName(String pluginName) { + return this.pluginMap.getOrDefault(pluginName, null); + } +} \ No newline at end of file diff --git a/src/main/java/org/sculk/scheduler/AsyncTask.java b/src/main/java/org/sculk/scheduler/AsyncTask.java new file mode 100644 index 0000000..fd139ca --- /dev/null +++ b/src/main/java/org/sculk/scheduler/AsyncTask.java @@ -0,0 +1,104 @@ +package org.sculk.scheduler; + + +import co.aikar.timings.Timing; +import co.aikar.timings.Timings; +import lombok.extern.java.Log; +import lombok.extern.log4j.Log4j2; +import org.sculk.Server; +import org.sculk.thread.ThreadStore; + +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@Log4j2 +public abstract class AsyncTask implements Runnable { + + public static final Queue LIST = new ConcurrentLinkedQueue<>(); + + private Object result; + private int taskId; + private boolean finish = false; + + @Override + public void run() { + this.result = null; + this.onRun(); + this.finish = true; + LIST.offer(this); + } + + public abstract void onRun(); + + public void onCompletion(Server server) {} + + public boolean isFinish() { + return finish; + } + + public Object getResult() { + return result; + } + + public boolean hasResult() { + return this.result != null; + } + + public void setResult(Object result) { + this.result = result; + } + + public void setTaskId(int taskId) { + this.taskId = taskId; + } + + public int getTaskId() { + return taskId; + } + + public Object getThreadStore(String identifier) { + return this.isFinish() ? null : ThreadStore.store.get(identifier); + } + + public void saveThreadStore(String identifier, Object value) { + if(this.isFinish()) { + return; + } + ThreadStore.store.put(identifier, value == null ? ThreadStore.store.remove(identifier) : value); + } + + public static void collectTask() { + try(Timing timing = Timings.schedulerSyncTimer.startTiming()) { + while(!LIST.isEmpty()) { + AsyncTask task = LIST.poll(); + try { + task.onCompletion(Server.getInstance()); + } catch(Exception e) { + Server.getInstance().getLogger().error("Exception while async task {} invoking onCompletion", e); + } + } + } + } + + public void clear() { + this.result = null; + this.taskId = 0; + this.finish = false; + } +} diff --git a/src/main/java/org/sculk/scheduler/Scheduler.java b/src/main/java/org/sculk/scheduler/Scheduler.java new file mode 100644 index 0000000..0b99c48 --- /dev/null +++ b/src/main/java/org/sculk/scheduler/Scheduler.java @@ -0,0 +1,159 @@ +package org.sculk.scheduler; + + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; +import org.sculk.exception.TaskException; + +import java.util.ArrayDeque; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.atomic.AtomicInteger; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@Singleton +@Log4j2 +public class Scheduler { + + private final AtomicInteger currentTaskId; + private final Queue taskHandlerQueue; + private final Map taskHandlerMap; + private final Map> integerArrayDequeMap; + private final ForkJoinPool asyncPool; + + private volatile int currentTick = -1; + + @Inject + public Scheduler() { + this.currentTaskId = new AtomicInteger(); + this.taskHandlerQueue = new ConcurrentLinkedQueue<>(); + this.taskHandlerMap = new ConcurrentHashMap<>(); + this.integerArrayDequeMap = new ConcurrentHashMap<>(); + this.asyncPool = ForkJoinPool.commonPool(); + } + + private int nextTaskId() { + return currentTaskId.incrementAndGet(); + } + + public AtomicInteger getCurrentTaskId() { + return currentTaskId; + } + + public TaskHandler scheduleAsyncTask(AsyncTask task) { + return addTask(task, 0, 0, true); + } + + public int getQueueSize() { + int size = taskHandlerQueue.size(); + for(ArrayDeque queue : integerArrayDequeMap.values()) { + size += queue.size(); + } + return size; + } + + public ForkJoinPool getAsyncPool() { + return asyncPool; + } + + public int getAsyncPoolSize() { + return getAsyncPool().getPoolSize(); + } + + @SneakyThrows + private TaskHandler addTask(Runnable task, int delay, int period, boolean async) { + if(delay < 0 || period < 0) { + throw new TaskException("Attempted to register a task with negative delay or period"); + } + + TaskHandler taskHandler = new TaskHandler(task, this.nextTaskId(), async); + taskHandler.setDelay(delay); + taskHandler.setPeriod(period); + taskHandler.setNextRunTick(taskHandler.isDelayed() ? currentTick + taskHandler.getDelay() : currentTick); + + if(task instanceof Task) { + ((Task) task).setTaskHandler(taskHandler); + } + taskHandlerQueue.offer(taskHandler); + taskHandlerMap.put(taskHandler.getTaskId(), taskHandler); + return taskHandler; + } + + public void mainThread(int currentTick) { + TaskHandler taskHandler; + while((taskHandler = taskHandlerQueue.poll()) != null) { + int tick = Math.max(currentTick, taskHandler.getNextRunTick()); + this.integerArrayDequeMap.computeIfAbsent(tick, integer -> new ArrayDeque<>()).add(taskHandler); + } + if(currentTick - this.currentTick > integerArrayDequeMap.size()) { + for(Map.Entry> entry : integerArrayDequeMap.entrySet()) { + int tick = entry.getKey(); + if(tick <= currentTick) { + runTask(currentTick); + } + } + } else { + for(int i = this.currentTick +1; i <= currentTick; i++) { + runTask(currentTick); + } + } + this.currentTick = currentTick; + AsyncTask.collectTask(); + } + + private void runTask(int currentTick) { + ArrayDeque queue = integerArrayDequeMap.remove(currentTick); + if(queue != null) { + for(TaskHandler taskHandler : queue) { + if(taskHandler.isCancelled()) { + taskHandlerMap.remove(taskHandler.getTaskId()); + } else if(taskHandler.isAsynchronous()) { + asyncPool.execute(taskHandler.getTask()); + } else { + taskHandler.timing.stopTiming(); + } + if(taskHandler.isRepeating()) { + taskHandler.setNextRunTick(currentTick + taskHandler.getPeriod()); + taskHandlerQueue.offer(taskHandler); + } else { + try { + TaskHandler remove = taskHandlerMap.remove(taskHandler.getTaskId()); + if(remove != null) { + remove.cancel(); + } + } catch(RuntimeException exception) { + log.error("Exception while invoking onCancel", exception); + } + } + } + } + } + + public TaskHandler scheduleDelayedTask(Task task, int delay, boolean async) { + return this.addTask(task, delay, 0, async); + } + + public TaskHandler scheduleDelayedTask(Runnable task, int delay, boolean async) { + return this.addTask(task, delay, 0, async); + } + +} diff --git a/src/main/java/org/sculk/scheduler/Task.java b/src/main/java/org/sculk/scheduler/Task.java new file mode 100644 index 0000000..a732896 --- /dev/null +++ b/src/main/java/org/sculk/scheduler/Task.java @@ -0,0 +1,57 @@ +package org.sculk.scheduler; + + +import co.aikar.timings.Timing; +import co.aikar.timings.Timings; +import lombok.extern.log4j.Log4j2; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@Log4j2 +public abstract class Task implements Runnable { + + private TaskHandler taskHandler = null; + + public TaskHandler getTaskHandler() { + return taskHandler; + } + + public final int getTaskId() { + return getTaskHandler() != null ? getTaskHandler().getTaskId() : -1; + } + + public void setTaskHandler(TaskHandler taskHandler) { + this.taskHandler = taskHandler; + } + + public abstract void onRun(int currentTick); + + @Override + public final void run() { + this.onRun(taskHandler.getLastRunTick()); + } + + public void onCancel() {} + + public void cancel() { + try { + this.getTaskHandler().cancel(); + } catch (RuntimeException ex) { + log.error("Exception while invoking onCancel", ex); + } + } + +} diff --git a/src/main/java/org/sculk/scheduler/TaskHandler.java b/src/main/java/org/sculk/scheduler/TaskHandler.java new file mode 100644 index 0000000..281f737 --- /dev/null +++ b/src/main/java/org/sculk/scheduler/TaskHandler.java @@ -0,0 +1,127 @@ +package org.sculk.scheduler; + + +import co.aikar.timings.Timing; +import co.aikar.timings.Timings; +import lombok.extern.log4j.Log4j2; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@Log4j2 +public class TaskHandler { + + private final int taskId; + private final boolean async; + private final Runnable task; + public final Timing timing; + + private int period; + private int delay; + private int lastRunTick; + private int nextRunTick; + private boolean cancelled; + + public TaskHandler(Runnable task, int taskId, boolean async) { + this.async = async; + this.task = task; + this.taskId = taskId; + this.timing = Timings.getTaskTiming(this, period); + } + + public boolean isCancelled() { + return this.cancelled; + } + + public int getNextRunTick() { + return this.nextRunTick; + } + + public void setNextRunTick(int nextRunTick) { + this.nextRunTick = nextRunTick; + } + + public int getTaskId() { + return this.taskId; + } + + public Runnable getTask() { + return this.task; + } + + public int getDelay() { + return this.delay; + } + + public boolean isDelayed() { + return this.delay > 0; + } + + public boolean isRepeating() { + return this.period > 0; + } + + public int getPeriod() { + return this.period; + } + + + public int getLastRunTick() { + return lastRunTick; + } + + public void setLastRunTick(int lastRunTick) { + this.lastRunTick = lastRunTick; + } + + public void cancel() { + if (!this.isCancelled() && this.task instanceof Task) { + ((Task) this.task).onCancel(); + } + this.cancelled = true; + } + + @Deprecated + public void remove() { + this.cancelled = true; + } + + public void run(int currentTick) { + try { + setLastRunTick(currentTick); + getTask().run(); + } catch (RuntimeException ex) { + log.error("Exception while invoking run", ex); + } + } + + @Deprecated + public String getTaskName() { + return "Unknown"; + } + + public boolean isAsynchronous() { + return async; + } + + public void setDelay(int delay) { + this.delay = delay; + } + + public void setPeriod(int period) { + this.period = period; + } + +} diff --git a/src/main/java/org/sculk/thread/ThreadFactoryBuilder.java b/src/main/java/org/sculk/thread/ThreadFactoryBuilder.java deleted file mode 100644 index 3f257cb..0000000 --- a/src/main/java/org/sculk/thread/ThreadFactoryBuilder.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.sculk.thread; - -import java.util.Locale; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; - -import lombok.Builder; - -/* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * @author: SculkTeams - * @link: http://www.sculkmp.org/ - */ -@Builder -public class ThreadFactoryBuilder implements ThreadFactory { - - private static final ThreadFactory threadFactory = Executors.defaultThreadFactory(); - private final AtomicInteger count = new AtomicInteger(0); - - private final boolean daemon; - private final String format; - - private final int priority = Thread.currentThread().getPriority(); - private final Thread.UncaughtExceptionHandler exceptionHandler; - - private static String format(String format, int count) { - return String.format(Locale.ROOT, format, count); - } - - @Override - public Thread newThread(Runnable r) { - Thread thread = threadFactory.newThread(r); - if(format != null) { - thread.setName(format(format, count.getAndIncrement())); - } - thread.setDaemon(daemon); - thread.setPriority(priority); - if(exceptionHandler != null) { - thread.setUncaughtExceptionHandler(exceptionHandler); - } - return thread; - } - -} diff --git a/src/main/java/org/sculk/thread/ThreadStore.java b/src/main/java/org/sculk/thread/ThreadStore.java new file mode 100644 index 0000000..aa3ee78 --- /dev/null +++ b/src/main/java/org/sculk/thread/ThreadStore.java @@ -0,0 +1,26 @@ +package org.sculk.thread; + + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class ThreadStore { + + public static final Map store = new ConcurrentHashMap<>(); + +} diff --git a/src/main/java/org/sculk/timings/JsonUtil.java b/src/main/java/org/sculk/timings/JsonUtil.java new file mode 100644 index 0000000..d7382f6 --- /dev/null +++ b/src/main/java/org/sculk/timings/JsonUtil.java @@ -0,0 +1,87 @@ +package org.sculk.timings; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public class JsonUtil { + + private static final JsonMapper MAPPER = new JsonMapper(); + + public static ArrayNode toArray(Object... objects) { + ArrayNode array = MAPPER.createArrayNode(); + for (Object object : objects) { + array.addPOJO(object); + } + return array; + } + + public static JsonNode toObject(Object object) { + return MAPPER.valueToTree(object); + } + + public static JsonNode mapToObject(Iterable collection, Function mapper) { + Map object = new LinkedHashMap(); + for (E e : collection) { + JSONPair pair = mapper.apply(e); + if (pair != null) { + object.put(pair.key, pair.value); + } + } + return MAPPER.valueToTree(object); + } + + public static ArrayNode mapToArray(E[] elements, Function mapper) { + ArrayList array = new ArrayList(); + Collections.addAll(array, elements); + return mapToArray(array, mapper); + } + + public static ArrayNode mapToArray(Iterable collection, Function mapper) { + ArrayNode node = MAPPER.createArrayNode(); + for (E e : collection) { + Object obj = mapper.apply(e); + if (obj != null) { + node.addPOJO(obj); + } + } + return node; + } + + public static class JSONPair { + public final String key; + public final Object value; + + public JSONPair(String key, Object value) { + this.key = key; + this.value = value; + } + + public JSONPair(int key, Object value) { + this.key = String.valueOf(key); + this.value = value; + } + } + +} diff --git a/src/main/java/org/sculk/utils/ExperienceUtils.java b/src/main/java/org/sculk/utils/ExperienceUtils.java new file mode 100644 index 0000000..bb15309 --- /dev/null +++ b/src/main/java/org/sculk/utils/ExperienceUtils.java @@ -0,0 +1,83 @@ +package org.sculk.utils; + + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public abstract class ExperienceUtils { + + public static int getXpToReachLevel(int level) { + if (level <= 16) { + return level * level + level * 6; + } else if (level <= 31) { + return (int) (level * level * 2.5 - 40.5 * level + 360); + } + return (int) (level * level * 4.5 - 162.5 * level + 2220); + } + + public static int getXpToCompleteLevel(int level) { + if(level <= 15) { + return 2 * level + 7; + } + else if(level <= 30) { + return 5 * level - 38; + } else { + return 9 * level - 158; + } + } + + public static float getLevelFromXp(int xp) { + if(xp < 0) { + throw new IllegalArgumentException("XP must be at least 0"); + } + double a, b, c; + + if (xp <= getXpToReachLevel(16)) { + a = 1; + b = 6; + c = 0; + } else if (xp <= getXpToReachLevel(31)) { + a = 2.5; + b = -40.5; + c = 360; + } else { + a = 4.5; + b = -162.5; + c = 2220; + } + + double[] solutions = solveQuadratic(a, b, c - xp); + if(solutions.length == 0) { + throw new RuntimeException("Expected at least 1 solution"); + } + return (float) Math.max(solutions[0], solutions[1]); + } + + private static double[] solveQuadratic(double a, double b, double c) { + double discriminant = b * b - 4 * a * c; + if (discriminant < 0) { + return new double[0]; + } else if (discriminant == 0) { + return new double[] { -b / (2 * a) }; + } else { + double sqrtDiscriminant = Math.sqrt(discriminant); + return new double[] { + (-b + sqrtDiscriminant) / (2 * a), + (-b - sqrtDiscriminant) / (2 * a) + }; + } + } + +} diff --git a/src/main/java/org/sculk/utils/Identifiers.java b/src/main/java/org/sculk/utils/Identifiers.java new file mode 100644 index 0000000..6384ef0 --- /dev/null +++ b/src/main/java/org/sculk/utils/Identifiers.java @@ -0,0 +1,23 @@ +package org.sculk.utils; + + +import lombok.experimental.UtilityClass; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@UtilityClass +public class Identifiers { +} diff --git a/src/main/java/org/sculk/utils/SkinUtils.java b/src/main/java/org/sculk/utils/SkinUtils.java new file mode 100644 index 0000000..fedabce --- /dev/null +++ b/src/main/java/org/sculk/utils/SkinUtils.java @@ -0,0 +1,211 @@ +package org.sculk.utils; + + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.experimental.UtilityClass; +import org.cloudburstmc.protocol.bedrock.data.skin.*; +import org.sculk.player.skin.Skin; +import org.sculk.player.skin.data.ImageData; +import org.sculk.player.skin.data.PersonaPiece; +import org.sculk.player.skin.data.PersonaPieceTint; +import org.sculk.player.skin.data.SkinAnimation; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +@UtilityClass +public class SkinUtils { + + public static Skin fromSerialized(SerializedSkin skin) { + Skin newSkin = new Skin(skin.getFullSkinId()); + newSkin.setSkinId(skin.getSkinId()); + newSkin.setPlayFabId(skin.getPlayFabId()); + newSkin.setSkinResourcePatch(skin.getSkinResourcePatch()); + newSkin.setSkinData(new ImageData(skin.getSkinData().getWidth(), skin.getSkinData().getWidth(), skin.getSkinData().getImage())); + List animations = new ArrayList<>(); + for (AnimationData data : skin.getAnimations()) { + animations.add(new SkinAnimation( + new ImageData(data.getImage().getWidth(), data.getImage().getHeight(), data.getImage().getImage()), + data.getTextureType().ordinal(), + data.getFrames(), + data.getExpressionType().ordinal())); + } + skin.getPersonaPieces().forEach((piece) -> newSkin.getPersonaPieces().add(new PersonaPiece(piece.getId(), piece.getType(), piece.getPackId(), piece.isDefault(), piece.getProductId()))); + skin.getTintColors().forEach(color -> newSkin.getTintColors().add(new PersonaPieceTint(color.getType(), color.getColors()))); + newSkin.setCapeData(new ImageData(skin.getCapeData().getWidth(), skin.getCapeData().getWidth(), skin.getCapeData().getImage())); + newSkin.setGeometryData(skin.getGeometryData()); + newSkin.setAnimationData(skin.getAnimationData()); + newSkin.setPremium(skin.isPremium()); + newSkin.setPersona(skin.isPersona()); + newSkin.setCapeOnClassic(skin.isCapeOnClassic()); + newSkin.setCapeId(skin.getCapeId()); + newSkin.setSkinColor(skin.getSkinColor()); + newSkin.setArmSize(skin.getArmSize()); + newSkin.setTrusted(false); + return newSkin; + } + + public static SerializedSkin toSerialized(Skin skin) { + SerializedSkin.Builder builder = SerializedSkin.builder(); + builder.skinId(skin.getSkinId()) + .fullSkinId(skin.getFullSkinId()) + .playFabId(skin.getPlayFabId()) + .skinResourcePatch(skin.getSkinResourcePatch()) + .skinData(org.cloudburstmc.protocol.bedrock.data.skin.ImageData.of(skin.getSkinData().getWidth(), skin.getSkinData().getHeight(), skin.getSkinData().getImage())) + .capeData(org.cloudburstmc.protocol.bedrock.data.skin.ImageData.of(skin.getCapeData().getWidth(), skin.getCapeData().getHeight(), skin.getCapeData().getImage())) + .geometryData(skin.getGeometryData()) + .animationData(skin.getAnimationData()) + .premium(skin.isPremium()) + .persona(skin.isPersona()) + .capeOnClassic(skin.isCapeOnClassic()) + .capeId(skin.getCapeId()) + .skinColor(skin.getSkinColor()) + .armSize(skin.getArmSize()); + + List animations = new ArrayList<>(); + List personas = new ArrayList<>(); + List tints = new ArrayList<>(); + + //skin.getAnimations().forEach(animation -> animations.add(new AnimationData(org.cloudburstmc.protocol.bedrock.data.skin.ImageData.of(animation.getImage().getWidth(), animation.getImage().getHeight(), animation.getImage().getImage()), AnimatedTextureType.values()[animation.getType()], animation.getFrames(), AnimationExpressionType.values()[animation.getExpression()]))); + skin.getPersonaPieces().forEach(piece -> personas.add(new PersonaPieceData(piece.getId(), piece.getType(), piece.getPackId(), piece.isDefault(), piece.getProductId()))); + skin.getTintColors().forEach(color -> tints.add(new PersonaPieceTintData(color.getPieceType(), color.getColors()))); + + builder.animations(animations).personaPieces(personas).tintColors(tints); + + return builder.build(); + } + + public static Skin fromToken(JsonNode skinToken) { + Skin newSkin = new Skin(); + + if (skinToken.has("SkinId")) { + newSkin.setSkinId(skinToken.get("SkinId").textValue()); + } + if (skinToken.has("PlayFabId")) { + newSkin.setPlayFabId(skinToken.get("PlayFabId").textValue()); + } + if (skinToken.has("SkinResourcePatch")) { + newSkin.setSkinResourcePatch(new String(Base64.getDecoder().decode(skinToken.get("SkinResourcePatch").textValue()), StandardCharsets.UTF_8)); + } + + newSkin.setSkinData(getImage(skinToken, "Skin")); + + if (skinToken.has("AnimatedImageData")) { + List animations = new ArrayList<>(); + JsonNode array = skinToken.get("AnimatedImageData"); + for (JsonNode element : array) { + animations.add(getAnimation(element)); + } + newSkin.setAnimations(animations); + } + + if (skinToken.has("PersonaSkin")) { + newSkin.setPersona(skinToken.get("PersonaSkin").booleanValue()); + if (skinToken.has("PersonaPieces")) { + for (JsonNode piece : skinToken.get("PersonaPieces")) { + newSkin.getPersonaPieces().add(new PersonaPiece( + piece.get("PieceId").textValue(), + piece.get("PieceType").textValue(), + piece.get("PackId").textValue(), + piece.get("IsDefault").booleanValue(), + piece.get("ProductId").textValue() + )); + } + } + if (skinToken.has("PieceTintColors")) { + for (JsonNode node : skinToken.get("PieceTintColors")) { + List colors = new ArrayList<>(); + for (JsonNode color : node.get("Colors")) { + colors.add(color.textValue()); + } + newSkin.getTintColors().add(new PersonaPieceTint( + node.get("PieceType").textValue(), + colors + )); + } + } + } + + newSkin.setCapeData(getImage(skinToken, "Cape")); + + if (skinToken.has("SkinGeometryData")) { + newSkin.setGeometryData(new String(Base64.getDecoder().decode(skinToken.get("SkinGeometryData").textValue()), StandardCharsets.UTF_8)); + } + if (skinToken.has("SkinAnimationData")) { + newSkin.setAnimationData(new String(Base64.getDecoder().decode(skinToken.get("SkinAnimationData").textValue()), StandardCharsets.UTF_8)); + } + if (skinToken.has("PremiumSkin")) { + newSkin.setPremium(skinToken.get("PremiumSkin").booleanValue()); + } + if (skinToken.has("CapeOnClassicSkin")) { + newSkin.setCapeOnClassic(skinToken.get("CapeOnClassicSkin").booleanValue()); + } + if (skinToken.has("CapeId")) { + newSkin.setCapeId(skinToken.get("CapeId").textValue()); + } + if (skinToken.has("SkinColor")) { + newSkin.setSkinColor(skinToken.get("SkinColor").textValue()); + } + if (skinToken.has("ArmSize")) { + newSkin.setArmSize(skinToken.get("ArmSize").textValue()); + } + + return newSkin; + } + + private static SkinAnimation getAnimation(JsonNode element) { + float frames = element.get("Frames").floatValue(); + int type = element.get("Type").intValue(); + byte[] data = Base64.getDecoder().decode(element.get("Image").textValue()); + int width = element.get("ImageWidth").intValue(); + int height = element.get("ImageHeight").intValue(); + int expression = 0; + if (element.hasNonNull("ExpressionType")) { + expression = element.get("ExpressionType").intValue(); + } + return new SkinAnimation(new ImageData(width, height, data), type, frames, expression); + } + + private static ImageData getImage(JsonNode token, String name) { + if (token.has(name + "Data")) { + byte[] skinImage = Base64.getDecoder().decode(token.get(name + "Data").textValue()); + if (token.has(name + "ImageHeight") && token.has(name + "ImageWidth")) { + int width = token.get(name + "ImageWidth").intValue(); + int height = token.get(name + "ImageHeight").intValue(); + return new ImageData(width, height, skinImage); + } else { + switch (skinImage.length / 4) { + case 2048: + return new ImageData(64, 32, skinImage); + case 4096: + return new ImageData(64, 64, skinImage); + case 8192: + return new ImageData(128, 64, skinImage); + case 16384: + return new ImageData(128, 128, skinImage); + default: + return ImageData.EMPTY; + } + } + } + return ImageData.EMPTY; + } + +} diff --git a/src/main/java/org/sculk/utils/TextFormat.java b/src/main/java/org/sculk/utils/TextFormat.java index 96c48e1..4d16b64 100644 --- a/src/main/java/org/sculk/utils/TextFormat.java +++ b/src/main/java/org/sculk/utils/TextFormat.java @@ -1,11 +1,11 @@ package org.sculk.utils; /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -17,21 +17,39 @@ */ public class TextFormat { - public static String BLACK = "§0"; - public static String DARK_BLUE = "§1"; - public static String DARK_GREEN = "§2"; - public static String DARK_AQUA = "§3"; - public static String DARK_RED = "§4"; - public static String DARK_PURPLE = "§5"; - public static String GOLD = "§6"; - public static String GRAY = "§7"; - public static String DARK_GRAY = "§8"; - public static String BLUE = "§9"; - public static String GREEN = "§a"; - public static String AQUA = "§b"; - public static String RED = "§c"; - public static String LIGHT_PURPLE = "§d"; - public static String YELLOW = "§e"; - public static String WHITE = "§f"; + public static String BLACK = "§0"; + public static String DARK_BLUE = "§1"; + public static String DARK_GREEN = "§2"; + public static String DARK_AQUA = "§3"; + public static String DARK_RED = "§4"; + public static String DARK_PURPLE = "§5"; + public static String GOLD = "§6"; + public static String GRAY = "§7"; + public static String DARK_GRAY = "§8"; + public static String BLUE = "§9"; + public static String GREEN = "§a"; + public static String AQUA = "§b"; + public static String RED = "§c"; + public static String LIGHT_PURPLE = "§d"; + public static String YELLOW = "§e"; + public static String WHITE = "§f"; + public static String MINECOIN_GOLD = "§g"; + public static String MATERIAL_QUARTZ = "§h"; + public static String MATERIAL_IRON = "§i"; + public static String MATERIAL_NETHERITE = "§j"; + public static String MATERIAL_REDSTONE = "§m"; + public static String MATERIAL_COPPER = "§n"; + public static String MATERIAL_GOLD = "§p"; + public static String MATERIAL_EMERALD = "§q"; + public static String MATERIAL_DIAMOND = "§s"; + public static String MATERIAL_LAPIS = "§t"; + public static String MATERIAL_AMETHYST = "§u"; + // Style modifiers (italic, bold, underline, etc) + public static String OBFUSCATED = "§k"; + public static String BOLD = "§l"; + public static String STRIKETHROUGH = "§m"; + public static String UNDERLINE = "§n"; + public static String ITALIC = "§o"; + public static String RESET = "§r"; } diff --git a/src/main/java/org/sculk/utils/Utils.java b/src/main/java/org/sculk/utils/Utils.java index a95568d..c1a8b2e 100644 --- a/src/main/java/org/sculk/utils/Utils.java +++ b/src/main/java/org/sculk/utils/Utils.java @@ -4,11 +4,11 @@ import java.nio.charset.StandardCharsets; /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by diff --git a/src/main/java/org/sculk/utils/json/Serializable.java b/src/main/java/org/sculk/utils/json/Serializable.java new file mode 100644 index 0000000..56e25d0 --- /dev/null +++ b/src/main/java/org/sculk/utils/json/Serializable.java @@ -0,0 +1,28 @@ +package org.sculk.utils.json; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +/* + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * @author: SculkTeams + * @link: http://www.sculkmp.org/ + */ +public interface Serializable { + + JsonObject toJson(); + + class GsonHolder { + public static Gson GSON = new Gson(); + } +} diff --git a/src/main/java/org/sculk/world/WorldManager.java b/src/main/java/org/sculk/world/WorldManager.java index 0a8c9fb..f4648ef 100644 --- a/src/main/java/org/sculk/world/WorldManager.java +++ b/src/main/java/org/sculk/world/WorldManager.java @@ -1,11 +1,11 @@ package org.sculk.world; /* - * ____ _ _ __ __ ____ - * / ___| ___ _ _| | | __ | \/ | _ \ - * \___ \ / __| | | | | |/ / _____ | |\/| | |_) | - * ___) | (__| |_| | | < |_____| | | | | __/ - * |____/ \___|\__,_|_|_|\_\ |_| |_|_| + * ____ _ _ + * / ___| ___ _ _| | | __ + * \___ \ / __| | | | | |/ / + * ___) | (__| |_| | | < + * |____/ \___|\__,_|_|_|\_\ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index 19337a5..b4d605b 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -1,5 +1,5 @@ - +