Compare commits

...

103 commits

Author SHA1 Message Date
4f752bef61
Quick mode
Some checks failed
/ build (push) Failing after 44s
2025-04-06 12:05:28 +02:00
41583939ac
Use component 2025-04-06 11:29:50 +02:00
22bb2d16d2
Fix encoding and decoding 2025-04-06 10:25:18 +02:00
Minecon724
598902ef33
update readme
Some checks failed
/ build (push) Failing after 7s
Signed-off-by: Minecon724 <git@m724.eu>
2025-02-19 11:24:22 +01:00
Minecon724
7cd334f4a2
feat: Initial word coords module
Some checks failed
/ build (push) Failing after 7s
Signed-off-by: Minecon724 <git@m724.eu>
2025-02-18 14:09:19 +01:00
Minecon724
b421b48e51
chore: Update mStats to fix am issue
Some checks failed
/ build (push) Failing after 6s
Signed-off-by: Minecon724 <git@m724.eu>
2025-02-17 09:06:39 +01:00
Minecon724
684e31bf07
fix: correct string formatting in debug logger
Some checks failed
/ build (push) Failing after 6s
Updated the debug logger to use proper string formatting syntax. This ensures compatibility and avoids runtime errors.

Signed-off-by: Minecon724 <git@m724.eu>
2025-02-04 17:20:45 +01:00
Minecon724
cf654fcb42
[maven-release-plugin] prepare for next development iteration
Some checks failed
/ build (push) Has been cancelled
2025-02-03 11:27:37 +01:00
Minecon724
0c4690e2e7
[maven-release-plugin] prepare release tweaks-0.1.14
Some checks failed
/ build (push) Has been cancelled
2025-02-03 11:27:34 +01:00
Minecon724
05b9ca4cb8
feat(chat): initialize ChatRoomLoader on plugin start
Some checks are pending
/ build (push) Waiting to run
- Added ChatRoomLoader initialization during ChatModule setup.
- Ensures necessary resources are loaded before chat operations.

Signed-off-by: Minecon724 <git@m724.eu>
2025-02-03 11:27:21 +01:00
Minecon724
316d479fd8
feat(chat): improve chat room management UX
- Added hover and click actions to chat commands for better interaction.
- Enhanced error and confirmation messages for clarity.
- Introduced validation for room IDs and improved handling of invalid IDs.

Signed-off-by: Minecon724 <git@m724.eu>
2025-02-03 11:25:26 +01:00
Minecon724
6ff6ec9d6b
fix: improve error handling in ChatCommands
Updated error handling to throw RuntimeException instead of printing stack traces. Enhanced user feedback by standardizing error messages across various chat command operations.

Signed-off-by: Minecon724 <git@m724.eu>
2025-02-03 10:52:04 +01:00
Minecon724
541f095075
fix(motd): handle FileAlreadyExistsException in directory creation
Added a catch block for FileAlreadyExistsException to prevent unnecessary exceptions when the "motd sets" directory already exists. This ensures smoother execution and improves error handling during initialization.

Signed-off-by: Minecon724 <git@m724.eu>
2025-02-03 10:42:12 +01:00
Minecon724
1fafc90e04
refactor(pomodoro): simplify plugin instance retrieval
Some checks are pending
/ build (push) Waiting to run
Replaced passing the plugin instance to PomodoroRunnable with direct access via TweaksPlugin singleton. This removes redundant code, improving clarity and maintainability.
2025-02-02 14:57:18 +01:00
Minecon724
ad1a699d38
feat(auth): improve error handling and key generation logic
Added new error handling for key assignment failures, including user-facing messaging. Enhanced secure key generation with fixed length and randomness via `SecureRandom`. Removed redundant TODO comments and replaced placeholder exception handling with actionable implementations.
2025-02-02 14:57:00 +01:00
Minecon724
7de46b879b
Refactor MOTD configuration and improve file handling
Revised the MOTD configuration setup to use a direct boolean flag for enabling/disabling instead of relying on string checks. Updated file handling to use `Path` and modernized directory creation logic for better error handling and clarity. Removed unused code and cleaned up resource management in `MotdModule`.
2025-02-02 14:12:19 +01:00
Minecon724
cbe9f77cca
Remove obsolete comment about static method in TweaksModule
The removed comment questioned making a method non-static, which is already resolved or irrelevant. This cleanup improves code readability and eliminates confusion.
2025-02-02 14:05:44 +01:00
Minecon724
c096674a15
Support sleep mechanics across multiple worlds
Refactored `TimeForwardRunnable` to handle multiple worlds instead of a single hardcoded world. The code now dynamically retrieves sleep percentage and processes time advancement for each world, improving flexibility and compatibility.
2025-02-02 14:03:22 +01:00
Minecon724
232e0bfa9a
Durability toggle
All checks were successful
/ build (push) Successful in 44s
2025-01-28 10:18:15 +01:00
Minecon724
9345efe1d4
Initial durability module
All checks were successful
/ build (push) Successful in 50s
2025-01-27 08:45:52 +01:00
Minecon724
2761ed8757
Slightly smarter updater notice
All checks were successful
/ build (push) Successful in 50s
2025-01-26 12:41:40 +01:00
Minecon724
2a65e9dbcb
Fix small debug log mistake
All checks were successful
/ build (push) Successful in 46s
2025-01-25 20:32:17 +01:00
Minecon724
29f79a8771
Improve config loader
All checks were successful
/ build (push) Successful in 50s
2025-01-24 18:42:39 +01:00
Minecon724
7b65be86ad
Move modules to modules 2025-01-24 10:05:49 +01:00
Minecon724
2788829967
[maven-release-plugin] prepare for next development iteration
All checks were successful
/ build (push) Successful in 47s
2025-01-20 13:54:42 +01:00
Minecon724
cbfbdf8ce3
[maven-release-plugin] prepare release tweaks-0.1.13
All checks were successful
/ build (push) Successful in 49s
2025-01-20 13:54:41 +01:00
Minecon724
1ac32a093f
Use boring syntax for permissions
Some checks failed
/ build (push) Has been cancelled
2025-01-20 13:54:19 +01:00
Minecon724
15c97ee256
Ask to keep metrics
All checks were successful
/ build (push) Successful in 52s
2025-01-20 12:50:54 +01:00
Minecon724
7ed9a702ca
Replace nonsensical text
All checks were successful
/ build (push) Successful in 47s
2025-01-20 12:49:37 +01:00
Minecon724
bcd6827c29
Fix workflow and make it on Alpine
All checks were successful
/ build (push) Successful in 44s
2025-01-20 12:12:42 +01:00
Minecon724
2890f00acd
Improve build guide 2025-01-20 12:12:12 +01:00
Minecon724
f43b17078e
[maven-release-plugin] prepare for next development iteration
Some checks failed
/ build (push) Failing after 39s
2025-01-20 11:21:57 +01:00
Minecon724
174d0ae10c
[maven-release-plugin] prepare release tweaks-0.1.12
Some checks failed
/ build (push) Failing after 40s
2025-01-20 11:21:56 +01:00
Minecon724
35ddd0563c
Remove unused deps
Some checks failed
/ build (push) Has been cancelled
2025-01-20 11:21:27 +01:00
Minecon724
832a5edccc
Add version notice 2025-01-20 11:16:20 +01:00
Minecon724
6e0d7e5a40
Fix sleep 2025-01-20 11:16:01 +01:00
Minecon724
b9b0484d24
Add building guide 2025-01-20 11:02:18 +01:00
Minecon724
f891fbbb90
Make 1.21.4 the native version 2025-01-20 10:36:50 +01:00
Minecon724
0c044febd5
Clarify updater in README
All checks were successful
/ build (push) Successful in 1m6s
2025-01-20 08:10:59 +01:00
Minecon724
e5d2938845
Refactor updater
Some checks failed
/ build (push) Has been cancelled
2025-01-20 08:10:38 +01:00
Minecon724
2430416915
Misc
All checks were successful
/ build (push) Successful in 1m10s
2025-01-18 18:36:24 +01:00
Minecon724
19ab653181
Fix config mistake
All checks were successful
/ build (push) Successful in 59s
2025-01-05 10:29:48 +01:00
Minecon724
e0d7e636a6
Unupdate to 1.21.1 2025-01-03 14:20:14 +01:00
Minecon724
9bcdcc17ba
Elaborate on warning 2025-01-03 14:12:24 +01:00
Minecon724
a48fe97b4d
Elaborate on slow module
Not even my fault
2025-01-03 13:54:48 +01:00
Minecon724
648535fd22
Make abstract
All checks were successful
/ build (push) Successful in 1m1s
Before it's too late
2025-01-03 13:14:21 +01:00
Minecon724
e59b95e2ef
Auto swing mode
All checks were successful
/ build (push) Successful in 59s
2025-01-02 20:29:59 +01:00
Minecon724
724a36c803
Add heal to README 2025-01-02 20:29:15 +01:00
Minecon724
6e45491508
Don't touch vertical knockback 2025-01-02 20:23:18 +01:00
Minecon724
afed2a8f78
Add sleep heal option 2025-01-02 20:21:12 +01:00
Minecon724
4bf6ec5ae7
Always use 1.21.1 API 2025-01-02 19:44:24 +01:00
Minecon724
fa96487ef6
Add swing module 2025-01-02 18:56:42 +01:00
Minecon724
81fa6440a0
Improve hardcore RNG
All checks were successful
/ build (push) Successful in 57s
2025-01-02 13:19:23 +01:00
Minecon724
6ad6550f5d
Fix logging in updater 2025-01-02 12:54:08 +01:00
Minecon724
0d87ca8f76
Add to README 2025-01-01 16:32:28 +01:00
Minecon724
32bacc96e8
[maven-release-plugin] prepare for next development iteration
All checks were successful
/ build (push) Successful in 1m2s
2025-01-01 16:22:44 +01:00
Minecon724
ef85f8f4b3
[maven-release-plugin] prepare release tweaks-0.1.11
Some checks failed
/ build (push) Failing after 21s
2025-01-01 16:22:42 +01:00
Minecon724
c5743dbb64
Fix missing library on Paper
All checks were successful
/ build (push) Successful in 1m0s
2025-01-01 16:20:28 +01:00
Minecon724
ec59b3d293
Create a directory for docs
All checks were successful
/ build (push) Successful in 1m6s
since it's getting crowded
2025-01-01 15:37:43 +01:00
Minecon724
c8d394c31e
Add killswitch feature 2025-01-01 15:37:05 +01:00
Minecon724
d03f8e35fb
Move cache where it should be 2025-01-01 14:52:36 +01:00
Minecon724
a157943187
JSON in motd
All checks were successful
/ build (push) Successful in 54s
2025-01-01 12:10:42 +01:00
Minecon724
38a0cc331d
Clarify some
All checks were successful
/ build (push) Successful in 54s
2025-01-01 11:28:52 +01:00
Minecon724
83da4ae872
[maven-release-plugin] prepare for next development iteration
All checks were successful
/ build (push) Successful in 59s
2024-12-31 21:36:27 +01:00
Minecon724
ba097e7f19
[maven-release-plugin] prepare release tweaks-0.1.10
Some checks failed
/ build (push) Failing after 20s
2024-12-31 21:36:25 +01:00
Minecon724
c0b85870e0
Rename to gateway, add crafting, some other changes
Some checks failed
/ build (push) Has been cancelled
2024-12-31 21:35:44 +01:00
Minecon724
225adf0354
Remove debug printlns
All checks were successful
/ build (push) Successful in 1m2s
2024-12-31 18:39:34 +01:00
Minecon724
812b16e4be
Click to copy repeater
Also added translation for copying
2024-12-31 18:15:09 +01:00
Minecon724
1acfcd273d
Forgot to enable knockback module 2024-12-31 17:07:51 +01:00
Minecon724
c92d4429da
Also move updater cache
From "cache/updater" to "storage/cache/updater"
2024-12-31 17:04:39 +01:00
Minecon724
6153da89a1
Fix wrong tip if wrong config version 2024-12-31 17:01:49 +01:00
Minecon724
3a6d1366ab
Bump config version because we're getting tons of changes 2024-12-31 16:38:33 +01:00
Minecon724
9e4125dd4e
Move chat storage
From "rooms" to "storage/rooms"
2024-12-31 16:37:32 +01:00
Minecon724
69cb2ef9af
Move auth storage
From "auth storage" to "storage/auth"
2024-12-31 16:29:39 +01:00
Minecon724
1cc787230c
Timer when pomodoro ends
All checks were successful
/ build (push) Successful in 58s
2024-12-31 15:35:36 +01:00
Minecon724
e409d3e4df
Knockback module
All checks were successful
/ build (push) Successful in 56s
2024-12-31 14:28:56 +01:00
Minecon724
ad36edd5cd
Use the new class here 2024-12-31 13:36:03 +01:00
Minecon724
d922221589
Refactoring and clean ups 2024-12-31 13:34:17 +01:00
Minecon724
916f44da47
redstone WIP 2
All checks were successful
/ build (push) Successful in 1m1s
2024-12-30 21:07:47 +01:00
Minecon724
dde8700248
redstone WIP
All checks were successful
/ build (push) Successful in 1m1s
2024-12-29 18:40:56 +01:00
Minecon724
3318863cf8
[maven-release-plugin] rollback the release of tweaks-0.1.10
All checks were successful
/ build (push) Successful in 54s
2024-12-28 19:57:16 +01:00
Minecon724
bf5f5515ad
[maven-release-plugin] prepare for next development iteration
Some checks are pending
/ build (push) Waiting to run
2024-12-28 19:57:09 +01:00
Minecon724
8f00220ce3
[maven-release-plugin] prepare release tweaks-0.1.10
Some checks failed
/ build (push) Failing after 20s
2024-12-28 19:57:07 +01:00
6c7e3fb77d Merge pull request 'Better build for other versions' (#7) from universal into master
Some checks failed
/ build (push) Has been cancelled
Reviewed-on: #7
2024-12-28 19:56:43 +01:00
Minecon724
e32efe572a
Better build for other versions
All checks were successful
/ build (push) Successful in 53s
2024-12-28 19:54:27 +01:00
c4df9764d4 Merge pull request 'universal' (#6) from universal into master
All checks were successful
/ build (push) Successful in 50s
Reviewed-on: #6
2024-12-28 18:53:10 +01:00
Minecon724
d70d65569f
[maven-release-plugin] prepare for next development iteration
Some checks failed
/ build (push) Has been cancelled
2024-12-28 18:52:02 +01:00
Minecon724
6eb8769998
[maven-release-plugin] prepare release tweaks-0.1.9
Some checks failed
/ build (push) Failing after 23s
2024-12-28 18:52:00 +01:00
Minecon724
c0a94eb7da
Fix warnings that are because I copied the line
Some checks failed
/ build (push) Has been cancelled
2024-12-28 18:51:42 +01:00
Minecon724
2320140073
Build for other versions
All checks were successful
/ build (push) Successful in 51s
Now supports all 1.21s
2024-12-28 18:49:58 +01:00
Minecon724
22171d7053
Reword README
All checks were successful
/ deploy (push) Successful in 1m18s
2024-12-13 16:53:47 +01:00
Minecon724
c22e95af3e
Add a polite notice if you don't have ProtocolLib
All checks were successful
/ deploy (push) Successful in 1m19s
2024-12-13 16:51:20 +01:00
Minecon724
8af95e3ac1
1.21.1 is not *required* 2024-12-13 16:50:34 +01:00
Minecon724
d3af20d421
[maven-release-plugin] prepare for next development iteration
All checks were successful
/ deploy (push) Successful in 1m20s
2024-12-13 16:12:33 +01:00
Minecon724
44abcab268
[maven-release-plugin] prepare release tweaks-0.1.8
All checks were successful
/ deploy (push) Successful in 1m19s
2024-12-13 16:12:26 +01:00
Minecon724
a3e3cc4b57
Proximity chat
All checks were successful
/ deploy (push) Successful in 1m20s
Also:
Removed login "Chat room:" notice
Removed "Joined chat room:" on command
The room in "has left the chat room" is now correct
Removed prefix from global
Fixed double kill message
2024-12-13 16:00:19 +01:00
Minecon724
0d54e71294
Remove unused module
All checks were successful
/ deploy (push) Successful in 1m51s
2024-12-13 13:46:47 +01:00
Minecon724
6a8f982588
[maven-release-plugin] prepare for next development iteration
All checks were successful
/ deploy (push) Successful in 1m19s
2024-12-12 17:12:52 +01:00
Minecon724
22786c2ac8
[maven-release-plugin] prepare release tweaks-0.1.7
All checks were successful
/ deploy (push) Successful in 1m26s
2024-12-12 17:12:50 +01:00
Minecon724
f534cd4c53
Alert
All checks were successful
/ deploy (push) Successful in 1m18s
2024-12-12 17:09:48 +01:00
Minecon724
d4f10496e9
Remove commented code 2024-12-11 15:51:54 +01:00
Minecon724
64d28128f8
Full 2024-12-11 15:51:43 +01:00
Minecon724
c9171f568a
[maven-release-plugin] prepare for next development iteration
All checks were successful
/ deploy (push) Successful in 1m22s
2024-12-08 13:51:49 +01:00
110 changed files with 4566 additions and 1568 deletions

View file

@ -1,25 +1,31 @@
on: [push]
jobs:
deploy:
build:
runs-on: docker
container: debian:sid
container: eclipse-temurin:21-alpine
steps:
- name: Prepare for installation
run: apt update
- name: Install JDK and other deps
run: 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: BuildTools for NMS
# run: mkdir /tmp/buildtools && cd /tmp/buildtools && curl -O https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar && java -jar BuildTools.jar --rev 1.21.1 --remapped && cd
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Download NMS
run: curl -O https://git.m724.eu/Minecon724/temporary/raw/branch/master/XRIH.tar.zst && tar -xaf XRIH.tar.zst -C ~ && rm XRIH.tar.zst
run: ./tools/download_nms.sh ~
- 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: ./mvnw package -Dproject.minecraft.version=1.21.3 -Dproject.craftbukkit.version=v1_21_R2
- name: Build for 1.21.1
run: ./mvnw package -Dproject.minecraft.version=1.21.1 -Dproject.craftbukkit.version=v1_21_R1
- name: Clone repository
run: git clone https://git.m724.eu/Minecon724/tweaks724.git .
- name: Build
run: mvn clean package
- name: Upload artifacts
uses: https://github.com/actions/upload-artifact@v3
with:
path: target
path: target/tweaks-*.jar

19
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View file

@ -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

View file

@ -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
```

101
README.md
View file

@ -12,31 +12,36 @@ Please report all suspicious behavior. You can do so on any of those:
Stuff no<sub><sup>t many</sup></sub> other plugins do.
Dependencies:
- **1.21.1** this is mandatory as the plugin uses NMS for some stuff\
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)
- [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) (optional, but you lose a lot)
- **1.21.1 and newer**
- [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/)
- To use modules marked <sup><sub>N</sub></sup>, you must use a JAR [made for the exact server version.](/Minecon724/tweaks724/src/branch/master/docs/BUILDING.md)
# Features
Those with <sup>P</sup> need ProtocolLib
<sup><sub>N</sub></sup> - requires a specific version \
<sup><sub>P</sub></sup> - requires ProtocolLib
### Expand world border
### Expand world border <sup><sub>N</sub></sup>
Expands the world border to 30,000,000 (from 29,999,984)
### Hide world border <sup><sup><sub>P</sub></sup></sup>
### Hide world border <sup><sub>P</sub></sup>
Hides the world border. It's still there, just invisible.
### Server brand <sup><sup><sub>P</sub></sup></sup>
### Server brand <sup><sub>N P</sub></sup>
Modify the F3 brand, optionally include player's ping and server performance
### Doors
Open two doors with one click. Knock on doors.
### MOTD <sup><sup><sub>P</sub></sup></sup>
### MOTD <sup><sub>N P</sub></sup>
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 <sup><sub>N</sub></sup>
- 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`)
### Compass
Holding a compass shows a bar with 4 directions and stuff like beds, lodestones, death pos (TODO) etc.
@ -44,48 +49,68 @@ Holding a compass shows a bar with 4 directions and stuff like beds, lodestones,
### Pomodoro
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 \
<sub>*Those on SpigotMC and that release updates there</sub>
### Hardcore <sup><sup><sub>P</sub></sup></sup>
`/updates` - shows available updates (`tweaks724.updates`)
### Hardcore <sup><sub>N P</sub></sup>
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
- 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
- Heal \
Sleeping heals
### Authentication
Players are given a unique subdomain like "\<key>.example.com" and they must use it to join \
It can be enabled that new players can't join the server without a key
# Commands
### /chat
Changes chatroom
`/tauth` (`tweaks724.tauth`)
### /chatmanage
`tweaks724.chatmanage` \
Manages chatroom, like create, delete etc.
### Full join
Players with `tweaks724.bypass-full` can join even when the server is full
### /ping
Displays your ping. \
### Emergency alerts <sup><sub>P</sub></sup>
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 <sup><sub>P</sub></sup> \
**Ping is calculated by the plugin**. \
That allows for more precision (decimal places) and to get the ping immediately after a player join
### /pomodoro
`tweaks724.pomodoro` \
Manage your pomodoro
- `/pom start` to start
- `/pom stop` to stop
- `/pom` to skip stage
### /tauth
`tweaks724.tauth` \
Manages authentication keys.
- `/tauth <key>` to see who the key is bound to
- `/tauth new` to create a key
- `/tauth delete <key>` to delete a key

32
docs/BUILDING.md Normal file
View file

@ -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/`

32
docs/KILLSWITCH.md Normal file
View file

@ -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/<base64 encoded 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

3
docs/README.md Normal file
View file

@ -0,0 +1,3 @@
Here's the documentation.
Click above on a file to read more about a topic.

48
docs/RETSTONE.md Normal file
View file

@ -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

44
docs/retstone.py Normal file
View file

@ -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)

259
mvnw vendored Executable file
View file

@ -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-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -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 "$@"

149
mvnw.cmd vendored Normal file
View file

@ -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-<version>,maven-mvnd-<version>-<platform>}/<hash>
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"

77
pom.xml
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
@ -10,16 +10,20 @@
<groupId>eu.m724</groupId>
<artifactId>tweaks</artifactId>
<version>0.1.6</version>
<version>0.1.15-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.spigot.version>1.21.1-R0.1-SNAPSHOT</project.spigot.version>
<project.craftbukkit.version>v1_21_R3</project.craftbukkit.version>
<project.minecraft.version>1.21.4</project.minecraft.version>
<project.spigot.version>${project.minecraft.version}-R0.1-SNAPSHOT</project.spigot.version>
</properties>
<build>
<finalName>${project.artifactId}-${project.version}+${project.minecraft.version}</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
@ -29,31 +33,36 @@
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<minimizeJar>true</minimizeJar>
<artifactSet>
<includes>
<include>eu.m724:tweaks</include>
</includes>
</artifactSet>
<filters>
<filter>
<artifact>*</artifact>
<excludes>
<exclude>META-INF/**</exclude>
</excludes>
</filter>
</filters>
</configuration>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>custom-nms-version</id>
<phase>generate-sources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<replace token="v1_21_R3" value="${project.craftbukkit.version}" dir="src/main">
<include name="**/*.java" />
</replace>
</target>
</configuration>
</execution>
<execution>
<id>cleanup-custom-nms-version</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
<goal>run</goal>
</goals>
<configuration>
<target>
<replace token="${project.craftbukkit.version}" value="v1_21_R3" dir="src/main">
<include name="**/*.java" />
</replace>
</target>
</configuration>
</execution>
</executions>
</plugin>
@ -71,10 +80,8 @@
<id>remap-obf</id>
<configuration>
<srgIn>org.spigotmc:minecraft-server:${project.spigot.version}:txt:maps-mojang</srgIn>
<reverse>true</reverse>
<remappedDependencies>org.spigotmc:spigot:${project.spigot.version}:jar:remapped-mojang</remappedDependencies>
<remappedArtifactAttached>true</remappedArtifactAttached>
<remappedClassifierName>remapped-obf-temp-dont-use</remappedClassifierName>
<reverse>true</reverse>
</configuration>
</execution>
<execution>
@ -84,7 +91,6 @@
</goals>
<id>remap-spigot</id>
<configuration>
<inputFile>${project.build.directory}/${project.artifactId}-${project.version}-remapped-obf-temp-dont-use.jar</inputFile>
<srgIn>org.spigotmc:minecraft-server:${project.spigot.version}:csrg:maps-spigot</srgIn>
<remappedDependencies>org.spigotmc:spigot:${project.spigot.version}:jar:remapped-obf</remappedDependencies>
</configuration>
@ -112,11 +118,6 @@
<id>dmulloy2-repo</id>
<url>https://repo.dmulloy2.net/repository/public/</url>
</repository>
<!-- this repo has no ipv6 so keep that in mind -->
<repository>
<id>maxhenkel-repo</id>
<url>https://maven.maxhenkel.de/repository/public</url>
</repository>
<repository>
<id>m724-repo</id>
<url>https://git.m724.eu/api/packages/Minecon724/maven</url>
@ -127,7 +128,7 @@
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>${project.spigot.version}</version>
<version>1.21.1-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
@ -143,16 +144,10 @@
<version>5.3.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>de.maxhenkel.voicechat</groupId>
<artifactId>voicechat-api</artifactId>
<version>2.5.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>eu.m724</groupId>
<artifactId>mstats-spigot</artifactId>
<version>0.1.0</version>
<version>0.1.2</version>
<scope>provided</scope>
</dependency>
<dependency>
@ -173,6 +168,6 @@
<scm>
<developerConnection>scm:git:git@git.m724.eu:Minecon724/tweaks724.git</developerConnection>
<tag>tweaks-0.1.6</tag>
<tag>HEAD</tag>
</scm>
</project>

View file

@ -0,0 +1,65 @@
/*
* 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;
public class 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.getLevel().intValue() > level.intValue()) return;
var caller = Thread.currentThread().getStackTrace()[3].getClassName();
if (caller.equals(TweaksModule.class.getName())) {
var pcaller = Thread.currentThread().getStackTrace()[4].getClassName();
if (pcaller.endsWith("Module"))
caller = pcaller;
}
if (caller.startsWith("eu.m724.tweaks."))
caller = caller.substring(15);
message = "[" + caller + "] " + message.formatted(format);
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);
}
}

View file

@ -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();
}
}

View file

@ -1,131 +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,
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");
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,
compassEnabled, compassWidth, compassPrecision,
pomodoroEnabled, pomodoroForce,
updaterEnabled,
hardcoreEnabled, hardcoreChance,
sleepEnabled, sleepInstant,
authEnabled, authForce, authHostname
);
return TweaksConfig.config;
}
}

View file

@ -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,105 +7,183 @@
package eu.m724.tweaks;
import eu.m724.mstats.MStatsPlugin;
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.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;
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);
if (getServer().getPluginManager().getPlugin("ProtocolLib") == null) {
getLogger().severe("ProtocolLib is required for this plugin.");
getLogger().severe("https://www.spigotmc.org/resources/protocollib.1997/");
getServer().getPluginManager().disablePlugin(this);
return;
}
if (config.doorEnabled()) {
new DoorManager().init(this);
TweaksConfig config;
try {
config = TweaksConfig.load(this);
} catch (Exception e) {
throw new RuntimeException("Exception loading config", e);
}
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);
}
if (config.metrics())
mStats(1);
TweaksModule.init(AlertModule.class);
getLogger().info("Took %.3f milliseconds".formatted((System.nanoTime() - start) / 1000000.0));
TweaksModule.init(FullModule.class);
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);
}
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;
}
}

View file

@ -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));
}
}

View file

@ -1,198 +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 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.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) {
manager.setPlayerChatRoom(newRoom, player);
component = new ComponentBuilder(Language.getComponent("chatJoined", ChatColor.GOLD))
.append(newRoom.id).color(newRoom.color)
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(newRoom.getInfoComponent())))
.append(Language.getComponent("chatPlayers", ChatColor.GOLD, newRoom.players.size()))
.build();
}
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;
}
}

View file

@ -1,124 +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.TweaksConfig;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ComponentBuilder;
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.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
public class ChatListener implements Listener {
private final ChatManager chatManager;
private final boolean localEvents;
public ChatListener(ChatManager chatManager) {
this.chatManager = chatManager;
this.localEvents = TweaksConfig.getConfig().chatLocalEvents();
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
ChatRoom chatRoom = chatManager.getPlayerChatRoom(player);
BaseComponent[] component = new ComponentBuilder("Chat room: ").color(ChatColor.GOLD)
.append(ChatFormatUtils.formatChatRoom(chatRoom))
.create();
player.spigot().sendMessage(component);
if (localEvents) {
chatRoom.broadcast(
new ComponentBuilder(ChatFormatUtils.chatRoomPrefixShort(chatRoom))
.append(new TranslatableComponent("multiplayer.player.joined", ChatFormatUtils.formatPlayer(player))).color(ChatColor.GREEN)
.create()
);
// remove Minecraft join message
event.setJoinMessage(null);
}
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
ChatRoom chatRoom = chatManager.removePlayer(player);
if (localEvents) {
chatRoom.broadcast(
new ComponentBuilder(ChatFormatUtils.chatRoomPrefixShort(chatRoom))
.append(new TranslatableComponent("multiplayer.player.left", ChatFormatUtils.formatPlayer(player))).color(ChatColor.RED)
.create()
);
// remove Minecraft quit message
event.setQuitMessage(null);
}
}
@EventHandler
public void onPlayerDeath(PlayerDeathEvent event) {
if (localEvents) {
Player player = event.getEntity();
ChatRoom chatRoom = chatManager.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()));
// TODO make players ChatFormatUtils
chatRoom.broadcast(
new ComponentBuilder()
.append(ChatFormatUtils.chatRoomPrefixShort(chatRoom))
.append(deathMessage)
.create()
);
// broadcast to killer if available
if (player.getLastDamageCause().getDamageSource().getCausingEntity() instanceof Player killer) {
ChatRoom chatRoom2 = chatManager.getPlayerChatRoom(killer);
chatRoom2.broadcast(
new ComponentBuilder()
.append(ChatFormatUtils.chatRoomPrefixShort(chatRoom2))
.append(deathMessage)
.create()
);
}
// remove Minecraft death message
event.setDeathMessage(null);
}
}
@EventHandler
public void onAsyncPlayerChat(AsyncPlayerChatEvent event) {
Player player = event.getPlayer();
ChatRoom chatRoom = chatManager.getPlayerChatRoom(player);
String message = event.getMessage();
ComponentBuilder builder = new ComponentBuilder();
builder.append(ChatFormatUtils.chatRoomPrefixShort(chatRoom));
builder.append(ChatFormatUtils.formatPlayer(player)).append(": ");
builder.append(message).color(chatRoom.color);
chatRoom.broadcast(builder.create());
// remove the original message
event.setCancelled(true);
}
}

View file

@ -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);
}
}

View file

@ -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<String> missing = new ArrayList<>();
ConfigLoader(FileConfiguration configuration) {
this.configuration = configuration;
}
List<String> 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<String, Object> 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<String, Object> 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);
}
}

View file

@ -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<String> missing;
MissingFieldsException(List<String> missing) {
this.missing = missing;
}
@Override
public String getMessage() {
return String.join(", ", missing);
}
}

View file

@ -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<String, Object> 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())
);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
});
}
}

View file

@ -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.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 {
protected abstract void onInit();
void init() {
var name = getClass().getSimpleName();
DebugLogger.finer("Initializing " + name);
long start = System.nanoTime();
this.onInit();
long end = System.nanoTime();
DebugLogger.fine("Initialized %s in %d µs", name, (end - start) / 1000);
}
protected TweaksPlugin getPlugin() {
return TweaksPlugin.getInstance();
}
protected TweaksConfig getConfig() {
return TweaksConfig.getConfig();
}
protected void registerEvents(Listener listener) {
DebugLogger.finer("Registered listener: " + listener.getClass().getName());
getPlugin().getServer().getPluginManager().registerEvents(listener, getPlugin());
}
protected void onPacketSend(PacketType packetType, Consumer<PacketEvent> consumer) {
ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
@Override
public void onPacketSending(PacketEvent event) {
consumer.accept(event);
}
});
DebugLogger.finer("Registered packet send: " + packetType.name());
}
protected void onPacketReceive(PacketType packetType, Consumer<PacketEvent> consumer) {
ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
@Override
public void onPacketReceiving(PacketEvent event) {
consumer.accept(event);
}
});
DebugLogger.finer("Registered packet receive: " + packetType.name());
}
protected void registerCommand(String command, CommandExecutor executor) {
getPlugin().getCommand(command).setExecutor(executor);
DebugLogger.finer("Registered command: " + command);
}
public static <T extends TweaksModule> T init(Class<T> clazz) {
T module;
try {
module = clazz.getDeclaredConstructor().newInstance();
} catch (InstantiationException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
module.init();
return module;
}
}

View file

@ -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.alert;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
public class Alert {
public final String[] content;
public final long issued;
public final Inventory inventory;
public Alert(String... content) {
this.content = content;
this.issued = System.currentTimeMillis();
this.inventory = inventory(content);
}
public boolean isOpen(Player player) {
var item = player.getOpenInventory().getTopInventory().getItem(0);
if (item != null) {
var meta = item.getItemMeta();
return meta != null && meta.getCustomModelData() == 4198203;
}
return false;
}
private Inventory inventory(String... pages) {
var inv = Bukkit.createInventory(null, InventoryType.LECTERN);
var book = new ItemStack(Material.WRITTEN_BOOK);
var bookMeta = (BookMeta) book.getItemMeta();
bookMeta.setTitle("ALERT");
//bookMeta.setAuthor("a");
bookMeta.setCustomModelData(4198203);
for (String page : pages) {
bookMeta.addPage(page);
}
book.setItemMeta(bookMeta);
inv.setItem(0, book);
return inv;
}
}

View file

@ -0,0 +1,101 @@
/*
* 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 org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class AlertCommand implements CommandExecutor {
private List<String> pending;
private long when;
private final AlertModule manager;
public AlertCommand(AlertModule manager) {
this.manager = manager;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (args.length == 0) {
sender.sendMessage("/emergencyalert manages emergency alerts");
sender.sendMessage("/emergencyalert cancel - cancels a pending or an active alert");
sender.sendMessage("/emergencyalert new <content> - creates a new alert with given text");
sender.sendMessage("/emergencyalert new - creates a new alert with contents of held book");
} else {
if (args[0].equals("CONFIRM")) {
if (pending != null) {
if (System.currentTimeMillis() - when > 15 * 1000) {
sender.sendMessage("There was a pending alert, but you took too long to confirm it, please create it again");
pending = null;
return true;
}
sender.sendMessage("Alert broadcast");
manager.start(pending.toArray(String[]::new));
pending = null;
} else {
sender.sendMessage("There is no pending alert to confirm");
}
} else if (args[0].equalsIgnoreCase("confirm")) {
sender.sendMessage("CONFIRM must be in all caps");
} else if (args[0].equalsIgnoreCase("cancel")) {
if (AlertModule.current != null) {
manager.stop();
sender.sendMessage("Cancelled alert");
} else if (pending != null) {
sender.sendMessage("Cancelled pending alert");
pending = null;
} else {
sender.sendMessage("There is no alert to cancel");
}
} else if (args[0].equalsIgnoreCase("new")) {
if (pending != null) {
sender.sendMessage("Another alert already waiting for confirmation. /emergencyalert cancel?");
return true;
}
if (args.length == 1) {
if (sender instanceof Player player) {
var is = player.getInventory().getItemInMainHand();
if (is.getType() == Material.WRITTEN_BOOK) {
player.swingMainHand();
pending = ((BookMeta) is.getItemMeta()).getPages();
sender.sendMessage("Used book content as alert text");
} else {
sender.sendMessage("You must hold a written book in your hand or pass a command argument to use as alert text");
}
} else {
sender.sendMessage("You must pass some text to alert, or be a player to use a book");
}
} else {
pending = List.of(String.join(" " , args));
sender.sendMessage("Used command argument as alert text");
}
if (pending != null) {
when = System.currentTimeMillis();
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");
}
} else {
sender.sendMessage("Unknown argument \"%s\". Run this command without any arguments to see help.".formatted(args[0]));
}
}
return true;
}
}

View file

@ -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<Player, Integer> 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;
}
}

View file

@ -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.alert;
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.Bukkit;
import org.bukkit.Sound;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.function.Consumer;
public class AlertRunnable extends BukkitRunnable {
private final Alert alert;
private final Consumer<Void> onEnd;
public AlertRunnable(Alert alert, Consumer<Void> onEnd) {
this.alert = alert;
this.onEnd = onEnd;
}
@Override
public void run() {
if (alert != null) {
var ago = (System.currentTimeMillis() - alert.issued) / 1000;
Bukkit.getOnlinePlayers().forEach(p -> {
if (ago < 15) {
p.playSound(p, Sound.BLOCK_PORTAL_TRIGGER, 1f, 2f);
//p.playSound(p, Sound.ENTITY_GHAST_HURT, 1f, 2f);
if (ago % 2 == 0) {
p.playSound(p, Sound.BLOCK_ANVIL_PLACE, 1f, 0.5f);
}
}
if (ago < 10) {
if (!alert.isOpen(p)) {
p.openInventory(alert.inventory);
}
}
p.spigot().sendMessage(
ChatMessageType.ACTION_BAR,
new ComponentBuilder("An important event is ongoing").color(ChatColor.YELLOW)
.append(" %d:%02d".formatted(ago / 60, ago % 60)).color(ChatColor.GRAY)
.build()
);
});
if (ago > 300)
onEnd.accept(null);
}
}
}

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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));
}
}

View file

@ -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<length; i++) {
key.append(chars[random.nextInt(chars.length)]);
for (int i=0; i<KEY_LENGTH; i++) {
builder.append(KEY_CHARS[RANDOM.nextInt(KEY_CHARS.length)]);
}
return key.toString();
return builder.toString();
}
static class InvalidKeyException extends RuntimeException {}
static class AlreadyClaimedException extends Exception {}
static class InvalidKeyException extends RuntimeException { }
static class AlreadyClaimedException extends Exception { }
}

View file

@ -0,0 +1,247 @@
/*
* 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.chat;
import eu.m724.tweaks.Language;
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.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 ChatModule manager;
public ChatCommands(ChatModule 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 {
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 <password>").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;
}
}

View file

@ -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;

View file

@ -0,0 +1,164 @@
/*
* 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.chat;
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;
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_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;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
public class ChatListener implements Listener {
private final ChatModule chatModule;
private final String defaultRoom = TweaksConfig.getConfig().chatDefaultName();
private final boolean localEvents = TweaksConfig.getConfig().chatLocalEvents();
private final int radius;
public ChatListener(ChatModule chatModule) {
this.chatModule = chatModule;
if (TweaksConfig.getConfig().chatRadius() < 0) {
radius = 0;
} else {
radius = (int) Math.pow(TweaksConfig.getConfig().chatRadius(), 2);
}
}
private BaseComponent chatPrefix(ChatRoom room) {
if (room.id.equals(defaultRoom))
return new TextComponent();
return ChatFormatUtils.chatRoomPrefixShort(room);
}
private boolean proximityFor(ChatRoom room) {
return radius > 0 && room.id.equals(defaultRoom);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
ChatRoom chatRoom = chatModule.getPlayerChatRoom(player);
if (localEvents) {
var cb = new ComponentBuilder()
.append(chatPrefix(chatRoom))
.append(new TranslatableComponent("multiplayer.player.joined", ChatFormatUtils.formatPlayer(player)));
if (proximityFor(chatRoom)) {
chatRoom.broadcastNearCond(
player.getLocation(), radius,
cb.color(ChatColor.GREEN).create(),
cb.color(ChatColor.of("#77AA77")).create()
);
} else {
chatRoom.broadcast(cb.color(ChatColor.GREEN).create());
}
// remove Minecraft join message
event.setJoinMessage(null);
}
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
ChatRoom chatRoom = chatModule.removePlayer(player);
if (localEvents) {
var cb = new ComponentBuilder()
.append(chatPrefix(chatRoom))
.append(new TranslatableComponent("multiplayer.player.left", ChatFormatUtils.formatPlayer(player)));
if (proximityFor(chatRoom)) {
chatRoom.broadcastNearCond(
player.getLocation(), radius,
cb.color(ChatColor.RED).create(),
cb.color(ChatColor.of("#AA7777")).create()
);
} else {
chatRoom.broadcast(cb.color(ChatColor.RED).create());
}
// remove Minecraft quit message
event.setQuitMessage(null);
}
}
@EventHandler
public void onPlayerDeath(PlayerDeathEvent event) {
if (localEvents) {
Player player = event.getEntity();
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()));
// TODO make players ChatFormatUtils
var component = new ComponentBuilder()
.append(chatPrefix(chatRoom))
.append(deathMessage)
.color(ChatColor.YELLOW)
.create();
if (proximityFor(chatRoom)) {
chatRoom.broadcastNear(player.getLocation(), radius, component);
} else {
chatRoom.broadcast(component);
}
// broadcast to killer if available
if (player.getLastDamageCause().getDamageSource().getCausingEntity() instanceof Player killer) {
ChatRoom chatRoom2 = chatModule.getPlayerChatRoom(killer);
if (chatRoom != chatRoom2) {
if (proximityFor(chatRoom)) {
chatRoom2.broadcastNear(killer.getLocation(), radius, component);
} else {
chatRoom2.broadcast(component);
}
}
}
// remove Minecraft death message
event.setDeathMessage(null);
}
}
@EventHandler
public void onAsyncPlayerChat(AsyncPlayerChatEvent event) {
Player player = event.getPlayer();
ChatRoom chatRoom = chatModule.getPlayerChatRoom(player);
String message = event.getMessage();
var component = new ComponentBuilder()
.append(chatPrefix(chatRoom))
.append(ChatFormatUtils.formatPlayer(player)).append(": ")
.append(message).color(chatRoom.color)
.create();
if (proximityFor(chatRoom)) {
chatRoom.broadcastNear(player.getLocation(), radius, component);
} else {
chatRoom.broadcast(component);
}
// remove the original message
event.setCancelled(true);
}
}

View file

@ -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<Player, ChatRoom> playerMap = new HashMap<>();
private final Map<String, ChatRoom> 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);
}
@ -97,7 +97,7 @@ public class ChatManager {
oldRoom.broadcast(
new ComponentBuilder()
.append(ChatFormatUtils.chatRoomPrefixShort(chatRoom))
.append(ChatFormatUtils.chatRoomPrefixShort(oldRoom))
.append(ChatFormatUtils.formatPlayer(player))
.append(" has left the chat room").color(ChatColor.RED)
.create()
@ -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));
}

View file

@ -1,14 +1,15 @@
/*
* 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;
import net.md_5.bungee.api.chat.ComponentBuilder;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
@ -69,4 +70,20 @@ public class ChatRoom {
players.forEach(p -> p.spigot().sendMessage(component));
}
public void broadcastNear(Location source, int diameter, BaseComponent[] component) {
players.forEach(p -> {
double distance = p.getLocation().distanceSquared(source);
if (distance < diameter) p.spigot().sendMessage(component);
});
}
public void broadcastNearCond(Location source, int diameter, BaseComponent[] near, BaseComponent[] far) {
players.forEach(p -> {
double distance = p.getLocation().distanceSquared(source);
if (distance < diameter) p.spigot().sendMessage(near);
else p.spigot().sendMessage(far);
});
}
}

View file

@ -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);
}
}

View file

@ -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<Integer, String> 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) {

View file

@ -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;

View file

@ -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();

View file

@ -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);
}
}
}

View file

@ -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<Player> 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<Player> 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);
}
}

View file

@ -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<Player, Long> lastFullReminder = new HashMap<>();
// BAD
private final Map<Player, Map<Material, Long>> lastUse = new HashMap<>();
// BAD
private final Map<Player, Map<Material, Long>> 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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -1,19 +1,25 @@
/*
* 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.full.exempt")) {
if (event.getResult() == PlayerLoginEvent.Result.KICK_FULL && event.getPlayer().hasPermission("tweaks724.bypass-full")) {
event.allow();
}
}

View file

@ -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);
});
}
}

View file

@ -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");
}
}
}

View file

@ -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<InetAddress> requests = new HashSet<>();
boolean submitRequest(InetAddress address) {
return requests.add(address);
}
@Override
public void run() {
requests = new HashSet<>();
}
}

View file

@ -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<EntityType, Vector> 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));
}
}
}

View file

@ -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)));
});
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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());
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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.pomodoro;
package eu.m724.tweaks.module.pomodoro;
public class PlayerPomodoro {
private int pomodori = 0;

View file

@ -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.pomodoro;
package eu.m724.tweaks.module.pomodoro;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;

View file

@ -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.pomodoro;
package eu.m724.tweaks.module.pomodoro;
import eu.m724.tweaks.TweaksConfig;
import eu.m724.tweaks.config.TweaksConfig;
import net.md_5.bungee.api.chat.ComponentBuilder;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;

View file

@ -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.pomodoro;
import eu.m724.tweaks.module.TweaksModule;
public class PomodoroModule extends TweaksModule {
@Override
protected void onInit() {
registerEvents(new PomodoroListener());
new PomodoroRunnable().runTaskTimerAsynchronously(getPlugin(), 0, 20L);
registerCommand("pomodoro", new PomodoroCommands());
}
}

View file

@ -1,13 +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.pomodoro;
package eu.m724.tweaks.module.pomodoro;
import eu.m724.tweaks.Language;
import eu.m724.tweaks.TweaksConfig;
import eu.m724.tweaks.TweaksPlugin;
import eu.m724.tweaks.config.TweaksConfig;
import net.md_5.bungee.api.ChatMessageType;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
@ -16,15 +16,12 @@ 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
}
private final Plugin plugin = TweaksPlugin.getInstance(); // used only to kick
@Override
public void run() {
long now = System.nanoTime();
Bukkit.getOnlinePlayers().forEach(player -> {
PlayerPomodoro pomodoro = Pomodoros.get(player);
if (pomodoro == null) return;
@ -37,7 +34,8 @@ public class PomodoroRunnable extends BukkitRunnable {
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"));
pomodoro.next();
player.kickPlayer(Pomodoros.formatTimer(pomodoro, pomodoro.getRemainingSeconds(now)).toLegacyText());
});
}
}

View file

@ -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.pomodoro;
package eu.m724.tweaks.module.pomodoro;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
@ -30,7 +30,7 @@ public class Pomodoros {
return timers.remove(player.getUniqueId()) != null;
}
static BaseComponent[] formatTimer(PlayerPomodoro pomodoro, long remaining) {
static BaseComponent formatTimer(PlayerPomodoro pomodoro, long remaining) {
ComponentBuilder builder = new ComponentBuilder();
if (pomodoro.isBreak()) {
@ -66,6 +66,6 @@ public class Pomodoros {
builder.append(" o").color(color);
}
return builder.create();
return builder.build();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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<Integer, Location> gatewaysById = HashBiMap.create();
private final Map<Integer, Byte> 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);
}
}

View file

@ -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);
}
}

View file

@ -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<Byte> consumer) {
DebugLogger.finer("Retrieve enqueued " + gatewayId);
runnable.enqueueRetrieve(gatewayId, consumer);
}
}

View file

@ -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<Integer, Byte> updateQueue = new HashMap<>();
private Map<Integer, Consumer<Byte>> 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<Byte> 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)));
}
}

View file

@ -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<Integer, Byte> 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());
}
}

View file

@ -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<Player> 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);
}
}
}

View file

@ -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
}
}

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -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<Material> 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());
}
}
}

View file

@ -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();

View file

@ -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<ResourceVersion> 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<SpigotResource> 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<SpigotResource> 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;
}
}

View file

@ -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<VersionedResource> resources;
private final Set<VersionedResource> availableUpdates = new HashSet<>();
private long lastChecked = -1;
public UpdateChecker(Logger logger, File cacheFile, Set<VersionedResource> 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<VersionedResource> getAvailableUpdates() {
return availableUpdates;
}
}

View file

@ -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<ResourceVersion> loadAll(FileInputStream inputStream) throws IOException {

View file

@ -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<VersionedResource> {
public class VersionScanner extends CompletableFuture<VersionedResource> {
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<VersionedResource> {
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<VersionedResource> {
String body = response.body();
if (body.isBlank()) {
DebugLogger.finer("Body is blank, stopping");
page--;
break;
}
@ -75,15 +79,7 @@ public class VersionFinder extends CompletableFuture<VersionedResource> {
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<VersionedResource> {
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<VersionedResource> {
}
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());
}
}

View file

@ -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;

View file

@ -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
) {
}
) { }

View file

@ -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
) {
}
) { }

View file

@ -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,

View file

@ -0,0 +1,36 @@
/*
* 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.converter.Decoder;
import eu.m724.tweaks.module.wordcoords.converter.Encoder;
import java.util.NoSuchElementException;
public class WordCoordsConverter {
private final Encoder encoder;
private final Decoder decoder;
public WordCoordsConverter(WordList wordList) {
this.encoder = new Encoder(wordList);
this.decoder = new Decoder(wordList);
DebugLogger.fine("Words: %d (%d bits)", wordList.getWordCount(), wordList.getBitsPerWord());
DebugLogger.fine("Bits per word: %d", wordList.getBitsPerWord());
}
public String[] encode(int x, int z) {
return encoder.encode(x, z);
}
public int[] decode(String[] words) throws NoSuchElementException {
return decoder.decode(words);
}
}

View file

@ -0,0 +1,166 @@
/*
* 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.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.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
public class WordCoordsModule extends TweaksModule implements CommandExecutor, Listener {
private WordList wordList;
private WordCoordsConverter converter;
@Override
protected void onInit() {
try {
this.wordList = WordList.fromFile(getPlugin().getDataFolder().toPath().resolve("storage/wordlist.txt"));
} catch (IOException e) {
throw new RuntimeException(e);
}
this.converter = new WordCoordsConverter(wordList);
registerCommand("wordcoords", this);
registerEvents(this);
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
int x = 0, z = 0;
String[] words = new String[0];
boolean encode = false; // means encode pos to words
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 (args.length > 1) {
try {
double dx = Double.parseDouble(args[0]);
double dz = Double.parseDouble(args[args.length > 2 ? 2 : 1]);
if (dx > Integer.MAX_VALUE || dx < Integer.MIN_VALUE || dz > Integer.MAX_VALUE || dz < Integer.MIN_VALUE) {
sender.spigot().sendMessage(Language.getComponent("wordCoordsOutOfRange", ChatColor.RED));
return true;
}
x = (int) dx;
z = (int) dz;
encode = true;
} catch (NumberFormatException ignored) { }
}
if (encode) {
words = converter.encode(x, z);
String encoded = "///" + String.join(".", words);
BaseComponent[] components = new ComponentBuilder()
.append(String.format("%d, %d encodes to ", 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("Click to copy")))
.create();
sender.spigot().sendMessage(components);
} else {
String strArgs = String.join(" ", args);
words = smartDetectWords(strArgs);
if (words.length == 0) {
sender.spigot().sendMessage(Language.getComponent("wordCoordsNoWords", ChatColor.GRAY));
return true;
}
try {
int[] xz = converter.decode(words);
x = xz[0];
z = xz[1];
} catch (NoSuchElementException e) {
sender.spigot().sendMessage(Language.getComponent("wordCoordsInvalidWord", ChatColor.RED, e.getMessage()));
return true;
}
String encoded = "///" + String.join(".", words);
BaseComponent[] components = new ComponentBuilder()
.append(encoded + " decodes to ")
.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("Click to copy")))
.append(" ±8")
.color(ChatColor.GRAY)
.create();
sender.spigot().sendMessage(components);
}
return true;
}
private String[] smartDetectWords(String str) {
List<String> words = new ArrayList<>();
StringBuilder currentWord = new StringBuilder();
for (int i=0; i<str.length(); i++) {
char c = str.charAt(i);
if (Character.isLetter(c)) {
currentWord.append(c);
} else {
if (!currentWord.isEmpty()) {
words.add(currentWord.toString());
currentWord.setLength(0);
}
}
}
if (!currentWord.isEmpty()) {
words.add(currentWord.toString());
}
return words.toArray(String[]::new);
}
@EventHandler
public void onCommand(PlayerCommandPreprocessEvent event) {
if (event.getMessage().startsWith("///")) {
event.setCancelled(true);
event.getPlayer().performCommand("wordcoords " + event.getMessage().substring(3));
}
}
}

View file

@ -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<String> wordList;
private final int bitsPerWord;
public WordList(List<String> 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())
.map(String::toLowerCase)
.distinct()
.toList();
return new WordList(list);
}
}
}

View file

@ -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.converter;
import eu.m724.tweaks.DebugLogger;
import eu.m724.tweaks.module.wordcoords.WordList;
import java.util.NoSuchElementException;
import java.util.Arrays;
public class Decoder {
private final WordList wordList;
private final int bitsPerWord;
public Decoder(WordList wordList) {
this.wordList = wordList;
this.bitsPerWord = wordList.getBitsPerWord();
}
public int[] decode(String[] words) throws NoSuchElementException {
int[] wordIndexes = new int[words.length];
for (int i=0; i<words.length; i++) {
wordIndexes[i] = wordList.getWordIndex(words[i]);
if (wordIndexes[i] == -1)
throw new NoSuchElementException(words[i]);
}
return decode(wordIndexes);
}
public int[] decode(int[] wordIndexes) {
DebugLogger.finer("Decoding word indexes: %s", Arrays.toString(wordIndexes));
int bitsRequired = wordIndexes.length * wordList.getBitsPerWord();
int bitsRequiredPerCoordinate = bitsRequired / 2;
DebugLogger.finer("Bits required: %d (per coord: %d)", bitsRequired, bitsRequiredPerCoordinate);
long combinedValue = wordIndexesToCombinedValue(wordIndexes);
DebugLogger.finer("Combined value: %d", combinedValue);
int[] decodedCoords = decodeCoords(combinedValue, bitsRequiredPerCoordinate);
int chunkX = decodedCoords[0];
int chunkZ = decodedCoords[1];
DebugLogger.finer("Chunk: %d, %d", chunkX, chunkZ);
// +8 to make it center of chunk
int xCoord = chunkX * 16 + 8;
int zCoord = chunkZ * 16 + 8;
DebugLogger.finer("Decoded to coordinates: %d, %d", xCoord, zCoord);
return new int[] { xCoord, zCoord };
}
private long wordIndexesToCombinedValue(int[] wordIndexes) {
long combinedValue = 0;
for (int i=0; i<wordIndexes.length; i++) {
combinedValue <<= bitsPerWord;
combinedValue |= wordIndexes[i];
}
return combinedValue;
}
private int[] decodeCoords(long combinedValue, int bitsRequiredPerCoordinate) {
int coordinateMask = (1 << bitsRequiredPerCoordinate) - 1;
int coordinateOffset = 1 << (bitsRequiredPerCoordinate - 1);
int z = (int) (combinedValue & coordinateMask) - coordinateOffset;
int x = (int) (combinedValue >> bitsRequiredPerCoordinate) - coordinateOffset;
return new int[] { x, z };
}
}

View file

@ -0,0 +1,132 @@
/*
* 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.converter;
import eu.m724.tweaks.DebugLogger;
import eu.m724.tweaks.module.wordcoords.WordList;
import java.util.Arrays;
public class Encoder {
private final WordList wordList;
private final int bitsPerWord;
public Encoder(WordList wordList) {
this.wordList = wordList;
this.bitsPerWord = wordList.getBitsPerWord();
}
public String[] encode(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;
}
} // else: coords are 0 or -1, minTotalBits=0, wordsRequired=0, actualTotalBits=0. Need special handling?
// If x/z are 0/-1, findBitsRequired returns 1, minTotalBits=2. The loop handles it.
// 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) {
// Occurs only if maxVal <= -1 and minVal >= 0, which is impossible,
// OR maxVal <= 0 and -minVal <= 0 => maxVal <= 0 and minVal >= 0.
// This means x and z are both 0.
// The range for 1 bit is [-1, 0]. If coords are 0, 1 bit is not enough for offset+coord.
// Example: bits=1. offset=1<<0=1. mask=(1<<1)-1=1.
// encodeCoord(0, 1) = (1+0)&1 = 1.
// decodeCoord(1, 1): val=1. mask=1. offset=1. (1&1)-1 = 0. Correct.
// What if we need to represent -1? encodeCoord(-1, 1) = (1-1)&1 = 0.
// decodeCoord(0, 1): val=0. (0&1)-1 = -1. Correct.
// So 1 bit works for range [-1, 0]. Let's check the condition:
// x=0, z=0 -> maxVal=0, minVal=0. reqPosMag = max(1, 0) = 1.
// x=-1, z=-1 -> maxVal=-1, minVal=-1. reqPosMag = max(0, 1) = 1.
// x=0, z=-1 -> maxVal=0, minVal=-1. reqPosMag = max(1, 1) = 1.
// So requiredPositiveMagnitude is 1 for the range [-1, 0].
requiredPositiveMagnitude = 1; // Ensure it's at least 1 if coords are 0 or -1.
}
// Calculate p = bits - 1
// We need the smallest integer p such that (1 << p) >= requiredPositiveMagnitude.
// If requiredPositiveMagnitude = 1, we need 1 << p >= 1, smallest p is 0.
// If requiredPositiveMagnitude > 1, this is equivalent to finding the number of bits
// needed to represent (requiredPositiveMagnitude - 1) in binary.
int p;
if (requiredPositiveMagnitude == 1) {
p = 0;
} else {
p = 32 - Integer.numberOfLeadingZeros(requiredPositiveMagnitude - 1);
}
// bits = p + 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 end of array
for (int remainingBits = bitsRequired; remainingBits > 0; remainingBits -= bitsPerWord) {
int wordMask = (1 << bitsPerWord) - 1;
wordIndexes[--currentIndex] = (int) (combinedValue & wordMask);
combinedValue >>= bitsPerWord;
}
return wordIndexes;
}
}

View file

@ -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);
}
}

View file

@ -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);
});
}
}

View file

@ -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)));
}
});
}
}

View file

@ -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.player;
import de.maxhenkel.voicechat.api.BukkitVoicechatService;
import de.maxhenkel.voicechat.api.VoicechatServerApi;
import de.maxhenkel.voicechat.api.VolumeCategory;
import de.maxhenkel.voicechat.api.audiochannel.AudioPlayer;
import de.maxhenkel.voicechat.api.audiochannel.EntityAudioChannel;
import de.maxhenkel.voicechat.api.opus.OpusEncoderMode;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.IOException;
import java.util.UUID;
public class MusicPlayer {
private static final String PLAYER_CATEGORY = "music_player";
private VoicechatServerApi voicechat = null;
private final Plugin plugin;
public MusicPlayer(Plugin plugin) {
this.plugin = plugin;
}
public void init() {
BukkitVoicechatService service = plugin.getServer().getServicesManager().load(BukkitVoicechatService.class);
service.registerPlugin(new MyVoicechatPlugin(this));
}
void unlock(VoicechatServerApi voicechat) {
VolumeCategory category = voicechat.volumeCategoryBuilder()
.setId(PLAYER_CATEGORY)
.setName("Music players")
.build();
voicechat.registerVolumeCategory(category);
this.voicechat = voicechat;
}
public void create(Player player) {
UUID channelID = UUID.randomUUID();
EntityAudioChannel channel = voicechat.createEntityAudioChannel(channelID, voicechat.fromEntity(player));
channel.setCategory(PLAYER_CATEGORY);
channel.setDistance(10);
short[] arr;
try {
AudioInputStream audio = AudioSystem.getAudioInputStream(plugin.getResource("music.flac"));
int samples = (int) (audio.available() / audio.getFrameLength());
arr = new short[samples];
for (int i=0; i<audio.available(); i++) {
}
} catch (UnsupportedAudioFileException | IOException e) {
throw new RuntimeException(e);
}
AudioPlayer audioPlayer = voicechat.createAudioPlayer(channel, voicechat.createEncoder(OpusEncoderMode.AUDIO), arr);
audioPlayer.startPlaying();
}
}

View file

@ -1,41 +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.player;
import de.maxhenkel.voicechat.api.VoicechatApi;
import de.maxhenkel.voicechat.api.VoicechatPlugin;
import de.maxhenkel.voicechat.api.VoicechatServerApi;
import de.maxhenkel.voicechat.api.events.EventRegistration;
import de.maxhenkel.voicechat.api.events.VoicechatServerStartedEvent;
public class MyVoicechatPlugin implements VoicechatPlugin {
private final MusicPlayer musicPlayer;
MyVoicechatPlugin(MusicPlayer musicPlayer) {
this.musicPlayer = musicPlayer;
}
@Override
public String getPluginId() {
return "tweaks724";
}
@Override
public void initialize(VoicechatApi api) {
}
@Override
public void registerEvents(EventRegistration registration) {
registration.registerEvent(VoicechatServerStartedEvent.class, this::onServerStarted);
}
public void onServerStarted(VoicechatServerStartedEvent event) {
VoicechatServerApi api = event.getVoicechat();
musicPlayer.unlock(api);
}
}

View file

@ -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);
}
}

View file

@ -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
}
}

View file

@ -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.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);
/*long perSkip = (long) (Math.pow(untilDay, 2) / 10000);
perSkip = Math.clamp(perSkip, 5, 200);
perSkip = (long) (perSkip * sleepPercentage);*/
/*System.out.println("asdasd");
System.out.println(sleepPercentage);
System.out.println(untilDay);
System.out.println(perSkip);*/
world.setTime(world.getTime() + perSkip);
}
}

Some files were not shown because too many files have changed in this diff Show more