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
+
+
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 extends Event> 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 extends Player> clazz = event.getPlayerClass();
+ Player player;
+
+ try {
+ Constructor extends Player> 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 extends Player> baseClass;
+ @Getter
+ @Setter
+ private Class extends Player> 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