diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml
index 6e1abd9..5afcc3c 100644
--- a/.forgejo/workflows/build.yml
+++ b/.forgejo/workflows/build.yml
@@ -2,31 +2,30 @@ on: [push]
jobs:
build:
runs-on: docker
- container: debian:sid
+ container: eclipse-temurin:21-alpine
steps:
- - name: Install JDK and other deps
- run: apt update && apt install --no-install-recommends -y openjdk-21-jdk-headless maven git nodejs curl zstd
+ - name: Install build dependencies
+ run: apk add nodejs curl tar zstd
- - name: Clone repository
- run: git clone -b ${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} https://git.m724.eu/Minecon724/tweaks724.git .
+ - name: Checkout
+ uses: https://github.com/actions/checkout@v4
- name: Download NMS
run: ./tools/download_nms.sh ~
-
- - name: Build for 1.21.1
- run: mkdir artifact && mvn clean package && mv target/tweaks-*+1.21.1.jar artifact/
+ - name: Build for 1.21.4
+ run: ./mvnw package -Dproject.minecraft.version=1.21.4 -Dproject.craftbukkit.version=v1_21_R3
- name: Build for 1.21.3
- run: find src \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s/v1_21_R1/v1_21_R2/g" && mvn clean package -Dproject.minecraft.version=1.21.3 && mv target/tweaks-*+1.21.3.jar artifact/
+ run: ./mvnw package -Dproject.minecraft.version=1.21.3 -Dproject.craftbukkit.version=v1_21_R2
- - name: Build for 1.21.4
- run: find src \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s/v1_21_R2/v1_21_R3/g" && mvn clean package -Dproject.minecraft.version=1.21.4 && mv target/tweaks-*+1.21.4.jar artifact/
+ - name: Build for 1.21.1
+ run: ./mvnw package -Dproject.minecraft.version=1.21.1 -Dproject.craftbukkit.version=v1_21_R1
- name: Upload artifacts
uses: https://github.com/actions/upload-artifact@v3
with:
- path: artifact
\ No newline at end of file
+ path: target/tweaks-*.jar
\ No newline at end of file
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..d58dfb7
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
deleted file mode 100644
index e1404cb..0000000
--- a/DEVELOPMENT.md
+++ /dev/null
@@ -1,5 +0,0 @@
-To setup NMS:
-1. Download BuildTools, move it into an empty directory and open terminal
-2. ```
- java -jar BuildTools.jar --rev 1.21.1 --remapped
- ```
\ No newline at end of file
diff --git a/README.md b/README.md
index 77f6fe9..3f18c19 100644
--- a/README.md
+++ b/README.md
@@ -12,37 +12,37 @@ Please report all suspicious behavior. You can do so on any of those:
Stuff not many other plugins do.
Dependencies:
-- **1.21.1**, recommended as the plugin uses NMS for some stuff \
- However, it's not forced, so it's quite probable it'll work on other versions. Please test. If you get an error, it's most often because of a module using NMS. Disable that module, and report the error \
- Why not latest? The focus is on [a widely used version](https://bstats.org/global/bukkit) that has [good mod support](https://modrinth.com/modpack/fabulously-optimized/versions?c=release)
+- **1.21.1 and newer**
- [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/)
+- To use modules marked N, you must use a JAR [made for the exact server version.](/Minecon724/tweaks724/src/branch/master/docs/BUILDING.md)
# Features
-### Expand world border
+N - requires a specific version \
+P - requires ProtocolLib
+
+### Expand world border N
Expands the world border to 30,000,000 (from 29,999,984)
-### Hide world border
+### Hide world border P
Hides the world border. It's still there, just invisible.
-### Server brand
+### Server brand N P
Modify the F3 brand, optionally include player's ping and server performance
### Doors
Open two doors with one click. Knock on doors.
-### MOTD
+### MOTD N P
Random MOTD for every ping
-### Chat rooms
-Chat rooms players can freely create and join. Alerts like death and join messages are only within the player's chat room.
+### Chat mods N
+- Chat rooms players can freely create and join. Alerts like death and join messages are only within the player's chat room.
+- Proximity chat
`/chat` - switch chat room \
`/chatmanage` - create, delete, modify etc. (`tweaks724.chatmanage`)
-### Proximity chat
-Self-explanatory
-
### Compass
Holding a compass shows a bar with 4 directions and stuff like beds, lodestones, death pos (TODO) etc.
@@ -52,20 +52,23 @@ Self-discipline with a pomodoro timer that's actually forced
`/pomodoro` (`tweaks724.pomodoro`)
### Updater
-Updates ALL* your plugins \
+Checks for updates of ALL* your plugins \
+Updates are still installed manually \
*Those on SpigotMC and that release updates there
`/updates` - shows available updates (`tweaks724.updates`)
-### Hardcore
+### Hardcore N P
Hardcore hearts by chance
### Sleep
Sleeping doesn't skip night, but speeds it up. The more players, the faster it goes.
- Instant sleep \
-One can instantly skip, but only a part of the night. \
-There's 5 players on the server. A night is 10 minutes long. \
-Each player can instantly skip 2 minutes of the night at any time, even if others aren't sleeping
+ One can instantly skip, but only a part of the night. \
+ There's 5 players on the server. A night is 10 minutes long. \
+ Each player can instantly skip 2 minutes of the night at any time, even if others aren't sleeping
+- Heal \
+ Sleeping heals
### Authentication
Players are given a unique subdomain like "\.example.com" and they must use it to join \
@@ -76,14 +79,38 @@ It can be enabled that new players can't join the server without a key
### Full join
Players with `tweaks724.bypass-full` can join even when the server is full
-### Emergency alerts
+### Emergency alerts P
Issue messages that the player needs to read to keep playing, and that make an attention grabbing sound
`/emergencyalerts` (`tweaks724.emergencyalerts`)
+### Remote redstone
+Adds a "gateway" item that are controlled over internet. \
+[RETSTONE.md for more info](/Minecon724/tweaks724/src/branch/master/docs/RETSTONE.md)
+
+### Knockback
+Control knockback dealt by entities
+
+### Kill switch
+Quickly kills (terminates) the server on trigger, via command or HTTP request.
+[KILLSWITCH.md for more info](/Minecon724/tweaks724/src/branch/master/docs/KILLSWITCH.md)
+
+### Swing through grass
+Self-explanatory
+
+### Durability alert
+Self-explanatory too.
+
+`/durabilityalert` (`tweaks724.durabilityalert`)
+
+### Word coords
+Convert coordinates to easier to remember words
+
+`/wordcoords` (`tweaks724.tauth`)
+
### Utility commands
-- `/ping` - displays player ping \
+- `/ping` - displays player ping P \
**Ping is calculated by the plugin**. \
That allows for more precision (decimal places) and to get the ping immediately after a player join
diff --git a/docs/BUILDING.md b/docs/BUILDING.md
new file mode 100644
index 0000000..7af000f
--- /dev/null
+++ b/docs/BUILDING.md
@@ -0,0 +1,32 @@
+First, download NMS. There are two ways:
+
+- Use `tools/download_nms.sh`
+- Download BuildTools, move it into an empty directory and run:
+ ```
+ java -jar BuildTools.jar --rev 1.21.4 --remapped
+ ```
+ You must run this for every version you want to build for.
+
+
+
+Then build the plugin:
+
+1. Clone this repository:
+ ```
+ git clone https://git.m724.eu/Minecon724/tweaks724
+ cd tweaks724
+ ```
+ - You might want to `checkout` a release:
+ ```
+ git checkout tags/tweaks-0.1.12
+ ```
+2. For the "native" version:
+ ```
+ ./mvnw package
+ ```
+ For another compatible version:
+ ```
+ ./mvnw package -Dproject.craftbukkit.version=v1_21_R3 -Dproject.minecraft.version=1.21.4
+ ```
+
+Look for `tweaks-0.1.12+1.21.4.jar` in `target/`
\ No newline at end of file
diff --git a/docs/KILLSWITCH.md b/docs/KILLSWITCH.md
new file mode 100644
index 0000000..4ac09e3
--- /dev/null
+++ b/docs/KILLSWITCH.md
@@ -0,0 +1,32 @@
+Killswitch immediately stops the server.
+
+### Warning
+This terminates the server process, meaning it's like you'd pull the power. \
+So you will lose some progress (since the last auto save), or worst case your world (or other data) gets corrupted.
+
+Terminal froze after kill? `reset`
+
+### Over a command
+No key is required. \
+`/servkill` is the command. Permission: `tweaks724.servkill`. \
+You must grant the permission manually, like with a permission plugin - it's not automatically assigned even to OPs.
+
+### Over HTTP
+HTTP is insecure, meaning others *could* intercept your request to the server and get your key. \
+To encrypt, put this behind a proxy to get HTTPS or a VPN (directly to the server, not a commercial VPN) \
+Or regenerate the key after every usage.
+
+Make a GET request to `/key/`
+
+Example:
+```
+https://127.0.0.1:57932/key/lNwANMSZhLiTWhNxSoqQ5Q==
+|_ server address _| |_ base64 encoded key _|
+```
+
+The response is a 404 no matter what. Either way, you will notice that the server has stopped.
+
+The key is generated to `plugins/Tweaks724/storage/killswitch key` \
+To use it with HTTP server, encode it to base64.
+
+Rate limit is 1 request / 5 minutes
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..c44787d
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,3 @@
+Here's the documentation.
+
+Click above on a file to read more about a topic.
\ No newline at end of file
diff --git a/docs/RETSTONE.md b/docs/RETSTONE.md
new file mode 100644
index 0000000..78d96ad
--- /dev/null
+++ b/docs/RETSTONE.md
@@ -0,0 +1,48 @@
+## Remote redstone
+
+See [retstone.py](retstone.py) for usage example
+
+### Glossary
+gateways - the blocks (daylight detector) that read or emit redstone with a specified power, controlled with UDP packets \
+packet - a bunch of bytes sent over the internet, that do something with a specified gateway
+
+### Crafting
+
+To craft a gateway, combine:
+- nether star
+- ender chest
+- chorus flower
+- daylight detector
+
+### Usage
+Each gateway has an ID assigned. To view it, shift right-click a gateway.
+
+To destroy a gateway, use silk touch.
+
+### How it works
+A packet is int / 4 bytes / 32 bits
+
+Packet format:
+```
+[ 01 ] [ 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ] [ 29 30 31 32 ]
+action bits 2-28 of repeater id payload
+```
+
+- action: `1` to write, `0` to read
+- payload: power level if writing
+
+If writing, no response \
+If reading, response is the power level, or 0 if not powered or no repeater with that ID (or not loaded)
+
+Reading powers the block down of course \
+BUT you should power it down (or read), wait some, and then read again
+
+**WARNING** the socket is not ratelimited or protected in any way. \
+Though it should be hard to bruteforce, because there's no feedback
+
+### Retstone
+
+**Network** translates to **reto** in Esperanto \
+So retsomething means networked something (posto - mail, retposto - email, ejo - place (site), retejo - website, etc.) \
+And sometimes we use network instead of internet, same is in that language \
+Hence retstone
\ No newline at end of file
diff --git a/docs/retstone.py b/docs/retstone.py
new file mode 100644
index 0000000..8bfeb7d
--- /dev/null
+++ b/docs/retstone.py
@@ -0,0 +1,44 @@
+# Copyright (C) 2024 Minecon724
+# Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+# in the project root for the full license text.
+
+import socket, struct
+
+ENDPOINT = ("127.0.0.1", 57931)
+
+def get_power(repeater_id: int) -> int | None:
+ message = repeater_id & 0x7FFFFFF0
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ sock.sendto(message.to_bytes(4), ENDPOINT)
+
+ return struct.unpack(">b", sock.recvfrom(1)[0])[0]
+
+def set_power(repeater_id: int, power: int):
+ message = repeater_id & 0x7FFFFFF0
+ message |= 0x80000000
+ message |= power & 0xF
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ sock.sendto(message.to_bytes(4), ENDPOINT)
+
+
+if __name__ == "__main__":
+ from argparse import ArgumentParser
+
+ parser = ArgumentParser()
+ parser.add_argument("repeater_id", help="The repeater ID", type=int)
+ parser.add_argument("-p", "--power", help="Power the repeater with specified power. Leave to read.", type=int)
+ parser.add_argument("-c", "--copy", help="Copy input of one repeater to another. If combined with power, power is added.", type=int)
+ args = parser.parse_args()
+
+ if args.copy is None:
+ if args.power is None:
+ power = get_power(args.repeater_id)
+ print(power)
+ else:
+ set_power(args.repeater_id, args.power)
+ else:
+ while True:
+ power = get_power(args.repeater_id)
+ set_power(args.copy, power)
\ No newline at end of file
diff --git a/mvnw b/mvnw
new file mode 100755
index 0000000..19529dd
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,259 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.3.2
+#
+# Optional ENV vars
+# -----------------
+# JAVA_HOME - location of a JDK home dir, required when download maven via java source
+# MVNW_REPOURL - repo url base for downloading maven distribution
+# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+ [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+ native_path() { cygpath --path --windows "$1"; }
+ ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+ # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+ if [ -n "${JAVA_HOME-}" ]; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACCMD="$JAVA_HOME/jre/sh/javac"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ JAVACCMD="$JAVA_HOME/bin/javac"
+
+ if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+ echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+ echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+ return 1
+ fi
+ fi
+ else
+ JAVACMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v java
+ )" || :
+ JAVACCMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v javac
+ )" || :
+
+ if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+ echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+ return 1
+ fi
+ fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+ str="${1:-}" h=0
+ while [ -n "$str" ]; do
+ char="${str%"${str#?}"}"
+ h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+ str="${str#?}"
+ done
+ printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+ printf %s\\n "$1" >&2
+ exit 1
+}
+
+trim() {
+ # MWRAPPER-139:
+ # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+ # Needed for removing poorly interpreted newline sequences when running in more
+ # exotic environments such as mingw bash on Windows.
+ printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+ case "${key-}" in
+ distributionUrl) distributionUrl=$(trim "${value-}") ;;
+ distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+ esac
+done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+ MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+ *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+ :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+ :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+ :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+ *)
+ echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+ distributionPlatform=linux-amd64
+ ;;
+ esac
+ distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+ ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+ unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+ exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+ verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+ clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+ trap clean HUP INT TERM EXIT
+else
+ die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+ distributionUrl="${distributionUrl%.zip}.tar.gz"
+ distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+ verbose "Found wget ... using wget"
+ wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+ verbose "Found curl ... using curl"
+ curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+ verbose "Falling back to use Java to download"
+ javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+ targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+ cat >"$javaSource" <<-END
+ public class Downloader extends java.net.Authenticator
+ {
+ protected java.net.PasswordAuthentication getPasswordAuthentication()
+ {
+ return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+ }
+ public static void main( String[] args ) throws Exception
+ {
+ setDefault( new Downloader() );
+ java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+ }
+ }
+ END
+ # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+ verbose " - Compiling Downloader.java ..."
+ "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+ verbose " - Running Downloader.java ..."
+ "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+ distributionSha256Result=false
+ if [ "$MVN_CMD" = mvnd.sh ]; then
+ echo "Checksum validation is not supported for maven-mvnd." >&2
+ echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ elif command -v sha256sum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $distributionSha256Result = false ]; then
+ echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+ echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+ unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+ tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 0000000..249bdf3
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,149 @@
+<# : batch portion
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.2
+@REM
+@REM Optional ENV vars
+@REM MVNW_REPOURL - repo url base for downloading maven distribution
+@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+ IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+ $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+ Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+ "maven-mvnd-*" {
+ $USE_MVND = $true
+ $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+ $MVN_CMD = "mvnd.cmd"
+ break
+ }
+ default {
+ $USE_MVND = $false
+ $MVN_CMD = $script -replace '^mvnw','mvn'
+ break
+ }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+if ($env:MVNW_REPOURL) {
+ $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
+}
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+ exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+ if ($TMP_DOWNLOAD_DIR.Exists) {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+ }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+ if ($USE_MVND) {
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+ }
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+ }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+ Write-Error "fail to move MAVEN_HOME"
+ }
+} finally {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/pom.xml b/pom.xml
index 72d9e5c..7c04e18 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,6 +1,6 @@
@@ -10,13 +10,15 @@
eu.m724
tweaks
- 0.1.9
+ 0.1.15-SNAPSHOT
21
21
UTF-8
- 1.21.1
+
+ v1_21_R3
+ 1.21.4
${project.minecraft.version}-R0.1-SNAPSHOT
@@ -31,31 +33,36 @@
org.apache.maven.plugins
- maven-shade-plugin
- 3.6.0
-
- false
- true
-
-
- eu.m724:tweaks
-
-
-
-
- *
-
- META-INF/**
-
-
-
-
+ maven-antrun-plugin
+ 3.1.0
+ custom-nms-version
+ generate-sources
+
+ run
+
+
+
+
+
+
+
+
+
+
+ cleanup-custom-nms-version
package
- shade
+ run
+
+
+
+
+
+
+
@@ -73,10 +80,8 @@
remap-obf
org.spigotmc:minecraft-server:${project.spigot.version}:txt:maps-mojang
- true
org.spigotmc:spigot:${project.spigot.version}:jar:remapped-mojang
- true
- remapped-obf-temp-dont-use
+ true
@@ -86,7 +91,6 @@
remap-spigot
- ${project.build.directory}/${project.artifactId}-${project.version}-remapped-obf-temp-dont-use.jar
org.spigotmc:minecraft-server:${project.spigot.version}:csrg:maps-spigot
org.spigotmc:spigot:${project.spigot.version}:jar:remapped-obf
@@ -114,11 +118,6 @@
dmulloy2-repo
https://repo.dmulloy2.net/repository/public/
-
-
- maxhenkel-repo
- https://maven.maxhenkel.de/repository/public
-
m724-repo
https://git.m724.eu/api/packages/Minecon724/maven
@@ -129,7 +128,7 @@
org.spigotmc
spigot-api
- ${project.spigot.version}
+ 1.21.1-R0.1-SNAPSHOT
provided
@@ -145,16 +144,10 @@
5.3.0
provided
-
- de.maxhenkel.voicechat
- voicechat-api
- 2.5.0
- provided
-
eu.m724
mstats-spigot
- 0.1.0
+ 0.1.2
provided
@@ -175,6 +168,6 @@
scm:git:git@git.m724.eu:Minecon724/tweaks724.git
- tweaks-0.1.9
+ HEAD
\ No newline at end of file
diff --git a/src/main/java/eu/m724/tweaks/DebugLogger.java b/src/main/java/eu/m724/tweaks/DebugLogger.java
new file mode 100644
index 0000000..803b52d
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/DebugLogger.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks;
+
+import eu.m724.tweaks.module.TweaksModule;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+public class DebugLogger {
+ private DebugLogger() {}
+ static Logger logger;
+
+ public static void info(String message, Object... format) {
+ log(Level.INFO, message, format);
+ }
+
+ public static void warning(String message, Object... format) {
+ log(Level.WARNING, message, format);
+ }
+
+ public static void severe(String message, Object... format) {
+ log(Level.SEVERE, message, format);
+ }
+
+ public static void fine(String message, Object... format) {
+ log(Level.FINE, message, format);
+ }
+
+ public static void finer(String message, Object... format) {
+ log(Level.FINER, message, format);
+ }
+
+ private static void log(Level level, String message, Object... format) {
+
+ if (!logger.isLoggable(level)) {
+ return;
+ }
+
+ message = message.formatted(format);
+
+ if (logger.getLevel().intValue() <= Level.FINE.intValue()) {
+ message = "[" + getCaller() + "] " + message;
+ }
+
+ if (level.intValue() < Level.INFO.intValue()) { // levels below info are never logged even if set for some reason
+ // colors text gray (cyan is close to gray)
+ if (level == Level.FINE) {
+ message = "\033[38;5;250m" + message + "\033[39m";
+ } else {
+ message = "\033[38;5;245m" + message + "\033[39m";
+ }
+ level = Level.INFO;
+ }
+
+ logger.log(level, message);
+
+ }
+
+ private static String getCaller() {
+ String caller = Thread.currentThread().getStackTrace()[4].getClassName();
+
+ // TweaksModule has helper functions that log, we want to label the logs
+ if (caller.equals(TweaksModule.class.getName())) {
+ String pcaller = Thread.currentThread().getStackTrace()[5].getClassName();
+ if (pcaller.endsWith("Module"))
+ caller = pcaller;
+ }
+
+ if (caller.startsWith("eu.m724.tweaks.")) {
+ caller = caller.substring(15);
+
+ String[] packages = caller.split("\\.");
+
+ caller = IntStream.range(0, packages.length - 1)
+ .mapToObj(i -> packages[i].substring(0, 2))
+ .collect(Collectors.joining(".")) + "." + packages[packages.length - 1];
+ // TODO leading dot or no dot?
+ }
+
+ return caller;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/Language.java b/src/main/java/eu/m724/tweaks/Language.java
index 5cefae6..55a2d33 100644
--- a/src/main/java/eu/m724/tweaks/Language.java
+++ b/src/main/java/eu/m724/tweaks/Language.java
@@ -28,11 +28,15 @@ public class Language {
return INSTANCE.resourceBundle.getString(key);
}
+ public static String getString(String key, Object... format) {
+ return INSTANCE.resourceBundle.getString(key).formatted(format);
+ }
+
public static BaseComponent getComponent(String key, ChatColor color) {
return new ComponentBuilder(getString(key)).color(color).build();
}
public static BaseComponent getComponent(String key, ChatColor color, Object... format) {
- return new ComponentBuilder(getString(key).formatted(format)).color(color).build();
+ return new ComponentBuilder(getString(key, format)).color(color).build();
}
}
diff --git a/src/main/java/eu/m724/tweaks/TweaksConfig.java b/src/main/java/eu/m724/tweaks/TweaksConfig.java
deleted file mode 100644
index 53e8972..0000000
--- a/src/main/java/eu/m724/tweaks/TweaksConfig.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks;
-
-import org.bukkit.configuration.file.FileConfiguration;
-import org.bukkit.plugin.Plugin;
-
-public record TweaksConfig(
- boolean metrics,
-
- boolean worldborderExpand,
- boolean worldborderHide,
-
- boolean brandEnabled,
- String brandText,
- boolean brandShowPing,
- boolean brandShowMspt,
-
- boolean doorEnabled,
- boolean doorDoubleOpen,
- boolean doorKnocking,
-
- boolean motdEnabled,
- String motdSet,
-
- boolean chatEnabled,
- boolean chatLocalEvents,
- String chatDefaultName,
- int chatRadius,
-
- boolean compassEnabled,
- int compassWidth,
- int compassPrecision,
-
- boolean pomodoroEnabled,
- boolean pomodoroForce,
-
- boolean updaterEnabled,
-
- boolean hardcoreEnabled,
- float hardcoreChance,
-
- boolean sleepEnabled,
- boolean sleepInstant,
-
- boolean authEnabled,
- boolean authForce,
- String authDomain
-) {
- public static final int CONFIG_VERSION = 1;
- private static TweaksConfig config;
-
- public static TweaksConfig getConfig() {
- return config;
- }
-
- public static TweaksConfig load(Plugin plugin) {
- plugin.saveDefaultConfig();
- FileConfiguration config = plugin.getConfig();
-
- int configVersion = config.getInt("magic number don't modify this", 0);
- RuntimeException exception = new RuntimeException("Config version is %d, expected %d".formatted(configVersion, CONFIG_VERSION));
- if (configVersion == 0) {
- throw exception;
- } else if (configVersion > CONFIG_VERSION) {
- throw new RuntimeException("Please follow update instructions", exception);
- } else if (configVersion < CONFIG_VERSION) {
- throw new RuntimeException("Did you downgrade the plugin? Remove config.yml and let the plugin re-create it", exception);
- }
-
- boolean metrics = config.getBoolean("metrics");
-
- boolean worldborderExpand = config.getBoolean("worldborder.expand");
- boolean worldborderHide = config.getBoolean("worldborder.hide");
-
- boolean brandEnabled = config.getBoolean("brand.enabled");
- String brandText = config.getString("brand.text");
- boolean brandShowPing = config.getBoolean("brand.showPing");
- boolean brandShowMspt = config.getBoolean("brand.showMspt");
-
- boolean doorDoubleOpen = config.getBoolean("doors.doubleOpen");
- boolean doorKnocking = config.getBoolean("doors.knocking");
- boolean doorEnabled = doorDoubleOpen || doorKnocking;
-
- String motdSet = config.getString("motd.set");
- boolean motdEnabled = !(motdSet.equals("false") || motdSet.isBlank());
-
- boolean chatEnabled = config.getBoolean("chat.enabled");
- boolean chatLocalEvents = config.getBoolean("chat.localEvents");
- String chatDefaultName = config.getString("chat.defaultName");
- int chatRadius = config.getInt("chat.radius");
-
- boolean compassEnabled = config.getBoolean("compass.enabled");
- int compassWidth = config.getInt("compass.width");
- int compassPrecision = config.getInt("compass.precision");
-
- boolean pomodoroEnabled = config.getBoolean("pomodoro.enabled");
- boolean pomodoroForce = config.getBoolean("pomodoro.force");
-
- boolean updaterEnabled = config.getBoolean("updater.enabled");
-
- boolean hardcoreEnabled = config.getBoolean("hardcore.enabled");
- float hardcoreChance = (float) config.getDouble("hardcore.chance");
-
- boolean sleepEnabled = config.getBoolean("sleep.enabled");
- boolean sleepInstant = config.getBoolean("sleep.instant");
-
- boolean authEnabled = config.getBoolean("auth.enabled");
- boolean authForce = config.getBoolean("auth.force");
- String authHostname = config.getString("auth.domain");
-
- TweaksConfig.config = new TweaksConfig(
- metrics,
- worldborderExpand, worldborderHide,
- brandEnabled, brandText, brandShowPing, brandShowMspt,
- doorEnabled, doorDoubleOpen, doorKnocking,
- motdEnabled, motdSet,
- chatEnabled, chatLocalEvents, chatDefaultName, chatRadius,
- compassEnabled, compassWidth, compassPrecision,
- pomodoroEnabled, pomodoroForce,
- updaterEnabled,
- hardcoreEnabled, hardcoreChance,
- sleepEnabled, sleepInstant,
- authEnabled, authForce, authHostname
- );
-
- return TweaksConfig.config;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/eu/m724/tweaks/TweaksPlugin.java b/src/main/java/eu/m724/tweaks/TweaksPlugin.java
index eff2bf2..82adee7 100644
--- a/src/main/java/eu/m724/tweaks/TweaksPlugin.java
+++ b/src/main/java/eu/m724/tweaks/TweaksPlugin.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
@@ -7,32 +7,40 @@
package eu.m724.tweaks;
import eu.m724.mstats.MStatsPlugin;
-import eu.m724.tweaks.alert.AlertManager;
-import eu.m724.tweaks.auth.AuthManager;
-import eu.m724.tweaks.chat.ChatCommands;
-import eu.m724.tweaks.chat.ChatManager;
-import eu.m724.tweaks.door.DoorManager;
-import eu.m724.tweaks.full.FullListener;
-import eu.m724.tweaks.hardcore.HardcoreManager;
-import eu.m724.tweaks.motd.MotdManager;
-import eu.m724.tweaks.ping.F3NameListener;
-import eu.m724.tweaks.ping.PingChecker;
-import eu.m724.tweaks.ping.PingCommands;
-import eu.m724.tweaks.pomodoro.PomodoroCommands;
-import eu.m724.tweaks.pomodoro.PomodoroManager;
-import eu.m724.tweaks.sleep.SleepManager;
-import eu.m724.tweaks.updater.UpdaterCommands;
-import eu.m724.tweaks.updater.UpdaterManager;
-import eu.m724.tweaks.worldborder.WorldBorderManager;
+import eu.m724.tweaks.config.TweaksConfig;
+import eu.m724.tweaks.module.TweaksModule;
+import eu.m724.tweaks.module.alert.AlertModule;
+import eu.m724.tweaks.module.auth.AuthModule;
+import eu.m724.tweaks.module.chat.ChatModule;
+import eu.m724.tweaks.module.door.DoorKnockModule;
+import eu.m724.tweaks.module.door.DoorOpenModule;
+import eu.m724.tweaks.module.durability.DurabilityModule;
+import eu.m724.tweaks.module.full.FullModule;
+import eu.m724.tweaks.module.hardcore.HardcoreModule;
+import eu.m724.tweaks.module.killswitch.KillswitchModule;
+import eu.m724.tweaks.module.knockback.KnockbackModule;
+import eu.m724.tweaks.module.motd.MotdModule;
+import eu.m724.tweaks.module.ping.F3NameListener;
+import eu.m724.tweaks.module.ping.PingChecker;
+import eu.m724.tweaks.module.pomodoro.PomodoroModule;
+import eu.m724.tweaks.module.redstone.RedstoneModule;
+import eu.m724.tweaks.module.sleep.SleepModule;
+import eu.m724.tweaks.module.swing.SwingModule;
+import eu.m724.tweaks.module.updater.UpdaterModule;
+import eu.m724.tweaks.module.wordcoords.WordCoordsModule;
+import eu.m724.tweaks.module.worldborder.WorldBorderExpandModule;
+import eu.m724.tweaks.module.worldborder.WorldBorderHideModule;
-import java.io.IOException;
import java.util.Locale;
-import java.util.Objects;
+import java.util.logging.Level;
public class TweaksPlugin extends MStatsPlugin {
+ private static TweaksPlugin INSTANCE;
+
@Override
public void onEnable() {
long start = System.nanoTime();
+ INSTANCE = this;
if (getServer().getPluginManager().getPlugin("ProtocolLib") == null) {
getLogger().severe("ProtocolLib is required for this plugin.");
@@ -41,84 +49,141 @@ public class TweaksPlugin extends MStatsPlugin {
return;
}
- TweaksConfig config = TweaksConfig.load(this);
- new Language(Locale.US); // TODO
-
- // whether enabled is handled inside
- new WorldBorderManager().init(this);
-
- if (config.chatEnabled()) {
- ChatManager chatManager = new ChatManager(this);
- chatManager.init();
-
- ChatCommands chatCommands = new ChatCommands(chatManager);
- Objects.requireNonNull(getCommand("chat")).setExecutor(chatCommands);
- Objects.requireNonNull(getCommand("chatmanage")).setExecutor(chatCommands);
+ TweaksConfig config;
+ try {
+ config = TweaksConfig.load(this);
+ } catch (Exception e) {
+ throw new RuntimeException("Exception loading config", e);
}
- if (config.doorEnabled()) {
- new DoorManager().init(this);
+ getLogger().setLevel(config.debug() ? Level.FINEST : Level.INFO);
+ DebugLogger.logger = getLogger();
+
+ if (config.debug()) {
+ DebugLogger.warning("Debug harms performance");
+ }
+
+ DebugLogger.fine("Language");
+ new Language(Locale.of(config.locale())); // TODO
+ DebugLogger.fine(Language.getString("languageNotice", Language.getString("language"), Language.getString("languageEnglish")));
+
+ var runningVersion = getServer().getBukkitVersion();
+ var targetVersion = getTargetVersion();
+
+ if (!runningVersion.equals(targetVersion)) {
+ // the incompatibility can be between 1.21.4-R0.1-SNAPSHOT and 1.21.4-R1-SNAPSHOT
+ var runningMc = runningVersion.split("-")[0];
+ var targetMc = targetVersion.split("-")[0];
+ if (!runningMc.equals(targetMc)) {
+ targetVersion = targetMc;
+ runningVersion = runningMc;
+ }
+
+ getLogger().warning("This plugin was built for %s. This server is running %s.".formatted(targetVersion, runningVersion));
+ getLogger().warning("Some modules will not work. Disable those modules, or make a compatible build yourself:");
+ getLogger().warning("https://git.m724.eu/Minecon724/tweaks724/src/branch/master/docs/BUILDING.md");
+ }
+
+ /* start modules */
+
+ if (config.worldborderHide()) {
+ TweaksModule.init(WorldBorderHideModule.class);
+ }
+
+ if (config.worldborderExpand()) {
+ TweaksModule.init(WorldBorderExpandModule.class);
+ }
+
+ if (config.chatEnabled()) {
+ TweaksModule.init(ChatModule.class);
+ }
+
+ if (config.doorKnocking()) {
+ TweaksModule.init(DoorKnockModule.class);
+ }
+
+ if (config.doorDoubleOpen()) {
+ TweaksModule.init(DoorOpenModule.class);
}
if (config.brandEnabled()) {
+ DebugLogger.fine("Enabling Brand");
new F3NameListener(this).init();
}
- new PingChecker(this).init();
- Objects.requireNonNull(getCommand("ping")).setExecutor(new PingCommands());
-
- /*if (getServer().getPluginManager().getPlugin("voicechat") != null) {
- new MusicPlayer(this).init();
- } else {
- getLogger().warning("To use voice extensions, install \"Simple Voice Chat\"");
- }*/
+ DebugLogger.fine("Enabling Ping");
+ new PingChecker(this).init(getCommand("ping"));
if (config.motdEnabled()) {
- try {
- new MotdManager(this).init();
- } catch (IOException e) {
- getLogger().severe("Failed to initialize MOTD extension");
- throw new RuntimeException(e);
- }
+ TweaksModule.init(MotdModule.class);
}
if (config.pomodoroEnabled()) {
- new PomodoroManager(this).init();
- getCommand("pomodoro").setExecutor(new PomodoroCommands());
+ TweaksModule.init(PomodoroModule.class);
}
if (config.updaterEnabled()) {
- try {
- new UpdaterManager(this).init();
- getCommand("updates").setExecutor(new UpdaterCommands());
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
+ TweaksModule.init(UpdaterModule.class);
}
if (config.hardcoreEnabled()) {
- new HardcoreManager().init(this);
+ TweaksModule.init(HardcoreModule.class);
}
if (config.sleepEnabled()) {
- new SleepManager().init(this);
+ TweaksModule.init(SleepModule.class);
}
if (config.authEnabled()) {
- new AuthManager(this).init(getCommand("tauth"));
+ TweaksModule.init(AuthModule.class);
}
- new AlertManager(this).init(getCommand("emergencyalert"));
+ TweaksModule.init(AlertModule.class);
- this.getServer().getPluginManager().registerEvents(new FullListener(), this);
+ TweaksModule.init(FullModule.class);
- if (config.metrics())
+ if (config.redstoneEnabled()) {
+ TweaksModule.init(RedstoneModule.class);
+ }
+
+ TweaksModule.init(KnockbackModule.class);
+
+ if (config.killswitchEnabled()) {
+ TweaksModule.init(KillswitchModule.class);
+ }
+
+ if (config.swingEnabled()) {
+ TweaksModule.init(SwingModule.class);
+ }
+
+ TweaksModule.init(DurabilityModule.class);
+
+ TweaksModule.init(WordCoordsModule.class);
+
+ /* end modules */
+
+ if (config.metrics()) {
+ DebugLogger.fine("Enabling Metrics");
mStats(1);
+ }
- getLogger().info("Took %.3f milliseconds".formatted((System.nanoTime() - start) / 1000000.0));
+ DebugLogger.fine("Took %.3f milliseconds", (System.nanoTime() - start) / 1000000.0);
+ }
+
+ private String getTargetVersion() {
+ var permission = getServer().getPluginManager().getPermission("7weaks724.ignore.this");
+
+ var desc = permission.getDescription().substring("Internal, not for use. ".length()).split(",");
+ var version = desc[0];
+
+ return version;
}
public boolean hasResource(String resource) {
return this.getClassLoader().getResource(resource) != null;
}
+
+ public static TweaksPlugin getInstance() {
+ return INSTANCE;
+ }
}
diff --git a/src/main/java/eu/m724/tweaks/alert/AlertManager.java b/src/main/java/eu/m724/tweaks/alert/AlertManager.java
deleted file mode 100644
index f2b72d9..0000000
--- a/src/main/java/eu/m724/tweaks/alert/AlertManager.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.alert;
-
-import com.comphenix.protocol.PacketType;
-import com.comphenix.protocol.ProtocolLibrary;
-import com.comphenix.protocol.events.ListenerPriority;
-import com.comphenix.protocol.events.PacketAdapter;
-import com.comphenix.protocol.events.PacketContainer;
-import com.comphenix.protocol.events.PacketEvent;
-import eu.m724.tweaks.TweaksPlugin;
-import org.bukkit.command.PluginCommand;
-import org.bukkit.entity.Player;
-import org.bukkit.scheduler.BukkitTask;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class AlertManager {
- private final TweaksPlugin plugin;
-
- private BukkitTask notifyTask;
- static Alert current;
- static Map pages = new HashMap<>();
-
- public AlertManager(TweaksPlugin plugin) {
- this.plugin = plugin;
- }
-
- public void init(PluginCommand command) {
- command.setExecutor(new AlertCommand(this));
-
- ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(
- plugin,
- ListenerPriority.NORMAL,
- PacketType.Play.Client.ENCHANT_ITEM
- ) {
- @Override
- public void onPacketReceiving(PacketEvent event) {
- if (current == null) return;
- if (!current.isOpen(event.getPlayer())) return;
-
- PacketContainer packet = event.getPacket();
-
- int windowId, buttonId;
- windowId = packet.getIntegers().read(0);
- buttonId = packet.getIntegers().read(1);
-
- var page = pages.getOrDefault(event.getPlayer(),1);
-
- if (buttonId == 1) { // prev page
- page--;
- } else if (buttonId == 2) { // nextc page
- page++;
- } else {
- return;
- }
-
- pages.put(event.getPlayer(), page);
- var npacket = new PacketContainer(PacketType.Play.Server.WINDOW_DATA);
- npacket.getIntegers().write(0, windowId);
- npacket.getIntegers().write(1, 0);
- npacket.getIntegers().write(2, page);
- ProtocolLibrary.getProtocolManager().sendServerPacket(event.getPlayer(), npacket);
- }
- });
- }
-
- public Alert start(String... content) {
- stop();
- current = new Alert(content);
- notifyTask = new AlertRunnable(current, v -> this.stop()).runTaskTimer(plugin, 0, 10);
- return current;
- }
-
- public void stop() {
- if (current == null) return;
- for (Player player : plugin.getServer().getOnlinePlayers()) {
- if (current.isOpen(player))
- player.closeInventory();
- }
- pages.clear();
- notifyTask.cancel();
- current = null;
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/auth/AuthManager.java b/src/main/java/eu/m724/tweaks/auth/AuthManager.java
deleted file mode 100644
index 402b376..0000000
--- a/src/main/java/eu/m724/tweaks/auth/AuthManager.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.auth;
-
-import org.bukkit.command.PluginCommand;
-import org.bukkit.plugin.Plugin;
-
-public class AuthManager {
- private final AuthStorage authStorage;
- private final Plugin plugin;
-
- public AuthManager(Plugin plugin) {
- this.plugin = plugin;
- this.authStorage = new AuthStorage(plugin);
- }
-
- public void init(PluginCommand command) {
- plugin.getServer().getPluginManager().registerEvents(new AuthListener(authStorage), plugin);
- command.setExecutor(new AuthCommands(authStorage));
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/chat/ChatCommands.java b/src/main/java/eu/m724/tweaks/chat/ChatCommands.java
deleted file mode 100644
index 09e19b7..0000000
--- a/src/main/java/eu/m724/tweaks/chat/ChatCommands.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.chat;
-
-import eu.m724.tweaks.Language;
-import net.md_5.bungee.api.ChatColor;
-import net.md_5.bungee.api.chat.BaseComponent;
-import org.bukkit.Bukkit;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandExecutor;
-import org.bukkit.command.CommandSender;
-import org.bukkit.entity.Player;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.stream.Collectors;
-
-public class ChatCommands implements CommandExecutor {
- private final ChatManager manager;
-
- public ChatCommands(ChatManager manager) {
- this.manager = manager;
- }
-
- @Override
- public boolean onCommand(@NotNull CommandSender sender, Command command, @NotNull String label, String[] args) {
- if (command.getName().equals("chat")) {
- Player player = (Player) sender;
- ChatRoom chatRoom = manager.getPlayerChatRoom(player);
-
- if (args.length == 0) { // show room
- player.spigot().sendMessage(chatRoom.getInfoComponent());
- } else { // join room
- String id = args[0];
-
- if (id.equals(chatRoom.id)) {
- sender.spigot().sendMessage(Language.getComponent("chatAlreadyHere", ChatColor.GRAY));
- return true;
- }
-
- String password = null;
- if (args.length > 1) {
- password = Arrays.stream(args).skip(1).collect(Collectors.joining(" "));
- }
-
- boolean authenticated = false;
- BaseComponent component = null;
- ChatRoom newRoom = manager.getById(id);
- if (newRoom != null) {
- if (newRoom.password != null) {
- if (newRoom.password.equals(password)) {
- authenticated = true;
- } else if (password == null) {
- component = Language.getComponent("chatPasswordProtected", ChatColor.RED);
- } else {
- component = Language.getComponent("chatWrongPassword", ChatColor.RED);
- }
- } else {
- authenticated = true;
- }
- } else {
- component = Language.getComponent("chatNoSuchRoom", ChatColor.RED, id);
- }
-
- if (authenticated) {
- /*component = new ComponentBuilder(Language.getComponent("chatJoined", ChatColor.GOLD))
- .append(" ")
- .append(ChatFormatUtils.formatChatRoom(chatRoom)).color(newRoom.color)
- .build();*/
- player.sendMessage("");
- manager.setPlayerChatRoom(newRoom, player);
- } else {
- player.spigot().sendMessage(component);
- }
-
- }
- } else if (command.getName().equals("chatmanage")) {
- Player player = (Player) sender;
- ChatRoom chatRoom = manager.getPlayerChatRoom(player);
- boolean isOwner = player.equals(chatRoom.owner);
-
- if (args.length > 1) {
- String action = args[0];
- String argument = args[1];
-
- switch (action) {
- case "create" -> {
- try {
- ChatRoom newRoom = manager.createChatRoom(argument, null, player);
- sender.sendMessage("Created a chat room. Join it: /c " + newRoom.id);
- sender.sendMessage("You might also want to protect it with a password: /cm setpassword");
- } catch (ChatManager.InvalidIdException e) {
- sender.sendMessage("ID is invalid: " + e.getMessage());
- } catch (ChatManager.ChatRoomExistsException e) {
- sender.sendMessage("Room %s already exists".formatted(argument));
- } catch (IOException e) {
- sender.sendMessage("Failed to create room");
- e.printStackTrace();
- }
- }
- case "delete" -> {
- if (argument.equals(chatRoom.id)) {
- if (isOwner) {
- manager.deleteChatRoom(chatRoom);
- sender.sendMessage("Room %s deleted".formatted(chatRoom.id));
- } else {
- sender.sendMessage("You're not the owner of %s, please enter the room you want to make changes in".formatted(chatRoom.id));
- }
- } else {
- sender.sendMessage("Pass %s as an argument to confirm".formatted(chatRoom.id));
- }
- }
- case "setowner" -> {
- if (isOwner) {
- Player newOwner = Bukkit.getPlayer(argument);
- if (newOwner != null && newOwner.isOnline()) {
- chatRoom.owner = newOwner;
- try {
- manager.saveChatRoom(chatRoom);
- sender.sendMessage("Owner changed to " + newOwner.getName());
- } catch (IOException e) {
- sender.sendMessage("Failed to change owner");
- e.printStackTrace();
- }
- } else {
- sender.sendMessage("Player must be online");
- }
- } else {
- sender.sendMessage("You're not the owner of %s, please enter the room you want to make changes in".formatted(chatRoom.id));
- }
- }
- case "setpassword" -> {
- if (isOwner) {
- chatRoom.password = Arrays.stream(args).skip(1).collect(Collectors.joining(" "));
- try {
- manager.saveChatRoom(chatRoom);
- sender.sendMessage("Password changed");
- } catch (IOException e) {
- sender.sendMessage("Failed to change password");
- e.printStackTrace();
- }
- } else {
- sender.sendMessage("You're not the owner of %s, please enter the room you want to make changes in".formatted(chatRoom.id));
- }
- }
- case "setcolor" -> {
- if (isOwner) {
- ChatColor newColor = ChatColor.of(argument);
- if (newColor != null) {
- chatRoom.color = newColor;
- try {
- manager.saveChatRoom(chatRoom);
- sender.sendMessage("Message color changed to " + newColor.getName());
- } catch (IOException e) {
- sender.sendMessage("Failed to change color");
- e.printStackTrace();
- }
- } else {
- sender.sendMessage("Invalid color");
- }
- } else {
- sender.sendMessage("You're not the owner of %s, please enter the room you want to make changes in".formatted(chatRoom.id));
- }
- }
- default -> {
- sender.sendMessage("Actions: create, delete, setowner, setpassword, setcolor");
- }
- }
- } else if (args.length > 0) {
- switch (args[0]) {
- case "create" ->
- sender.sendMessage("Please pass a room name as an argument. The room name must be of characters and digits.");
- case "delete" ->
- sender.sendMessage("You want to delete room %s. Confirm by passing its name as an argument for this action.".formatted(chatRoom.id));
- case "setowner" ->
- sender.sendMessage("To transfer ownership of room %s, pass the new owner name as an argument for this action.".formatted(chatRoom.id));
- case "setpassword" ->
- sender.sendMessage("To change the password of room %s, pass the new password as an argument for this action.".formatted(chatRoom.id));
- case "setcolor" ->
- sender.sendMessage("To change the message color of room %s, pass the new color as an argument for this action. #hex or color name.".formatted(chatRoom.id));
- default ->
- sender.sendMessage("Actions: create, delete, setowner, setpassword, setcolor");
- }
- } else {
- sender.sendMessage("Actions: create, delete, setowner, setpassword, setcolor");
- }
- }
-
- return true;
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/compass/CompassManager.java b/src/main/java/eu/m724/tweaks/compass/CompassManager.java
deleted file mode 100644
index 0ba9113..0000000
--- a/src/main/java/eu/m724/tweaks/compass/CompassManager.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.compass;
-
-import org.bukkit.plugin.Plugin;
-
-public class CompassManager {
- private final Plugin plugin;
-
- public CompassManager(Plugin plugin) {
- this.plugin = plugin;
- }
-
- public void init() {
- plugin.getServer().getPluginManager().registerEvents(new CompassListener(), plugin);
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/config/ConfigLoader.java b/src/main/java/eu/m724/tweaks/config/ConfigLoader.java
new file mode 100644
index 0000000..cf725f9
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/config/ConfigLoader.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.config;
+
+import org.bukkit.configuration.file.FileConfiguration;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class ConfigLoader {
+ private final FileConfiguration configuration;
+ private final List missing = new ArrayList<>();
+
+ ConfigLoader(FileConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ List getMissing() {
+ return missing;
+ }
+
+ TweaksConfig load() {
+ boolean metrics = configuration.getBoolean("metrics", false);
+ boolean debug = configuration.getBoolean("debug", false);
+ String locale = configuration.getString("locale", "US");
+
+ boolean worldborderExpand = getBoolean("worldborder.expand");
+ boolean worldborderHide = getBoolean("worldborder.hide");
+
+ boolean brandEnabled = getBoolean("brand.enabled");
+ String brandText = getString("brand.text");
+ boolean brandShowPing = getBoolean("brand.showPing");
+ boolean brandShowMspt = getBoolean("brand.showMspt");
+
+ boolean doorDoubleOpen = getBoolean("doors.doubleOpen");
+ boolean doorKnocking = getBoolean("doors.knocking");
+
+ boolean motdEnabled = getBoolean("motd.enabled");
+ String motdSet = getString("motd.set");
+
+ boolean chatEnabled = getBoolean("chat.enabled");
+ boolean chatLocalEvents = getBoolean("chat.localEvents");
+ String chatDefaultName = getString("chat.defaultName");
+ int chatRadius = getInt("chat.radius");
+
+ boolean compassEnabled = getBoolean("compass.enabled");
+ int compassWidth = getInt("compass.width");
+ int compassPrecision = getInt("compass.precision");
+
+ boolean pomodoroEnabled = getBoolean("pomodoro.enabled");
+ boolean pomodoroForce = getBoolean("pomodoro.force");
+
+ boolean updaterEnabled = getBoolean("updater.enabled");
+
+ boolean hardcoreEnabled = getBoolean("hardcore.enabled");
+ double hardcoreChance = getDouble("hardcore.chance");
+
+ boolean sleepEnabled = getBoolean("sleep.enabled");
+ boolean sleepInstant = getBoolean("sleep.instant");
+ double sleepHeal = getDouble("sleep.heal");
+
+ boolean authEnabled = getBoolean("auth.enabled");
+ boolean authForce = getBoolean("auth.force");
+ String authHostname = getString("auth.domain");
+
+ boolean redstoneEnabled = getBoolean("retstone.enabled");
+ String redstoneListen = getString("retstone.listen");
+
+ // this is processed when initing knockback module
+ Map knockbackModifiers = getValues("knockback");
+
+ boolean killswitchEnabled = getBoolean("killswitch.enabled");
+ String killswitchListen = getString("killswitch.listen");
+
+ boolean swingEnabled = getBoolean("swing.enabled");
+
+ return new TweaksConfig(
+ metrics, debug, locale,
+ worldborderExpand, worldborderHide,
+ brandEnabled, brandText, brandShowPing, brandShowMspt,
+ doorDoubleOpen, doorKnocking,
+ motdEnabled, motdSet,
+ chatEnabled, chatLocalEvents, chatDefaultName, chatRadius,
+ compassEnabled, compassWidth, compassPrecision,
+ pomodoroEnabled, pomodoroForce,
+ updaterEnabled,
+ hardcoreEnabled, hardcoreChance,
+ sleepEnabled, sleepInstant, sleepHeal,
+ authEnabled, authForce, authHostname,
+ redstoneEnabled, redstoneListen,
+ knockbackModifiers,
+ killswitchEnabled, killswitchListen,
+ swingEnabled
+ );
+ }
+
+ private double getDouble(String key) {
+ if (!configuration.contains(key))
+ missing.add(key);
+
+ // we return the whatever default value
+ return configuration.getDouble(key);
+ }
+
+ private int getInt(String key) {
+ if (!configuration.contains(key))
+ missing.add(key);
+
+ return configuration.getInt(key);
+ }
+
+ private boolean getBoolean(String key) {
+ if (!configuration.contains(key))
+ missing.add(key);
+
+ return configuration.getBoolean(key);
+ }
+
+ private String getString(String key) {
+ if (!configuration.contains(key))
+ missing.add(key);
+
+ return configuration.getString(key);
+ }
+
+ private Map getValues(String key) {
+ var cs = configuration.getConfigurationSection(key);
+
+ if (cs == null) {
+ missing.add(key);
+ // the default is null, which is bad
+ return new HashMap<>();
+ }
+
+ return cs.getValues(false);
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/config/MissingFieldsException.java b/src/main/java/eu/m724/tweaks/config/MissingFieldsException.java
new file mode 100644
index 0000000..24f1be0
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/config/MissingFieldsException.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.config;
+
+import java.util.List;
+
+class MissingFieldsException extends Exception {
+ private final List missing;
+
+ MissingFieldsException(List missing) {
+ this.missing = missing;
+ }
+
+ @Override
+ public String getMessage() {
+ return String.join(", ", missing);
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/config/TweaksConfig.java b/src/main/java/eu/m724/tweaks/config/TweaksConfig.java
new file mode 100644
index 0000000..7442813
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/config/TweaksConfig.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.config;
+
+import org.bukkit.plugin.Plugin;
+
+import java.util.Map;
+
+public record TweaksConfig(
+ boolean metrics,
+ boolean debug,
+ String locale,
+
+ boolean worldborderExpand,
+ boolean worldborderHide,
+
+ boolean brandEnabled,
+ String brandText,
+ boolean brandShowPing,
+ boolean brandShowMspt,
+
+ boolean doorDoubleOpen,
+ boolean doorKnocking,
+
+ boolean motdEnabled,
+ String motdSet,
+
+ boolean chatEnabled,
+ boolean chatLocalEvents,
+ String chatDefaultName,
+ int chatRadius,
+
+ boolean compassEnabled,
+ int compassWidth,
+ int compassPrecision,
+
+ boolean pomodoroEnabled,
+ boolean pomodoroForce,
+
+ boolean updaterEnabled,
+
+ boolean hardcoreEnabled,
+ double hardcoreChance,
+
+ boolean sleepEnabled,
+ boolean sleepInstant,
+ double sleepHeal,
+
+ boolean authEnabled,
+ boolean authForce,
+ String authDomain,
+
+ boolean redstoneEnabled,
+ String redstoneListen,
+
+ Map knockbackModifiers,
+
+ boolean killswitchEnabled,
+ String killswitchListen,
+
+ boolean swingEnabled
+) {
+ public static final int CONFIG_VERSION = 2;
+ private static TweaksConfig config;
+
+ public static TweaksConfig getConfig() {
+ return config;
+ }
+
+ public static TweaksConfig load(Plugin plugin) throws Exception {
+ plugin.saveDefaultConfig();
+ var pluginConfig = plugin.getConfig();
+
+ var configVersion = pluginConfig.getInt("magic number don't modify this", 0);
+ var exception = new RuntimeException("Config version is %d, expected %d".formatted(configVersion, CONFIG_VERSION));
+
+ if (configVersion == 0) {
+ throw exception;
+ } else if (configVersion < CONFIG_VERSION) {
+ throw new Exception("Please follow update instructions https://www.spigotmc.org/resources/tweaks724.121057/updates", exception);
+ } else if (configVersion > CONFIG_VERSION) {
+ throw new Exception("Did you downgrade the plugin? Delete config.yml and let the plugin re-create it", exception);
+ }
+
+ var loader = new ConfigLoader(pluginConfig);
+ var config = loader.load();
+
+ if (loader.getMissing().isEmpty()) {
+ TweaksConfig.config = config;
+ return config;
+ } else {
+ throw new Exception(
+ "One or more fields are missing from config.yml. Did you follow the update instructions? https://www.spigotmc.org/resources/tweaks724.121057/updates",
+ new MissingFieldsException(loader.getMissing())
+ );
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/eu/m724/tweaks/door/DoorManager.java b/src/main/java/eu/m724/tweaks/door/DoorManager.java
deleted file mode 100644
index 52553b5..0000000
--- a/src/main/java/eu/m724/tweaks/door/DoorManager.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.door;
-
-import eu.m724.tweaks.TweaksConfig;
-import org.bukkit.plugin.Plugin;
-
-public class DoorManager {
-
- public void init(Plugin plugin) {
- if (TweaksConfig.getConfig().doorKnocking()) {
- plugin.getServer().getPluginManager().registerEvents(new DoorKnockListener(), plugin);
- }
-
- if (TweaksConfig.getConfig().doorDoubleOpen()) {
- plugin.getServer().getPluginManager().registerEvents(new DoorOpenListener(), plugin);
- }
- }
-
-}
diff --git a/src/main/java/eu/m724/tweaks/hardcore/HardcoreManager.java b/src/main/java/eu/m724/tweaks/hardcore/HardcoreManager.java
deleted file mode 100644
index 0dc632b..0000000
--- a/src/main/java/eu/m724/tweaks/hardcore/HardcoreManager.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.hardcore;
-
-import com.comphenix.protocol.PacketType;
-import com.comphenix.protocol.ProtocolLibrary;
-import com.comphenix.protocol.events.*;
-import eu.m724.tweaks.TweaksConfig;
-import org.bukkit.plugin.Plugin;
-
-public class HardcoreManager {
- private final float chance = TweaksConfig.getConfig().hardcoreChance();
-
- public void init(Plugin plugin) {
- ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(
- plugin,
- ListenerPriority.NORMAL,
- PacketType.Play.Server.LOGIN
- ) {
- @Override
- public void onPacketSending(PacketEvent event) {
- PacketContainer packet = event.getPacket();
- int entityId = packet.getIntegers().read(0);
-
- if (chance > ((48271 * entityId) % 65537) / 65537f) // gotta be fast
- // the "is hardcore" boolean https://wiki.vg/Protocol#Login_.28play.29
- packet.getBooleans().write(0, true);
- }
- });
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/module/TweaksModule.java b/src/main/java/eu/m724/tweaks/module/TweaksModule.java
new file mode 100644
index 0000000..6cf6191
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/TweaksModule.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.ProtocolLibrary;
+import com.comphenix.protocol.events.ListenerPriority;
+import com.comphenix.protocol.events.PacketAdapter;
+import com.comphenix.protocol.events.PacketEvent;
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.config.TweaksConfig;
+import eu.m724.tweaks.TweaksPlugin;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.event.Listener;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.function.Consumer;
+
+public abstract class TweaksModule {
+ /**
+ * Called on module initialize.
+ */
+ protected abstract void onInit();
+
+ void init() {
+ var name = getClass().getSimpleName();
+ DebugLogger.finer("Initializing module " + name);
+
+ long start = System.nanoTime();
+
+ this.onInit();
+
+ long end = System.nanoTime();
+
+ DebugLogger.fine("Initialized %s in %d µs", name, (end - start) / 1000);
+ }
+
+ /**
+ * Gets the plugin instance.
+ *
+ * @return The plugin instance
+ */
+ protected TweaksPlugin getPlugin() {
+ return TweaksPlugin.getInstance();
+ }
+
+ /**
+ * Gets the plugin config.
+ *
+ * @return The plugin config
+ */
+ protected TweaksConfig getConfig() {
+ return TweaksConfig.getConfig();
+ }
+
+ /**
+ * Registers an event listener.
+ *
+ * @param listener The event listener
+ */
+ protected void registerEvents(Listener listener) {
+ getPlugin().getServer().getPluginManager().registerEvents(listener, getPlugin());
+
+ DebugLogger.finer("Registered event listener: " + listener.getClass().getName());
+ }
+
+ /**
+ * Registers an OUTGOING packet listener.
+ * Priority is {@link ListenerPriority}.NORMAL.
+ *
+ * @param packetType The {@link PacketType} to listen for
+ * @param consumer The consumer that will be called when the packet is received.
+ */
+ protected void onPacketSend(PacketType packetType, Consumer consumer) {
+ ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
+ @Override
+ public void onPacketSending(PacketEvent event) {
+ consumer.accept(event);
+ }
+ });
+
+ DebugLogger.finer("Registered outgoing packet listener: " + packetType.name());
+ }
+
+ /**
+ * Registers an INCOMING packet listener.
+ * Priority is {@link ListenerPriority}.NORMAL.
+ *
+ * @param packetType The {@link PacketType} to listen for
+ * @param consumer The consumer that will be called when the packet is received.
+ */
+ protected void onPacketReceive(PacketType packetType, Consumer consumer) {
+ ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
+ @Override
+ public void onPacketReceiving(PacketEvent event) {
+ consumer.accept(event);
+ }
+ });
+
+ DebugLogger.finer("Registered incoming packet listener: " + packetType.name());
+ }
+
+ /**
+ * Registers a command.
+ *
+ * @param command The command
+ * @param executor The command executor
+ */
+ protected void registerCommand(String command, CommandExecutor executor) {
+ getPlugin().getCommand(command).setExecutor(executor);
+ DebugLogger.finer("Registered command: " + command);
+ }
+
+ /**
+ * Initializes a module.
+ *
+ * @param clazz The class of the initialized module
+ * @return The module instance
+ * @param The type of the initialized module
+ */
+ public static T init(Class clazz) {
+ T module;
+ try {
+ module = clazz.getDeclaredConstructor().newInstance();
+ } catch (InstantiationException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+
+ module.init();
+
+ return module;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/alert/Alert.java b/src/main/java/eu/m724/tweaks/module/alert/Alert.java
similarity index 95%
rename from src/main/java/eu/m724/tweaks/alert/Alert.java
rename to src/main/java/eu/m724/tweaks/module/alert/Alert.java
index be73459..2c802b6 100644
--- a/src/main/java/eu/m724/tweaks/alert/Alert.java
+++ b/src/main/java/eu/m724/tweaks/module/alert/Alert.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.alert;
+package eu.m724.tweaks.module.alert;
import org.bukkit.Bukkit;
import org.bukkit.Material;
diff --git a/src/main/java/eu/m724/tweaks/alert/AlertCommand.java b/src/main/java/eu/m724/tweaks/module/alert/AlertCommand.java
similarity index 94%
rename from src/main/java/eu/m724/tweaks/alert/AlertCommand.java
rename to src/main/java/eu/m724/tweaks/module/alert/AlertCommand.java
index e5748a7..2a17c77 100644
--- a/src/main/java/eu/m724/tweaks/alert/AlertCommand.java
+++ b/src/main/java/eu/m724/tweaks/module/alert/AlertCommand.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.alert;
+package eu.m724.tweaks.module.alert;
import org.bukkit.Material;
import org.bukkit.command.Command;
@@ -20,9 +20,9 @@ public class AlertCommand implements CommandExecutor {
private List pending;
private long when;
- private final AlertManager manager;
+ private final AlertModule manager;
- public AlertCommand(AlertManager manager) {
+ public AlertCommand(AlertModule manager) {
this.manager = manager;
}
@@ -51,7 +51,7 @@ public class AlertCommand implements CommandExecutor {
} else if (args[0].equalsIgnoreCase("confirm")) {
sender.sendMessage("CONFIRM must be in all caps");
} else if (args[0].equalsIgnoreCase("cancel")) {
- if (AlertManager.current != null) {
+ if (AlertModule.current != null) {
manager.stop();
sender.sendMessage("Cancelled alert");
} else if (pending != null) {
@@ -86,7 +86,7 @@ public class AlertCommand implements CommandExecutor {
if (pending != null) {
when = System.currentTimeMillis();
- if (AlertManager.current != null) {
+ if (AlertModule.current != null) {
sender.sendMessage("Broadcasting a new alert will cancel the currently active one");
}
sender.sendMessage("Please confirm broadcast with /emergencyalert CONFIRM within 15 seconds");
diff --git a/src/main/java/eu/m724/tweaks/module/alert/AlertModule.java b/src/main/java/eu/m724/tweaks/module/alert/AlertModule.java
new file mode 100644
index 0000000..c900b86
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/alert/AlertModule.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.alert;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.ProtocolLibrary;
+import com.comphenix.protocol.events.PacketContainer;
+import eu.m724.tweaks.module.TweaksModule;
+import org.bukkit.entity.Player;
+import org.bukkit.scheduler.BukkitTask;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AlertModule extends TweaksModule {
+ private BukkitTask notifyTask;
+ static Alert current;
+ static Map pages = new HashMap<>();
+
+ @Override
+ protected void onInit() {
+ registerCommand("emergencyalert", new AlertCommand(this));
+
+ onPacketReceive(PacketType.Play.Client.ENCHANT_ITEM, (event) -> {
+ if (current == null) return;
+ if (!current.isOpen(event.getPlayer())) return;
+
+ PacketContainer packet = event.getPacket();
+
+ int windowId, buttonId;
+ windowId = packet.getIntegers().read(0);
+ buttonId = packet.getIntegers().read(1);
+
+ var page = pages.getOrDefault(event.getPlayer(),1);
+
+ if (buttonId == 1) { // prev page
+ page--;
+ } else if (buttonId == 2) { // nextc page
+ page++;
+ } else {
+ return;
+ }
+
+ pages.put(event.getPlayer(), page);
+ var npacket = new PacketContainer(PacketType.Play.Server.WINDOW_DATA);
+ npacket.getIntegers().write(0, windowId);
+ npacket.getIntegers().write(1, 0);
+ npacket.getIntegers().write(2, page);
+ ProtocolLibrary.getProtocolManager().sendServerPacket(event.getPlayer(), npacket);
+ });
+ }
+
+ public Alert start(String... content) {
+ stop();
+ current = new Alert(content);
+ notifyTask = new AlertRunnable(current, v -> this.stop()).runTaskTimer(getPlugin(), 0, 10);
+ return current;
+ }
+
+ public void stop() {
+ if (current == null) return;
+ for (Player player : getPlugin().getServer().getOnlinePlayers()) {
+ if (current.isOpen(player))
+ player.closeInventory();
+ }
+ pages.clear();
+ notifyTask.cancel();
+ current = null;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/alert/AlertRunnable.java b/src/main/java/eu/m724/tweaks/module/alert/AlertRunnable.java
similarity index 96%
rename from src/main/java/eu/m724/tweaks/alert/AlertRunnable.java
rename to src/main/java/eu/m724/tweaks/module/alert/AlertRunnable.java
index d2c3400..9ce8989 100644
--- a/src/main/java/eu/m724/tweaks/alert/AlertRunnable.java
+++ b/src/main/java/eu/m724/tweaks/module/alert/AlertRunnable.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.alert;
+package eu.m724.tweaks.module.alert;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ChatMessageType;
diff --git a/src/main/java/eu/m724/tweaks/auth/AuthCommands.java b/src/main/java/eu/m724/tweaks/module/auth/AuthCommands.java
similarity index 94%
rename from src/main/java/eu/m724/tweaks/auth/AuthCommands.java
rename to src/main/java/eu/m724/tweaks/module/auth/AuthCommands.java
index 5463fb4..5aae83f 100644
--- a/src/main/java/eu/m724/tweaks/auth/AuthCommands.java
+++ b/src/main/java/eu/m724/tweaks/module/auth/AuthCommands.java
@@ -1,12 +1,13 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.auth;
+package eu.m724.tweaks.module.auth;
-import eu.m724.tweaks.TweaksConfig;
+import eu.m724.tweaks.Language;
+import eu.m724.tweaks.config.TweaksConfig;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ClickEvent;
@@ -50,7 +51,7 @@ public class AuthCommands implements CommandExecutor {
.underlined(true)
.color(ChatColor.GRAY)
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, hostname))
- .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to copy")))
+ .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(Language.getString("clickToCopy"))))
.build();
sender.spigot().sendMessage(component);
} else {
diff --git a/src/main/java/eu/m724/tweaks/auth/AuthListener.java b/src/main/java/eu/m724/tweaks/module/auth/AuthListener.java
similarity index 79%
rename from src/main/java/eu/m724/tweaks/auth/AuthListener.java
rename to src/main/java/eu/m724/tweaks/module/auth/AuthListener.java
index 1ed2a4f..b22b677 100644
--- a/src/main/java/eu/m724/tweaks/auth/AuthListener.java
+++ b/src/main/java/eu/m724/tweaks/module/auth/AuthListener.java
@@ -1,19 +1,21 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.auth;
+package eu.m724.tweaks.module.auth;
+import eu.m724.tweaks.DebugLogger;
import eu.m724.tweaks.Language;
-import eu.m724.tweaks.TweaksConfig;
+import eu.m724.tweaks.config.TweaksConfig;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerLoginEvent;
import java.io.FileNotFoundException;
+import java.io.IOException;
public class AuthListener implements Listener {
private final AuthStorage authStorage;
@@ -40,6 +42,10 @@ public class AuthListener implements Listener {
allowed = true; // key just assigned
} catch (FileNotFoundException | AuthStorage.AlreadyClaimedException | AuthStorage.InvalidKeyException e) {
allowed = !force; // If forced all players must have a key
+ } catch (IOException e) {
+ DebugLogger.severe("Error assigning key to player. " + e.getMessage());
+ event.disallow(PlayerLoginEvent.Result.KICK_OTHER, Language.getString("authKickError"));
+ allowed = true; // to skip the below checks
}
}
diff --git a/src/main/java/eu/m724/tweaks/module/auth/AuthModule.java b/src/main/java/eu/m724/tweaks/module/auth/AuthModule.java
new file mode 100644
index 0000000..f09af23
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/auth/AuthModule.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.auth;
+
+import eu.m724.tweaks.module.TweaksModule;
+
+public class AuthModule extends TweaksModule {
+ @Override
+ protected void onInit() {
+ var authStorage = new AuthStorage(getPlugin());
+
+ registerEvents(new AuthListener(authStorage));
+ registerCommand("tauth", new AuthCommands(authStorage));
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/auth/AuthStorage.java b/src/main/java/eu/m724/tweaks/module/auth/AuthStorage.java
similarity index 80%
rename from src/main/java/eu/m724/tweaks/auth/AuthStorage.java
rename to src/main/java/eu/m724/tweaks/module/auth/AuthStorage.java
index 3b08b6a..6692723 100644
--- a/src/main/java/eu/m724/tweaks/auth/AuthStorage.java
+++ b/src/main/java/eu/m724/tweaks/module/auth/AuthStorage.java
@@ -1,30 +1,34 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.auth;
+package eu.m724.tweaks.module.auth;
import org.bukkit.plugin.Plugin;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
import java.util.Random;
import java.util.UUID;
public class AuthStorage {
+ private static final char[] KEY_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
+ private static final int KEY_LENGTH = 10;
+ private static final SecureRandom RANDOM = new SecureRandom();
+
private final File playersDirectory;
private final File keysDirectory;
AuthStorage(Plugin plugin) {
- File directory = new File(plugin.getDataFolder(), "auth storage");
+ File directory = new File(plugin.getDataFolder(), "storage/auth");
this.playersDirectory = new File(directory, "players");
this.keysDirectory = new File(directory, "keys");
- directory.mkdir();
- keysDirectory.mkdir();
+ keysDirectory.mkdirs();
playersDirectory.mkdir();
}
@@ -72,7 +76,7 @@ public class AuthStorage {
byte[] bytes = is.readNBytes(50);
return new String(bytes, StandardCharsets.UTF_8);
} catch (IOException e) {
- throw new RuntimeException(e); // TODO
+ throw new RuntimeException(e);
}
}
@@ -122,7 +126,7 @@ public class AuthStorage {
* @throws FileNotFoundException if no such key
* @throws AlreadyClaimedException if key is claimed or user owns another key
*/
- void assignOwner(String key, UUID uuid) throws FileNotFoundException, AlreadyClaimedException {
+ void assignOwner(String key, UUID uuid) throws IOException, FileNotFoundException, AlreadyClaimedException {
if (isInvalid(key)) throw new InvalidKeyException();
if (getUserOfKey(key) != null) throw new AlreadyClaimedException();
@@ -137,34 +141,25 @@ public class AuthStorage {
try (FileOutputStream os = new FileOutputStream(file)) {
os.write(byteBuffer.array());
- } catch (IOException e) {
- throw new RuntimeException(e); // TODO
}
File file2 = new File(playersDirectory, uuid.toString());
try (FileOutputStream os = new FileOutputStream(file2)) {
os.write(key.getBytes(StandardCharsets.UTF_8));
- } catch (IOException e) {
- throw new RuntimeException(e); // TODO
}
}
- // TODO improve
String generateKey() {
- char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
- Random random = new Random();
- int length = random.nextInt(8, 10);
+ StringBuilder builder = new StringBuilder();
- StringBuilder key = new StringBuilder();
-
- for (int i=0; i 1) {
+ password = Arrays.stream(args).skip(1).collect(Collectors.joining(" "));
+ }
+
+ boolean authenticated = false;
+ BaseComponent component = null;
+ ChatRoom newRoom = manager.getById(id);
+ if (newRoom != null) {
+ if (newRoom.password != null) {
+ if (newRoom.password.equals(password)) {
+ authenticated = true;
+ } else if (password == null) {
+ component = Language.getComponent("chatPasswordProtected", ChatColor.RED);
+ } else {
+ component = Language.getComponent("chatWrongPassword", ChatColor.RED);
+ }
+ } else {
+ authenticated = true;
+ }
+ } else {
+ if (ChatRoomLoader.validateId(id) == 0) {
+ component = Language.getComponent("chatNoSuchRoom", ChatColor.RED, id);
+ } else {
+ component = Language.getComponent("chatNoSuchRoomInvalidId", ChatColor.RED, id);
+ }
+ }
+
+ if (authenticated) {
+ /*component = new ComponentBuilder(Language.getComponent("chatJoined", ChatColor.GOLD))
+ .append(" ")
+ .append(ChatFormatUtils.formatChatRoom(chatRoom)).color(newRoom.color)
+ .build();*/
+ player.sendMessage("");
+ manager.setPlayerChatRoom(newRoom, player);
+ } else {
+ player.spigot().sendMessage(component);
+ }
+
+ }
+ } else if (command.getName().equals("chatmanage")) {
+ Player player = (Player) sender;
+ ChatRoom chatRoom = manager.getPlayerChatRoom(player);
+ boolean isOwner = player.equals(chatRoom.owner);
+
+ String action = args.length > 0 ? args[0] : null;
+ String argument = args.length > 1 ? args[1] : null;
+
+ switch (action) {
+ case "create" -> {
+ if (argument == null) {
+ sender.sendMessage("Please pass a room name as an argument. The room name can contain only characters and digits.");
+ return true;
+ }
+
+ try {
+ ChatRoom newRoom = manager.createChatRoom(argument, null, player);
+
+ var component = new ComponentBuilder("Created a chat room. Join it: ").color(ChatColor.GOLD)
+ .append("/c " + newRoom.id).color(ChatColor.AQUA)
+ .event(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/c " + newRoom.id))
+ .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(Language.getString("clickToExecuteCommand"))))
+ .append("\n")
+ .append("To protect it with a password, join it and use ").color(ChatColor.GRAY)
+ .append("/cm setpassword ").color(ChatColor.DARK_PURPLE)
+ .event(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/cm setpassword "))
+ .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(Language.getString("clickToExecuteCommand"))));
+
+ sender.spigot().sendMessage(component.build());
+ } catch (ChatModule.InvalidIdException e) {
+ var component = new ComponentBuilder("ID is invalid: ").color(ChatColor.GRAY)
+ .append(e.getMessage()).color(ChatColor.RED);
+
+ sender.spigot().sendMessage(component.build());
+ } catch (ChatModule.ChatRoomExistsException e) {
+ sender.sendMessage("Room %s already exists".formatted(argument));
+ } catch (IOException e) {
+ sender.sendMessage("Error creating room");
+ throw new RuntimeException(e);
+ }
+ }
+ case "delete" -> {
+ if (isOwner) {
+ if (argument == null) {
+ sender.sendMessage("You want to delete room \"%s\". Confirm by passing the ID as an argument.".formatted(chatRoom.id));
+ return true;
+ }
+
+ if (argument.equals(chatRoom.id)) {
+ manager.deleteChatRoom(chatRoom);
+ sender.sendMessage("Room %s deleted".formatted(chatRoom.id));
+ } else {
+ sender.sendMessage("Pass \"%s\" as an argument to confirm".formatted(chatRoom.id));
+ }
+ } else {
+ sender.sendMessage("You're not the owner of %s, please enter the room you want to make changes in".formatted(chatRoom.id));
+ }
+ }
+ case "setowner" -> {
+ if (isOwner) {
+ if (argument == null) {
+ sender.sendMessage("To transfer ownership of room %s, pass the new owner name as an argument for this action.".formatted(chatRoom.id));
+ return true;
+ }
+
+ Player newOwner = Bukkit.getPlayer(argument);
+ if (newOwner != null && newOwner.isOnline()) {
+ chatRoom.owner = newOwner;
+ try {
+ manager.saveChatRoom(chatRoom);
+ sender.sendMessage("Owner changed to " + newOwner.getName());
+ } catch (IOException e) {
+ sender.sendMessage("Error changing owner");
+ throw new RuntimeException(e);
+ }
+ } else {
+ sender.sendMessage("Player must be online");
+ }
+ } else {
+ sender.sendMessage("You're not the owner of %s, please enter the room you want to make changes in".formatted(chatRoom.id));
+ }
+ }
+ case "setpassword" -> {
+ if (isOwner) {
+ if (argument == null) {
+ sender.sendMessage("To change the password of room %s, pass the new password as an argument for this action.".formatted(chatRoom.id));
+ return true;
+ }
+
+ chatRoom.password = Arrays.stream(args).skip(1).collect(Collectors.joining(" "));
+ try {
+ manager.saveChatRoom(chatRoom);
+
+ var component = new ComponentBuilder("Password changed to ").color(ChatColor.GREEN)
+ .append("(hover to view)").color(ChatColor.AQUA)
+ .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(chatRoom.password)))
+ .append("\n")
+ .append("To unset, ").color(ChatColor.GRAY)
+ .append("/cm unsetpassword").color(ChatColor.DARK_PURPLE)
+ .event(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/cm unsetpassword"))
+ .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(Language.getString("clickToExecuteCommand"))));
+
+ sender.spigot().sendMessage(component.build());
+ } catch (IOException e) {
+ sender.sendMessage("Error changing password");
+ throw new RuntimeException(e);
+ }
+ } else {
+ sender.sendMessage("You're not the owner of %s, please enter the room you want to make changes in".formatted(chatRoom.id));
+ }
+ }
+ case "unsetpassword" -> {
+ if (isOwner) {
+ chatRoom.password = null;
+ try {
+ manager.saveChatRoom(chatRoom);
+ sender.sendMessage("Password removed from " + chatRoom.id);
+ } catch (IOException e) {
+ sender.sendMessage("Error removing password");
+ throw new RuntimeException(e);
+ }
+ } else {
+ sender.sendMessage("You're not the owner of %s, please enter the room you want to make changes in".formatted(chatRoom.id));
+ }
+ }
+ case "setcolor" -> {
+ if (isOwner) {
+ if (argument == null) {
+ sender.sendMessage("To change the message color of room %s, pass the new color as an argument for this action. #hex or color name.".formatted(chatRoom.id));
+ return true;
+ }
+
+ ChatColor newColor = ChatColor.of(argument);
+ if (newColor != null) {
+ chatRoom.color = newColor;
+ try {
+ manager.saveChatRoom(chatRoom);
+ sender.sendMessage("Message color changed to " + newColor.getName());
+ } catch (IOException e) {
+ sender.sendMessage("Error changing color");
+ throw new RuntimeException(e);
+ }
+ } else {
+ sender.sendMessage("Invalid color");
+ }
+ } else {
+ sender.sendMessage("You're not the owner of %s, please enter the room you want to make changes in".formatted(chatRoom.id));
+ }
+ }
+ case null, default -> {
+ sender.sendMessage("Actions: create, delete, setowner, setpassword, unsetpassword, setcolor");
+ }
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/chat/ChatFormatUtils.java b/src/main/java/eu/m724/tweaks/module/chat/ChatFormatUtils.java
similarity index 96%
rename from src/main/java/eu/m724/tweaks/chat/ChatFormatUtils.java
rename to src/main/java/eu/m724/tweaks/module/chat/ChatFormatUtils.java
index b152f9e..016beeb 100644
--- a/src/main/java/eu/m724/tweaks/chat/ChatFormatUtils.java
+++ b/src/main/java/eu/m724/tweaks/module/chat/ChatFormatUtils.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.chat;
+package eu.m724.tweaks.module.chat;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
diff --git a/src/main/java/eu/m724/tweaks/chat/ChatListener.java b/src/main/java/eu/m724/tweaks/module/chat/ChatListener.java
similarity index 88%
rename from src/main/java/eu/m724/tweaks/chat/ChatListener.java
rename to src/main/java/eu/m724/tweaks/module/chat/ChatListener.java
index f114e58..21b65f9 100644
--- a/src/main/java/eu/m724/tweaks/chat/ChatListener.java
+++ b/src/main/java/eu/m724/tweaks/module/chat/ChatListener.java
@@ -1,12 +1,12 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.chat;
+package eu.m724.tweaks.module.chat;
-import eu.m724.tweaks.TweaksConfig;
+import eu.m724.tweaks.config.TweaksConfig;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ComponentBuilder;
@@ -14,8 +14,8 @@ import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import net.minecraft.network.chat.Component;
-import org.bukkit.craftbukkit.v1_21_R1.CraftRegistry;
-import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
+import org.bukkit.craftbukkit.v1_21_R3.CraftRegistry;
+import org.bukkit.craftbukkit.v1_21_R3.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@@ -25,13 +25,14 @@ import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
public class ChatListener implements Listener {
- private final ChatManager chatManager;
+ private final ChatModule chatModule;
private final String defaultRoom = TweaksConfig.getConfig().chatDefaultName();
private final boolean localEvents = TweaksConfig.getConfig().chatLocalEvents();
private final int radius;
- public ChatListener(ChatManager chatManager) {
- this.chatManager = chatManager;
+ public ChatListener(ChatModule chatModule) {
+ this.chatModule = chatModule;
+
if (TweaksConfig.getConfig().chatRadius() < 0) {
radius = 0;
} else {
@@ -52,7 +53,7 @@ public class ChatListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
- ChatRoom chatRoom = chatManager.getPlayerChatRoom(player);
+ ChatRoom chatRoom = chatModule.getPlayerChatRoom(player);
if (localEvents) {
var cb = new ComponentBuilder()
@@ -77,7 +78,7 @@ public class ChatListener implements Listener {
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
- ChatRoom chatRoom = chatManager.removePlayer(player);
+ ChatRoom chatRoom = chatModule.removePlayer(player);
if (localEvents) {
var cb = new ComponentBuilder()
@@ -103,7 +104,7 @@ public class ChatListener implements Listener {
public void onPlayerDeath(PlayerDeathEvent event) {
if (localEvents) {
Player player = event.getEntity();
- ChatRoom chatRoom = chatManager.getPlayerChatRoom(player);
+ ChatRoom chatRoom = chatModule.getPlayerChatRoom(player);
// would be easier on Paper but this is not Paper
BaseComponent deathMessage = ComponentSerializer.deserialize(Component.Serializer.toJson(((CraftPlayer)player).getHandle().getCombatTracker().getDeathMessage(), CraftRegistry.getMinecraftRegistry()));
@@ -124,7 +125,7 @@ public class ChatListener implements Listener {
// broadcast to killer if available
if (player.getLastDamageCause().getDamageSource().getCausingEntity() instanceof Player killer) {
- ChatRoom chatRoom2 = chatManager.getPlayerChatRoom(killer);
+ ChatRoom chatRoom2 = chatModule.getPlayerChatRoom(killer);
if (chatRoom != chatRoom2) {
if (proximityFor(chatRoom)) {
chatRoom2.broadcastNear(killer.getLocation(), radius, component);
@@ -142,7 +143,7 @@ public class ChatListener implements Listener {
@EventHandler
public void onAsyncPlayerChat(AsyncPlayerChatEvent event) {
Player player = event.getPlayer();
- ChatRoom chatRoom = chatManager.getPlayerChatRoom(player);
+ ChatRoom chatRoom = chatModule.getPlayerChatRoom(player);
String message = event.getMessage();
var component = new ComponentBuilder()
diff --git a/src/main/java/eu/m724/tweaks/chat/ChatManager.java b/src/main/java/eu/m724/tweaks/module/chat/ChatModule.java
similarity index 81%
rename from src/main/java/eu/m724/tweaks/chat/ChatManager.java
rename to src/main/java/eu/m724/tweaks/module/chat/ChatModule.java
index ec4461f..ad8ae65 100644
--- a/src/main/java/eu/m724/tweaks/chat/ChatManager.java
+++ b/src/main/java/eu/m724/tweaks/module/chat/ChatModule.java
@@ -1,45 +1,45 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.chat;
+package eu.m724.tweaks.module.chat;
-import eu.m724.tweaks.TweaksConfig;
+import eu.m724.tweaks.module.TweaksModule;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ComponentBuilder;
import org.bukkit.NamespacedKey;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.persistence.PersistentDataType;
-import org.bukkit.plugin.Plugin;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
-public class ChatManager {
- private final Plugin plugin;
- private final NamespacedKey chatRoomKey;
- private final String defaultRoom;
+public class ChatModule extends TweaksModule {
+ private final NamespacedKey chatRoomKey = new NamespacedKey(getPlugin(), "chatRoom");
+ private final String defaultRoom = getConfig().chatDefaultName();
private final Map playerMap = new HashMap<>();
private final Map roomIdMap = new HashMap<>();
- public ChatManager(Plugin plugin) {
- this.plugin = plugin;
- this.chatRoomKey = new NamespacedKey(plugin, "chatRoom");
- this.defaultRoom = TweaksConfig.getConfig().chatDefaultName();
- }
- public void init() {
- if (plugin.getServer().isEnforcingSecureProfiles()) {
+ @Override
+ protected void onInit() {
+ if (getPlugin().getServer().isEnforcingSecureProfiles()) {
throw new RuntimeException("Please disable enforce-secure-profile in server.properties to use chatrooms");
}
+ ChatRoomLoader.init(getPlugin());
getById(defaultRoom);
- plugin.getServer().getPluginManager().registerEvents(new ChatListener(this), plugin);
+ registerEvents(new ChatListener(this));
+
+ var chatCommands = new ChatCommands(this);
+ registerCommand("chat", chatCommands);
+ registerCommand("chatmanage", chatCommands);
+
}
/**
@@ -69,7 +69,7 @@ public class ChatManager {
if (id.equals(defaultRoom)) {
chatRoom = new ChatRoom(defaultRoom, null, null);
} else {
- chatRoom = ChatRoomLoader.load(plugin, id);
+ chatRoom = ChatRoomLoader.load(id);
}
roomIdMap.put(id, chatRoom);
}
@@ -155,28 +155,28 @@ public class ChatManager {
case 0:
break;
case 1:
- throw new InvalidIdException("ID is too short, make it at least 2 chars");
+ throw new InvalidIdException("ID is too short, it must be at least 2 chars long");
case 2:
- throw new InvalidIdException("ID is too long, make it 20 chars or shorter");
+ throw new InvalidIdException("ID is too long, it mustn't be longer than 20 chars");
case 4:
- throw new InvalidIdException("ID must be composed from characters a-z and numbers 0-9");
+ throw new InvalidIdException("ID must be of characters a-z and numbers 0-9");
}
if (getById(id) != null)
throw new ChatRoomExistsException();
ChatRoom chatRoom = new ChatRoom(id, password, owner);
- ChatRoomLoader.save(plugin, chatRoom);
+ ChatRoomLoader.save(chatRoom);
return chatRoom;
}
void saveChatRoom(ChatRoom chatRoom) throws IOException {
- ChatRoomLoader.save(plugin, chatRoom);
+ ChatRoomLoader.save(chatRoom);
}
public void deleteChatRoom(ChatRoom chatRoom) {
roomIdMap.remove(chatRoom.id);
- ChatRoomLoader.getFile(plugin, chatRoom.id).delete();
+ ChatRoomLoader.getFile(chatRoom.id).delete();
chatRoom.players.forEach(player -> setPlayerChatRoom(getById(defaultRoom), player));
}
diff --git a/src/main/java/eu/m724/tweaks/chat/ChatRoom.java b/src/main/java/eu/m724/tweaks/module/chat/ChatRoom.java
similarity index 97%
rename from src/main/java/eu/m724/tweaks/chat/ChatRoom.java
rename to src/main/java/eu/m724/tweaks/module/chat/ChatRoom.java
index a659694..3eb34fe 100644
--- a/src/main/java/eu/m724/tweaks/chat/ChatRoom.java
+++ b/src/main/java/eu/m724/tweaks/module/chat/ChatRoom.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.chat;
+package eu.m724.tweaks.module.chat;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
diff --git a/src/main/java/eu/m724/tweaks/chat/ChatRoomLoader.java b/src/main/java/eu/m724/tweaks/module/chat/ChatRoomLoader.java
similarity index 78%
rename from src/main/java/eu/m724/tweaks/chat/ChatRoomLoader.java
rename to src/main/java/eu/m724/tweaks/module/chat/ChatRoomLoader.java
index f2c73fa..0af0f59 100644
--- a/src/main/java/eu/m724/tweaks/chat/ChatRoomLoader.java
+++ b/src/main/java/eu/m724/tweaks/module/chat/ChatRoomLoader.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.chat;
+package eu.m724.tweaks.module.chat;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
@@ -13,23 +13,25 @@ import org.bukkit.plugin.Plugin;
import java.io.File;
import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.UUID;
public class ChatRoomLoader {
+ private static File chatRoomsDir;
+
+ static void init(Plugin plugin) {
+ chatRoomsDir = new File(plugin.getDataFolder(), "storage/rooms");
+ chatRoomsDir.mkdirs();
+ }
+
/**
* Get the file of persistent storage of a chat room
* @return the file or null if ID is invalid
*/
- static File getFile(Plugin plugin, String id) {
- Path chatRoomsPath = Paths.get(plugin.getDataFolder().getPath(), "rooms");
- chatRoomsPath.toFile().mkdirs();
-
+ static File getFile(String id) {
if (validateId(id) != 0)
- throw new RuntimeException("Invalid id: " + id);
+ return null;
- return Paths.get(chatRoomsPath.toFile().getPath(), id + ".yml").toFile();
+ return new File(chatRoomsDir, id + ".yml");
}
/**
@@ -62,9 +64,10 @@ public class ChatRoomLoader {
* @param id the id of the chat room
* @return the chat room or null if no such chat room
*/
- static ChatRoom load(Plugin plugin, String id) {
- File chatRoomFile = getFile(plugin, id);
- if (!chatRoomFile.exists()) return null;
+ static ChatRoom load(String id) {
+ File chatRoomFile = getFile(id);
+ if (chatRoomFile == null || !chatRoomFile.exists())
+ return null;
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(chatRoomFile);
@@ -89,14 +92,15 @@ public class ChatRoomLoader {
*
* @throws IOException if saving failed
*/
- static void save(Plugin plugin, ChatRoom chatRoom) throws IOException {
+ static void save(ChatRoom chatRoom) throws IOException {
YamlConfiguration configuration = new YamlConfiguration();
configuration.set("password", chatRoom.password);
configuration.set("color", chatRoom.color.getName());
+ // TODO consider just making this str to make it easier
configuration.set("owner.msb", chatRoom.owner.getUniqueId().getMostSignificantBits());
configuration.set("owner.lsb", chatRoom.owner.getUniqueId().getLeastSignificantBits());
- File chatRoomFile = getFile(plugin, chatRoom.id);
+ File chatRoomFile = getFile(chatRoom.id);
configuration.save(chatRoomFile);
}
}
diff --git a/src/main/java/eu/m724/tweaks/compass/CompassListener.java b/src/main/java/eu/m724/tweaks/module/compass/CompassModule.java
similarity index 91%
rename from src/main/java/eu/m724/tweaks/compass/CompassListener.java
rename to src/main/java/eu/m724/tweaks/module/compass/CompassModule.java
index f1873cd..5516f93 100644
--- a/src/main/java/eu/m724/tweaks/compass/CompassListener.java
+++ b/src/main/java/eu/m724/tweaks/module/compass/CompassModule.java
@@ -1,12 +1,12 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.compass;
+package eu.m724.tweaks.module.compass;
-import eu.m724.tweaks.TweaksConfig;
+import eu.m724.tweaks.module.TweaksModule;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.ComponentBuilder;
@@ -25,9 +25,9 @@ import java.util.LinkedHashMap;
import java.util.Map;
// TODO dimension check
-public class CompassListener implements Listener {
- private final int precision = TweaksConfig.getConfig().compassPrecision(); // degrees every point
- private final int width = TweaksConfig.getConfig().compassWidth(); // points left to right
+public class CompassModule extends TweaksModule implements Listener {
+ private final int precision = getConfig().compassPrecision(); // degrees every point
+ private final int width = getConfig().compassWidth(); // points left to right
private final Map points = Map.of(
0, ChatColor.DARK_GRAY + "S",
@@ -36,6 +36,11 @@ public class CompassListener implements Listener {
270, ChatColor.DARK_GRAY + "E"
);
+ @Override
+ protected void onInit() {
+ registerEvents(this);
+ }
+
@EventHandler
public void onPlayerSwapHandItems(PlayerSwapHandItemsEvent event) {
if (event.getMainHandItem().getType() == Material.COMPASS || event.getOffHandItem().getType() == Material.COMPASS) {
diff --git a/src/main/java/eu/m724/tweaks/compass/CompassPlayerPreferences.java b/src/main/java/eu/m724/tweaks/module/compass/CompassPlayerPreferences.java
similarity index 89%
rename from src/main/java/eu/m724/tweaks/compass/CompassPlayerPreferences.java
rename to src/main/java/eu/m724/tweaks/module/compass/CompassPlayerPreferences.java
index f4d3b19..9e2fec2 100644
--- a/src/main/java/eu/m724/tweaks/compass/CompassPlayerPreferences.java
+++ b/src/main/java/eu/m724/tweaks/module/compass/CompassPlayerPreferences.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.compass;
+package eu.m724.tweaks.module.compass;
import org.bukkit.entity.Player;
diff --git a/src/main/java/eu/m724/tweaks/door/DoorKnockListener.java b/src/main/java/eu/m724/tweaks/module/door/DoorKnockModule.java
similarity index 89%
rename from src/main/java/eu/m724/tweaks/door/DoorKnockListener.java
rename to src/main/java/eu/m724/tweaks/module/door/DoorKnockModule.java
index b9c718d..2213363 100644
--- a/src/main/java/eu/m724/tweaks/door/DoorKnockListener.java
+++ b/src/main/java/eu/m724/tweaks/module/door/DoorKnockModule.java
@@ -1,11 +1,12 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.door;
+package eu.m724.tweaks.module.door;
+import eu.m724.tweaks.module.TweaksModule;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
@@ -22,7 +23,12 @@ import org.bukkit.util.RayTraceResult;
import java.util.concurrent.ThreadLocalRandom;
-public class DoorKnockListener implements Listener {
+public class DoorKnockModule extends TweaksModule implements Listener {
+ @Override
+ protected void onInit() {
+ registerEvents(this);
+ }
+
@EventHandler
public void onBlockDamageAbort(BlockDamageAbortEvent event) {
Block block = event.getBlock();
diff --git a/src/main/java/eu/m724/tweaks/door/DoorOpenListener.java b/src/main/java/eu/m724/tweaks/module/door/DoorOpenModule.java
similarity index 91%
rename from src/main/java/eu/m724/tweaks/door/DoorOpenListener.java
rename to src/main/java/eu/m724/tweaks/module/door/DoorOpenModule.java
index 1b431eb..7705c68 100644
--- a/src/main/java/eu/m724/tweaks/door/DoorOpenListener.java
+++ b/src/main/java/eu/m724/tweaks/module/door/DoorOpenModule.java
@@ -1,11 +1,12 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.door;
+package eu.m724.tweaks.module.door;
+import eu.m724.tweaks.module.TweaksModule;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
@@ -15,7 +16,11 @@ import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
-public class DoorOpenListener implements Listener {
+public class DoorOpenModule extends TweaksModule implements Listener {
+ @Override
+ protected void onInit() {
+ registerEvents(this);
+ }
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
@@ -71,5 +76,4 @@ public class DoorOpenListener implements Listener {
location.getBlock().setBlockData(nextDoor);
}
}
-
}
diff --git a/src/main/java/eu/m724/tweaks/module/durability/DPlayerProperties.java b/src/main/java/eu/m724/tweaks/module/durability/DPlayerProperties.java
new file mode 100644
index 0000000..d3a92d3
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/durability/DPlayerProperties.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.durability;
+
+import eu.m724.tweaks.TweaksPlugin;
+import org.bukkit.NamespacedKey;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
+import org.bukkit.persistence.PersistentDataType;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class DPlayerProperties implements Listener {
+ private final NamespacedKey namespacedKey = new NamespacedKey(TweaksPlugin.getInstance(), "durability_enabled");
+ private final Set players = new HashSet<>();
+
+ @EventHandler
+ public void onPlayerJoin(PlayerJoinEvent event) {
+ var player = event.getPlayer();
+
+ if (player.hasPermission("tweaks724.durabilityalert")) {
+ var enabled = player.getPersistentDataContainer().get(namespacedKey, PersistentDataType.BOOLEAN);
+ if (enabled != null && enabled) {
+ players.add(player);
+ }
+ }
+ }
+
+ @EventHandler
+ public void onPlayerQuit(PlayerQuitEvent event) {
+ players.remove(event.getPlayer());
+ }
+
+ Set getPlayers() {
+ return Set.copyOf(players);
+ }
+
+ boolean isPlayerEnabled(Player player) {
+ return players.contains(player);
+ }
+
+ void disableForPlayer(Player player) {
+ players.remove(player);
+ player.getPersistentDataContainer().set(namespacedKey, PersistentDataType.BOOLEAN, false);
+ }
+
+ void enableForPlayer(Player player) {
+ players.add(player);
+ player.getPersistentDataContainer().set(namespacedKey, PersistentDataType.BOOLEAN, true);
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/durability/DurabilityCaches.java b/src/main/java/eu/m724/tweaks/module/durability/DurabilityCaches.java
new file mode 100644
index 0000000..4ab41d7
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/durability/DurabilityCaches.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.durability;
+
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class DurabilityCaches {
+ private final Map lastFullReminder = new HashMap<>();
+ // BAD
+ private final Map> lastUse = new HashMap<>();
+ // BAD
+ private final Map> lastPing = new HashMap<>();
+
+ boolean shouldFullRemind(Player player, long now) {
+ var lfr = lastFullReminder.getOrDefault(player, 0L);
+
+ if (now - lfr > 300 * 1000) {
+ lastFullReminder.put(player, now);
+ return true;
+ } else if (now - lfr < 3 * 1000) {
+ return true;
+ }
+
+ return false;
+ }
+
+ boolean shouldRemind(Player player, ItemStack itemStack, long now) {
+ var lu = lastUse.computeIfAbsent(player, (k) -> new HashMap<>()).getOrDefault(itemStack.getType(), 0L);
+
+ if (now - lu > 180 * 1000) {
+ lastUse.get(player).put(itemStack.getType(), now);
+ return true;
+ } else if (now - lu < 3 * 1000) {
+ return true;
+ }
+
+ return false;
+ }
+
+ boolean shouldPing(Player player, ItemStack itemStack, long now) {
+ var lp = lastPing.computeIfAbsent(player, (k) -> new HashMap<>()).getOrDefault(itemStack.getType(), 0L);
+
+ if (now - lp > 60 * 1000) {
+ lastPing.get(player).put(itemStack.getType(), now);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/durability/DurabilityCommands.java b/src/main/java/eu/m724/tweaks/module/durability/DurabilityCommands.java
new file mode 100644
index 0000000..2d6501c
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/durability/DurabilityCommands.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.durability;
+
+import eu.m724.tweaks.Language;
+import net.md_5.bungee.api.ChatColor;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+public class DurabilityCommands implements CommandExecutor {
+ private final DPlayerProperties properties;
+
+ public DurabilityCommands(DPlayerProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage("Only players can use this command");
+ return true;
+ }
+
+ if (properties.isPlayerEnabled(player)) {
+ properties.disableForPlayer(player);
+ sender.spigot().sendMessage(Language.getComponent("durabilityDisabled", ChatColor.GRAY));
+ } else {
+ properties.enableForPlayer(player);
+ sender.spigot().sendMessage(Language.getComponent("durabilityEnabled", ChatColor.GRAY));
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/durability/DurabilityModule.java b/src/main/java/eu/m724/tweaks/module/durability/DurabilityModule.java
new file mode 100644
index 0000000..b582bfc
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/durability/DurabilityModule.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.durability;
+
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.module.TweaksModule;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.ChatMessageType;
+import net.md_5.bungee.api.chat.ComponentBuilder;
+import org.bukkit.Material;
+import org.bukkit.Sound;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerItemDamageEvent;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.Damageable;
+import org.bukkit.scheduler.BukkitRunnable;
+
+import java.awt.Color;
+
+public class DurabilityModule extends TweaksModule implements Listener {
+ private final DurabilityCaches cache = new DurabilityCaches();
+ private final DPlayerProperties properties = new DPlayerProperties();
+
+ @Override
+ protected void onInit() {
+ registerEvents(this);
+
+ registerCommand("durabilityalert", new DurabilityCommands(properties));
+
+ new BukkitRunnable() {
+ @Override
+ public void run() {
+ properties.getPlayers().forEach(p -> refreshBar(p));
+ }
+ }.runTaskTimerAsynchronously(getPlugin(), 0, 40);
+ }
+
+ @EventHandler
+ public void onPlayerItemDamage(PlayerItemDamageEvent event) {
+ refreshBar(event.getPlayer(), event.getItem(), event.getDamage());
+ }
+
+ private void refreshBar(Player player) {
+ refreshBar(player, null, -1);
+ }
+
+ private void refreshBar(Player player, ItemStack justDamaged, int damage) {
+ if (!properties.isPlayerEnabled(player)) return;
+
+ var items = new ItemStack[] {
+ player.getInventory().getHelmet(),
+ player.getInventory().getChestplate(),
+ player.getInventory().getLeggings(),
+ player.getInventory().getBoots(),
+ player.getInventory().getItemInMainHand(),
+ player.getInventory().getItemInOffHand()
+ };
+
+ var builder = new ComponentBuilder();
+
+ var now = System.currentTimeMillis();
+ var all = cache.shouldFullRemind(player, now);
+
+ for (var itemStack : items) {
+ if (itemStack == null || !itemStack.hasItemMeta()) continue;
+
+ if (itemStack.getItemMeta() instanceof Damageable meta) {
+ var target = itemStack.equals(justDamaged);
+
+ var maxDurability = itemStack.getType().getMaxDurability();
+ var durability = maxDurability - meta.getDamage() - (target ? damage : 0);
+ durability = Math.max(0, durability);
+ var percentage = (double) durability / maxDurability;
+
+ var notify = durability < 30 && (durability < 10 || percentage < 0.1);
+
+ var remind = cache.shouldRemind(player, itemStack, now);
+ var important = target && notify && cache.shouldPing(player, itemStack, now);
+
+ DebugLogger.finer("%s's %s: %d / %d (%.2f%%)%s%s", player.getName(), itemStack.getType().name(), durability, maxDurability, percentage * 100, notify ? " notify" : "", important ? " important" : "");
+
+ if (notify || all || remind) {
+ var longName = remind || important;
+ var label = longName ? getMaterialLongName(itemStack.getType()) : getMaterialShortName(itemStack.getType());
+ var labelColor = percentage > 0 ? matColor(itemStack.getType()) : ChatColor.DARK_RED;
+
+ var percentageStr = (int) (percentage * 100) + "%";
+ var percentageColor = mixColor(labelColor, ChatColor.DARK_RED, 1.0 - percentage * 10);
+
+ builder.append(label + " ").color(labelColor);
+ builder.append(percentageStr + " ").color(percentageColor);
+
+ if (important) {
+ player.playSound(player, Sound.BLOCK_ANVIL_PLACE, 0.5f, 1.5f);
+ player.sendTitle("", labelColor + label + " " + percentageColor + percentageStr, 5, 20, 5);
+ }
+ }
+ }
+ }
+
+ var component = builder.create();
+ if (component.length > 0)
+ player.spigot().sendMessage(ChatMessageType.ACTION_BAR, component);
+ }
+
+ private String getMaterialLongName(Material material) {
+ var sp = material.name().split("_");
+ var str = sp[sp.length - 1];
+ return str.charAt(0) + str.substring(1).toLowerCase();
+ }
+
+ private String getMaterialShortName(Material material) {
+ return getMaterialLongName(material).substring(0, 2);
+ }
+
+ private ChatColor mixColor(ChatColor from, ChatColor to, double percentage) {
+ percentage = Math.clamp(percentage, 0.0, 1.0);
+
+ var diffR = to.getColor().getRed() - from.getColor().getRed();
+ var diffG = to.getColor().getGreen() - from.getColor().getGreen();
+ var diffB = to.getColor().getBlue() - from.getColor().getBlue();
+
+ var r = from.getColor().getRed() + (int) (diffR * percentage);
+ var g = from.getColor().getGreen() + (int) (diffG * percentage);
+ var b = from.getColor().getBlue() + (int) (diffB * percentage);
+
+ return ChatColor.of(new Color(r, g, b));
+ }
+
+ private ChatColor matColor(Material material) {
+ var color = ChatColor.DARK_GRAY;
+
+ if (material.name().startsWith("DIAMOND_")) {
+ color = ChatColor.AQUA;
+ } else if (material.name().startsWith("NETHERITE_")) {
+ color = ChatColor.DARK_PURPLE;
+ } else if (material.name().startsWith("IRON_")) {
+ color = ChatColor.WHITE;
+ } else if (material.name().startsWith("STONE_")) {
+ color = ChatColor.GRAY;
+ } else if (material.name().startsWith("WOODEN_")) {
+ color = ChatColor.DARK_GREEN;
+ } else if (material.name().startsWith("GOLDEN_")) {
+ color = ChatColor.GOLD;
+ }
+
+ return color;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/full/FullListener.java b/src/main/java/eu/m724/tweaks/module/full/FullModule.java
similarity index 66%
rename from src/main/java/eu/m724/tweaks/full/FullListener.java
rename to src/main/java/eu/m724/tweaks/module/full/FullModule.java
index 29f4572..e9569f2 100644
--- a/src/main/java/eu/m724/tweaks/full/FullListener.java
+++ b/src/main/java/eu/m724/tweaks/module/full/FullModule.java
@@ -1,16 +1,22 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.full;
+package eu.m724.tweaks.module.full;
+import eu.m724.tweaks.module.TweaksModule;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerLoginEvent;
-public class FullListener implements Listener {
+public class FullModule extends TweaksModule implements Listener {
+ @Override
+ protected void onInit() {
+ registerEvents(this);
+ }
+
@EventHandler
public void onPlayerLogin(PlayerLoginEvent event) {
if (event.getResult() == PlayerLoginEvent.Result.KICK_FULL && event.getPlayer().hasPermission("tweaks724.bypass-full")) {
diff --git a/src/main/java/eu/m724/tweaks/module/hardcore/HardcoreModule.java b/src/main/java/eu/m724/tweaks/module/hardcore/HardcoreModule.java
new file mode 100644
index 0000000..387d51b
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/hardcore/HardcoreModule.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.hardcore;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.events.PacketContainer;
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.module.TweaksModule;
+import net.minecraft.world.level.levelgen.RandomSupport;
+import net.minecraft.world.level.levelgen.Xoroshiro128PlusPlus;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+// how we do it is much faster than any Random
+public class HardcoreModule extends TweaksModule {
+ private final Xoroshiro128PlusPlus rng;
+ private final long chanceLong;
+
+ public HardcoreModule() {
+ this.rng = new Xoroshiro128PlusPlus(
+ RandomSupport.generateUniqueSeed(),
+ RandomSupport.generateUniqueSeed()
+ );
+
+ this.chanceLong = BigInteger.valueOf(Long.MIN_VALUE)
+ .add(
+ new BigDecimal(
+ BigInteger.valueOf(Long.MAX_VALUE).subtract(BigInteger.valueOf(Long.MIN_VALUE))
+ ).multiply(
+ BigDecimal.valueOf(getConfig().hardcoreChance())
+ ).toBigInteger()
+ ).longValue();
+ }
+
+ @Override
+ protected void onInit() {
+ DebugLogger.fine("Chance long: " + chanceLong);
+
+ onPacketSend(PacketType.Play.Server.LOGIN, (event) -> {
+ PacketContainer packet = event.getPacket();
+
+ if (rng.nextLong() < chanceLong)
+ // the "is hardcore" boolean https://wiki.vg/Protocol#Login_.28play.29
+ packet.getBooleans().write(0, true);
+ });
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/killswitch/KillswitchModule.java b/src/main/java/eu/m724/tweaks/module/killswitch/KillswitchModule.java
new file mode 100644
index 0000000..9756b88
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/killswitch/KillswitchModule.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.killswitch;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.module.TweaksModule;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Base64;
+
+public class KillswitchModule extends TweaksModule implements CommandExecutor, HttpHandler {
+ private final Ratelimit ratelimit = new Ratelimit();
+
+ private byte[] secret;
+ private String secretEncoded;
+
+ private void loadKey(File file) {
+ if (file.exists()) {
+ try {
+ this.secret = Files.readAllBytes(file.toPath());
+ } catch (IOException e) {
+ throw new RuntimeException("Reading killswitch key", e);
+ }
+ DebugLogger.fine("Loaded key");
+ } else {
+ byte[] buf = new byte[16];
+
+ try {
+ SecureRandom.getInstanceStrong().nextBytes(buf);
+ Files.write(file.toPath(), buf);
+ } catch (IOException | NoSuchAlgorithmException e) {
+ throw new RuntimeException("Generating killswitch key", e);
+ }
+
+ this.secret = buf;
+ DebugLogger.info("Killswitch key generated and saved to " + file.getPath());
+ }
+
+ this.secretEncoded = Base64.getEncoder().encodeToString(secret);
+ }
+
+ @Override
+ protected void onInit() {
+ registerCommand("servkill", this);
+
+ if (getConfig().killswitchListen() != null) {
+ loadKey(new File(getPlugin().getDataFolder(), "storage/killswitch key"));
+
+ ratelimit.runTaskTimerAsynchronously(getPlugin(), 0, 20 * 300);
+
+ var listenAddress = getConfig().killswitchListen().split(":");
+ InetSocketAddress bindAddress;
+ if (listenAddress.length == 1) {
+ bindAddress = new InetSocketAddress(Integer.parseInt(listenAddress[0]));
+ } else {
+ bindAddress = new InetSocketAddress(listenAddress[0], Integer.parseInt(listenAddress[1]));
+ }
+
+ try {
+ HttpServer server = HttpServer.create(bindAddress, 0);
+ server.createContext("/", this);
+ server.setExecutor(null);
+ server.start();
+ DebugLogger.fine("server started");
+ } catch (IOException e) {
+ throw new RuntimeException("Starting HTTP server", e);
+ }
+ }
+ }
+
+ private void kill() {
+ Runtime.getRuntime().halt(0);
+ }
+
+ @Override
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
+ kill();
+ return true;
+ }
+
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ exchange.sendResponseHeaders(404, -1);
+
+ var path = exchange.getRequestURI().getPath();
+ if (!path.startsWith("/key/")) return;
+
+ var address = exchange.getRemoteAddress().getAddress();
+ if (!ratelimit.submitRequest(address)) {
+ DebugLogger.fine(address + " is ratelimited");
+ return;
+ }
+
+ var key = path.substring(5);
+ if (key.equals(secretEncoded)) {
+ DebugLogger.fine("Got a request with valid key");
+ kill();
+ } else {
+ DebugLogger.fine("Got a request with invalid key");
+ }
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/killswitch/Ratelimit.java b/src/main/java/eu/m724/tweaks/module/killswitch/Ratelimit.java
new file mode 100644
index 0000000..311c30a
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/killswitch/Ratelimit.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.killswitch;
+
+import org.bukkit.scheduler.BukkitRunnable;
+
+import java.net.InetAddress;
+import java.util.HashSet;
+import java.util.Set;
+
+public class Ratelimit extends BukkitRunnable {
+ private Set requests = new HashSet<>();
+
+ boolean submitRequest(InetAddress address) {
+ return requests.add(address);
+ }
+
+ @Override
+ public void run() {
+ requests = new HashSet<>();
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/knockback/KnockbackModule.java b/src/main/java/eu/m724/tweaks/module/knockback/KnockbackModule.java
new file mode 100644
index 0000000..29092fd
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/knockback/KnockbackModule.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.knockback;
+
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.module.TweaksModule;
+import org.bukkit.entity.EntityType;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityKnockbackByEntityEvent;
+import org.bukkit.util.Vector;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class KnockbackModule extends TweaksModule implements Listener {
+ private final Map modifiers = new HashMap<>();
+
+ @Override
+ protected void onInit() {
+ getConfig().knockbackModifiers().forEach((k, v) -> {
+ EntityType type;
+ double mod;
+
+ String line = "(%s: %s)".formatted(k, v);
+
+ if (v instanceof Double d) {
+ mod = d;
+ } else if (v instanceof Integer i) {
+ mod = i;
+ } else {
+ DebugLogger.warning("In " + line + " the value is not a number ");
+ return;
+ }
+
+ try {
+ type = EntityType.valueOf(k);
+ } catch (IllegalArgumentException e) {
+ DebugLogger.warning("In" + line + " the key is not a valid entity type");
+ DebugLogger.warning("Valid entity types: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/EntityType.html");
+ return;
+ }
+
+ if (mod == 1) return;
+ modifiers.put(type, new Vector(mod, mod >= 1 ? mod : 1, mod)); // don't touch vertical
+ });
+
+ if (!modifiers.isEmpty()) {
+ registerEvents(this);
+
+ try {
+ Class.forName("com.destroystokyo.paper.event.entity.EntityKnockbackByEntityEvent");
+ DebugLogger.warning("Ignore that. Server performance will NOT be affected.");
+ } catch (ClassNotFoundException ignored) { }
+ }
+ }
+
+ @EventHandler
+ public void onEntityKnockbackByEntity(EntityKnockbackByEntityEvent event) {
+ var modifier = modifiers.get(event.getSourceEntity().getType());
+ if (modifier != null) {
+ event.setFinalKnockback(event.getKnockback().multiply(modifier));
+ }
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/motd/MotdModule.java b/src/main/java/eu/m724/tweaks/module/motd/MotdModule.java
new file mode 100644
index 0000000..63f1b6c
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/motd/MotdModule.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.motd;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.events.InternalStructure;
+import com.comphenix.protocol.events.PacketContainer;
+import com.comphenix.protocol.reflect.StructureModifier;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonParser;
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.module.TweaksModule;
+import net.md_5.bungee.api.chat.TextComponent;
+import net.md_5.bungee.chat.ComponentSerializer;
+import net.minecraft.SharedConstants;
+import net.minecraft.core.RegistryAccess;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.protocol.status.ServerStatus;
+
+import java.io.IOException;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class MotdModule extends TweaksModule {
+ @Override
+ protected void onInit() {
+ Path motdSetPath = getPlugin().getDataFolder().toPath().resolve("motd sets").resolve(getConfig().motdSet() + ".txt");
+
+ // create "motd sets" directory
+ try {
+ Files.createDirectories(motdSetPath);
+ } catch (FileAlreadyExistsException ignored) {
+
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ // if this is a builtin set
+ if (!Files.exists(motdSetPath) && getPlugin().hasResource("motd sets/" + motdSetPath.getFileName()))
+ getPlugin().saveResource("motd sets/" + motdSetPath.getFileName(), false);
+
+ if (!Files.exists(motdSetPath)) {
+ throw new RuntimeException("MOTD set \"%s\" doesn't exist".formatted(getConfig().motdSet()));
+ }
+
+ String fileContent;
+ try {
+ fileContent = Files.readString(motdSetPath);
+ } catch (IOException e) {
+ throw new RuntimeException("Reading motd set", e);
+ }
+
+ // MOTDs are split with an empty line
+ Component[] motds = Arrays.stream(fileContent.split("\n\n"))
+ .map(entry -> {
+ entry = entry.strip();
+ JsonElement json = null;
+
+ try {
+ json = JsonParser.parseString(entry);
+ DebugLogger.finer("JSON line: %s...", entry.substring(0, Math.min(entry.length(), 10)));
+ } catch (JsonParseException e) {
+ DebugLogger.finer("JSON line: %s...", entry.substring(0, Math.min(entry.length(), 10)));
+ }
+
+ if (json == null) {
+ json = ComponentSerializer.toJson(TextComponent.fromLegacy(entry));
+ }
+
+ return Component.Serializer.fromJson(json, RegistryAccess.EMPTY);
+ })
+ .toArray(Component[]::new);
+
+ onPacketSend(PacketType.Status.Server.SERVER_INFO, (event) -> {
+ PacketContainer packet = event.getPacket();
+
+ Component motd = motds[ThreadLocalRandom.current().nextInt(motds.length)];
+
+ ServerStatus serverStatus = (ServerStatus) packet.getStructures().read(0).getHandle();
+
+ /* this:
+ * removes server mod prefix (Paper, Spigot, any brand)
+ * hides players
+ */
+ ServerStatus newStatus = new ServerStatus(
+ motd,
+ Optional.empty(),
+ Optional.of(new ServerStatus.Version(
+ SharedConstants.getCurrentVersion().getName(),
+ SharedConstants.getProtocolVersion()
+ )),
+ serverStatus.favicon(),
+ false
+ );
+
+ packet.getStructures().write(0, new InternalStructure(newStatus, new StructureModifier<>(ServerStatus.class)));
+ });
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/ping/F3NameListener.java b/src/main/java/eu/m724/tweaks/module/ping/F3NameListener.java
similarity index 97%
rename from src/main/java/eu/m724/tweaks/ping/F3NameListener.java
rename to src/main/java/eu/m724/tweaks/module/ping/F3NameListener.java
index 9577841..bf43d14 100644
--- a/src/main/java/eu/m724/tweaks/ping/F3NameListener.java
+++ b/src/main/java/eu/m724/tweaks/module/ping/F3NameListener.java
@@ -1,16 +1,16 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.ping;
+package eu.m724.tweaks.module.ping;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.*;
import com.comphenix.protocol.reflect.StructureModifier;
-import eu.m724.tweaks.TweaksConfig;
+import eu.m724.tweaks.config.TweaksConfig;
import net.minecraft.network.protocol.common.custom.BrandPayload;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
diff --git a/src/main/java/eu/m724/tweaks/ping/KeepAlivePingChecker.java b/src/main/java/eu/m724/tweaks/module/ping/KeepAlivePingChecker.java
similarity index 96%
rename from src/main/java/eu/m724/tweaks/ping/KeepAlivePingChecker.java
rename to src/main/java/eu/m724/tweaks/module/ping/KeepAlivePingChecker.java
index 45fa7bd..d88d340 100644
--- a/src/main/java/eu/m724/tweaks/ping/KeepAlivePingChecker.java
+++ b/src/main/java/eu/m724/tweaks/module/ping/KeepAlivePingChecker.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.ping;
+package eu.m724.tweaks.module.ping;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
diff --git a/src/main/java/eu/m724/tweaks/ping/MsptChecker.java b/src/main/java/eu/m724/tweaks/module/ping/MsptChecker.java
similarity index 89%
rename from src/main/java/eu/m724/tweaks/ping/MsptChecker.java
rename to src/main/java/eu/m724/tweaks/module/ping/MsptChecker.java
index dec46a0..a030c2d 100644
--- a/src/main/java/eu/m724/tweaks/ping/MsptChecker.java
+++ b/src/main/java/eu/m724/tweaks/module/ping/MsptChecker.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.ping;
+package eu.m724.tweaks.module.ping;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
diff --git a/src/main/java/eu/m724/tweaks/ping/PingChecker.java b/src/main/java/eu/m724/tweaks/module/ping/PingChecker.java
similarity index 67%
rename from src/main/java/eu/m724/tweaks/ping/PingChecker.java
rename to src/main/java/eu/m724/tweaks/module/ping/PingChecker.java
index d5d982f..c1c61e6 100644
--- a/src/main/java/eu/m724/tweaks/ping/PingChecker.java
+++ b/src/main/java/eu/m724/tweaks/module/ping/PingChecker.java
@@ -1,11 +1,12 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.ping;
+package eu.m724.tweaks.module.ping;
+import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.Plugin;
public class PingChecker {
@@ -15,8 +16,10 @@ public class PingChecker {
this.plugin = plugin;
}
- public void init() {
+ public void init(PluginCommand pingCommand) {
new KeepAlivePingChecker(plugin).start();
new MsptChecker().init(plugin); // TODO should this be here
+
+ pingCommand.setExecutor(new PingCommands());
}
}
diff --git a/src/main/java/eu/m724/tweaks/ping/PingCommands.java b/src/main/java/eu/m724/tweaks/module/ping/PingCommands.java
similarity index 94%
rename from src/main/java/eu/m724/tweaks/ping/PingCommands.java
rename to src/main/java/eu/m724/tweaks/module/ping/PingCommands.java
index 01d9a1c..1e0f31d 100644
--- a/src/main/java/eu/m724/tweaks/ping/PingCommands.java
+++ b/src/main/java/eu/m724/tweaks/module/ping/PingCommands.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.ping;
+package eu.m724.tweaks.module.ping;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
diff --git a/src/main/java/eu/m724/tweaks/ping/PlayerPingStorage.java b/src/main/java/eu/m724/tweaks/module/ping/PlayerPingStorage.java
similarity index 95%
rename from src/main/java/eu/m724/tweaks/ping/PlayerPingStorage.java
rename to src/main/java/eu/m724/tweaks/module/ping/PlayerPingStorage.java
index f41285f..f8eb30c 100644
--- a/src/main/java/eu/m724/tweaks/ping/PlayerPingStorage.java
+++ b/src/main/java/eu/m724/tweaks/module/ping/PlayerPingStorage.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.ping;
+package eu.m724.tweaks.module.ping;
import org.bukkit.entity.Player;
diff --git a/src/main/java/eu/m724/tweaks/module/pomodoro/PlayerPomodoro.java b/src/main/java/eu/m724/tweaks/module/pomodoro/PlayerPomodoro.java
new file mode 100644
index 0000000..331c1c6
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/pomodoro/PlayerPomodoro.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.pomodoro;
+
+import java.util.concurrent.TimeUnit;
+
+public class PlayerPomodoro {
+ private int intervalsDone = 0;
+
+ private boolean breaktime = false;
+ private long currentIntervalStartedAtNanos = -1;
+
+ public int getIntervalsDone() {
+ return intervalsDone;
+ }
+
+ public boolean isBreaktime() {
+ return breaktime;
+ }
+
+ public boolean isLongBreak() {
+ return (intervalsDone + 1) % PomodoroModule.INTERVALS_BEFORE_LONG_BREAK == 0;
+ }
+
+ /**
+ * @return The time the current interval or break started, in milliseconds
+ */
+ public long getCurrentIntervalStartedAtNanos() {
+ return currentIntervalStartedAtNanos;
+ }
+
+ public int getCurrentIntervalDurationSeconds() {
+ if (breaktime) {
+ if (isLongBreak()) {
+ return PomodoroModule.LONG_BREAK_DURATION_SECONDS;
+ }
+
+ return PomodoroModule.SHORT_BREAK_DURATION_SECONDS;
+ } else {
+ return PomodoroModule.INTERVAL_DURATION_SECONDS;
+ }
+ }
+
+ public long getCurrentIntervalDurationNanos() {
+ return TimeUnit.SECONDS.toNanos(getCurrentIntervalDurationSeconds());
+ }
+
+ public long getCurrentIntervalRemainingNanos(long nowNanos) {
+ long elapsed = nowNanos - getCurrentIntervalStartedAtNanos();
+ return getCurrentIntervalDurationNanos() - elapsed;
+ }
+
+ public boolean isIntervalComplete(long nowNanos) {
+ return currentIntervalStartedAtNanos + getCurrentIntervalDurationNanos() < nowNanos;
+ }
+
+ /**
+ * Resets and starts the timer
+ */
+ public void start() {
+ this.intervalsDone = 0;
+ this.breaktime = false;
+ this.currentIntervalStartedAtNanos = System.nanoTime();
+ }
+
+ /**
+ * Completes a cycle
+ */
+ public void startBreakOrNextInterval() {
+ if (breaktime) { // from break to interval
+ this.intervalsDone++;
+ this.intervalsDone %= PomodoroModule.INTERVALS_BEFORE_LONG_BREAK;
+ }
+
+ this.currentIntervalStartedAtNanos = System.nanoTime();
+ breaktime = !breaktime;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/pomodoro/PlayerPomodoroTracker.java b/src/main/java/eu/m724/tweaks/module/pomodoro/PlayerPomodoroTracker.java
new file mode 100644
index 0000000..92e3c6f
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/pomodoro/PlayerPomodoroTracker.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.pomodoro;
+
+import org.bukkit.entity.Player;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+public class PlayerPomodoroTracker {
+ static final Map timers = new HashMap<>();
+
+ public static PlayerPomodoro get(Player player) {
+ return timers.get(player.getUniqueId());
+ }
+
+ public static PlayerPomodoro create(Player player) {
+ return timers.computeIfAbsent(player.getUniqueId(), (k) -> new PlayerPomodoro());
+ }
+
+ public static boolean remove(Player player) {
+ return timers.remove(player.getUniqueId()) != null;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroCommands.java b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroCommands.java
new file mode 100644
index 0000000..ee452ab
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroCommands.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.pomodoro;
+
+import eu.m724.tweaks.Language;
+import eu.m724.tweaks.config.TweaksConfig;
+import net.md_5.bungee.api.ChatColor;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+public class PomodoroCommands implements CommandExecutor {
+ @Override
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
+ Player player = (Player) sender;
+ String action = args.length > 0 ? args[0] : null;
+
+ PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
+
+ long now = System.nanoTime();
+
+ if (pomodoro != null) {
+ if ("stop".equals(action)) {
+ PlayerPomodoroTracker.remove(player);
+ sender.spigot().sendMessage(Language.getComponent("pomodoroStopped", ChatColor.GREEN, label));
+ } else {
+ if (pomodoro.isIntervalComplete(now)) {
+ pomodoro.startBreakOrNextInterval();
+
+ if (pomodoro.isBreaktime() && TweaksConfig.getConfig().pomodoroForce()) {
+ player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText());
+ }
+ }
+
+ sender.spigot().sendMessage(PomodoroModule.formatTimer(pomodoro, now));
+ }
+ } else {
+ if ("start".equals(action)) {
+ pomodoro = PlayerPomodoroTracker.create(player);
+ pomodoro.start();
+
+ sender.spigot().sendMessage(PomodoroModule.formatTimer(pomodoro, now));
+ } else {
+ // TODO help?
+ sender.spigot().sendMessage(Language.getComponent("pomodoroStart", ChatColor.GOLD, label));
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroListener.java b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroListener.java
new file mode 100644
index 0000000..853215e
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroListener.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.pomodoro;
+
+import eu.m724.tweaks.config.TweaksConfig;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerLoginEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
+import org.bukkit.event.player.PlayerToggleSneakEvent;
+
+public class PomodoroListener implements Listener {
+ private final TweaksConfig config = TweaksConfig.getConfig();
+
+ @EventHandler
+ public void onPlayerLogin(PlayerLoginEvent event) {
+ // Joining ends break
+ Player player = event.getPlayer();
+ PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
+ if (pomodoro == null) return;
+
+ long now = System.nanoTime();
+ long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
+
+ if (pomodoro.isBreaktime()) {
+ if (remaining > 0 && config.pomodoroForce()) {
+ player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText());
+ } else {
+ pomodoro.startBreakOrNextInterval();
+ }
+ }
+ }
+
+ @EventHandler
+ public void onPlayerQuit(PlayerQuitEvent event) {
+ // Quitting starts break
+ Player player = event.getPlayer();
+ PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
+ if (pomodoro == null) return;
+
+ if (pomodoro.isBreaktime()) return;
+
+ long now = System.nanoTime();
+ long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
+
+ if (remaining <= 0) {
+ pomodoro.startBreakOrNextInterval();
+ }
+ }
+
+ @EventHandler
+ public void onPlayerToggleSneak(PlayerToggleSneakEvent event) {
+ // Sneaking ends break
+ if (!event.isSneaking()) return;
+
+ Player player = event.getPlayer();
+ PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
+ if (pomodoro == null) return;
+
+ if (!pomodoro.isBreaktime()) return;
+
+ long now = System.nanoTime();
+ long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
+
+ if (remaining <= 0) {
+ pomodoro.startBreakOrNextInterval();
+ }
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroModule.java b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroModule.java
new file mode 100644
index 0000000..c97da7d
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroModule.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.pomodoro;
+
+import eu.m724.tweaks.Language;
+import eu.m724.tweaks.config.TweaksConfig;
+import eu.m724.tweaks.module.TweaksModule;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.chat.BaseComponent;
+import net.md_5.bungee.api.chat.ComponentBuilder;
+
+import java.util.concurrent.TimeUnit;
+
+public class PomodoroModule extends TweaksModule {
+ static final int INTERVAL_DURATION_SECONDS = 10;
+ static final int SHORT_BREAK_DURATION_SECONDS = (int) TimeUnit.MINUTES.toSeconds(5);
+ static final int LONG_BREAK_DURATION_SECONDS = (int) TimeUnit.MINUTES.toSeconds(20);
+ static final int INTERVALS_BEFORE_LONG_BREAK = 4;
+
+ static final int KICK_DELAY_SECONDS = 60;
+
+ @Override
+ protected void onInit() {
+ registerEvents(new PomodoroListener());
+ new PomodoroRunnable().runTaskTimerAsynchronously(getPlugin(), 0, 20L);
+
+ registerCommand("pomodoro", new PomodoroCommands());
+ }
+
+ /**
+ * Gets a formatted timer for a player.
+ *
+ * @param pomodoro the player's {@link PlayerPomodoro} instance
+ * @param nowNanos unix now timestamp in nanoseconds
+ * @return the timer as {@link BaseComponent}
+ */
+ static BaseComponent formatTimer(PlayerPomodoro pomodoro, long nowNanos) {
+ ComponentBuilder builder = new ComponentBuilder();
+
+ long remainingNanos = pomodoro.getCurrentIntervalRemainingNanos(nowNanos);
+ long remainingSeconds = TimeUnit.NANOSECONDS.toSeconds(remainingNanos);
+
+ if (pomodoro.isBreaktime()) {
+ if (pomodoro.isLongBreak()) {
+ builder.append(Language.getComponent("pomodoroLongBreak", ChatColor.LIGHT_PURPLE));
+ } else {
+ builder.append(Language.getComponent("pomodoroShortBreak", ChatColor.LIGHT_PURPLE));
+ }
+
+ if (remainingNanos > 0) {
+ builder.append(" %02d:%02d".formatted(remainingSeconds / 60, remainingSeconds % 60))
+ .color(ChatColor.GOLD);
+ } else {
+ builder.append(" 00:00")
+ .color(ChatColor.GREEN);
+
+ if (!TweaksConfig.getConfig().pomodoroForce()) {
+ builder.append( "/pom").color(ChatColor.GOLD);
+ }
+ }
+ } else {
+ if (remainingNanos > 0) {
+ builder
+ .append("%02d:%02d".formatted(remainingSeconds / 60, remainingSeconds % 60))
+ .color(ChatColor.GRAY);
+ } else {
+ builder
+ .append("00:00")
+ .color(remainingSeconds % 2 == 0 ? ChatColor.RED : ChatColor.GRAY);
+
+ if (!TweaksConfig.getConfig().pomodoroForce()) {
+ builder.append( "/pom").color(ChatColor.GOLD);
+ }
+ }
+ }
+
+
+ for (int i=0; i pomodoro.getIntervalsDone()) {
+ color = ChatColor.DARK_GRAY;
+ }
+
+ builder.append(" o").color(color);
+ }
+
+ return builder.build();
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroRunnable.java b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroRunnable.java
new file mode 100644
index 0000000..f9bd924
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroRunnable.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.pomodoro;
+
+import eu.m724.tweaks.TweaksPlugin;
+import eu.m724.tweaks.config.TweaksConfig;
+import net.md_5.bungee.api.ChatMessageType;
+import org.bukkit.Sound;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.scheduler.BukkitRunnable;
+
+import java.util.concurrent.TimeUnit;
+
+public class PomodoroRunnable extends BukkitRunnable {
+ private final TweaksConfig config = TweaksConfig.getConfig();
+ private final Plugin plugin = TweaksPlugin.getInstance(); // used only to kick
+
+ @Override
+ public void run() {
+ long now = System.nanoTime();
+
+ PlayerPomodoroTracker.timers.forEach((uuid, pomodoro) -> {
+ long remainingNanos = pomodoro.getCurrentIntervalRemainingNanos(now);
+ long remainingSecs = TimeUnit.NANOSECONDS.toSeconds(remainingNanos);
+
+ // TODO optimize?
+ Player player = plugin.getServer().getPlayer(uuid);
+
+ if (player != null && player.isOnline()) {
+ // TODO make not always on
+ player.spigot().sendMessage(ChatMessageType.ACTION_BAR, PomodoroModule.formatTimer(pomodoro, now));
+
+ if (remainingNanos <= 0) {
+ player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_FALL, 1.0f, 0.5f);
+
+ if (remainingSecs < -PomodoroModule.KICK_DELAY_SECONDS && config.pomodoroForce()) {
+ pomodoro.startBreakOrNextInterval();
+
+ plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () ->
+ player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText())
+ );
+ }
+ }
+ } else {
+ if (remainingNanos <= 0) {
+ // Start break automatically if the player is offline
+ if (!pomodoro.isBreaktime()) {
+ pomodoro.startBreakOrNextInterval();
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/redstone/GatewayItem.java b/src/main/java/eu/m724/tweaks/module/redstone/GatewayItem.java
new file mode 100644
index 0000000..29863dc
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/redstone/GatewayItem.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.redstone;
+
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.Language;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.ShapelessRecipe;
+import org.bukkit.persistence.PersistentDataType;
+import org.bukkit.plugin.Plugin;
+
+public class GatewayItem {
+ private final NamespacedKey gatewayKey;
+
+ public GatewayItem(Plugin plugin) {
+ var start = System.nanoTime();
+
+ this.gatewayKey = new NamespacedKey(plugin, "gateway");
+ var recipe = new ShapelessRecipe(gatewayKey, itemStack());
+
+ // this takes a long time for some reason. The first one especially
+ // do this somewhere off measure to JIT and skew the measurement: new RecipeChoice.MaterialChoice(Material.DIRT);
+ recipe.addIngredient(Material.NETHER_STAR);
+ recipe.addIngredient(Material.ENDER_CHEST);
+ recipe.addIngredient(Material.CHORUS_FLOWER);
+ recipe.addIngredient(Material.DAYLIGHT_DETECTOR);
+
+ plugin.getServer().addRecipe(recipe);
+
+ DebugLogger.finer("Adding the recipe took %d ms, which is a long time", (System.nanoTime() - start) / 1000000);
+ }
+
+ public ItemStack itemStack() {
+ var itemStack = new ItemStack(Material.DAYLIGHT_DETECTOR);
+ var meta = itemStack.getItemMeta();
+
+ meta.setItemName(Language.getString("redstoneGatewayItem"));
+ meta.getPersistentDataContainer().set(gatewayKey, PersistentDataType.BOOLEAN, true);
+ meta.setEnchantmentGlintOverride(true);
+
+ itemStack.setItemMeta(meta);
+ return itemStack;
+ }
+
+ public boolean isGateway(ItemStack itemStack) {
+ var meta = itemStack.getItemMeta();
+ if (meta == null) return false;
+ var value = meta.getPersistentDataContainer().get(gatewayKey, PersistentDataType.BOOLEAN);
+
+ return value != null;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/redstone/RedstoneCommands.java b/src/main/java/eu/m724/tweaks/module/redstone/RedstoneCommands.java
new file mode 100644
index 0000000..c489a8e
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/redstone/RedstoneCommands.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.redstone;
+
+import org.bukkit.Bukkit;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+public class RedstoneCommands implements CommandExecutor {
+ private final GatewayItem gatewayItem;
+
+ public RedstoneCommands(GatewayItem gatewayItem) {
+ this.gatewayItem = gatewayItem;
+ }
+
+
+ @Override
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
+ if (args.length > 0) {
+ if (args[0].equals("give")) {
+ Player player = null;
+
+ if (args.length > 1) {
+ player = Bukkit.getPlayerExact(args[1]);
+ if (player == null) {
+ sender.sendMessage("No player named " + args[1]);
+ return true;
+ }
+ } else {
+ if (sender instanceof Player) {
+ player = (Player) sender;
+ } else {
+ sender.sendMessage("Specify a player to give to, or be a player");
+ }
+ }
+
+ var itemStack = gatewayItem.itemStack();
+ player.getInventory().addItem(itemStack);
+ sender.sendMessage("Given to " + player.getName());
+ }
+ } else {
+ sender.sendMessage("Argument needed");
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/redstone/RedstoneGateways.java b/src/main/java/eu/m724/tweaks/module/redstone/RedstoneGateways.java
new file mode 100644
index 0000000..9f17572
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/redstone/RedstoneGateways.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.redstone;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import eu.m724.tweaks.DebugLogger;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.Particle;
+import org.bukkit.block.Block;
+import org.bukkit.block.data.AnaloguePowerable;
+import org.bukkit.metadata.FixedMetadataValue;
+import org.bukkit.plugin.Plugin;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class RedstoneGateways {
+ private final Plugin plugin;
+
+ private final BiMap gatewaysById = HashBiMap.create();
+ private final Map gatewaysByPower = new HashMap<>();
+
+ public RedstoneGateways(Plugin plugin) {
+ this.plugin = plugin;
+ }
+
+ /* Event */
+
+ int onPlace(Block block) {
+ var repeaterId = ThreadLocalRandom.current().nextInt() & 0x7FFFFFF0;
+
+ gatewaysById.put(repeaterId, block.getLocation());
+ block.setMetadata("rid", new FixedMetadataValue(plugin, repeaterId));
+
+ RedstoneStore.getInstance().saveRepeaterData(block.getLocation(), repeaterId, (byte) 0);
+
+ return repeaterId;
+ }
+
+ void onBreak(int repeaterId) {
+ delete(repeaterId);
+ }
+
+ void delete(int repeaterId) {
+ var location = gatewaysById.remove(repeaterId);
+ if (location == null) return;
+ gatewaysByPower.remove(repeaterId);
+
+ RedstoneStore.getInstance().deleteSavedRepeaterData(location);
+ }
+
+ /* Get functions */
+
+ private boolean isValid(int repeaterId) {
+ // check if there's a repeater with such ID stored
+ // if not, we're not loading because it's loaded as the block is
+ var loc = gatewaysById.get(repeaterId);
+ if (loc == null) {
+ DebugLogger.fine("isValid: Delete because no loc");
+ delete(repeaterId);
+ return false;
+ }
+
+ // check if chunk the block is in is loaded
+ // you may think it could be simplified, but it can't without loading the chunk (to check if it's loaded)
+ var isLoaded = loc.getWorld().isChunkLoaded(loc.getBlockX() / 16, loc.getBlockZ() / 16);
+ if (!isLoaded) return false;
+
+ // check if the block is correct type
+ if (loc.getBlock().getType() != Material.DAYLIGHT_DETECTOR) {
+ DebugLogger.fine("isValid: Delete because not sensor");
+ delete(repeaterId);
+ return false;
+ }
+
+ // check if the block has the same ID bound
+ var meta = loc.getBlock().getMetadata("rid");
+ if (meta.isEmpty() || meta.getFirst().asInt() != repeaterId) {
+ DebugLogger.fine("isValid: Delete because no meta");
+ delete(repeaterId);
+ return false;
+ }
+
+ return true;
+ }
+
+ public int getId(Block block) {
+ if (block.hasMetadata("rid")) {
+ var id = block.getMetadata("rid").getFirst().asInt();
+ return id;
+ }
+
+ var id = gatewaysById.inverse().get(block.getLocation());
+
+ if (id == null) {
+ // not in memory, check if repeater
+ var d = RedstoneStore.getInstance().getSavedRepeaterData(block.getLocation());
+
+ if (d == null) {
+ block.setMetadata("rid", new FixedMetadataValue(plugin, Integer.MIN_VALUE));
+ return Integer.MIN_VALUE;
+ }
+
+ id = d.getKey();
+ block.setMetadata("rid", new FixedMetadataValue(plugin, id));
+
+ gatewaysById.put(id, block.getLocation());
+ gatewaysByPower.put(id, d.getValue());
+ }
+
+ if (!isValid(id)) return Integer.MIN_VALUE;
+
+ return id;
+ }
+
+ Block getBlock(int repeaterId) {
+ var location = gatewaysById.get(repeaterId);
+ if (location == null) return null;
+
+ if (!isValid(repeaterId)) return null;
+
+ var storedId = location.getBlock().getMetadata("rid").getFirst().asInt();
+ if (storedId != repeaterId) {
+ DebugLogger.fine("attempted retrieve, but doesn't exist, deleting " + repeaterId);
+ delete(repeaterId);
+ return null;
+ }
+
+ DebugLogger.fine("retrieved " + repeaterId);
+
+ return location.getBlock();
+ }
+
+ /* Control functions */
+
+ byte getInboundPower(int repeaterId) {
+ var block = getBlock(repeaterId);
+ if (block == null) return -1;
+
+ block.getWorld().spawnParticle(Particle.LAVA, block.getLocation().add(0.5, 0.5, 0.5), 3);
+
+ var power = (byte) block.getBlockPower();
+ DebugLogger.fine("Got " + repeaterId + " receives " + power);
+ return power;
+ }
+
+ byte getOutboundPower(int repeaterId) {
+ var block = getBlock(repeaterId);
+ if (block == null) return -1;
+
+ var power = gatewaysByPower.getOrDefault(repeaterId, (byte) 0);
+ DebugLogger.fine("Got " + repeaterId + " outputs " + power);
+ return power;
+ }
+
+ void setPower(int repeaterId, byte power) {
+ if (power < 0 || power > 15)
+ throw new IllegalArgumentException("Power should be 0-15, but is " + power);
+
+ var block = getBlock(repeaterId);
+ if (block == null) return;
+
+ var data = (AnaloguePowerable) block.getBlockData();
+
+ gatewaysByPower.put(repeaterId, power);
+ data.setPower(power);
+
+ block.getWorld().spawnParticle(Particle.LAVA, block.getLocation().add(0.5, 0.5, 0.5), 3);
+
+ DebugLogger.fine("Set power of " + repeaterId + " to " + power);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/m724/tweaks/module/redstone/RedstoneListener.java b/src/main/java/eu/m724/tweaks/module/redstone/RedstoneListener.java
new file mode 100644
index 0000000..0e1abfa
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/redstone/RedstoneListener.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.redstone;
+
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.Language;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.chat.ComponentBuilder;
+import net.md_5.bungee.api.chat.HoverEvent;
+import net.md_5.bungee.api.chat.hover.content.Text;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.Action;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockPlaceEvent;
+import org.bukkit.event.block.BlockRedstoneEvent;
+import org.bukkit.event.player.PlayerInteractEvent;
+
+public class RedstoneListener implements Listener {
+ private final RedstoneGateways redstoneGateways;
+ private final GatewayItem gatewayItem;
+
+ public RedstoneListener(RedstoneGateways redstoneGateways, GatewayItem gatewayItem) {
+ this.redstoneGateways = redstoneGateways;
+ this.gatewayItem = gatewayItem;
+ }
+
+ @EventHandler
+ public void onBlockPlace(BlockPlaceEvent event) {
+ if (!gatewayItem.isGateway(event.getItemInHand())) return;
+
+ var block = event.getBlockPlaced();
+ var id = redstoneGateways.onPlace(block);
+
+ DebugLogger.fine("Gateway placed: " + id);
+ }
+
+ @EventHandler
+ public void onBlockBreak(BlockBreakEvent event) {
+ var id = redstoneGateways.getId(event.getBlock());
+ if (id == Integer.MIN_VALUE) return;
+
+ redstoneGateways.onBreak(id);
+
+ if (event.getPlayer().getInventory().getItemInMainHand().containsEnchantment(Enchantment.SILK_TOUCH)) {
+ event.setDropItems(false);
+ event.getBlock().getWorld().dropItemNaturally(event.getBlock().getLocation(), gatewayItem.itemStack());
+ }
+
+ DebugLogger.fine("Gateway broken: " + id);
+ }
+
+ @EventHandler
+ public void onPlayerInteract(PlayerInteractEvent event) {
+ if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
+ if (!event.getPlayer().isSneaking()) return;
+
+ var id = redstoneGateways.getId(event.getClickedBlock());
+ if (id == Integer.MIN_VALUE) return;
+
+ // TODO find a less lame way of showing ID
+ var component = new ComponentBuilder("Gateway ID: ").color(ChatColor.GOLD)
+ .append(String.valueOf(id)).color(ChatColor.AQUA)
+ .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(Language.getString("clickToCopy"))))
+ .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, String.valueOf(id)))
+ .build();
+
+ event.getPlayer().spigot().sendMessage(component);
+
+ // cancel block place
+ event.setCancelled(true);
+ }
+
+ @EventHandler
+ public void onBlockRedstone(BlockRedstoneEvent event) {
+ var block = event.getBlock();
+
+ var id = redstoneGateways.getId(block);
+ if (id == Integer.MIN_VALUE) return;
+
+ event.setNewCurrent(redstoneGateways.getOutboundPower(id));
+
+ DebugLogger.fine("Gateway redstone event: " + id);
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/redstone/RedstoneModule.java b/src/main/java/eu/m724/tweaks/module/redstone/RedstoneModule.java
new file mode 100644
index 0000000..b574786
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/redstone/RedstoneModule.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.redstone;
+
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.config.TweaksConfig;
+import eu.m724.tweaks.module.TweaksModule;
+
+import java.io.IOException;
+import java.net.*;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
+
+public class RedstoneModule extends TweaksModule {
+ private final RedstoneGateways redstoneGateways = new RedstoneGateways(getPlugin());
+
+ private DatagramSocket socket;
+ private RedstoneStateUpdateRunnable runnable;
+
+ @Override
+ protected void onInit() {
+ RedstoneStore.init(getPlugin());
+ var gatewayItem = new GatewayItem(getPlugin());
+
+ registerEvents(new RedstoneListener(redstoneGateways, gatewayItem));
+ registerCommand("retstone", new RedstoneCommands(gatewayItem));
+
+ this.runnable = new RedstoneStateUpdateRunnable(redstoneGateways);
+ this.runnable.runTaskTimer(getPlugin(), 0, 20); // TODO configurable
+
+ var listenAddress = TweaksConfig.getConfig().redstoneListen().split(":");
+ InetSocketAddress bindAddress;
+ if (listenAddress.length == 1) {
+ bindAddress = new InetSocketAddress(Integer.parseInt(listenAddress[0]));
+ } else {
+ bindAddress = new InetSocketAddress(listenAddress[0], Integer.parseInt(listenAddress[1]));
+ }
+
+ try {
+ initSocket(bindAddress);
+ } catch (SocketException e) {
+ throw new RuntimeException("Starting socket", e);
+ }
+ }
+
+ private void initSocket(SocketAddress bindAddress) throws SocketException {
+ socket = new DatagramSocket(bindAddress);
+
+ Executors.newSingleThreadExecutor().execute(() -> {
+ byte[] buf = new byte[4];
+
+ while (!socket.isClosed()) {
+ DatagramPacket packet
+ = new DatagramPacket(buf, buf.length);
+ try {
+ socket.receive(packet);
+ } catch (IOException e) {
+ DebugLogger.severe("Error reading packet: " + e.getMessage());
+ continue;
+ }
+
+ boolean write = (buf[0] >> 7 & 1) == 1;
+ byte data = (byte) (buf[3] & 0xF);
+ int gatewayId = ((buf[0] & 0x7F) << 24) | ((buf[1] & 0xFF) << 16) | ((buf[2] & 0xFF) << 8) | (buf[3] & 0xF0);
+
+ if (write) {
+ enqueueUpdate(gatewayId, data);
+ } else {
+ var newPacket = new DatagramPacket(new byte[1], 1, packet.getSocketAddress());
+
+ enqueueRetrieve(gatewayId, value -> {
+ DebugLogger.fine("Retrieved for " + gatewayId + " power " + value);
+ newPacket.setData(new byte[] { (byte) Math.max(value, 0) });
+
+ try {
+ socket.send(newPacket);
+ } catch (IOException e) {
+ throw new RuntimeException("Sending response to get repeater value", e);
+ }
+ });
+ }
+ }
+ });
+ }
+
+ private void enqueueUpdate(int gatewayId, byte power) {
+ DebugLogger.finer("Update enqueued " + gatewayId + " " + power);
+ runnable.enqueueUpdate(gatewayId, power);
+ }
+
+ private void enqueueRetrieve(int gatewayId, Consumer consumer) {
+ DebugLogger.finer("Retrieve enqueued " + gatewayId);
+ runnable.enqueueRetrieve(gatewayId, consumer);
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/redstone/RedstoneStateUpdateRunnable.java b/src/main/java/eu/m724/tweaks/module/redstone/RedstoneStateUpdateRunnable.java
new file mode 100644
index 0000000..2ab5410
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/redstone/RedstoneStateUpdateRunnable.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.redstone;
+
+import org.bukkit.scheduler.BukkitRunnable;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+public class RedstoneStateUpdateRunnable extends BukkitRunnable {
+ private Map updateQueue = new HashMap<>();
+ private Map> retrieveQueue = new HashMap<>();
+
+ private final RedstoneGateways redstoneGateways;
+
+ RedstoneStateUpdateRunnable(RedstoneGateways redstoneGateways) {
+ this.redstoneGateways = redstoneGateways;
+ }
+
+ void enqueueUpdate(int gatewayId, byte power) {
+ updateQueue.put(gatewayId, power);
+ }
+
+ void enqueueRetrieve(int repeaterId, Consumer consumer) {
+ retrieveQueue.put(repeaterId, consumer);
+ }
+
+ @Override
+ public void run() {
+ var updateQueue = this.updateQueue;
+ this.updateQueue = new HashMap<>();
+
+ var retrieveQueue = this.retrieveQueue;
+ this.retrieveQueue = new HashMap<>();
+
+ updateQueue.forEach((key, value) -> redstoneGateways.setPower(key, value));
+ retrieveQueue.forEach((key, value) -> value.accept(redstoneGateways.getInboundPower(key)));
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/redstone/RedstoneStore.java b/src/main/java/eu/m724/tweaks/module/redstone/RedstoneStore.java
new file mode 100644
index 0000000..3b2bc38
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/redstone/RedstoneStore.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.redstone;
+
+import com.google.common.primitives.Ints;
+import eu.m724.tweaks.DebugLogger;
+import org.apache.commons.lang3.tuple.Pair;
+import org.bukkit.Location;
+import org.bukkit.plugin.Plugin;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+public class RedstoneStore {
+ private static RedstoneStore INSTANCE;
+
+ private final File directory;
+
+ private RedstoneStore(Plugin plugin) {
+ this.directory = new File(plugin.getDataFolder(), "storage/redstone");
+ directory.mkdirs();
+ }
+
+ static void init(Plugin plugin) {
+ INSTANCE = new RedstoneStore(plugin);
+ }
+
+ static RedstoneStore getInstance() {
+ return INSTANCE;
+ }
+
+ Pair getSavedRepeaterData(Location location) {
+ var file = getFile(location);
+ if (!file.exists()) return null;
+
+ byte[] bytes;
+
+ try {
+ // TODO read just 4 bytes
+ bytes = Files.readAllBytes(file.toPath());
+ } catch (IOException e) {
+ throw new RuntimeException("Loading saved gateway data", e);
+ }
+
+ var gatewayId = Ints.fromByteArray(bytes) & ~0xF;
+ var powerLevel = (byte) (bytes[3] & 0xF);
+
+ DebugLogger.fine("load " + location + " " + gatewayId + " " + powerLevel);
+
+ return Pair.of(gatewayId, powerLevel);
+ }
+
+ void saveRepeaterData(Location location, int gatewayId, byte powerLevel) {
+ var file = getFile(location);
+ byte[] bytes = Ints.toByteArray((gatewayId & ~0xF) | (powerLevel & 0xF));
+
+ try {
+ Files.write(file.toPath(), bytes);
+ } catch (IOException e) {
+ throw new RuntimeException("Saving repeater data", e);
+ }
+
+ DebugLogger.fine("save " + location + " " + gatewayId + " " + powerLevel);
+ }
+
+ void deleteSavedRepeaterData(Location location) {
+ getFile(location).delete();
+ }
+
+ private File getFile(Location location) {
+ return new File(directory, location.getWorld().getName() + " " + location.getBlockX() + " " + location.getBlockY() + " " + location.getBlockZ());
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/sleep/SleepListener.java b/src/main/java/eu/m724/tweaks/module/sleep/SleepListener.java
similarity index 57%
rename from src/main/java/eu/m724/tweaks/sleep/SleepListener.java
rename to src/main/java/eu/m724/tweaks/module/sleep/SleepListener.java
index 6e0c23c..b1d0f85 100644
--- a/src/main/java/eu/m724/tweaks/sleep/SleepListener.java
+++ b/src/main/java/eu/m724/tweaks/module/sleep/SleepListener.java
@@ -1,14 +1,14 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.sleep;
+package eu.m724.tweaks.module.sleep;
-import eu.m724.tweaks.TweaksConfig;
+import eu.m724.tweaks.config.TweaksConfig;
import org.bukkit.GameRule;
-import org.bukkit.World;
+import org.bukkit.attribute.Attribute;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@@ -21,6 +21,7 @@ import java.util.Set;
public class SleepListener implements Listener {
private final boolean instant = TweaksConfig.getConfig().sleepInstant();
+ private final double heal = TweaksConfig.getConfig().sleepHeal() * 2; // hearts to half hearts
private final Set skippedCurrentNight = new HashSet<>();
private long lastDay = 0;
@@ -30,18 +31,21 @@ public class SleepListener implements Listener {
if (event.getBedEnterResult() == PlayerBedEnterEvent.BedEnterResult.OK) {
SleepState.playersSleeping++;
- if (instant) {
- World world = event.getPlayer().getWorld();
+ var player = event.getPlayer();
+ var world = player.getWorld();
- long day = world.getFullTime() / 24000;
- if (day != lastDay) skippedCurrentNight.clear();
- lastDay = day;
+ long day = world.getFullTime() / 24000;
+ if (day != lastDay) skippedCurrentNight.clear();
+ lastDay = day;
- if (!skippedCurrentNight.contains(event.getPlayer())) {
- double onePlayerRatio = 1 / (event.getPlayer().getServer().getOnlinePlayers().size() * (world.getGameRuleValue(GameRule.PLAYERS_SLEEPING_PERCENTAGE) / 100.0));
+ if (skippedCurrentNight.add(player)) {
+ if (instant) {
+ double onePlayerRatio = 1 / (player.getServer().getOnlinePlayers().size() * (world.getGameRuleValue(GameRule.PLAYERS_SLEEPING_PERCENTAGE) / 100.0));
world.setTime(Math.min(world.getTime() + (long) (10917 * onePlayerRatio), 23459));
- skippedCurrentNight.add(event.getPlayer());
}
+
+ var maxHealth = player.getAttribute(Attribute.GENERIC_MAX_HEALTH).getBaseValue();
+ player.setHealth(Math.min(player.getHealth() + heal, maxHealth));
}
}
@@ -53,8 +57,9 @@ public class SleepListener implements Listener {
}
@EventHandler
- public void onPlayerBedLeave(TimeSkipEvent event) {
- if (event.getSkipReason() == TimeSkipEvent.SkipReason.NIGHT_SKIP)
+ public void onTimeSkip(TimeSkipEvent event) {
+ if (event.getSkipReason() == TimeSkipEvent.SkipReason.NIGHT_SKIP) {
event.setCancelled(true);
+ }
}
}
diff --git a/src/main/java/eu/m724/tweaks/module/sleep/SleepModule.java b/src/main/java/eu/m724/tweaks/module/sleep/SleepModule.java
new file mode 100644
index 0000000..e2d45f8
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/sleep/SleepModule.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.sleep;
+
+import eu.m724.tweaks.module.TweaksModule;
+
+public class SleepModule extends TweaksModule {
+ @Override
+ protected void onInit() {
+ registerEvents(new SleepListener());
+
+ if (!getConfig().sleepInstant())
+ new TimeForwardRunnable(getPlugin()).runTaskTimer(getPlugin(), 0, 1); // TODO maybe not
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/sleep/SleepState.java b/src/main/java/eu/m724/tweaks/module/sleep/SleepState.java
similarity index 74%
rename from src/main/java/eu/m724/tweaks/sleep/SleepState.java
rename to src/main/java/eu/m724/tweaks/module/sleep/SleepState.java
index f810e9e..e8f0991 100644
--- a/src/main/java/eu/m724/tweaks/sleep/SleepState.java
+++ b/src/main/java/eu/m724/tweaks/module/sleep/SleepState.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.sleep;
+package eu.m724.tweaks.module.sleep;
public class SleepState {
static int playersSleeping;
diff --git a/src/main/java/eu/m724/tweaks/module/sleep/TimeForwardRunnable.java b/src/main/java/eu/m724/tweaks/module/sleep/TimeForwardRunnable.java
new file mode 100644
index 0000000..d435be9
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/sleep/TimeForwardRunnable.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.sleep;
+
+import org.bukkit.GameRule;
+import org.bukkit.Server;
+import org.bukkit.World;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.scheduler.BukkitRunnable;
+
+public class TimeForwardRunnable extends BukkitRunnable {
+ private final Server server;
+
+ public TimeForwardRunnable(Plugin plugin) {
+ this.server = plugin.getServer();
+ }
+
+ @Override
+ public void run() {
+ for (World world : server.getWorlds()) {
+ var gameRuleValue = world.getGameRuleValue(GameRule.PLAYERS_SLEEPING_PERCENTAGE);
+ if (gameRuleValue == null) gameRuleValue = 100;
+ double percentage = gameRuleValue / 100.0;
+
+ int playersSleeping = SleepState.playersSleeping;
+ //System.out.println(playersSleeping);
+ if (playersSleeping == 0) return;
+
+ int onlinePlayers = (int) (world.getPlayers().size() / percentage);
+
+ double sleepPercentage = (double) playersSleeping / onlinePlayers;
+
+ // we want sleep to take 200 ticks which is 10 seconds assuming all palyres onilien
+
+ long time = world.getTime();
+ long untilDay = 23459 - time;
+
+ if (untilDay == 0) return;
+
+ long perSkip = 200 + (100000 / -untilDay);
+ perSkip = Math.clamp(perSkip, 20, 200);
+ perSkip = (long) (perSkip * sleepPercentage);
+
+ world.setTime(world.getTime() + perSkip);
+ }
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/swing/SwingModule.java b/src/main/java/eu/m724/tweaks/module/swing/SwingModule.java
new file mode 100644
index 0000000..392c13f
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/swing/SwingModule.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.swing;
+
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.module.TweaksModule;
+import org.bukkit.Material;
+import org.bukkit.attribute.Attribute;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class SwingModule extends TweaksModule implements Listener {
+ private final Set tools = new HashSet<>();
+
+ @Override
+ protected void onInit() {
+ Arrays.stream(Material.values())
+ .filter(m -> m.name().contains("SWORD"))
+ .forEach(tools::add);
+
+ DebugLogger.finer("Tools: " + tools.size());
+
+ registerEvents(this);
+ }
+
+ @EventHandler
+ public void onBreak(BlockBreakEvent event) {
+ var type = event.getBlock().getType();
+ if (type.isOccluding()) return;
+
+ var player = event.getPlayer();
+ var tool = player.getInventory().getItemInMainHand().getType();
+ Entity entity = null;
+
+ if (tools.contains(tool)) { // if sword, raycast to hit farther
+ var result = player.getWorld().rayTraceEntities(
+ player.getEyeLocation(),
+ player.getEyeLocation().getDirection(),
+ player.getAttribute(Attribute.PLAYER_ENTITY_INTERACTION_RANGE).getValue(),
+ e -> e != player
+ );
+
+ if (result != null)
+ entity = result.getHitEntity();
+ } else {
+ entity = event.getBlock().getWorld()
+ .getNearbyEntities(event.getBlock().getLocation().add(0.5, 0.5, 0.5), 0.5, 0.5, 0.5)
+ .stream().filter(e -> (e instanceof LivingEntity && e != player))
+ .findFirst().orElse(null);
+ }
+
+ if (entity != null) {
+ player.attack(entity);
+ DebugLogger.fine("Swing " + player.getName() + " hit " + entity.getName());
+ }
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/updater/UpdaterCommands.java b/src/main/java/eu/m724/tweaks/module/updater/UpdaterCommands.java
similarity index 72%
rename from src/main/java/eu/m724/tweaks/updater/UpdaterCommands.java
rename to src/main/java/eu/m724/tweaks/module/updater/UpdaterCommands.java
index 731255c..8e399be 100644
--- a/src/main/java/eu/m724/tweaks/updater/UpdaterCommands.java
+++ b/src/main/java/eu/m724/tweaks/module/updater/UpdaterCommands.java
@@ -1,13 +1,14 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.updater;
+package eu.m724.tweaks.module.updater;
import eu.m724.tweaks.Language;
-import eu.m724.tweaks.updater.cache.VersionedResource;
+import eu.m724.tweaks.module.updater.backend.UpdateChecker;
+import eu.m724.tweaks.module.updater.object.VersionedResource;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ClickEvent;
@@ -20,32 +21,40 @@ import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
+import java.time.Instant;
import java.time.format.DateTimeFormatter;
public class UpdaterCommands implements CommandExecutor {
+ private final UpdateChecker updateChecker;
+
+ public UpdaterCommands(UpdateChecker updateChecker) {
+ this.updateChecker = updateChecker;
+ }
+
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
- if (UpdateChecker.lastChecked == null) {
+ var lastChecked = updateChecker.getLastChecked();
+ if (updateChecker.getLastChecked() == -1) {
sender.sendMessage(Language.getString("updatesNotChecked"));
return true;
}
- String lastChecked = UpdateChecker.lastChecked.format(DateTimeFormatter.ofPattern("HH:mm"));
- int n = UpdateChecker.availableUpdates.size();
+ int n = updateChecker.getAvailableUpdates().size();
if (n > 0) {
sender.spigot().sendMessage(
- new ComponentBuilder(Language.getString("updateAvailableNotice").formatted(n)).color(ChatColor.GRAY).build()
+ Language.getComponent("updateAvailableNotice", ChatColor.GRAY, n)
);
int i = 0;
- for (VersionedResource v : UpdateChecker.availableUpdates) {
+ for (VersionedResource v : updateChecker.getAvailableUpdates()) {
sender.spigot().sendMessage(
new ComponentBuilder(++i + ". ").color(ChatColor.GRAY).build(), resourceToBaseComponent(v)
);
}
} else {
- sender.sendMessage(Language.getString("updatesNoUpdates").formatted(lastChecked));
+ var lastCheckedFormat = DateTimeFormatter.ofPattern("HH:mm").format(Instant.ofEpochMilli(lastChecked));
+ sender.spigot().sendMessage(Language.getComponent("updatesNoUpdates", ChatColor.GREEN, lastCheckedFormat));
}
return true;
@@ -71,7 +80,7 @@ public class UpdaterCommands implements CommandExecutor {
.event(
new HoverEvent(
HoverEvent.Action.SHOW_TEXT,
- new Text(Language.getString("updatesClickToOpen").formatted(v.latest().description().title()))
+ new Text(Language.getString("updatesClickToOpen", v.latest().description().title()))
)
)
.build();
diff --git a/src/main/java/eu/m724/tweaks/module/updater/UpdaterModule.java b/src/main/java/eu/m724/tweaks/module/updater/UpdaterModule.java
new file mode 100644
index 0000000..6c39e17
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/updater/UpdaterModule.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.updater;
+
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.module.TweaksModule;
+import eu.m724.tweaks.module.updater.backend.UpdateChecker;
+import eu.m724.tweaks.module.updater.backend.VersionCache;
+import eu.m724.tweaks.module.updater.object.ResourceVersion;
+import eu.m724.tweaks.module.updater.object.SpigotResource;
+import eu.m724.tweaks.module.updater.object.VersionedResource;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.plugin.Plugin;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class UpdaterModule extends TweaksModule {
+ @Override
+ protected void onInit() {
+ var resources = loadInstalledPlugins();
+
+ // load installed versions from cache
+ var cacheFile = new File(getPlugin().getDataFolder(), "storage/cache/updater");
+
+ final var installedVersions = loadInstalledVersionCache(cacheFile);
+
+ var versionedResources = resources.stream()
+ .map(res -> new VersionedResource(
+ res, installedVersions.stream().filter(iv -> iv.resourceId() == res.resourceId()).findFirst().orElse(null), null
+ ))
+ .collect(Collectors.toSet());
+
+
+ var updateChecker = new UpdateChecker(getPlugin().getLogger(), cacheFile, versionedResources);
+ updateChecker.runTaskTimerAsynchronously(getPlugin(), 600, 12 * 3600 * 20); // 12 hours
+
+ registerCommand("updates", new UpdaterCommands(updateChecker));
+ }
+
+ private Set loadInstalledVersionCache(File cacheFile) {
+ try (FileInputStream inputStream = new FileInputStream(cacheFile)) {
+ return VersionCache.loadAll(inputStream);
+ } catch (FileNotFoundException ignored) {
+ } catch (IOException e) {
+ DebugLogger.warning("Error loading installed version cache, starting fresh. " + e.getMessage());
+ }
+
+ return new HashSet<>();
+ }
+
+ private Set loadInstalledPlugins() {
+ File installedPluginsYml = new File(getPlugin().getDataFolder(), "installed_plugins.yml");
+
+ if (!installedPluginsYml.exists()) {
+ getPlugin().saveResource("installed_plugins.yml", false);
+ }
+
+ YamlConfiguration configuration = YamlConfiguration.loadConfiguration(installedPluginsYml);
+
+ Plugin[] plugins = getPlugin().getServer().getPluginManager().getPlugins();
+ Set spigotResources = new HashSet<>();
+
+ for (Plugin plugin : plugins) {
+ String pluginName = plugin.getName();
+
+ if (!configuration.isSet(pluginName)) {
+ configuration.set(pluginName, -1);
+ continue;
+ }
+
+ int pluginId = configuration.getInt(pluginName);
+ if (pluginId > 0) {
+ spigotResources.add(
+ new SpigotResource(plugin, pluginId, pluginName)
+ );
+ }
+ }
+
+ try {
+ configuration.save(installedPluginsYml);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to update installed_plugins.yml", e);
+ }
+
+ return spigotResources;
+
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/updater/backend/UpdateChecker.java b/src/main/java/eu/m724/tweaks/module/updater/backend/UpdateChecker.java
new file mode 100644
index 0000000..92afcb1
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/updater/backend/UpdateChecker.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.updater.backend;
+
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.Language;
+import eu.m724.tweaks.module.updater.object.VersionedResource;
+import org.bukkit.scheduler.BukkitRunnable;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletionException;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+public class UpdateChecker extends BukkitRunnable {
+ private final Logger logger;
+ private final File cacheFile;
+ private final Set resources;
+
+ private final Set availableUpdates = new HashSet<>();
+ private long lastChecked = -1;
+
+ public UpdateChecker(Logger logger, File cacheFile, Set resources) {
+ this.logger = logger;
+ this.cacheFile = cacheFile;
+ this.resources = resources; // TODO make a copy?
+ }
+
+ private void checkAll() {
+ DebugLogger.fine("Checking for updates");
+ lastChecked = System.currentTimeMillis();
+ availableUpdates.clear();
+ var errors = 0;
+
+ for (VersionedResource versionedResource : Set.copyOf(resources)) {
+ String pluginName = versionedResource.resource().plugin().getName();
+ int page = versionedResource.running() != null ? versionedResource.running().page() : 1;
+
+ try {
+ VersionedResource newResource = new VersionScanner(versionedResource.resource(), page).join(); // this runs async so it's ok
+ if (!versionedResource.equals(newResource)) {
+ resources.remove(versionedResource);
+
+ if (newResource.running() == null) {
+ var pluginVersion = versionedResource.resource().plugin().getDescription().getVersion();
+ var message = "";
+
+ if (pluginVersion.endsWith("-SNAPSHOT")) {
+ message = "Is it a development build?";
+ } else if (versionedResource.running() != null) {
+ message = "Did you downgrade it? If so, clear cache (delete Tweaks724/storage/cache/updater)";
+ }
+
+ DebugLogger.warning("This version of %s doesn't exist on SpigotMC. %s", pluginName, message);
+ errors++;
+ } else {
+ if (!newResource.running().equals(newResource.latest())) {
+ availableUpdates.add(newResource);
+ }
+ }
+
+ resources.add(newResource);
+ }
+ } catch (CompletionException e) {
+ DebugLogger.severe("Unable to refresh %s: %s".formatted(pluginName, e.getMessage()));
+ }
+ }
+
+ if (errors > 0) {
+ DebugLogger.info("To disable the updater for specific plugins, refer to updater_config.yml");
+ }
+ }
+
+ private void alert() {
+ int n = availableUpdates.size();
+ if (n == 0) return;
+ logger.info(Language.getString("updateAvailableNotice", n));
+
+ availableUpdates.stream()
+ .map(u -> "- %s (%s -> %s)".formatted(u.resource().name(), u.running().label(), u.latest().label()))
+ .forEach(logger::info);
+ }
+
+ @Override
+ public void run() {
+ checkAll();
+
+ DebugLogger.finer("Done checking, now saving");
+ cacheFile.getParentFile().mkdirs();
+ try (FileOutputStream outputStream = new FileOutputStream(cacheFile)) {
+ VersionCache.writeAll(outputStream, resources.stream().map(VersionedResource::running).filter(Objects::nonNull).collect(Collectors.toSet()));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ alert();
+ }
+
+ public long getLastChecked() {
+ return lastChecked;
+ }
+
+ public Set getAvailableUpdates() {
+ return availableUpdates;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/updater/VersionCheckCache.java b/src/main/java/eu/m724/tweaks/module/updater/backend/VersionCache.java
similarity index 94%
rename from src/main/java/eu/m724/tweaks/updater/VersionCheckCache.java
rename to src/main/java/eu/m724/tweaks/module/updater/backend/VersionCache.java
index 7bc9b44..e6952ad 100644
--- a/src/main/java/eu/m724/tweaks/updater/VersionCheckCache.java
+++ b/src/main/java/eu/m724/tweaks/module/updater/backend/VersionCache.java
@@ -1,12 +1,12 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.updater;
+package eu.m724.tweaks.module.updater.backend;
-import eu.m724.tweaks.updater.cache.ResourceVersion;
+import eu.m724.tweaks.module.updater.object.ResourceVersion;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -15,7 +15,7 @@ import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;
-public class VersionCheckCache {
+public class VersionCache {
private static final byte FILE_VERSION = 1;
public static Set loadAll(FileInputStream inputStream) throws IOException {
diff --git a/src/main/java/eu/m724/tweaks/updater/VersionFinder.java b/src/main/java/eu/m724/tweaks/module/updater/backend/VersionScanner.java
similarity index 72%
rename from src/main/java/eu/m724/tweaks/updater/VersionFinder.java
rename to src/main/java/eu/m724/tweaks/module/updater/backend/VersionScanner.java
index 35676ef..2eabcea 100644
--- a/src/main/java/eu/m724/tweaks/updater/VersionFinder.java
+++ b/src/main/java/eu/m724/tweaks/module/updater/backend/VersionScanner.java
@@ -1,19 +1,20 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.updater;
+package eu.m724.tweaks.module.updater.backend;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
-import eu.m724.tweaks.updater.cache.SpigotResource;
-import eu.m724.tweaks.updater.cache.ResourceVersion;
-import eu.m724.tweaks.updater.cache.UpdateDescription;
-import eu.m724.tweaks.updater.cache.VersionedResource;
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.module.updater.object.SpigotResource;
+import eu.m724.tweaks.module.updater.object.ResourceVersion;
+import eu.m724.tweaks.module.updater.object.UpdateDescription;
+import eu.m724.tweaks.module.updater.object.VersionedResource;
import java.net.URI;
import java.net.URISyntaxException;
@@ -26,23 +27,24 @@ import java.util.concurrent.Executors;
// TODO optimize
-public class VersionFinder extends CompletableFuture {
+public class VersionScanner extends CompletableFuture {
private final SpigotResource resource;
private final int fromPage;
- VersionFinder(SpigotResource resource, int fromPage) {
+ VersionScanner(SpigotResource resource, int fromPage) {
this.resource = resource;
this.fromPage = fromPage;
start();
}
- VersionFinder(SpigotResource resource) {
+ VersionScanner(SpigotResource resource) {
this(resource, 1);
}
private void start() {
- //System.out.printf("STarting for %d %s\n", resource.resourceId(), resource.plugin().getName());
+ DebugLogger.finer("Scanning %s (#%d) from page %d", resource.name(), resource.resourceId(), fromPage);
+
try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
executor.execute(() -> {
try {
@@ -51,13 +53,14 @@ public class VersionFinder extends CompletableFuture {
int page;
for (page = fromPage; page < 1000; page++) {
- //System.out.println("Page " + page);
+ DebugLogger.finer("Scan %s now at page %d", resource.name(), page);
+
String url = "https://api.spigotmc.org/simple/0.2/index.php?action=getResourceUpdates&page=%d&id=%d".formatted(page, resource.resourceId());
HttpRequest request;
try {
request = HttpRequest.newBuilder(new URI(url))
- .header("User-Agent", "twu/1")
+ .header("User-Agent", "twu/1") // tweaks updater v1
.build();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
@@ -68,6 +71,7 @@ public class VersionFinder extends CompletableFuture {
String body = response.body();
if (body.isBlank()) {
+ DebugLogger.finer("Body is blank, stopping");
page--;
break;
}
@@ -75,15 +79,7 @@ public class VersionFinder extends CompletableFuture {
JsonArray jsonArray = JsonParser.parseString(body).getAsJsonArray();
for (JsonElement ele : jsonArray) {
JsonObject versionJson = ele.getAsJsonObject();
- if (isRunningVersion(versionJson)) {
- runningVersion = new ResourceVersion(
- resource.resourceId(),
- page,
- versionJson.get("id").getAsInt(),
- versionJson.get("resource_version").getAsString(),
- null // no need for changelog of running version
- );
- }
+
latestVersion = new ResourceVersion(
resource.resourceId(),
page,
@@ -94,14 +90,17 @@ public class VersionFinder extends CompletableFuture {
versionJson.get("message").getAsString()
)
);
- //System.out.printf("%d %d %s\n", page, versionJson.get("id").getAsInt(), versionJson.get("resource_version").getAsString());
+
+ if (isRunningVersion(versionJson))
+ runningVersion = latestVersion;
+
+ DebugLogger.finer("%s - %s #%d", resource.name(), latestVersion.updateId(), latestVersion.updateId());
}
if (jsonArray.size() < 10) break;
}
}
- //System.out.println("Done");
if (page > 999) {
throw new Exception("Too many pages");
@@ -117,7 +116,8 @@ public class VersionFinder extends CompletableFuture {
}
private boolean isRunningVersion(JsonObject versionJson) {
- // TODO
- return versionJson.get("resource_version").getAsString().equals(resource.plugin().getDescription().getVersion());
+ // TODO make it work with more advanced strings
+ return versionJson.get("resource_version").getAsString()
+ .equals(resource.plugin().getDescription().getVersion());
}
}
diff --git a/src/main/java/eu/m724/tweaks/updater/cache/ResourceVersion.java b/src/main/java/eu/m724/tweaks/module/updater/object/ResourceVersion.java
similarity index 89%
rename from src/main/java/eu/m724/tweaks/updater/cache/ResourceVersion.java
rename to src/main/java/eu/m724/tweaks/module/updater/object/ResourceVersion.java
index d5d0fd3..90efb34 100644
--- a/src/main/java/eu/m724/tweaks/updater/cache/ResourceVersion.java
+++ b/src/main/java/eu/m724/tweaks/module/updater/object/ResourceVersion.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.updater.cache;
+package eu.m724.tweaks.module.updater.object;
import java.util.Objects;
diff --git a/src/main/java/eu/m724/tweaks/updater/cache/SpigotResource.java b/src/main/java/eu/m724/tweaks/module/updater/object/SpigotResource.java
similarity index 76%
rename from src/main/java/eu/m724/tweaks/updater/cache/SpigotResource.java
rename to src/main/java/eu/m724/tweaks/module/updater/object/SpigotResource.java
index dce97cf..28b0987 100644
--- a/src/main/java/eu/m724/tweaks/updater/cache/SpigotResource.java
+++ b/src/main/java/eu/m724/tweaks/module/updater/object/SpigotResource.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.updater.cache;
+package eu.m724.tweaks.module.updater.object;
import org.bukkit.plugin.Plugin;
@@ -12,5 +12,4 @@ public record SpigotResource(
Plugin plugin,
int resourceId,
String name
-) {
-}
+) { }
diff --git a/src/main/java/eu/m724/tweaks/updater/cache/UpdateDescription.java b/src/main/java/eu/m724/tweaks/module/updater/object/UpdateDescription.java
similarity index 72%
rename from src/main/java/eu/m724/tweaks/updater/cache/UpdateDescription.java
rename to src/main/java/eu/m724/tweaks/module/updater/object/UpdateDescription.java
index 1837716..d22f588 100644
--- a/src/main/java/eu/m724/tweaks/updater/cache/UpdateDescription.java
+++ b/src/main/java/eu/m724/tweaks/module/updater/object/UpdateDescription.java
@@ -1,13 +1,12 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.updater.cache;
+package eu.m724.tweaks.module.updater.object;
public record UpdateDescription(
String title,
String description
-) {
-}
+) { }
diff --git a/src/main/java/eu/m724/tweaks/updater/cache/VersionedResource.java b/src/main/java/eu/m724/tweaks/module/updater/object/VersionedResource.java
similarity index 77%
rename from src/main/java/eu/m724/tweaks/updater/cache/VersionedResource.java
rename to src/main/java/eu/m724/tweaks/module/updater/object/VersionedResource.java
index 18d4bb2..4c274e3 100644
--- a/src/main/java/eu/m724/tweaks/updater/cache/VersionedResource.java
+++ b/src/main/java/eu/m724/tweaks/module/updater/object/VersionedResource.java
@@ -1,10 +1,10 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.updater.cache;
+package eu.m724.tweaks.module.updater.object;
public record VersionedResource(
SpigotResource resource,
diff --git a/src/main/java/eu/m724/tweaks/module/wordcoords/WordCoordsCodec.java b/src/main/java/eu/m724/tweaks/module/wordcoords/WordCoordsCodec.java
new file mode 100644
index 0000000..302ecab
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/wordcoords/WordCoordsCodec.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.wordcoords;
+
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.module.wordcoords.codec.DecoderWordsToCoords;
+import eu.m724.tweaks.module.wordcoords.codec.EncoderCoordsToWords;
+
+import java.util.NoSuchElementException;
+
+public class WordCoordsCodec {
+ private final EncoderCoordsToWords encoder;
+ private final DecoderWordsToCoords decoder;
+
+ public WordCoordsCodec(WordList wordList) {
+ this.encoder = new EncoderCoordsToWords(wordList);
+ this.decoder = new DecoderWordsToCoords(wordList);
+
+ DebugLogger.fine("Words: %d (%d bits/w)", wordList.getWordCount(), wordList.getBitsPerWord());
+ }
+
+ /**
+ * Encodes coords to words
+ * @param x The X coordinate
+ * @param z The Z coordinate
+ * @return The words
+ */
+ public String[] encodeWords(int x, int z) {
+ return encoder.encodeWords(x, z);
+ }
+
+ /**
+ * Decodes words to coords
+ * @param words The words
+ * @return The X,Z coordinates
+ * @throws NoSuchElementException if one or more words are invalid
+ */
+ public int[] decodeCoords(String[] words) throws NoSuchElementException {
+ return decoder.decodeCoords(words);
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/wordcoords/WordCoordsModule.java b/src/main/java/eu/m724/tweaks/module/wordcoords/WordCoordsModule.java
new file mode 100644
index 0000000..01f9101
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/wordcoords/WordCoordsModule.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.wordcoords;
+
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.Language;
+import eu.m724.tweaks.module.TweaksModule;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.chat.BaseComponent;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.chat.ComponentBuilder;
+import net.md_5.bungee.api.chat.HoverEvent;
+import net.md_5.bungee.api.chat.hover.content.Text;
+
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerCommandPreprocessEvent;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.NoSuchElementException;
+
+public class WordCoordsModule extends TweaksModule implements CommandExecutor, Listener {
+ private static final int MAX_RADIUS = 30_000_000;
+
+ private WordCoordsCodec converter;
+
+ @Override
+ protected void onInit() {
+ Path wordListFile = getPlugin().getDataFolder().toPath().resolve("storage/wordlist.txt");
+
+ if (Files.notExists(wordListFile)) {
+ try {
+ saveDefaultWordList(wordListFile);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to save default word list", e);
+ }
+ }
+
+ WordList wordList;
+ try {
+ wordList = WordList.fromFile(wordListFile);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load word list", e);
+ }
+
+ this.converter = new WordCoordsCodec(wordList);
+
+ registerCommand("wordcoords", this);
+ registerEvents(this);
+ }
+
+ private void saveDefaultWordList(Path wordListFile) throws IOException {
+ try (InputStream is = getPlugin().getResource("wordlist.txt")) {
+ assert is != null;
+ Files.copy(is, wordListFile);
+ }
+ }
+
+ @Override
+ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
+ int x = 0, z = 0;
+
+ boolean encode = false; // means encode pos to words
+
+ args = String.join(" ", args)
+ .replaceAll("[^\\p{L}\\p{N}\\s]", " ")
+ .trim()
+ .split(" +");
+
+ if (args.length == 1 && args[0].isEmpty())
+ args = new String[0]; // empty split fix
+
+ DebugLogger.fine("Args: %s %d", String.join(", ", args), args.length);
+
+ if (args.length == 0) {
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage(Language.getString("wordCoordsPlayerOnly"));
+ return true;
+ }
+
+ x = player.getLocation().getBlockX();
+ z = player.getLocation().getBlockZ();
+
+ encode = true;
+ } else {
+ if (Character.isDigit(args[0].codePointAt(0))) {
+ if (args.length > 1) {
+ try {
+ double dx = Double.parseDouble(args[0]);
+ double dz = Double.parseDouble(args[args.length > 2 ? 2 : 1]);
+
+ if (dx > MAX_RADIUS || dx < -MAX_RADIUS || dz > MAX_RADIUS || dz < -MAX_RADIUS) {
+ sender.spigot().sendMessage(Language.getComponent("wordCoordsOutOfRange", ChatColor.RED));
+ return true;
+ }
+
+ x = (int) dx;
+ z = (int) dz;
+
+ encode = true;
+ } catch (NumberFormatException ignored) { }
+ } else {
+ sender.spigot().sendMessage(Language.getComponent("wordCoordsProvideZ", ChatColor.RED));
+ return true;
+ }
+ }
+ }
+
+ if (encode) {
+ encodeAndSend(x, z, sender);
+ } else {
+ decodeAndSend(args, sender);
+ }
+
+ return true;
+ }
+
+ private void encodeAndSend(int x, int z, CommandSender sender) {
+ String[] words = converter.encodeWords(x, z);
+ String encoded = "///" + String.join(".", words);
+
+ BaseComponent[] components = new ComponentBuilder()
+ .append("%d, %d -> ".formatted(x, z))
+ .color(ChatColor.GRAY)
+ .append(encoded)
+ .color(ChatColor.AQUA) // TODO improve color
+ .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, encoded))
+ .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(Language.getString("clickToCopy"))))
+ .create();
+
+ sender.spigot().sendMessage(components);
+ }
+
+ private void decodeAndSend(String[] words, CommandSender sender) {
+ int x, z;
+
+ try {
+ int[] xz = converter.decodeCoords(words);
+ x = xz[0];
+ z = xz[1];
+ } catch (NoSuchElementException e) {
+ sender.spigot().sendMessage(Language.getComponent("wordCoordsInvalidWord", ChatColor.RED, e.getMessage()));
+ return;
+ }
+
+ String encoded = "///" + String.join(".", words);
+
+ BaseComponent[] components = new ComponentBuilder()
+ .append(encoded + " -> ")
+ .color(ChatColor.GRAY)
+ .append("%d, %d".formatted(x, z))
+ .color(ChatColor.AQUA) // TODO improve color
+ .event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, "%d, %d".formatted(x, z)))
+ .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(Language.getString("clickToCopy"))))
+ .append(" ±8")
+ .color(ChatColor.GRAY)
+ .create();
+ sender.spigot().sendMessage(components);
+ }
+
+ @EventHandler
+ public void onCommand(PlayerCommandPreprocessEvent event) {
+ if (event.getMessage().startsWith("///")) {
+ event.setCancelled(true);
+
+ event.getPlayer().performCommand("wordcoords " + event.getMessage().substring(3));
+ }
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/wordcoords/WordList.java b/src/main/java/eu/m724/tweaks/module/wordcoords/WordList.java
new file mode 100644
index 0000000..6ccc0ec
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/wordcoords/WordList.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.wordcoords;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+
+public class WordList {
+ private final List wordList;
+ private final int bitsPerWord;
+
+ public WordList(List words) {
+ this.wordList = words;
+ this.bitsPerWord = 32 - Integer.numberOfLeadingZeros(words.size()) - 1;
+ }
+
+ public String getWord(int index) {
+ return wordList.get(index);
+ }
+
+ public int getWordIndex(String word) {
+ return wordList.indexOf(word);
+ }
+
+ public String[] getWords(int... indexes) {
+ return Arrays.stream(indexes)
+ .mapToObj(this::getWord)
+ .toArray(String[]::new);
+ }
+
+ public int[] getIndexes(String... words) {
+ return Arrays.stream(words)
+ .mapToInt(wordList::indexOf)
+ .toArray();
+ }
+
+ public int getWordCount() {
+ return wordList.size();
+ }
+
+ public int getBitsPerWord() {
+ return bitsPerWord;
+ }
+
+ public static WordList fromFile(Path path) throws IOException {
+ try (var lines = Files.lines(path)) {
+ var list = lines.filter(s -> !s.isBlank() && !s.startsWith("#"))
+ .map(String::toLowerCase)
+ .distinct()
+ .toList();
+
+ return new WordList(list);
+ }
+
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/module/wordcoords/codec/DecoderWordsToCoords.java b/src/main/java/eu/m724/tweaks/module/wordcoords/codec/DecoderWordsToCoords.java
new file mode 100644
index 0000000..66afef5
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/wordcoords/codec/DecoderWordsToCoords.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.wordcoords.codec;
+
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.module.wordcoords.WordList;
+
+import java.util.NoSuchElementException;
+import java.util.Arrays;
+
+public class DecoderWordsToCoords {
+ private final WordList wordList;
+ private final int bitsPerWord;
+
+ public DecoderWordsToCoords(WordList wordList) {
+ this.wordList = wordList;
+ this.bitsPerWord = wordList.getBitsPerWord();
+ }
+
+ public int[] decodeCoords(String[] words) throws NoSuchElementException {
+ int[] wordIndexes = new int[words.length];
+
+ for (int i=0; i> bitsRequiredPerCoordinate) - coordinateOffset;
+
+ return new int[] { x, z };
+ }
+
+}
diff --git a/src/main/java/eu/m724/tweaks/module/wordcoords/codec/EncoderCoordsToWords.java b/src/main/java/eu/m724/tweaks/module/wordcoords/codec/EncoderCoordsToWords.java
new file mode 100644
index 0000000..e63fc00
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/wordcoords/codec/EncoderCoordsToWords.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.wordcoords.codec;
+
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.module.wordcoords.WordList;
+
+import java.util.Arrays;
+
+public class EncoderCoordsToWords {
+ private final WordList wordList;
+ private final int bitsPerWord;
+
+ public EncoderCoordsToWords(WordList wordList) {
+ this.wordList = wordList;
+ this.bitsPerWord = wordList.getBitsPerWord();
+ }
+
+ public String[] encodeWords(int xCoord, int zCoord) {
+ int chunkX = Math.floorDiv(xCoord, 16);
+ int chunkZ = Math.floorDiv(zCoord, 16);
+ DebugLogger.finer("Chunk: %d, %d", chunkX, chunkZ);
+
+ // Calculate minimum bits required per coordinate based on range
+ int bitsRequiredPerCoordinate = findBitsRequiredPerCoordinate(chunkX, chunkZ);
+ int minTotalBits = bitsRequiredPerCoordinate * 2;
+ DebugLogger.finer("Min bits required per coordinate: %d (total: %d)", bitsRequiredPerCoordinate, minTotalBits);
+
+ // Calculate words required, ensuring total bits is sufficient and even
+ int wordsRequired = 0;
+ int actualTotalBits = 0;
+ if (minTotalBits > 0) { // Avoid division by zero if bitsPerWord is 0, or log(0)
+ wordsRequired = Math.ceilDiv(minTotalBits, bitsPerWord);
+ actualTotalBits = wordsRequired * bitsPerWord;
+ // Ensure total bits is sufficient
+ while (actualTotalBits < minTotalBits) {
+ wordsRequired++;
+ actualTotalBits = wordsRequired * bitsPerWord;
+ }
+ }
+
+ // Final bits per coordinate based on words
+ bitsRequiredPerCoordinate = actualTotalBits / 2;
+ DebugLogger.finer("Final Words required: %d", wordsRequired);
+ DebugLogger.finer("Final Bits required: %d (per coord: %d)", actualTotalBits, bitsRequiredPerCoordinate);
+
+ int encodedX = encodeCoord(chunkX, bitsRequiredPerCoordinate);
+ int encodedZ = encodeCoord(chunkZ, bitsRequiredPerCoordinate);
+ DebugLogger.finer("Encoded coordinates: %d, %d", encodedX, encodedZ);
+
+ long combinedValue = ((long) encodedX << bitsRequiredPerCoordinate) | encodedZ;
+ DebugLogger.finer("Combined value: %d", combinedValue);
+
+ int[] wordIndexes = combinedValueToWordIndexes(combinedValue, wordsRequired);
+ DebugLogger.finer("Word indexes: %s", Arrays.toString(wordIndexes));
+
+ return wordList.getWords(wordIndexes);
+ }
+
+ /** Calculates the minimum number of bits required to represent the coordinate
+ * using the encoding scheme (offset + coord) & mask, such that the coordinate
+ * fits within the range [-(1 << (bits - 1)), (1 << (bits - 1)) - 1]. */
+ private int findBitsRequiredPerCoordinate(int x, int z) {
+ int maxVal = Math.max(x, z);
+ int minVal = Math.min(x, z);
+
+ // Determine the required positive magnitude for the encoding range's positive side.
+ // We need `(1 << (bits - 1)) >= max(maxVal + 1, -minVal)`
+ int requiredPositiveMagnitude = Math.max(maxVal + 1, -minVal);
+
+ if (requiredPositiveMagnitude <= 0) {
+ requiredPositiveMagnitude = 1; // Ensure it's at least 1 if coords are 0 or -1.
+ }
+
+ int p;
+ if (requiredPositiveMagnitude == 1) {
+ p = 0;
+ } else {
+ p = 32 - Integer.numberOfLeadingZeros(requiredPositiveMagnitude - 1);
+ }
+
+ return p + 1;
+ }
+
+ private int encodeCoord(int coord, int bitsRequiredPerCoordinate) {
+ // Bitmask and offset for positive integer conversion
+ int coordinateMask = (1 << bitsRequiredPerCoordinate) - 1;
+ int coordinateOffset = 1 << (bitsRequiredPerCoordinate - 1);
+
+ // Encode coordinates with offset into positive range
+ return (coordinateOffset + coord) & coordinateMask;
+ }
+
+ private int[] combinedValueToWordIndexes(long combinedValue, int wordsRequired) {
+ int bitsRequired = wordsRequired * bitsPerWord;
+
+ // Break into word indexes
+ int[] wordIndexes = new int[wordsRequired];
+ int currentIndex = wordsRequired; // Start filling from the end of the array
+
+ for (int remainingBits = bitsRequired; remainingBits > 0; remainingBits -= bitsPerWord) {
+ int wordMask = (1 << bitsPerWord) - 1;
+ wordIndexes[--currentIndex] = (int) (combinedValue & wordMask);
+ combinedValue >>= bitsPerWord;
+ }
+
+ return wordIndexes;
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderExpanderListener.java b/src/main/java/eu/m724/tweaks/module/worldborder/WorldBorderExpandModule.java
similarity index 56%
rename from src/main/java/eu/m724/tweaks/worldborder/WorldBorderExpanderListener.java
rename to src/main/java/eu/m724/tweaks/module/worldborder/WorldBorderExpandModule.java
index e94051c..b4a4a30 100644
--- a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderExpanderListener.java
+++ b/src/main/java/eu/m724/tweaks/module/worldborder/WorldBorderExpandModule.java
@@ -1,18 +1,29 @@
/*
- * Copyright (C) 2024 Minecon724
+ * Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
-package eu.m724.tweaks.worldborder;
+package eu.m724.tweaks.module.worldborder;
+import eu.m724.tweaks.module.TweaksModule;
import net.minecraft.server.level.ServerLevel;
-import org.bukkit.craftbukkit.v1_21_R1.CraftWorld;
+import org.bukkit.craftbukkit.v1_21_R3.CraftWorld;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldLoadEvent;
-public class WorldBorderExpanderListener implements Listener {
+public class WorldBorderExpandModule extends TweaksModule implements Listener {
+ @Override
+ protected void onInit() {
+ registerEvents(this);
+
+ // because the plugin loads "post world"
+ getPlugin().getServer().getWorlds().forEach(w -> {
+ onWorldLoad(new WorldLoadEvent(w));
+ });
+ }
+
@EventHandler
public void onWorldLoad(WorldLoadEvent event) {
ServerLevel level = ((CraftWorld) event.getWorld()).getHandle();
@@ -23,7 +34,7 @@ public class WorldBorderExpanderListener implements Listener {
}
level.getWorldBorder().setAbsoluteMaxSize(30000000);
- // to align with player hitbox because player can't go beyond 30m - 1 and player's hitbox is 0.6 wide so we make it "hug" the border
+ // to align with player hitbox because player can't go beyond 30m - 1 and player's hitbox is 0.6 wide, multiply by 2 for - and +, that's 1.2 and 60000000 - 1.2 = 59999998.6
level.getWorldBorder().setSize(59999998.6);
}
}
diff --git a/src/main/java/eu/m724/tweaks/module/worldborder/WorldBorderHideModule.java b/src/main/java/eu/m724/tweaks/module/worldborder/WorldBorderHideModule.java
new file mode 100644
index 0000000..52f90cd
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/module/worldborder/WorldBorderHideModule.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2025 Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.module.worldborder;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.events.PacketContainer;
+import eu.m724.tweaks.module.TweaksModule;
+
+import java.nio.ByteBuffer;
+
+public class WorldBorderHideModule extends TweaksModule {
+ private static final int EXTENSION_RADIUS = 512;
+
+ @Override
+ protected void onInit() {
+ getPlugin().getServer().getMessenger().registerOutgoingPluginChannel(getPlugin(), "tweaks724:worldborder");
+
+ byte[] infoArray = ByteBuffer.allocate(4).putInt(EXTENSION_RADIUS).array();
+
+ onPacketSend(PacketType.Play.Server.INITIALIZE_BORDER, (event) -> {
+ PacketContainer packet = event.getPacket();
+ // old diameter
+ packet.getDoubles().write(2, packet.getDoubles().read(2) + EXTENSION_RADIUS * 2);
+ // new diameter
+ packet.getDoubles().write(3, packet.getDoubles().read(3) + EXTENSION_RADIUS * 2);
+
+ // border radius
+ packet.getIntegers().write(0, packet.getIntegers().read(0) + EXTENSION_RADIUS);
+ // warning distance
+ packet.getIntegers().write(1, packet.getIntegers().read(1) + EXTENSION_RADIUS);
+
+ event.getPlayer().sendPluginMessage(getPlugin(), "tweaks724:worldborder", infoArray);
+ });
+
+ onPacketSend(PacketType.Play.Server.SET_BORDER_SIZE, (event) -> {
+ PacketContainer packet = event.getPacket();
+ // diameter
+ packet.getDoubles().write(0, packet.getDoubles().read(0) + EXTENSION_RADIUS * 2);
+ });
+
+ onPacketSend(PacketType.Play.Server.SET_BORDER_WARNING_DISTANCE, (event) -> {
+ PacketContainer packet = event.getPacket();
+ // warning distance
+ packet.getIntegers().write(0, packet.getIntegers().read(0) + EXTENSION_RADIUS);
+ });
+ }
+}
diff --git a/src/main/java/eu/m724/tweaks/motd/MotdManager.java b/src/main/java/eu/m724/tweaks/motd/MotdManager.java
deleted file mode 100644
index 4ef367b..0000000
--- a/src/main/java/eu/m724/tweaks/motd/MotdManager.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.motd;
-
-import com.comphenix.protocol.PacketType;
-import com.comphenix.protocol.ProtocolLibrary;
-import com.comphenix.protocol.events.*;
-import com.comphenix.protocol.reflect.StructureModifier;
-import com.google.gson.JsonElement;
-import eu.m724.tweaks.TweaksConfig;
-import eu.m724.tweaks.TweaksPlugin;
-import net.md_5.bungee.api.chat.TextComponent;
-import net.md_5.bungee.chat.ComponentSerializer;
-import net.minecraft.SharedConstants;
-import net.minecraft.core.RegistryAccess;
-import net.minecraft.network.chat.Component;
-import net.minecraft.network.protocol.status.ServerStatus;
-import org.bukkit.plugin.Plugin;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.util.Arrays;
-import java.util.Optional;
-import java.util.concurrent.ThreadLocalRandom;
-
-public class MotdManager {
- private final TweaksPlugin plugin;
-
- private Component[] motds;
-
- public MotdManager(TweaksPlugin plugin) {
- this.plugin = plugin;
- }
-
- public void init() throws IOException {
- // TODO adding more MOTD features would require checking whether to enable set
-
- String motdSetName = TweaksConfig.getConfig().motdSet();
- String motdSetPath = "motd sets/" + motdSetName + ".txt";
- File motdSetsFile = new File(plugin.getDataFolder(), motdSetPath);
-
- // create "motd sets" directory
- motdSetsFile.getParentFile().mkdirs();
-
- // if this is a builtin set
- if (!motdSetsFile.exists() && plugin.hasResource(motdSetPath))
- plugin.saveResource(motdSetPath, false);
-
- if (!motdSetsFile.exists()) {
- throw new RuntimeException("MOTD set \"%s\" doesn't exist".formatted(motdSetName));
- }
-
- String fileContent = Files.readString(motdSetsFile.toPath());
- // MOTDs are split with an empty line
- motds = Arrays.stream(fileContent.split("\n\n"))
- .map(s -> {
- JsonElement json = ComponentSerializer.toJson(TextComponent.fromLegacy(s.strip()));
- return Component.Serializer.fromJson(json, RegistryAccess.EMPTY);
- })
- .toArray(Component[]::new);
-
- registerListener(plugin);
- }
-
- private void registerListener(Plugin plugin) {
- ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(
- plugin,
- ListenerPriority.NORMAL,
- PacketType.Status.Server.SERVER_INFO
- ) {
- @Override
- public void onPacketSending(PacketEvent event) {
- PacketContainer packet = event.getPacket();
-
- Component motd = motds[ThreadLocalRandom.current().nextInt(motds.length)];
-
- ServerStatus serverStatus = (ServerStatus) packet.getStructures().read(0).getHandle();
-
- /* this:
- * removes server mod prefix (Paper, Spigot, any brand)
- * hides players
- */
- ServerStatus newStatus = new ServerStatus(
- motd,
- Optional.empty(),
- Optional.of(new ServerStatus.Version(
- SharedConstants.getCurrentVersion().getName(),
- SharedConstants.getProtocolVersion()
- )),
- serverStatus.favicon(),
- false
- );
-
- packet.getStructures().write(0, new InternalStructure(newStatus, new StructureModifier<>(ServerStatus.class)));
- }
- });
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/pomodoro/PlayerPomodoro.java b/src/main/java/eu/m724/tweaks/pomodoro/PlayerPomodoro.java
deleted file mode 100644
index 3aba1be..0000000
--- a/src/main/java/eu/m724/tweaks/pomodoro/PlayerPomodoro.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.pomodoro;
-
-public class PlayerPomodoro {
- private int pomodori = 0;
-
- private boolean isBreak = false;
- // this is for both break and not break
- private long intervalStart = -1;
-
- /**
- * A "pomodoro" is the 25-minute cycle you take breaks after
- * This returns how many cycles already elapsed, so if this is the first cycle this is 0
- * The break after the "pomodoro," so if it's breaktime after the first "pomodoro" it stays at 0
- */
- public int getPomodori() {
- return pomodori;
- }
-
- /**
- * When did the current interval start
- * Or when did the break start
- *
- * @see PlayerPomodoro#isBreak()
- */
- public long getIntervalStart() {
- return intervalStart;
- }
-
- public int getCycleDurationSeconds() {
- return isBreak ? (pomodori < 3 ? 300 : 1200) : 1500;
- }
-
- public long getRemainingSeconds(long now) {
- return getCycleDurationSeconds() - (now - getIntervalStart()) / 1000000000;
- }
-
- /**
- * Is it a break currently
- */
- public boolean isBreak() {
- return isBreak;
- }
-
- public boolean isCycleComplete() {
- return intervalStart + getCycleDurationSeconds() * 1000000000L < System.nanoTime();
- }
-
- /**
- * Resets and starts the timer
- */
- public void start() {
- this.pomodori = 0;
- this.isBreak = false;
- this.intervalStart = System.nanoTime();
- }
-
- /**
- * Completes a cycle
- */
- public void next() {
- if (isBreak) { // from break to interval
- this.pomodori++;
- this.pomodori %= 4;
- }
-
- this.intervalStart = System.nanoTime();
- isBreak = !isBreak;
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/pomodoro/PomodoroCommands.java b/src/main/java/eu/m724/tweaks/pomodoro/PomodoroCommands.java
deleted file mode 100644
index fc5af95..0000000
--- a/src/main/java/eu/m724/tweaks/pomodoro/PomodoroCommands.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.pomodoro;
-
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandExecutor;
-import org.bukkit.command.CommandSender;
-import org.bukkit.entity.Player;
-import org.jetbrains.annotations.NotNull;
-
-public class PomodoroCommands implements CommandExecutor {
- @Override
- public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
- Player player = (Player) sender;
- String action = args.length > 0 ? args[0] : null;
-
- PlayerPomodoro pomodoro = Pomodoros.get(player);
-
- if (pomodoro != null) {
- if ("stop".equals(action)) {
- Pomodoros.remove(player);
- sender.sendMessage("Pomodoro disabled");
- } else {
- if (pomodoro.isCycleComplete()) {
- pomodoro.next();
- }
- sender.spigot().sendMessage(Pomodoros.formatTimer(pomodoro, pomodoro.getRemainingSeconds(System.nanoTime())));
- }
- } else {
- if ("start".equals(action)) {
- pomodoro = Pomodoros.create(player);
- pomodoro.start();
- sender.spigot().sendMessage(Pomodoros.formatTimer(pomodoro, pomodoro.getCycleDurationSeconds()));
- } else {
- sender.sendMessage("Start pomodoro with /pom start");
- }
- }
-
- return true;
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/pomodoro/PomodoroListener.java b/src/main/java/eu/m724/tweaks/pomodoro/PomodoroListener.java
deleted file mode 100644
index 9859e64..0000000
--- a/src/main/java/eu/m724/tweaks/pomodoro/PomodoroListener.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.pomodoro;
-
-import eu.m724.tweaks.TweaksConfig;
-import net.md_5.bungee.api.chat.ComponentBuilder;
-import org.bukkit.entity.Player;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.Listener;
-import org.bukkit.event.player.*;
-
-public class PomodoroListener implements Listener {
- private final boolean force = TweaksConfig.getConfig().pomodoroForce();
-
- @EventHandler
- public void onPlayerLogin(PlayerLoginEvent event) {
- Player player = event.getPlayer();
- PlayerPomodoro pomodoro = Pomodoros.get(player);
- if (pomodoro == null) return;
-
- long remaining = pomodoro.getRemainingSeconds(System.nanoTime());
-
- if (pomodoro.isBreak()) {
- if (pomodoro.isCycleComplete()) {
- pomodoro.next();
- } else {
- if (force) {
- event.getPlayer().kickPlayer(
- new ComponentBuilder()
- .append(Pomodoros.formatTimer(pomodoro, remaining))
- .build().toLegacyText()
- );
- }
- }
- }
- }
-
- @EventHandler
- public void onPlayerQuit(PlayerQuitEvent event) {
- Player player = event.getPlayer();
- PlayerPomodoro pomodoro = Pomodoros.get(player);
- if (pomodoro == null) return;
-
- if (!pomodoro.isBreak() && pomodoro.isCycleComplete()) {
- pomodoro.next();
- }
- }
-
- @EventHandler
- public void onPlayerMove(PlayerMoveEvent event) {
- Player player = event.getPlayer();
- PlayerPomodoro timer = Pomodoros.get(player);
- if (timer == null) return;
-
- if (timer.isBreak() && timer.getRemainingSeconds(System.nanoTime()) <= 0)
- timer.next(); // resume timer if break ended
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/pomodoro/PomodoroManager.java b/src/main/java/eu/m724/tweaks/pomodoro/PomodoroManager.java
deleted file mode 100644
index bc1dcaf..0000000
--- a/src/main/java/eu/m724/tweaks/pomodoro/PomodoroManager.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.pomodoro;
-
-import org.bukkit.plugin.Plugin;
-
-public class PomodoroManager {
- private final Plugin plugin;
-
- public PomodoroManager(Plugin plugin) {
- this.plugin = plugin;
- }
-
- public void init() {
- plugin.getServer().getPluginManager().registerEvents(new PomodoroListener(), plugin);
- new PomodoroRunnable(plugin).runTaskTimerAsynchronously(plugin, 0, 20L);
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/pomodoro/PomodoroRunnable.java b/src/main/java/eu/m724/tweaks/pomodoro/PomodoroRunnable.java
deleted file mode 100644
index 08a170c..0000000
--- a/src/main/java/eu/m724/tweaks/pomodoro/PomodoroRunnable.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.pomodoro;
-
-import eu.m724.tweaks.Language;
-import eu.m724.tweaks.TweaksConfig;
-import net.md_5.bungee.api.ChatMessageType;
-import org.bukkit.Bukkit;
-import org.bukkit.Sound;
-import org.bukkit.plugin.Plugin;
-import org.bukkit.scheduler.BukkitRunnable;
-
-public class PomodoroRunnable extends BukkitRunnable {
- private final boolean force = TweaksConfig.getConfig().pomodoroForce();
- private final Plugin plugin;
-
- public PomodoroRunnable(Plugin plugin) {
- this.plugin = plugin; // only used for kicking
- }
-
- @Override
- public void run() {
- long now = System.nanoTime();
- Bukkit.getOnlinePlayers().forEach(player -> {
- PlayerPomodoro pomodoro = Pomodoros.get(player);
- if (pomodoro == null) return;
-
- long remaining = pomodoro.getRemainingSeconds(now);
- // TODO make not always on
- player.spigot().sendMessage(ChatMessageType.ACTION_BAR, Pomodoros.formatTimer(pomodoro, remaining));
-
- if (remaining <= 0) {
- player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_FALL, 1.0f, 0.5f);
- if (remaining < -60 && force) {
- plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
- player.kickPlayer(Language.getString("pomodoroEndKick"));
- });
- }
- }
- });
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/pomodoro/Pomodoros.java b/src/main/java/eu/m724/tweaks/pomodoro/Pomodoros.java
deleted file mode 100644
index 799d08c..0000000
--- a/src/main/java/eu/m724/tweaks/pomodoro/Pomodoros.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.pomodoro;
-
-import net.md_5.bungee.api.ChatColor;
-import net.md_5.bungee.api.chat.BaseComponent;
-import net.md_5.bungee.api.chat.ComponentBuilder;
-import org.bukkit.entity.Player;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
-
-public class Pomodoros {
- static final Map timers = new HashMap<>();
-
- public static PlayerPomodoro get(Player player) {
- return timers.get(player.getUniqueId());
- }
-
- public static PlayerPomodoro create(Player player) {
- return timers.computeIfAbsent(player.getUniqueId(), (k) -> new PlayerPomodoro());
- }
-
- public static boolean remove(Player player) {
- return timers.remove(player.getUniqueId()) != null;
- }
-
- static BaseComponent[] formatTimer(PlayerPomodoro pomodoro, long remaining) {
- ComponentBuilder builder = new ComponentBuilder();
-
- if (pomodoro.isBreak()) {
- builder.append("Break ").color(ChatColor.LIGHT_PURPLE);
- if (remaining > 0) {
- builder.append("%02d:%02d".formatted(remaining / 60, remaining % 60))
- .color(ChatColor.GOLD);
- } else {
- builder.append("00:00")
- .color(ChatColor.GREEN);
- }
- } else {
- if (remaining > 0) {
- builder
- .append("%02d:%02d".formatted(remaining / 60, remaining % 60))
- .color(ChatColor.GRAY);
- } else {
- builder
- .append("00:00")
- .color(remaining % 2 == 0 ? ChatColor.RED : ChatColor.YELLOW);
- }
- }
-
-
- for (int i=0; i<4; i++) {
- ChatColor color = ChatColor.GRAY;
- if (i == pomodoro.getPomodori()) {
- color = ChatColor.LIGHT_PURPLE;
- } else if (i > pomodoro.getPomodori()) {
- color = ChatColor.DARK_GRAY;
- }
-
- builder.append(" o").color(color);
- }
-
- return builder.create();
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/sleep/SleepManager.java b/src/main/java/eu/m724/tweaks/sleep/SleepManager.java
deleted file mode 100644
index 2aec5cf..0000000
--- a/src/main/java/eu/m724/tweaks/sleep/SleepManager.java
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.sleep;
-
-import eu.m724.tweaks.TweaksConfig;
-import org.bukkit.plugin.Plugin;
-
-public class SleepManager {
- public void init(Plugin plugin) {
- plugin.getServer().getPluginManager().registerEvents(new SleepListener(), plugin);
- if (!TweaksConfig.getConfig().sleepInstant())
- new TimeForwardRunnable(plugin).runTaskTimer(plugin, 0, 1); // TODO maybe not
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/sleep/TimeForwardRunnable.java b/src/main/java/eu/m724/tweaks/sleep/TimeForwardRunnable.java
deleted file mode 100644
index 41f91d5..0000000
--- a/src/main/java/eu/m724/tweaks/sleep/TimeForwardRunnable.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.sleep;
-
-import org.bukkit.GameRule;
-import org.bukkit.Server;
-import org.bukkit.World;
-import org.bukkit.plugin.Plugin;
-import org.bukkit.scheduler.BukkitRunnable;
-
-public class TimeForwardRunnable extends BukkitRunnable {
- private final Server server;
- private final World world; // TODO multi worlds
-
- private final double percentage;
-
- public TimeForwardRunnable(Plugin plugin) {
- this.server = plugin.getServer();
- this.world = server.getWorld("world");
- this.percentage = (world.getGameRuleValue(GameRule.PLAYERS_SLEEPING_PERCENTAGE) / 100.0);
- }
-
- @Override
- public void run() {
- int playersSleeping = SleepState.playersSleeping;
- //System.out.println(playersSleeping);
- if (playersSleeping == 0) return;
-
- int onlinePlayers = (int) (server.getOnlinePlayers().size() / percentage); // TODO optimize remove size every tick maybe
-
- double sleepPercentage = (double) playersSleeping / onlinePlayers;
-
- // we want sleep to take 200 ticks which is 10 seconds assuming all palyres onilien
-
- long time = world.getTime();
- long untilDay = 23459 - time;
-
- if (untilDay == 0) return;
-
- long perSkip = 200 + (100000 / -untilDay);
- perSkip = Math.clamp(perSkip, 20, 200);
- perSkip = (long) (perSkip * sleepPercentage);
-
- world.setTime(world.getTime() + perSkip);
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/updater/PluginScanner.java b/src/main/java/eu/m724/tweaks/updater/PluginScanner.java
deleted file mode 100644
index ac4a921..0000000
--- a/src/main/java/eu/m724/tweaks/updater/PluginScanner.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.updater;
-
-import eu.m724.tweaks.updater.cache.SpigotResource;
-import org.bukkit.configuration.file.YamlConfiguration;
-import org.bukkit.plugin.Plugin;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.Set;
-
-public class PluginScanner {
- private final Plugin thisPlugin;
-
- PluginScanner(Plugin thisPlugin) {
- this.thisPlugin = thisPlugin;
- }
-
- public Set load() throws IOException {
- File installedPluginsYml = new File(thisPlugin.getDataFolder(), "installed_plugins.yml");
-
- if (!installedPluginsYml.exists()) {
- thisPlugin.saveResource("installed_plugins.yml", false);
- }
-
- YamlConfiguration configuration = YamlConfiguration.loadConfiguration(installedPluginsYml);
-
-
- Plugin[] plugins = thisPlugin.getServer().getPluginManager().getPlugins();
- Set spigotResources = new HashSet<>();
-
- for (Plugin plugin : plugins) {
- // System.out.println("Found " + plugin.getName());
- String pluginName = plugin.getName();
-
- if (!configuration.isSet(pluginName)) {
- configuration.set(pluginName, -1);
- continue;
- }
-
- int pluginId = configuration.getInt(pluginName);
- if (pluginId > 0) {
- spigotResources.add(
- new SpigotResource(plugin, pluginId, pluginName)
- );
- }
- }
-
- configuration.save(installedPluginsYml);
-
- return spigotResources;
-
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/updater/UpdateChecker.java b/src/main/java/eu/m724/tweaks/updater/UpdateChecker.java
deleted file mode 100644
index 5216e2b..0000000
--- a/src/main/java/eu/m724/tweaks/updater/UpdateChecker.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.updater;
-
-import eu.m724.tweaks.Language;
-import eu.m724.tweaks.updater.cache.VersionedResource;
-import org.bukkit.plugin.Plugin;
-import org.bukkit.scheduler.BukkitRunnable;
-
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.time.LocalTime;
-import java.time.ZoneOffset;
-import java.util.HashSet;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.CompletionException;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-public class UpdateChecker extends BukkitRunnable {
- private final Set resources;
- private final Logger logger;
-
- static final Set availableUpdates = new HashSet<>();
- static LocalTime lastChecked = null;
-
- UpdateChecker(Plugin plugin, Set resources) {
- this.logger = Logger.getLogger(plugin.getLogger().getName() + "." + getClass().getSimpleName());
- this.resources = resources; // TODO make a copy?
- }
-
- private void checkAll() {
- //logger.info("Checking for updates");
- lastChecked = LocalTime.now(ZoneOffset.UTC);
- availableUpdates.clear();
-
- for (VersionedResource versionedResource : Set.copyOf(resources)) {
- String pluginName = versionedResource.resource().plugin().getName();
- //logger.info(versionedResource.resource().resourceId() + " " + versionedResource.resource().plugin().getName());
- int page = versionedResource.running() != null ? versionedResource.running().page() : 1;
-
- try {
- VersionedResource newResource = new VersionFinder(versionedResource.resource(), page).join(); // this runs async so it's ok
- if (!versionedResource.equals(newResource)) {
- resources.remove(versionedResource);
- if (newResource.running() == null) {
- logger.info("Unable to find installed version of %s".formatted(pluginName));
- if (versionedResource.running() != null) {
- logger.info("Did you downgrade %s? If so, clear cache".formatted(pluginName));
- }
- } else {
- if (!newResource.running().equals(newResource.latest())) {
- availableUpdates.add(newResource);
- //logger.info("Update available for %s. %d -> %d".formatted(pluginName, newResource.running().updateId(), newResource.latest().updateId()));
- }
- }
- resources.add(newResource);
- }
- } catch (CompletionException e) {
- logger.severe("Unable to refresh %s: %s".formatted(pluginName, e.getMessage()));
- }
- }
- }
-
- private void alert() {
- int n = availableUpdates.size();
- if (n == 0) return;
- logger.info(Language.getString("updateAvailableNotice").formatted(n));
-
- availableUpdates.stream()
- .map(u -> "- %s (%s -> %s)".formatted(u.resource().name(), u.running().label(), u.latest().label()))
- .forEach(logger::info);
- }
-
- @Override
- public void run() {
- checkAll();
-
- try (FileOutputStream outputStream = new FileOutputStream(UpdaterManager.cacheFile)) {
- VersionCheckCache.writeAll(outputStream, resources.stream().map(VersionedResource::running).filter(Objects::nonNull).collect(Collectors.toSet()));
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
-
- alert();
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/updater/UpdaterManager.java b/src/main/java/eu/m724/tweaks/updater/UpdaterManager.java
deleted file mode 100644
index e335ce2..0000000
--- a/src/main/java/eu/m724/tweaks/updater/UpdaterManager.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.updater;
-
-import eu.m724.tweaks.updater.cache.ResourceVersion;
-import eu.m724.tweaks.updater.cache.SpigotResource;
-import eu.m724.tweaks.updater.cache.VersionedResource;
-import org.bukkit.plugin.Plugin;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-public class UpdaterManager {
- static File cacheFile;
-
- private final Plugin plugin;
-
- public UpdaterManager(Plugin plugin) {
- this.plugin = plugin;
- cacheFile = new File(plugin.getDataFolder(), "cache/updater");
- }
-
- public void init() throws IOException {
- // scan installed plugins
- Set resources = new PluginScanner(plugin).load();
-
- cacheFile.getParentFile().mkdirs(); // TODO move this somewhere else
-
- // load installed versions from cache
- Set installedVersions;
- try (FileInputStream inputStream = new FileInputStream(cacheFile)) {
- installedVersions = VersionCheckCache.loadAll(inputStream);
- } catch (FileNotFoundException e) {
- installedVersions = new HashSet<>();
- }
-
- final Set ivf = installedVersions;
- Set versionedResources = resources.stream()
- .map(res -> new VersionedResource(
- res, ivf.stream().filter(iv -> iv.resourceId() == res.resourceId()).findFirst().orElse(null), null
- )).collect(Collectors.toSet());
-
-
- new UpdateChecker(plugin, versionedResources)
- .runTaskTimerAsynchronously(plugin, 600, 12 * 3600 * 20);
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderHider.java b/src/main/java/eu/m724/tweaks/worldborder/WorldBorderHider.java
deleted file mode 100644
index 06b3491..0000000
--- a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderHider.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.worldborder;
-
-import com.comphenix.protocol.PacketType;
-import com.comphenix.protocol.ProtocolLibrary;
-import com.comphenix.protocol.events.ListenerPriority;
-import com.comphenix.protocol.events.PacketAdapter;
-import com.comphenix.protocol.events.PacketContainer;
-import com.comphenix.protocol.events.PacketEvent;
-import org.bukkit.plugin.Plugin;
-
-import java.nio.ByteBuffer;
-
-public class WorldBorderHider {
- private static final int EXTENSION_RADIUS = 512;
-
- public void init(Plugin plugin) {
- plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, "tweaks724:worldborder");
- byte[] infoArray = ByteBuffer.allocate(4).putInt(EXTENSION_RADIUS).array();
-
- ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(
- plugin,
- ListenerPriority.NORMAL,
- PacketType.Play.Server.INITIALIZE_BORDER
- ) {
- @Override
- public void onPacketSending(PacketEvent event) {
- PacketContainer packet = event.getPacket();
- // old diameter
- packet.getDoubles().write(2, packet.getDoubles().read(2) + EXTENSION_RADIUS * 2);
- // new diameter
- packet.getDoubles().write(3, packet.getDoubles().read(3) + EXTENSION_RADIUS * 2);
-
- // border radius
- packet.getIntegers().write(0, packet.getIntegers().read(0) + EXTENSION_RADIUS);
- // warning distance
- packet.getIntegers().write(1, packet.getIntegers().read(1) + EXTENSION_RADIUS);
-
- event.getPlayer().sendPluginMessage(plugin, "tweaks724:worldborder", infoArray);
- }
- });
-
- ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(
- plugin,
- ListenerPriority.NORMAL,
- PacketType.Play.Server.SET_BORDER_SIZE
- ) {
- @Override
- public void onPacketSending(PacketEvent event) {
- PacketContainer packet = event.getPacket();
- // diameter
- packet.getDoubles().write(0, packet.getDoubles().read(0) + EXTENSION_RADIUS * 2);
- }
- });
-
- ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(
- plugin,
- ListenerPriority.NORMAL,
- PacketType.Play.Server.SET_BORDER_WARNING_DISTANCE
- ) {
- @Override
- public void onPacketSending(PacketEvent event) {
- PacketContainer packet = event.getPacket();
- // warning distance
- packet.getIntegers().write(0, packet.getIntegers().read(0) + EXTENSION_RADIUS);
- }
- });
- }
-}
diff --git a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderManager.java b/src/main/java/eu/m724/tweaks/worldborder/WorldBorderManager.java
deleted file mode 100644
index 2354a83..0000000
--- a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderManager.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2024 Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.worldborder;
-
-import eu.m724.tweaks.TweaksConfig;
-import org.bukkit.event.world.WorldLoadEvent;
-import org.bukkit.plugin.Plugin;
-
-public class WorldBorderManager {
- public void init(Plugin plugin) {
- if (TweaksConfig.getConfig().worldborderExpand()) {
- WorldBorderExpanderListener wbrl = new WorldBorderExpanderListener();
- plugin.getServer().getPluginManager().registerEvents(wbrl, plugin);
-
- // because the plugin loads "post world"
- plugin.getServer().getWorlds().forEach(w -> {
- wbrl.onWorldLoad(new WorldLoadEvent(w));
- });
- }
-
- if (TweaksConfig.getConfig().worldborderHide()) {
- new WorldBorderHider().init(plugin);
- }
- }
-}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index a5d2d38..6b48d5f 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -5,7 +5,7 @@
# - https://discord.gg/86X4Z5JUeq
# - https://www.spigotmc.org/threads/tweaks724.670906/
-# Metrics toggle. Ideally opt-in, but the system is very new and it needs testing.
+# Metrics toggle. Please keep this on.
metrics: true
# Warning: Don't use /worldborder while this is on
@@ -36,9 +36,9 @@ doors:
knocking: true
motd:
+ enabled: true
# Name of the set containing the MOTDs
# (random displayed every ping)
- # "" or false to disable
set: "example"
chat:
@@ -81,8 +81,10 @@ hardcore:
# 0.0 - 1.0 decimal. This is if you want to make it like an Easter egg
chance: 1.0
-# Makes sleeping
-# And adds a nice animation
+# Sleep tweaks
+# Percentage: playersSleepingPercentage gamerule
+# If instant: how much % of players to skip the night
+# If not: how much % make skipping full speed
sleep:
enabled: true
# This gives every player a "share" of the night
@@ -90,9 +92,8 @@ sleep:
# For example, if 5 players online and night is 5 minutes, one can go to sleep and skip 1 minute of the night
# Leaving the bed and reentering it does nothing
instant: false
- # Percentage: playersSleepingPercentage gamerule
- # If instant: how much % of players to skip the night
- # If not: how much % make skipping full speed
+ # How many hearts to heal after sleeping
+ heal: 2.0
# "Hostname" authentication
# This makes a player need to join a unique hostname like "asd123.example.com" where "asd123" is the key
@@ -103,7 +104,37 @@ auth:
# The domain of the server. Doesn't do anything other than showing in /tauth new
domain: "replace.me"
+# Adds gateways emitting redstone, controlled over internet
+# https://git.m724.eu/Minecon724/tweaks724/src/branch/master/RETSTONE.md
+retstone:
+ enabled: true
+ # This takes host:port, listens on UDP
+ listen: 127.0.0.1:57931
+
+# Knockback dealt BY those entities is multiplied by value
+# Entity must be one of https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/EntityType.html
+# 1 means no change
+knockback:
+ player: 0.7
+ tnt: 5
+ creeper: 0.7
+
+# Kills server after /servkill or HTTP request
+# https://git.m724.eu/Minecon724/tweaks724/src/branch/master/KILLSWITCH.md
+killswitch:
+ enabled: true
+ # This takes host:port, starts an HTTP server
+ # To disable HTTP server, set to null
+ listen: 127.0.0.1:57932
+
+# Swing through grass (and alike)
+# If using sword, you can also hit behind the grass
+# If not, you can only hit the entity in the grass
+swing:
+ enabled: true
+
+
# Finally, thank you for downloading Tweaks724, I hope you enjoy!
# Don't modify unless told to
-magic number don't modify this: 1
\ No newline at end of file
+magic number don't modify this: 2
\ No newline at end of file
diff --git a/src/main/resources/motd sets/example.txt b/src/main/resources/motd sets/example.txt
index 0970723..d4486bf 100644
--- a/src/main/resources/motd sets/example.txt
+++ b/src/main/resources/motd sets/example.txt
@@ -7,3 +7,5 @@ Random MOTD for every ping
Or just one line. If it doesn't fit it will be passed onto the second line.
Put your own files in this directory
+
+["",{"text":"Also supports","color":"#A18D94"},{"text":" JSON","color":"#FF00FF"},{"text":"\n"},{"text":"Java","color":"dark_aqua"},{"text":"Script ","color":"aqua"},{"text":"Object","color":"gray"},{"text":" Notation","color":"red"}]
\ No newline at end of file
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index c8b67c3..40473fb 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -8,7 +8,7 @@ api-version: 1.21.1
softdepend: [ProtocolLib]
libraries:
- - eu.m724:mstats-spigot:0.1.0
+ - eu.m724:mstats-spigot:0.1.2
commands:
chat:
@@ -34,19 +34,42 @@ commands:
emergencyalert:
description: Send emergency alert
permission: tweaks724.emergencyalert
+ retstone:
+ description: Retstone commands
+ permission: tweaks724.retstone
+ servkill:
+ description: Immediately stop the server
+ permission: tweaks724.servkill
+ durabilityalert:
+ description: Durability alert toggle
+ permission: tweaks724.durabilityalert
+ wordcoords:
+ description: Word to coords conversion
+ permission: tweaks724.wordcoords
+ aliases: [woco, wc, w3w]
permissions:
- tweaks724:
- chatmanage:
- default: true
- pomodoro:
- default: true
- updates:
- default: op
- tauth:
- default: op
- bypass-full:
- default: op
- emergencyalert:
- default: op
+ tweaks724.chatmanage:
+ default: true
+ tweaks724.pomodoro:
+ default: true
+ tweaks724.updates:
+ default: op
+ tweaks724.tauth:
+ default: op
+ tweaks724.bypass-full:
+ default: op
+ tweaks724.emergencyalert:
+ default: op
+ tweaks724.retstone:
+ default: op
+ tweaks724.servkill:
+ default: false
+ tweaks724.durabilityalert:
+ default: true
+ tweaks724.wordcoords:
+ default: true
+ 7weaks724.ignore.this:
+ description: "Internal, not for use. ${project.spigot.version}"
+ default: false
diff --git a/src/main/resources/strings.properties b/src/main/resources/strings.properties
index a7dde07..88a5ffd 100644
--- a/src/main/resources/strings.properties
+++ b/src/main/resources/strings.properties
@@ -1,26 +1,53 @@
#
-# Copyright (C) 2024 Minecon724
+# Copyright (C) 2025 Minecon724
# Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
# in the project root for the full license text.
#
+languageNotice = Language: %s (%s)
+# Language name in your language
+language = English
+# Language name in English
+languageEnglish = English
+
updateAvailableNotice = Available updates (%d):
-pomodoroEndKick = Break time! Come back in 5 minutes.
# Used in /updates
-updatesNotChecked = Not checked yet
+updatesNotChecked = Not checked yet.
# %s is time as HH:mm
updatesNoUpdates = No available updates. Last checked: %s
# %s is update title
updatesClickToOpen = Click to open on SpigotMC "%s"
# Used in /chat
-chatPasswordProtected = This room is password protected
-chatWrongPassword = Wrong password
-chatNoSuchRoom = No room named %s
-chatAlreadyHere = You're already in this room
+chatPasswordProtected = This room is password protected.
+chatWrongPassword = Wrong password.
+chatNoSuchRoom = Room %s doesn't exist.
+chatNoSuchRoomInvalidId = Room %s doesn't exist, because the ID is invalid.
+chatAlreadyHere = You're already in this room.
# Used when a player joins using the wrong key or no key
authKickWrongKey = You're connecting to the wrong server address. You must connect to the one you're registered to.
# If force is enabled and player is not registered. Changing this reveals you're using this plugin
-authKickUnregistered = You are not whitelisted on this server!
\ No newline at end of file
+authKickUnregistered = You are not whitelisted on this server!
+authKickError = An error occurred. Please try again. If this persists, contact an administrator.
+
+redstoneGatewayItem = Redstone gateway
+
+clickToCopy = Click to copy
+clickToExecuteCommand = Click to execute command
+
+durabilityEnabled = Enabled durability alert
+durabilityDisabled = Disabled durability alert
+
+# When console executes /wordcoords without arguments
+wordCoordsPlayerOnly = Only players can execute this command without arguments.
+wordCoordsOutOfRange = Those coordinates are invalid.
+wordCoordsInvalidWord = Invalid word or coordinate: "%s"
+wordCoordsProvideZ = Please provide the Z coordinate.
+
+# /pomodoro
+pomodoroStopped = Pomodoro stopped. Restart it with /%s start
+pomodoroStart = Start pomodoro with /%s start
+pomodoroShortBreak = Short break
+pomodoroLongBreak = Long break
\ No newline at end of file
diff --git a/src/main/resources/wordlist.txt b/src/main/resources/wordlist.txt
new file mode 100644
index 0000000..40300c5
--- /dev/null
+++ b/src/main/resources/wordlist.txt
@@ -0,0 +1,2054 @@
+# This file is the word list for WordCoords.
+# You can replace this file with your own words.
+
+# Below is the BIP-39 English list
+# https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md
+
+abandon
+ability
+able
+about
+above
+absent
+absorb
+abstract
+absurd
+abuse
+access
+accident
+account
+accuse
+achieve
+acid
+acoustic
+acquire
+across
+act
+action
+actor
+actress
+actual
+adapt
+add
+addict
+address
+adjust
+admit
+adult
+advance
+advice
+aerobic
+affair
+afford
+afraid
+again
+age
+agent
+agree
+ahead
+aim
+air
+airport
+aisle
+alarm
+album
+alcohol
+alert
+alien
+all
+alley
+allow
+almost
+alone
+alpha
+already
+also
+alter
+always
+amateur
+amazing
+among
+amount
+amused
+analyst
+anchor
+ancient
+anger
+angle
+angry
+animal
+ankle
+announce
+annual
+another
+answer
+antenna
+antique
+anxiety
+any
+apart
+apology
+appear
+apple
+approve
+april
+arch
+arctic
+area
+arena
+argue
+arm
+armed
+armor
+army
+around
+arrange
+arrest
+arrive
+arrow
+art
+artefact
+artist
+artwork
+ask
+aspect
+assault
+asset
+assist
+assume
+asthma
+athlete
+atom
+attack
+attend
+attitude
+attract
+auction
+audit
+august
+aunt
+author
+auto
+autumn
+average
+avocado
+avoid
+awake
+aware
+away
+awesome
+awful
+awkward
+axis
+baby
+bachelor
+bacon
+badge
+bag
+balance
+balcony
+ball
+bamboo
+banana
+banner
+bar
+barely
+bargain
+barrel
+base
+basic
+basket
+battle
+beach
+bean
+beauty
+because
+become
+beef
+before
+begin
+behave
+behind
+believe
+below
+belt
+bench
+benefit
+best
+betray
+better
+between
+beyond
+bicycle
+bid
+bike
+bind
+biology
+bird
+birth
+bitter
+black
+blade
+blame
+blanket
+blast
+bleak
+bless
+blind
+blood
+blossom
+blouse
+blue
+blur
+blush
+board
+boat
+body
+boil
+bomb
+bone
+bonus
+book
+boost
+border
+boring
+borrow
+boss
+bottom
+bounce
+box
+boy
+bracket
+brain
+brand
+brass
+brave
+bread
+breeze
+brick
+bridge
+brief
+bright
+bring
+brisk
+broccoli
+broken
+bronze
+broom
+brother
+brown
+brush
+bubble
+buddy
+budget
+buffalo
+build
+bulb
+bulk
+bullet
+bundle
+bunker
+burden
+burger
+burst
+bus
+business
+busy
+butter
+buyer
+buzz
+cabbage
+cabin
+cable
+cactus
+cage
+cake
+call
+calm
+camera
+camp
+can
+canal
+cancel
+candy
+cannon
+canoe
+canvas
+canyon
+capable
+capital
+captain
+car
+carbon
+card
+cargo
+carpet
+carry
+cart
+case
+cash
+casino
+castle
+casual
+cat
+catalog
+catch
+category
+cattle
+caught
+cause
+caution
+cave
+ceiling
+celery
+cement
+census
+century
+cereal
+certain
+chair
+chalk
+champion
+change
+chaos
+chapter
+charge
+chase
+chat
+cheap
+check
+cheese
+chef
+cherry
+chest
+chicken
+chief
+child
+chimney
+choice
+choose
+chronic
+chuckle
+chunk
+churn
+cigar
+cinnamon
+circle
+citizen
+city
+civil
+claim
+clap
+clarify
+claw
+clay
+clean
+clerk
+clever
+click
+client
+cliff
+climb
+clinic
+clip
+clock
+clog
+close
+cloth
+cloud
+clown
+club
+clump
+cluster
+clutch
+coach
+coast
+coconut
+code
+coffee
+coil
+coin
+collect
+color
+column
+combine
+come
+comfort
+comic
+common
+company
+concert
+conduct
+confirm
+congress
+connect
+consider
+control
+convince
+cook
+cool
+copper
+copy
+coral
+core
+corn
+correct
+cost
+cotton
+couch
+country
+couple
+course
+cousin
+cover
+coyote
+crack
+cradle
+craft
+cram
+crane
+crash
+crater
+crawl
+crazy
+cream
+credit
+creek
+crew
+cricket
+crime
+crisp
+critic
+crop
+cross
+crouch
+crowd
+crucial
+cruel
+cruise
+crumble
+crunch
+crush
+cry
+crystal
+cube
+culture
+cup
+cupboard
+curious
+current
+curtain
+curve
+cushion
+custom
+cute
+cycle
+dad
+damage
+damp
+dance
+danger
+daring
+dash
+daughter
+dawn
+day
+deal
+debate
+debris
+decade
+december
+decide
+decline
+decorate
+decrease
+deer
+defense
+define
+defy
+degree
+delay
+deliver
+demand
+demise
+denial
+dentist
+deny
+depart
+depend
+deposit
+depth
+deputy
+derive
+describe
+desert
+design
+desk
+despair
+destroy
+detail
+detect
+develop
+device
+devote
+diagram
+dial
+diamond
+diary
+dice
+diesel
+diet
+differ
+digital
+dignity
+dilemma
+dinner
+dinosaur
+direct
+dirt
+disagree
+discover
+disease
+dish
+dismiss
+disorder
+display
+distance
+divert
+divide
+divorce
+dizzy
+doctor
+document
+dog
+doll
+dolphin
+domain
+donate
+donkey
+donor
+door
+dose
+double
+dove
+draft
+dragon
+drama
+drastic
+draw
+dream
+dress
+drift
+drill
+drink
+drip
+drive
+drop
+drum
+dry
+duck
+dumb
+dune
+during
+dust
+dutch
+duty
+dwarf
+dynamic
+eager
+eagle
+early
+earn
+earth
+easily
+east
+easy
+echo
+ecology
+economy
+edge
+edit
+educate
+effort
+egg
+eight
+either
+elbow
+elder
+electric
+elegant
+element
+elephant
+elevator
+elite
+else
+embark
+embody
+embrace
+emerge
+emotion
+employ
+empower
+empty
+enable
+enact
+end
+endless
+endorse
+enemy
+energy
+enforce
+engage
+engine
+enhance
+enjoy
+enlist
+enough
+enrich
+enroll
+ensure
+enter
+entire
+entry
+envelope
+episode
+equal
+equip
+era
+erase
+erode
+erosion
+error
+erupt
+escape
+essay
+essence
+estate
+eternal
+ethics
+evidence
+evil
+evoke
+evolve
+exact
+example
+excess
+exchange
+excite
+exclude
+excuse
+execute
+exercise
+exhaust
+exhibit
+exile
+exist
+exit
+exotic
+expand
+expect
+expire
+explain
+expose
+express
+extend
+extra
+eye
+eyebrow
+fabric
+face
+faculty
+fade
+faint
+faith
+fall
+false
+fame
+family
+famous
+fan
+fancy
+fantasy
+farm
+fashion
+fat
+fatal
+father
+fatigue
+fault
+favorite
+feature
+february
+federal
+fee
+feed
+feel
+female
+fence
+festival
+fetch
+fever
+few
+fiber
+fiction
+field
+figure
+file
+film
+filter
+final
+find
+fine
+finger
+finish
+fire
+firm
+first
+fiscal
+fish
+fit
+fitness
+fix
+flag
+flame
+flash
+flat
+flavor
+flee
+flight
+flip
+float
+flock
+floor
+flower
+fluid
+flush
+fly
+foam
+focus
+fog
+foil
+fold
+follow
+food
+foot
+force
+forest
+forget
+fork
+fortune
+forum
+forward
+fossil
+foster
+found
+fox
+fragile
+frame
+frequent
+fresh
+friend
+fringe
+frog
+front
+frost
+frown
+frozen
+fruit
+fuel
+fun
+funny
+furnace
+fury
+future
+gadget
+gain
+galaxy
+gallery
+game
+gap
+garage
+garbage
+garden
+garlic
+garment
+gas
+gasp
+gate
+gather
+gauge
+gaze
+general
+genius
+genre
+gentle
+genuine
+gesture
+ghost
+giant
+gift
+giggle
+ginger
+giraffe
+girl
+give
+glad
+glance
+glare
+glass
+glide
+glimpse
+globe
+gloom
+glory
+glove
+glow
+glue
+goat
+goddess
+gold
+good
+goose
+gorilla
+gospel
+gossip
+govern
+gown
+grab
+grace
+grain
+grant
+grape
+grass
+gravity
+great
+green
+grid
+grief
+grit
+grocery
+group
+grow
+grunt
+guard
+guess
+guide
+guilt
+guitar
+gun
+gym
+habit
+hair
+half
+hammer
+hamster
+hand
+happy
+harbor
+hard
+harsh
+harvest
+hat
+have
+hawk
+hazard
+head
+health
+heart
+heavy
+hedgehog
+height
+hello
+helmet
+help
+hen
+hero
+hidden
+high
+hill
+hint
+hip
+hire
+history
+hobby
+hockey
+hold
+hole
+holiday
+hollow
+home
+honey
+hood
+hope
+horn
+horror
+horse
+hospital
+host
+hotel
+hour
+hover
+hub
+huge
+human
+humble
+humor
+hundred
+hungry
+hunt
+hurdle
+hurry
+hurt
+husband
+hybrid
+ice
+icon
+idea
+identify
+idle
+ignore
+ill
+illegal
+illness
+image
+imitate
+immense
+immune
+impact
+impose
+improve
+impulse
+inch
+include
+income
+increase
+index
+indicate
+indoor
+industry
+infant
+inflict
+inform
+inhale
+inherit
+initial
+inject
+injury
+inmate
+inner
+innocent
+input
+inquiry
+insane
+insect
+inside
+inspire
+install
+intact
+interest
+into
+invest
+invite
+involve
+iron
+island
+isolate
+issue
+item
+ivory
+jacket
+jaguar
+jar
+jazz
+jealous
+jeans
+jelly
+jewel
+job
+join
+joke
+journey
+joy
+judge
+juice
+jump
+jungle
+junior
+junk
+just
+kangaroo
+keen
+keep
+ketchup
+key
+kick
+kid
+kidney
+kind
+kingdom
+kiss
+kit
+kitchen
+kite
+kitten
+kiwi
+knee
+knife
+knock
+know
+lab
+label
+labor
+ladder
+lady
+lake
+lamp
+language
+laptop
+large
+later
+latin
+laugh
+laundry
+lava
+law
+lawn
+lawsuit
+layer
+lazy
+leader
+leaf
+learn
+leave
+lecture
+left
+leg
+legal
+legend
+leisure
+lemon
+lend
+length
+lens
+leopard
+lesson
+letter
+level
+liar
+liberty
+library
+license
+life
+lift
+light
+like
+limb
+limit
+link
+lion
+liquid
+list
+little
+live
+lizard
+load
+loan
+lobster
+local
+lock
+logic
+lonely
+long
+loop
+lottery
+loud
+lounge
+love
+loyal
+lucky
+luggage
+lumber
+lunar
+lunch
+luxury
+lyrics
+machine
+mad
+magic
+magnet
+maid
+mail
+main
+major
+make
+mammal
+man
+manage
+mandate
+mango
+mansion
+manual
+maple
+marble
+march
+margin
+marine
+market
+marriage
+mask
+mass
+master
+match
+material
+math
+matrix
+matter
+maximum
+maze
+meadow
+mean
+measure
+meat
+mechanic
+medal
+media
+melody
+melt
+member
+memory
+mention
+menu
+mercy
+merge
+merit
+merry
+mesh
+message
+metal
+method
+middle
+midnight
+milk
+million
+mimic
+mind
+minimum
+minor
+minute
+miracle
+mirror
+misery
+miss
+mistake
+mix
+mixed
+mixture
+mobile
+model
+modify
+mom
+moment
+monitor
+monkey
+monster
+month
+moon
+moral
+more
+morning
+mosquito
+mother
+motion
+motor
+mountain
+mouse
+move
+movie
+much
+muffin
+mule
+multiply
+muscle
+museum
+mushroom
+music
+must
+mutual
+myself
+mystery
+myth
+naive
+name
+napkin
+narrow
+nasty
+nation
+nature
+near
+neck
+need
+negative
+neglect
+neither
+nephew
+nerve
+nest
+net
+network
+neutral
+never
+news
+next
+nice
+night
+noble
+noise
+nominee
+noodle
+normal
+north
+nose
+notable
+note
+nothing
+notice
+novel
+now
+nuclear
+number
+nurse
+nut
+oak
+obey
+object
+oblige
+obscure
+observe
+obtain
+obvious
+occur
+ocean
+october
+odor
+off
+offer
+office
+often
+oil
+okay
+old
+olive
+olympic
+omit
+once
+one
+onion
+online
+only
+open
+opera
+opinion
+oppose
+option
+orange
+orbit
+orchard
+order
+ordinary
+organ
+orient
+original
+orphan
+ostrich
+other
+outdoor
+outer
+output
+outside
+oval
+oven
+over
+own
+owner
+oxygen
+oyster
+ozone
+pact
+paddle
+page
+pair
+palace
+palm
+panda
+panel
+panic
+panther
+paper
+parade
+parent
+park
+parrot
+party
+pass
+patch
+path
+patient
+patrol
+pattern
+pause
+pave
+payment
+peace
+peanut
+pear
+peasant
+pelican
+pen
+penalty
+pencil
+people
+pepper
+perfect
+permit
+person
+pet
+phone
+photo
+phrase
+physical
+piano
+picnic
+picture
+piece
+pig
+pigeon
+pill
+pilot
+pink
+pioneer
+pipe
+pistol
+pitch
+pizza
+place
+planet
+plastic
+plate
+play
+please
+pledge
+pluck
+plug
+plunge
+poem
+poet
+point
+polar
+pole
+police
+pond
+pony
+pool
+popular
+portion
+position
+possible
+post
+potato
+pottery
+poverty
+powder
+power
+practice
+praise
+predict
+prefer
+prepare
+present
+pretty
+prevent
+price
+pride
+primary
+print
+priority
+prison
+private
+prize
+problem
+process
+produce
+profit
+program
+project
+promote
+proof
+property
+prosper
+protect
+proud
+provide
+public
+pudding
+pull
+pulp
+pulse
+pumpkin
+punch
+pupil
+puppy
+purchase
+purity
+purpose
+purse
+push
+put
+puzzle
+pyramid
+quality
+quantum
+quarter
+question
+quick
+quit
+quiz
+quote
+rabbit
+raccoon
+race
+rack
+radar
+radio
+rail
+rain
+raise
+rally
+ramp
+ranch
+random
+range
+rapid
+rare
+rate
+rather
+raven
+raw
+razor
+ready
+real
+reason
+rebel
+rebuild
+recall
+receive
+recipe
+record
+recycle
+reduce
+reflect
+reform
+refuse
+region
+regret
+regular
+reject
+relax
+release
+relief
+rely
+remain
+remember
+remind
+remove
+render
+renew
+rent
+reopen
+repair
+repeat
+replace
+report
+require
+rescue
+resemble
+resist
+resource
+response
+result
+retire
+retreat
+return
+reunion
+reveal
+review
+reward
+rhythm
+rib
+ribbon
+rice
+rich
+ride
+ridge
+rifle
+right
+rigid
+ring
+riot
+ripple
+risk
+ritual
+rival
+river
+road
+roast
+robot
+robust
+rocket
+romance
+roof
+rookie
+room
+rose
+rotate
+rough
+round
+route
+royal
+rubber
+rude
+rug
+rule
+run
+runway
+rural
+sad
+saddle
+sadness
+safe
+sail
+salad
+salmon
+salon
+salt
+salute
+same
+sample
+sand
+satisfy
+satoshi
+sauce
+sausage
+save
+say
+scale
+scan
+scare
+scatter
+scene
+scheme
+school
+science
+scissors
+scorpion
+scout
+scrap
+screen
+script
+scrub
+sea
+search
+season
+seat
+second
+secret
+section
+security
+seed
+seek
+segment
+select
+sell
+seminar
+senior
+sense
+sentence
+series
+service
+session
+settle
+setup
+seven
+shadow
+shaft
+shallow
+share
+shed
+shell
+sheriff
+shield
+shift
+shine
+ship
+shiver
+shock
+shoe
+shoot
+shop
+short
+shoulder
+shove
+shrimp
+shrug
+shuffle
+shy
+sibling
+sick
+side
+siege
+sight
+sign
+silent
+silk
+silly
+silver
+similar
+simple
+since
+sing
+siren
+sister
+situate
+six
+size
+skate
+sketch
+ski
+skill
+skin
+skirt
+skull
+slab
+slam
+sleep
+slender
+slice
+slide
+slight
+slim
+slogan
+slot
+slow
+slush
+small
+smart
+smile
+smoke
+smooth
+snack
+snake
+snap
+sniff
+snow
+soap
+soccer
+social
+sock
+soda
+soft
+solar
+soldier
+solid
+solution
+solve
+someone
+song
+soon
+sorry
+sort
+soul
+sound
+soup
+source
+south
+space
+spare
+spatial
+spawn
+speak
+special
+speed
+spell
+spend
+sphere
+spice
+spider
+spike
+spin
+spirit
+split
+spoil
+sponsor
+spoon
+sport
+spot
+spray
+spread
+spring
+spy
+square
+squeeze
+squirrel
+stable
+stadium
+staff
+stage
+stairs
+stamp
+stand
+start
+state
+stay
+steak
+steel
+stem
+step
+stereo
+stick
+still
+sting
+stock
+stomach
+stone
+stool
+story
+stove
+strategy
+street
+strike
+strong
+struggle
+student
+stuff
+stumble
+style
+subject
+submit
+subway
+success
+such
+sudden
+suffer
+sugar
+suggest
+suit
+summer
+sun
+sunny
+sunset
+super
+supply
+supreme
+sure
+surface
+surge
+surprise
+surround
+survey
+suspect
+sustain
+swallow
+swamp
+swap
+swarm
+swear
+sweet
+swift
+swim
+swing
+switch
+sword
+symbol
+symptom
+syrup
+system
+table
+tackle
+tag
+tail
+talent
+talk
+tank
+tape
+target
+task
+taste
+tattoo
+taxi
+teach
+team
+tell
+ten
+tenant
+tennis
+tent
+term
+test
+text
+thank
+that
+theme
+then
+theory
+there
+they
+thing
+this
+thought
+three
+thrive
+throw
+thumb
+thunder
+ticket
+tide
+tiger
+tilt
+timber
+time
+tiny
+tip
+tired
+tissue
+title
+toast
+tobacco
+today
+toddler
+toe
+together
+toilet
+token
+tomato
+tomorrow
+tone
+tongue
+tonight
+tool
+tooth
+top
+topic
+topple
+torch
+tornado
+tortoise
+toss
+total
+tourist
+toward
+tower
+town
+toy
+track
+trade
+traffic
+tragic
+train
+transfer
+trap
+trash
+travel
+tray
+treat
+tree
+trend
+trial
+tribe
+trick
+trigger
+trim
+trip
+trophy
+trouble
+truck
+true
+truly
+trumpet
+trust
+truth
+try
+tube
+tuition
+tumble
+tuna
+tunnel
+turkey
+turn
+turtle
+twelve
+twenty
+twice
+twin
+twist
+two
+type
+typical
+ugly
+umbrella
+unable
+unaware
+uncle
+uncover
+under
+undo
+unfair
+unfold
+unhappy
+uniform
+unique
+unit
+universe
+unknown
+unlock
+until
+unusual
+unveil
+update
+upgrade
+uphold
+upon
+upper
+upset
+urban
+urge
+usage
+use
+used
+useful
+useless
+usual
+utility
+vacant
+vacuum
+vague
+valid
+valley
+valve
+van
+vanish
+vapor
+various
+vast
+vault
+vehicle
+velvet
+vendor
+venture
+venue
+verb
+verify
+version
+very
+vessel
+veteran
+viable
+vibrant
+vicious
+victory
+video
+view
+village
+vintage
+violin
+virtual
+virus
+visa
+visit
+visual
+vital
+vivid
+vocal
+voice
+void
+volcano
+volume
+vote
+voyage
+wage
+wagon
+wait
+walk
+wall
+walnut
+want
+warfare
+warm
+warrior
+wash
+wasp
+waste
+water
+wave
+way
+wealth
+weapon
+wear
+weasel
+weather
+web
+wedding
+weekend
+weird
+welcome
+west
+wet
+whale
+what
+wheat
+wheel
+when
+where
+whip
+whisper
+wide
+width
+wife
+wild
+will
+win
+window
+wine
+wing
+wink
+winner
+winter
+wire
+wisdom
+wise
+wish
+witness
+wolf
+woman
+wonder
+wood
+wool
+word
+work
+world
+worry
+worth
+wrap
+wreck
+wrestle
+wrist
+write
+wrong
+yard
+year
+yellow
+you
+young
+youth
+zebra
+zero
+zone
+zoo
diff --git a/tools/download_nms.sh b/tools/download_nms.sh
index c22aac4..95a44be 100755
--- a/tools/download_nms.sh
+++ b/tools/download_nms.sh
@@ -1,7 +1,7 @@
-#!/bin/bash
+#!/bin/sh
#
-# Copyright (C) 2024 Minecon724
+# Copyright (C) 2025 Minecon724
# Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
# in the project root for the full license text.
#