Compare commits

..

19 commits

Author SHA1 Message Date
1c19a713d8
[maven-release-plugin] prepare for next development iteration 2025-05-30 13:54:23 +02:00
30bd73e5be
[maven-release-plugin] prepare release tweaks-0.1.15 2025-05-30 13:54:20 +02:00
2773275e0b
Support 1.21.5 2025-05-30 13:48:00 +02:00
670cbd9635
Update IDEA files 2025-05-30 08:29:14 +02:00
ba7f1aa226
Improve README
Signed-off-by: Minecon724 <minecon724@noreply.git.m724.eu>
2025-05-18 10:37:01 +02:00
e67c187f4e
Refactor
Signed-off-by: Minecon724 <minecon724@noreply.git.m724.eu>
2025-05-17 10:18:58 +02:00
2213ebe3cf
Refactor kill switch
Signed-off-by: Minecon724 <minecon724@noreply.git.m724.eu>
2025-05-17 08:49:13 +02:00
2c835a4eab
Improve WordCoords
Signed-off-by: Minecon724 <minecon724@noreply.git.m724.eu>
2025-05-16 10:22:01 +02:00
352807483c
Refactor DebugLogger
Signed-off-by: Minecon724 <minecon724@noreply.git.m724.eu>
2025-05-15 09:03:48 +02:00
e3f34ec46f
Refactor pomodoro
Signed-off-by: Minecon724 <minecon724@noreply.git.m724.eu>
2025-05-14 19:06:11 +02:00
a8e67dbe26
Document TweaksModule
Signed-off-by: Minecon724 <minecon724@noreply.git.m724.eu>
2025-05-14 19:06:04 +02:00
4f752bef61
Quick mode 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
Signed-off-by: Minecon724 <git@m724.eu>
2025-02-19 11:24:22 +01:00
Minecon724
7cd334f4a2
feat: Initial word coords module
Signed-off-by: Minecon724 <git@m724.eu>
2025-02-18 14:09:19 +01:00
Minecon724
b421b48e51
chore: Update mStats to fix am issue
Signed-off-by: Minecon724 <git@m724.eu>
2025-02-17 09:06:39 +01:00
Minecon724
684e31bf07
fix: correct string formatting in debug logger
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 2025-02-03 11:27:37 +01:00
35 changed files with 3203 additions and 456 deletions

View file

@ -13,6 +13,8 @@ jobs:
- name: Download NMS - name: Download NMS
run: ./tools/download_nms.sh ~ run: ./tools/download_nms.sh ~
- name: Build for 1.21.5
run: ./mvnw package -Dproject.minecraft.version=1.21.5 -Dproject.craftbukkit.version=v1_21_R4
- name: Build for 1.21.4 - name: Build for 1.21.4
run: ./mvnw package -Dproject.minecraft.version=1.21.4 -Dproject.craftbukkit.version=v1_21_R3 run: ./mvnw package -Dproject.minecraft.version=1.21.4 -Dproject.craftbukkit.version=v1_21_R3

3
.idea/.gitignore generated vendored
View file

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

View file

@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright (C) &amp;#36;originalComment.match(&quot;Copyright \(c\) (\d+)&quot;, 1, &quot;-&quot;, &quot;&amp;#36;today.year&quot;)&amp;#36;today.year Minecon724&#10;Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file&#10;in the project root for the full license text." />
<option name="myName" value="gpl3" />
</copyright>
</component>

View file

@ -1,7 +0,0 @@
<component name="CopyrightManager">
<settings default="gpl3">
<module2copyright>
<element module="All" copyright="gpl3" />
</module2copyright>
</settings>
</component>

4
.idea/misc.xml generated
View file

@ -8,7 +8,5 @@
</list> </list>
</option> </option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK" />
<output url="file://$PROJECT_DIR$/out" />
</component>
</project> </project>

124
.idea/uiDesigner.xml generated
View file

@ -1,124 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

110
.idea/workspace.xml generated Normal file
View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="b6c6d76f-f438-4423-a70b-1459280aa431" name="Changes" comment="Update IDEA files" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="MavenImportPreferences">
<option name="generalSettings">
<MavenGeneralSettings>
<option name="mavenHomeTypeForPersistence" value="WRAPPER" />
</MavenGeneralSettings>
</option>
</component>
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 1
}</component>
<component name="ProjectId" id="2xdUuJ0x0gqOBEPQKKoh4gRpUH9" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;
}
}</component>
<component name="RunManager">
<configuration default="true" type="JetRunConfigurationType">
<module name="mutils" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration default="true" type="KotlinStandaloneScriptRunConfigurationType">
<module name="mutils" />
<option name="filePath" />
<method v="2" />
</configuration>
<configuration default="true" type="PythonConfigurationType" factoryName="Python">
<module name="mutils" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration default="true" type="Tox" factoryName="Tox">
<module name="mutils" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<method v="2" />
</configuration>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="b6c6d76f-f438-4423-a70b-1459280aa431" name="Changes" comment="" />
<created>1748267665173</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1748267665173</updated>
</task>
<task id="LOCAL-00001" summary="Update IDEA files">
<option name="closed" value="true" />
<created>1748523422048</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1748523422048</updated>
</task>
<option name="localTasksCounter" value="2" />
<servers />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="Update IDEA files" />
<option name="LAST_COMMIT_MESSAGE" value="Update IDEA files" />
</component>
</project>

View file

@ -14,7 +14,7 @@ Stuff no<sub><sup>t many</sup></sub> other plugins do.
Dependencies: Dependencies:
- **1.21.1 and newer** - **1.21.1 and newer**
- [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) - [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) - To use modules marked <sup><sub>N</sub></sup>, you must use a JAR [made for the exact server version.](docs/BUILDING.md)
# Features # Features
@ -44,7 +44,7 @@ Random MOTD for every ping
`/chatmanage` - create, delete, modify etc. (`tweaks724.chatmanage`) `/chatmanage` - create, delete, modify etc. (`tweaks724.chatmanage`)
### Compass ### Compass
Holding a compass shows a bar with 4 directions and stuff like beds, lodestones, death pos (TODO) etc. Holding a compass shows a bar with four directions and stuff like beds, lodestones, death pos (TODO) etc.
### Pomodoro ### Pomodoro
Self-discipline with a pomodoro timer that's actually forced Self-discipline with a pomodoro timer that's actually forced
@ -65,7 +65,7 @@ Hardcore hearts by chance
Sleeping doesn't skip night, but speeds it up. The more players, the faster it goes. Sleeping doesn't skip night, but speeds it up. The more players, the faster it goes.
- Instant sleep \ - Instant sleep \
One can instantly skip, but only a part of the night. \ One can instantly skip, but only a part of the night. \
There's 5 players on the server. A night is 10 minutes long. \ There are five 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 Each player can instantly skip 2 minutes of the night at any time, even if others aren't sleeping
- Heal \ - Heal \
Sleeping heals Sleeping heals
@ -85,26 +85,30 @@ Issue messages that the player needs to read to keep playing, and that make an a
`/emergencyalerts` (`tweaks724.emergencyalerts`) `/emergencyalerts` (`tweaks724.emergencyalerts`)
### Remote redstone ### Remote redstone
Adds a "gateway" item that are controlled over internet. \ Adds a "gateway" item controlled over the internet. \
[RETSTONE.md for more info](/Minecon724/tweaks724/src/branch/master/docs/RETSTONE.md) [RETSTONE.md for more info](docs/RETSTONE.md)
### Knockback ### Knockback
Control knockback dealt by entities Control knockback dealt by entities
### Kill switch ### Kill switch
Quickly kills (terminates) the server on trigger, via command or HTTP request. Quickly kills (terminates) the server on trigger, by command or HTTP request.
[KILLSWITCH.md for more info](/Minecon724/tweaks724/src/branch/master/docs/KILLSWITCH.md) [KILLSWITCH.md for more info](docs/KILLSWITCH.md)
### Swing through grass ### Swing through grasses
Self-explanatory Self-explanatory
### Durability alert ### Durability alert
Self-explanatory too. \ Self-explanatory too.
For simplicity, there's no configuration. Control with `tweaks724.durabilityalert`
### WordCoords
Converts coords to words so remembering is easier.
`/wordcoords` (`tweaks724.wordcoords`)
### Utility commands ### Utility commands
- `/ping` - displays player ping <sup><sub>P</sub></sup> \ - `/ping` - display player ping <sup><sub>P</sub></sup> \
**Ping is calculated by the plugin**. \ **Ping is calculated by the plugin**. \
That allows for more precision (decimal places) and to get the ping immediately after a player join That allows for more precision (decimal places) and to get the ping immediately after a player joins

View file

@ -1,32 +1,34 @@
Killswitch immediately stops the server. Killswitch immediately stops (kills) the server.
### Warning ### Warning
This terminates the server process, meaning it's like you'd pull the power. \ This terminates the server process (not the OS), meaning it's **like you pulled the power cable.** \
So you will lose some progress (since the last auto save), or worst case your world (or other data) gets corrupted. You lose some progress (since the last auto save), or in the worst case, **data gets corrupted.**
Terminal froze after kill? `reset` Terminal froze after kill? Do `reset`
### Over a command ### By command
No key is required. \ No key is required. \
`/servkill` is the command. Permission: `tweaks724.servkill`. \ `/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. **You must grant the permission manually** with a permission plugin—it's not automatically assigned, not even to OPs.
### Over HTTP ### Over the internet
HTTP is insecure, meaning others *could* intercept your request to the server and get your key. \ **HTTP is insecure.** The secret key travels in plain text, meaning whoever oversees the path[^1] to your server *could*[^2] 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) \ I recommend putting this behind a controlled[^3] VPN, or an HTTPS proxy with good access control. \
Or regenerate the key after every usage. Or regenerate the key every use.
Make a GET request to `/key/<base64 encoded key>` Make a GET request to `/kill/<secret key>`:
Example:
``` ```
https://127.0.0.1:57932/key/lNwANMSZhLiTWhNxSoqQ5Q== GET http://127.0.0.1:57932/kill/lNwANMSZhLiTWhNxSoqQ5Q==
|_ server address _| |_ base64 encoded key _| |_ endpoint _| |_ secret key _|
``` ```
The response is a 404 no matter what. Either way, you will notice that the server has stopped. There is no response; the connection is closed immediately, no matter what.[^4]
The key is generated to `plugins/Tweaks724/storage/killswitch key` \ The key is in `plugins/Tweaks724/killswitch secret key.txt`. You can provide your own key. The key should be plaintext (not bytes).
To use it with HTTP server, encode it to base64.
Rate limit is 1 request / 5 minutes The ratelimit is one request per 2 minutes. **Do not send requests when blocked, it resets the timer!**
[^1]: Typically you, then it's your ISP, maybe its upstream, then the internet backbone (various entities), then your hosting, and finally your server. That's how the Internet works!
[^2]: Though unlikely
[^3]: Direct to the server, think WireGuard. Not a commercial offering!
[^4]: *Stealth*. You will notice that the server has stopped.

16
pom.xml
View file

@ -10,15 +10,15 @@
<groupId>eu.m724</groupId> <groupId>eu.m724</groupId>
<artifactId>tweaks</artifactId> <artifactId>tweaks</artifactId>
<version>0.1.14</version> <version>0.1.16-SNAPSHOT</version>
<properties> <properties>
<maven.compiler.source>21</maven.compiler.source> <maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target> <maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.craftbukkit.version>v1_21_R3</project.craftbukkit.version> <project.craftbukkit.version>v1_21_R4</project.craftbukkit.version>
<project.minecraft.version>1.21.4</project.minecraft.version> <project.minecraft.version>1.21.5</project.minecraft.version>
<project.spigot.version>${project.minecraft.version}-R0.1-SNAPSHOT</project.spigot.version> <project.spigot.version>${project.minecraft.version}-R0.1-SNAPSHOT</project.spigot.version>
</properties> </properties>
@ -44,7 +44,7 @@
</goals> </goals>
<configuration> <configuration>
<target> <target>
<replace token="v1_21_R3" value="${project.craftbukkit.version}" dir="src/main"> <replace token="v1_21_R4" value="${project.craftbukkit.version}" dir="src/main">
<include name="**/*.java" /> <include name="**/*.java" />
</replace> </replace>
</target> </target>
@ -58,7 +58,7 @@
</goals> </goals>
<configuration> <configuration>
<target> <target>
<replace token="${project.craftbukkit.version}" value="v1_21_R3" dir="src/main"> <replace token="${project.craftbukkit.version}" value="v1_21_R4" dir="src/main">
<include name="**/*.java" /> <include name="**/*.java" />
</replace> </replace>
</target> </target>
@ -128,7 +128,7 @@
<dependency> <dependency>
<groupId>org.spigotmc</groupId> <groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId> <artifactId>spigot-api</artifactId>
<version>1.21.1-R0.1-SNAPSHOT</version> <version>1.21.1-R0.1-SNAPSHOT</version> <!-- oldest supported version -->
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
@ -147,7 +147,7 @@
<dependency> <dependency>
<groupId>eu.m724</groupId> <groupId>eu.m724</groupId>
<artifactId>mstats-spigot</artifactId> <artifactId>mstats-spigot</artifactId>
<version>0.1.0</version> <version>0.1.2</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
@ -168,6 +168,6 @@
<scm> <scm>
<developerConnection>scm:git:git@git.m724.eu:Minecon724/tweaks724.git</developerConnection> <developerConnection>scm:git:git@git.m724.eu:Minecon724/tweaks724.git</developerConnection>
<tag>tweaks-0.1.14</tag> <tag>HEAD</tag>
</scm> </scm>
</project> </project>

View file

@ -10,8 +10,11 @@ import eu.m724.tweaks.module.TweaksModule;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class DebugLogger { public class DebugLogger {
private DebugLogger() {}
static Logger logger; static Logger logger;
public static void info(String message, Object... format) { public static void info(String message, Object... format) {
@ -35,20 +38,16 @@ public class DebugLogger {
} }
private static void log(Level level, String message, Object... 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 (!logger.isLoggable(level)) {
return;
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.")) message = message.formatted(format);
caller = caller.substring(15);
message = "[" + caller + "] " + message.formatted(format); if (logger.getLevel().intValue() <= Level.FINE.intValue()) {
message = "[" + getCaller() + "] " + message;
}
if (level.intValue() < Level.INFO.intValue()) { // levels below info are never logged even if set for some reason 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) // colors text gray (cyan is close to gray)
@ -61,5 +60,30 @@ public class DebugLogger {
} }
logger.log(level, message); logger.log(level, message);
}
private static String getCaller() {
String caller = Thread.currentThread().getStackTrace()[4].getClassName();
// TweaksModule has helper functions that log, we want to label the logs
if (caller.equals(TweaksModule.class.getName())) {
String pcaller = Thread.currentThread().getStackTrace()[5].getClassName();
if (pcaller.endsWith("Module"))
caller = pcaller;
}
if (caller.startsWith("eu.m724.tweaks.")) {
caller = caller.substring(15);
String[] packages = caller.split("\\.");
caller = IntStream.range(0, packages.length - 1)
.mapToObj(i -> packages[i].substring(0, 2))
.collect(Collectors.joining(".")) + "." + packages[packages.length - 1];
// TODO leading dot or no dot?
}
return caller;
} }
} }

View file

@ -27,6 +27,7 @@ import eu.m724.tweaks.module.redstone.RedstoneModule;
import eu.m724.tweaks.module.sleep.SleepModule; import eu.m724.tweaks.module.sleep.SleepModule;
import eu.m724.tweaks.module.swing.SwingModule; import eu.m724.tweaks.module.swing.SwingModule;
import eu.m724.tweaks.module.updater.UpdaterModule; 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.WorldBorderExpandModule;
import eu.m724.tweaks.module.worldborder.WorldBorderHideModule; import eu.m724.tweaks.module.worldborder.WorldBorderHideModule;
@ -157,6 +158,8 @@ public class TweaksPlugin extends MStatsPlugin {
TweaksModule.init(DurabilityModule.class); TweaksModule.init(DurabilityModule.class);
TweaksModule.init(WordCoordsModule.class);
/* end modules */ /* end modules */
if (config.metrics()) { if (config.metrics()) {
@ -164,7 +167,7 @@ public class TweaksPlugin extends MStatsPlugin {
mStats(1); mStats(1);
} }
DebugLogger.fine("Took %.3f milliseconds".formatted((System.nanoTime() - start) / 1000000.0)); DebugLogger.fine("Took %.3f milliseconds", (System.nanoTime() - start) / 1000000.0);
} }
private String getTargetVersion() { private String getTargetVersion() {

View file

@ -21,32 +21,60 @@ import java.lang.reflect.InvocationTargetException;
import java.util.function.Consumer; import java.util.function.Consumer;
public abstract class TweaksModule { public abstract class TweaksModule {
/**
* Called on module initialize.
*/
protected abstract void onInit(); protected abstract void onInit();
void init() { void init() {
var name = getClass().getSimpleName(); var name = getClass().getSimpleName();
DebugLogger.finer("Initializing " + name); DebugLogger.finer("Initializing module " + name);
long start = System.nanoTime(); long start = System.nanoTime();
this.onInit(); this.onInit();
long end = System.nanoTime(); long end = System.nanoTime();
DebugLogger.fine("Initialized %s in %d µs", name, (end - start) / 1000); DebugLogger.fine("Initialized %s in %d µs", name, (end - start) / 1000);
} }
/**
* Gets the plugin instance.
*
* @return The plugin instance
*/
protected TweaksPlugin getPlugin() { protected TweaksPlugin getPlugin() {
return TweaksPlugin.getInstance(); return TweaksPlugin.getInstance();
} }
/**
* Gets the plugin config.
*
* @return The plugin config
*/
protected TweaksConfig getConfig() { protected TweaksConfig getConfig() {
return TweaksConfig.getConfig(); return TweaksConfig.getConfig();
} }
/**
* Registers an event listener.
*
* @param listener The event listener
*/
protected void registerEvents(Listener listener) { protected void registerEvents(Listener listener) {
DebugLogger.finer("Registered listener: " + listener.getClass().getName());
getPlugin().getServer().getPluginManager().registerEvents(listener, getPlugin()); getPlugin().getServer().getPluginManager().registerEvents(listener, getPlugin());
DebugLogger.finer("Registered event listener: " + listener.getClass().getName());
} }
/**
* Registers an OUTGOING packet listener.
* Priority is {@link ListenerPriority}.NORMAL.
*
* @param packetType The {@link PacketType} to listen for
* @param consumer The consumer that will be called when the packet is received.
*/
protected void onPacketSend(PacketType packetType, Consumer<PacketEvent> consumer) { protected void onPacketSend(PacketType packetType, Consumer<PacketEvent> consumer) {
ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) { ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
@Override @Override
@ -55,9 +83,16 @@ public abstract class TweaksModule {
} }
}); });
DebugLogger.finer("Registered packet send: " + packetType.name()); DebugLogger.finer("Registered outgoing packet listener: " + packetType.name());
} }
/**
* Registers an INCOMING packet listener.
* Priority is {@link ListenerPriority}.NORMAL.
*
* @param packetType The {@link PacketType} to listen for
* @param consumer The consumer that will be called when the packet is received.
*/
protected void onPacketReceive(PacketType packetType, Consumer<PacketEvent> consumer) { protected void onPacketReceive(PacketType packetType, Consumer<PacketEvent> consumer) {
ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) { ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
@Override @Override
@ -66,14 +101,27 @@ public abstract class TweaksModule {
} }
}); });
DebugLogger.finer("Registered packet receive: " + packetType.name()); DebugLogger.finer("Registered incoming packet listener: " + packetType.name());
} }
/**
* Registers a command.
*
* @param command The command
* @param executor The command executor
*/
protected void registerCommand(String command, CommandExecutor executor) { protected void registerCommand(String command, CommandExecutor executor) {
getPlugin().getCommand(command).setExecutor(executor); getPlugin().getCommand(command).setExecutor(executor);
DebugLogger.finer("Registered command: " + command); DebugLogger.finer("Registered command: " + command);
} }
/**
* Initializes a module.
*
* @param clazz The class of the initialized module
* @return The module instance
* @param <T> The type of the initialized module
*/
public static <T extends TweaksModule> T init(Class<T> clazz) { public static <T extends TweaksModule> T init(Class<T> clazz) {
T module; T module;
try { try {

View file

@ -14,8 +14,8 @@ import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.TranslatableComponent; import net.md_5.bungee.api.chat.TranslatableComponent;
import net.md_5.bungee.chat.ComponentSerializer; import net.md_5.bungee.chat.ComponentSerializer;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import org.bukkit.craftbukkit.v1_21_R3.CraftRegistry; import org.bukkit.craftbukkit.v1_21_R4.CraftRegistry;
import org.bukkit.craftbukkit.v1_21_R3.entity.CraftPlayer; import org.bukkit.craftbukkit.v1_21_R4.entity.CraftPlayer;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;

View file

@ -6,63 +6,59 @@
package eu.m724.tweaks.module.killswitch; 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.DebugLogger;
import eu.m724.tweaks.module.TweaksModule; import eu.m724.tweaks.module.TweaksModule;
import eu.m724.tweaks.module.killswitch.server.KillswitchSecureHttpServer;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Base64; import java.util.Base64;
public class KillswitchModule extends TweaksModule implements CommandExecutor, HttpHandler { public class KillswitchModule extends TweaksModule implements CommandExecutor {
private final Ratelimit ratelimit = new Ratelimit(); private String loadSecret(Path file) {
String secret;
private byte[] secret; if (Files.exists(file)) {
private String secretEncoded;
private void loadKey(File file) {
if (file.exists()) {
try { try {
this.secret = Files.readAllBytes(file.toPath()); secret = Files.readString(file);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Reading killswitch key", e); throw new RuntimeException("Reading killswitch key", e);
} }
DebugLogger.fine("Loaded key"); DebugLogger.fine("Loaded key");
} else { } else {
byte[] buf = new byte[16];
try { try {
byte[] buf = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(buf); SecureRandom.getInstanceStrong().nextBytes(buf);
Files.write(file.toPath(), buf);
secret = Base64.getEncoder().encodeToString(buf);
Files.writeString(file, secret);
} catch (IOException | NoSuchAlgorithmException e) { } catch (IOException | NoSuchAlgorithmException e) {
throw new RuntimeException("Generating killswitch key", e); throw new RuntimeException("Generating killswitch key", e);
} }
this.secret = buf; DebugLogger.info("Killswitch secret key generated and saved to %s", file);
DebugLogger.info("Killswitch key generated and saved to " + file.getPath());
} }
this.secretEncoded = Base64.getEncoder().encodeToString(secret); return secret;
} }
@Override @Override
protected void onInit() { protected void onInit() {
registerCommand("servkill", this); registerCommand("servkill", this);
if (getConfig().killswitchListen() != null) { if (getConfig().killswitchListen() == null) {
loadKey(new File(getPlugin().getDataFolder(), "storage/killswitch key")); return;
}
ratelimit.runTaskTimerAsynchronously(getPlugin(), 0, 20 * 300); String secret = loadSecret(getPlugin().getDataFolder().toPath().resolve("killswitch secret key.txt"));
var listenAddress = getConfig().killswitchListen().split(":"); var listenAddress = getConfig().killswitchListen().split(":");
InetSocketAddress bindAddress; InetSocketAddress bindAddress;
@ -72,47 +68,23 @@ public class KillswitchModule extends TweaksModule implements CommandExecutor, H
bindAddress = new InetSocketAddress(listenAddress[0], Integer.parseInt(listenAddress[1])); bindAddress = new InetSocketAddress(listenAddress[0], Integer.parseInt(listenAddress[1]));
} }
try { new KillswitchSecureHttpServer(secret, this::kill).start(bindAddress);
HttpServer server = HttpServer.create(bindAddress, 0);
server.createContext("/", this); DebugLogger.fine("HTTP server started");
server.setExecutor(null);
server.start();
DebugLogger.fine("server started");
} catch (IOException e) {
throw new RuntimeException("Starting HTTP server", e);
}
}
} }
private void kill() { private void kill() {
DebugLogger.info("Killing server on request");
Runtime.getRuntime().halt(0); Runtime.getRuntime().halt(0);
} }
@Override @Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
DebugLogger.fine("Kill requested by: %s", sender.getName());
sender.sendMessage("Killing server");
kill(); kill();
return true; 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

@ -1,26 +0,0 @@
/*
* 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,84 @@
/*
* 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.server;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import eu.m724.tweaks.DebugLogger;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.time.Duration;
import java.util.concurrent.Executors;
public class KillswitchSecureHttpServer implements HttpHandler {
private static final String PATH_PREFIX = "/kill/";
private final Ratelimit ratelimit;
private final String secret;
private final Runnable onTrigger;
public KillswitchSecureHttpServer(String secret, Runnable onTrigger) {
this.secret = secret;
this.onTrigger = onTrigger;
this.ratelimit = new Ratelimit(Duration.ofMinutes(2), 1);
}
public void start(InetSocketAddress bindAddress) {
HttpServer httpServer;
try {
httpServer = HttpServer.create(bindAddress, 0);
} catch (IOException e) {
throw new RuntimeException("Creating HTTP server", e);
}
httpServer.createContext("/", this);
httpServer.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
httpServer.start();
}
@Override
public void handle(HttpExchange exchange) {
exchange.close();
InetAddress clientAddress = exchange.getRemoteAddress().getAddress();
if (!ratelimit.makeRequest(clientAddress)) {
logWithIp(clientAddress, "Ratelimited");
return;
}
String path = exchange.getRequestURI().getRawPath();
if (!path.startsWith(PATH_PREFIX)) {
logWithIp(clientAddress, "Wrong path");
return;
}
String key = path.substring(PATH_PREFIX.length());
if (key.equals(secret)) {
logWithIpFine(clientAddress, "Correct key");
onTrigger.run();
} else {
logWithIp(clientAddress, "Invalid key");
}
}
private void logWithIpFine(InetAddress address, String message, Object... format) {
DebugLogger.fine("[Remote %s] %s", address.getHostAddress(), message, format);
}
private void logWithIp(InetAddress address, String message, Object... format) {
DebugLogger.finer("[Remote %s] %s", address.getHostAddress(), message, format);
}
}

View file

@ -0,0 +1,37 @@
/*
* 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.server;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.net.InetAddress;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
public class Ratelimit {
private final Duration blockDuration;
private final int requestsAllowed;
private final LoadingCache<InetAddress, AtomicInteger> requestCounts;
public Ratelimit(Duration blockDuration, int requestsAllowed) {
this.blockDuration = blockDuration;
this.requestsAllowed = requestsAllowed;
this.requestCounts = CacheBuilder.newBuilder()
.expireAfterWrite(blockDuration)
.build(CacheLoader.from(key -> new AtomicInteger(0)));
}
boolean makeRequest(InetAddress address) {
AtomicInteger requestCount = this.requestCounts.getUnchecked(address);
return requestCount.getAndIncrement() < requestsAllowed;
}
}

View file

@ -6,70 +6,77 @@
package eu.m724.tweaks.module.pomodoro; package eu.m724.tweaks.module.pomodoro;
import java.util.concurrent.TimeUnit;
public class PlayerPomodoro { public class PlayerPomodoro {
private int pomodori = 0; private int intervalsDone = 0;
private boolean isBreak = false; private boolean breaktime = false;
// this is for both break and not break private long currentIntervalStartedAtNanos = -1;
private long intervalStart = -1;
/** public int getIntervalsDone() {
* A "pomodoro" is the 25-minute cycle you take breaks after<br> return intervalsDone;
* This returns how many cycles already elapsed, so if this is the first cycle this is 0<br> }
* The break after the "pomodoro," so if it's breaktime after the first "pomodoro" it stays at 0
*/ public boolean isBreaktime() {
public int getPomodori() { return breaktime;
return pomodori; }
public boolean isLongBreak() {
return (intervalsDone + 1) % PomodoroModule.INTERVALS_BEFORE_LONG_BREAK == 0;
} }
/** /**
* When did the current interval start<br> * @return The time the current interval or break started, in milliseconds
* Or when did the break start
*
* @see PlayerPomodoro#isBreak()
*/ */
public long getIntervalStart() { public long getCurrentIntervalStartedAtNanos() {
return intervalStart; return currentIntervalStartedAtNanos;
} }
public int getCycleDurationSeconds() { public int getCurrentIntervalDurationSeconds() {
return isBreak ? (pomodori < 3 ? 300 : 1200) : 1500; if (breaktime) {
if (isLongBreak()) {
return PomodoroModule.LONG_BREAK_DURATION_SECONDS;
} }
public long getRemainingSeconds(long now) { return PomodoroModule.SHORT_BREAK_DURATION_SECONDS;
return getCycleDurationSeconds() - (now - getIntervalStart()) / 1000000000; } else {
return PomodoroModule.INTERVAL_DURATION_SECONDS;
}
} }
/** public long getCurrentIntervalDurationNanos() {
* Is it a break currently return TimeUnit.SECONDS.toNanos(getCurrentIntervalDurationSeconds());
*/
public boolean isBreak() {
return isBreak;
} }
public boolean isCycleComplete() { public long getCurrentIntervalRemainingNanos(long nowNanos) {
return intervalStart + getCycleDurationSeconds() * 1000000000L < System.nanoTime(); long elapsed = nowNanos - getCurrentIntervalStartedAtNanos();
return getCurrentIntervalDurationNanos() - elapsed;
}
public boolean isIntervalComplete(long nowNanos) {
return currentIntervalStartedAtNanos + getCurrentIntervalDurationNanos() < nowNanos;
} }
/** /**
* Resets and starts the timer * Resets and starts the timer
*/ */
public void start() { public void start() {
this.pomodori = 0; this.intervalsDone = 0;
this.isBreak = false; this.breaktime = false;
this.intervalStart = System.nanoTime(); this.currentIntervalStartedAtNanos = System.nanoTime();
} }
/** /**
* Completes a cycle * Completes a cycle
*/ */
public void next() { public void startBreakOrNextInterval() {
if (isBreak) { // from break to interval if (breaktime) { // from break to interval
this.pomodori++; this.intervalsDone++;
this.pomodori %= 4; this.intervalsDone %= PomodoroModule.INTERVALS_BEFORE_LONG_BREAK;
} }
this.intervalStart = System.nanoTime(); this.currentIntervalStartedAtNanos = System.nanoTime();
isBreak = !isBreak; breaktime = !breaktime;
} }
} }

View file

@ -0,0 +1,29 @@
/*
* Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
package eu.m724.tweaks.module.pomodoro;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class PlayerPomodoroTracker {
static final Map<UUID, PlayerPomodoro> timers = new HashMap<>();
public static PlayerPomodoro get(Player player) {
return timers.get(player.getUniqueId());
}
public static PlayerPomodoro create(Player player) {
return timers.computeIfAbsent(player.getUniqueId(), (k) -> new PlayerPomodoro());
}
public static boolean remove(Player player) {
return timers.remove(player.getUniqueId()) != null;
}
}

View file

@ -6,6 +6,9 @@
package eu.m724.tweaks.module.pomodoro; package eu.m724.tweaks.module.pomodoro;
import eu.m724.tweaks.Language;
import eu.m724.tweaks.config.TweaksConfig;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
@ -18,25 +21,34 @@ public class PomodoroCommands implements CommandExecutor {
Player player = (Player) sender; Player player = (Player) sender;
String action = args.length > 0 ? args[0] : null; String action = args.length > 0 ? args[0] : null;
PlayerPomodoro pomodoro = Pomodoros.get(player); PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
long now = System.nanoTime();
if (pomodoro != null) { if (pomodoro != null) {
if ("stop".equals(action)) { if ("stop".equals(action)) {
Pomodoros.remove(player); PlayerPomodoroTracker.remove(player);
sender.sendMessage("Pomodoro disabled"); sender.spigot().sendMessage(Language.getComponent("pomodoroStopped", ChatColor.GREEN, label));
} else { } else {
if (pomodoro.isCycleComplete()) { if (pomodoro.isIntervalComplete(now)) {
pomodoro.next(); pomodoro.startBreakOrNextInterval();
if (pomodoro.isBreaktime() && TweaksConfig.getConfig().pomodoroForce()) {
player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText());
} }
sender.spigot().sendMessage(Pomodoros.formatTimer(pomodoro, pomodoro.getRemainingSeconds(System.nanoTime()))); }
sender.spigot().sendMessage(PomodoroModule.formatTimer(pomodoro, now));
} }
} else { } else {
if ("start".equals(action)) { if ("start".equals(action)) {
pomodoro = Pomodoros.create(player); pomodoro = PlayerPomodoroTracker.create(player);
pomodoro.start(); pomodoro.start();
sender.spigot().sendMessage(Pomodoros.formatTimer(pomodoro, pomodoro.getCycleDurationSeconds()));
sender.spigot().sendMessage(PomodoroModule.formatTimer(pomodoro, now));
} else { } else {
sender.sendMessage("Start pomodoro with /pom start"); // TODO help?
sender.spigot().sendMessage(Language.getComponent("pomodoroStart", ChatColor.GOLD, label));
} }
} }

View file

@ -7,56 +7,68 @@
package eu.m724.tweaks.module.pomodoro; package eu.m724.tweaks.module.pomodoro;
import eu.m724.tweaks.config.TweaksConfig; import eu.m724.tweaks.config.TweaksConfig;
import net.md_5.bungee.api.chat.ComponentBuilder;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.*; import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
public class PomodoroListener implements Listener { public class PomodoroListener implements Listener {
private final boolean force = TweaksConfig.getConfig().pomodoroForce(); private final TweaksConfig config = TweaksConfig.getConfig();
@EventHandler @EventHandler
public void onPlayerLogin(PlayerLoginEvent event) { public void onPlayerLogin(PlayerLoginEvent event) {
// Joining ends break
Player player = event.getPlayer(); Player player = event.getPlayer();
PlayerPomodoro pomodoro = Pomodoros.get(player); PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
if (pomodoro == null) return; if (pomodoro == null) return;
long remaining = pomodoro.getRemainingSeconds(System.nanoTime()); long now = System.nanoTime();
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (pomodoro.isBreak()) { if (pomodoro.isBreaktime()) {
if (pomodoro.isCycleComplete()) { if (remaining > 0 && config.pomodoroForce()) {
pomodoro.next(); player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText());
} else { } else {
if (force) { pomodoro.startBreakOrNextInterval();
event.getPlayer().kickPlayer(
new ComponentBuilder()
.append(Pomodoros.formatTimer(pomodoro, remaining))
.build().toLegacyText()
);
}
} }
} }
} }
@EventHandler @EventHandler
public void onPlayerQuit(PlayerQuitEvent event) { public void onPlayerQuit(PlayerQuitEvent event) {
// Quitting starts break
Player player = event.getPlayer(); Player player = event.getPlayer();
PlayerPomodoro pomodoro = Pomodoros.get(player); PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
if (pomodoro == null) return; if (pomodoro == null) return;
if (!pomodoro.isBreak() && pomodoro.isCycleComplete()) { if (pomodoro.isBreaktime()) return;
pomodoro.next();
long now = System.nanoTime();
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (remaining <= 0) {
pomodoro.startBreakOrNextInterval();
} }
} }
@EventHandler @EventHandler
public void onPlayerMove(PlayerMoveEvent event) { public void onPlayerToggleSneak(PlayerToggleSneakEvent event) {
Player player = event.getPlayer(); // Sneaking ends break
PlayerPomodoro timer = Pomodoros.get(player); if (!event.isSneaking()) return;
if (timer == null) return;
if (timer.isBreak() && timer.getRemainingSeconds(System.nanoTime()) <= 0) Player player = event.getPlayer();
timer.next(); // resume timer if break ended PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
if (pomodoro == null) return;
if (!pomodoro.isBreaktime()) return;
long now = System.nanoTime();
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (remaining <= 0) {
pomodoro.startBreakOrNextInterval();
}
} }
} }

View file

@ -6,9 +6,23 @@
package eu.m724.tweaks.module.pomodoro; package eu.m724.tweaks.module.pomodoro;
import eu.m724.tweaks.Language;
import eu.m724.tweaks.config.TweaksConfig;
import eu.m724.tweaks.module.TweaksModule; import eu.m724.tweaks.module.TweaksModule;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import java.util.concurrent.TimeUnit;
public class PomodoroModule extends TweaksModule { public class PomodoroModule extends TweaksModule {
static final int INTERVAL_DURATION_SECONDS = 10;
static final int SHORT_BREAK_DURATION_SECONDS = (int) TimeUnit.MINUTES.toSeconds(5);
static final int LONG_BREAK_DURATION_SECONDS = (int) TimeUnit.MINUTES.toSeconds(20);
static final int INTERVALS_BEFORE_LONG_BREAK = 4;
static final int KICK_DELAY_SECONDS = 60;
@Override @Override
protected void onInit() { protected void onInit() {
registerEvents(new PomodoroListener()); registerEvents(new PomodoroListener());
@ -16,4 +30,66 @@ public class PomodoroModule extends TweaksModule {
registerCommand("pomodoro", new PomodoroCommands()); registerCommand("pomodoro", new PomodoroCommands());
} }
/**
* Gets a formatted timer for a player.
*
* @param pomodoro the player's {@link PlayerPomodoro} instance
* @param nowNanos unix now timestamp in nanoseconds
* @return the timer as {@link BaseComponent}
*/
static BaseComponent formatTimer(PlayerPomodoro pomodoro, long nowNanos) {
ComponentBuilder builder = new ComponentBuilder();
long remainingNanos = pomodoro.getCurrentIntervalRemainingNanos(nowNanos);
long remainingSeconds = TimeUnit.NANOSECONDS.toSeconds(remainingNanos);
if (pomodoro.isBreaktime()) {
if (pomodoro.isLongBreak()) {
builder.append(Language.getComponent("pomodoroLongBreak", ChatColor.LIGHT_PURPLE));
} else {
builder.append(Language.getComponent("pomodoroShortBreak", ChatColor.LIGHT_PURPLE));
}
if (remainingNanos > 0) {
builder.append(" %02d:%02d".formatted(remainingSeconds / 60, remainingSeconds % 60))
.color(ChatColor.GOLD);
} else {
builder.append(" 00:00")
.color(ChatColor.GREEN);
if (!TweaksConfig.getConfig().pomodoroForce()) {
builder.append( "/pom").color(ChatColor.GOLD);
}
}
} else {
if (remainingNanos > 0) {
builder
.append("%02d:%02d".formatted(remainingSeconds / 60, remainingSeconds % 60))
.color(ChatColor.GRAY);
} else {
builder
.append("00:00")
.color(remainingSeconds % 2 == 0 ? ChatColor.RED : ChatColor.GRAY);
if (!TweaksConfig.getConfig().pomodoroForce()) {
builder.append( "/pom").color(ChatColor.GOLD);
}
}
}
for (int i=0; i<INTERVALS_BEFORE_LONG_BREAK; i++) {
ChatColor color = ChatColor.GRAY;
if (i == pomodoro.getIntervalsDone()) {
color = ChatColor.LIGHT_PURPLE;
} else if (i > pomodoro.getIntervalsDone()) {
color = ChatColor.DARK_GRAY;
}
builder.append(" o").color(color);
}
return builder.build();
}
} }

View file

@ -9,34 +9,49 @@ package eu.m724.tweaks.module.pomodoro;
import eu.m724.tweaks.TweaksPlugin; import eu.m724.tweaks.TweaksPlugin;
import eu.m724.tweaks.config.TweaksConfig; import eu.m724.tweaks.config.TweaksConfig;
import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.ChatMessageType;
import org.bukkit.Bukkit;
import org.bukkit.Sound; import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitRunnable;
import java.util.concurrent.TimeUnit;
public class PomodoroRunnable extends BukkitRunnable { public class PomodoroRunnable extends BukkitRunnable {
private final boolean force = TweaksConfig.getConfig().pomodoroForce(); private final TweaksConfig config = TweaksConfig.getConfig();
private final Plugin plugin = TweaksPlugin.getInstance(); // used only to kick private final Plugin plugin = TweaksPlugin.getInstance(); // used only to kick
@Override @Override
public void run() { public void run() {
long now = System.nanoTime(); long now = System.nanoTime();
Bukkit.getOnlinePlayers().forEach(player -> { PlayerPomodoroTracker.timers.forEach((uuid, pomodoro) -> {
PlayerPomodoro pomodoro = Pomodoros.get(player); long remainingNanos = pomodoro.getCurrentIntervalRemainingNanos(now);
if (pomodoro == null) return; long remainingSecs = TimeUnit.NANOSECONDS.toSeconds(remainingNanos);
long remaining = pomodoro.getRemainingSeconds(now); // TODO optimize?
Player player = plugin.getServer().getPlayer(uuid);
if (player != null && player.isOnline()) {
// TODO make not always on // TODO make not always on
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, Pomodoros.formatTimer(pomodoro, remaining)); player.spigot().sendMessage(ChatMessageType.ACTION_BAR, PomodoroModule.formatTimer(pomodoro, now));
if (remaining <= 0) { if (remainingNanos <= 0) {
player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_FALL, 1.0f, 0.5f); player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_FALL, 1.0f, 0.5f);
if (remaining < -60 && force) {
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> { if (remainingSecs < -PomodoroModule.KICK_DELAY_SECONDS && config.pomodoroForce()) {
pomodoro.next(); pomodoro.startBreakOrNextInterval();
player.kickPlayer(Pomodoros.formatTimer(pomodoro, pomodoro.getRemainingSeconds(now)).toLegacyText());
}); plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () ->
player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText())
);
}
}
} else {
if (remainingNanos <= 0) {
// Start break automatically if the player is offline
if (!pomodoro.isBreaktime()) {
pomodoro.startBreakOrNextInterval();
}
} }
} }
}); });

View file

@ -1,71 +0,0 @@
/*
* 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 net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class Pomodoros {
static final Map<UUID, PlayerPomodoro> timers = new HashMap<>();
public static PlayerPomodoro get(Player player) {
return timers.get(player.getUniqueId());
}
public static PlayerPomodoro create(Player player) {
return timers.computeIfAbsent(player.getUniqueId(), (k) -> new PlayerPomodoro());
}
public static boolean remove(Player player) {
return timers.remove(player.getUniqueId()) != null;
}
static BaseComponent formatTimer(PlayerPomodoro pomodoro, long remaining) {
ComponentBuilder builder = new ComponentBuilder();
if (pomodoro.isBreak()) {
builder.append("Break ").color(ChatColor.LIGHT_PURPLE);
if (remaining > 0) {
builder.append("%02d:%02d".formatted(remaining / 60, remaining % 60))
.color(ChatColor.GOLD);
} else {
builder.append("00:00")
.color(ChatColor.GREEN);
}
} else {
if (remaining > 0) {
builder
.append("%02d:%02d".formatted(remaining / 60, remaining % 60))
.color(ChatColor.GRAY);
} else {
builder
.append("00:00")
.color(remaining % 2 == 0 ? ChatColor.RED : ChatColor.YELLOW);
}
}
for (int i=0; i<4; i++) {
ChatColor color = ChatColor.GRAY;
if (i == pomodoro.getPomodori()) {
color = ChatColor.LIGHT_PURPLE;
} else if (i > pomodoro.getPomodori()) {
color = ChatColor.DARK_GRAY;
}
builder.append(" o").color(color);
}
return builder.build();
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
package eu.m724.tweaks.module.wordcoords;
import eu.m724.tweaks.DebugLogger;
import eu.m724.tweaks.module.wordcoords.codec.DecoderWordsToCoords;
import eu.m724.tweaks.module.wordcoords.codec.EncoderCoordsToWords;
import java.util.NoSuchElementException;
public class WordCoordsCodec {
private final EncoderCoordsToWords encoder;
private final DecoderWordsToCoords decoder;
public WordCoordsCodec(WordList wordList) {
this.encoder = new EncoderCoordsToWords(wordList);
this.decoder = new DecoderWordsToCoords(wordList);
DebugLogger.fine("Words: %d (%d bits/w)", wordList.getWordCount(), wordList.getBitsPerWord());
}
/**
* Encodes coords to words
* @param x The X coordinate
* @param z The Z coordinate
* @return The words
*/
public String[] encodeWords(int x, int z) {
return encoder.encodeWords(x, z);
}
/**
* Decodes words to coords
* @param words The words
* @return The X,Z coordinates
* @throws NoSuchElementException if one or more words are invalid
*/
public int[] decodeCoords(String[] words) throws NoSuchElementException {
return decoder.decodeCoords(words);
}
}

View file

@ -0,0 +1,180 @@
/*
* Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
package eu.m724.tweaks.module.wordcoords;
import eu.m724.tweaks.DebugLogger;
import eu.m724.tweaks.Language;
import eu.m724.tweaks.module.TweaksModule;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.NoSuchElementException;
public class WordCoordsModule extends TweaksModule implements CommandExecutor, Listener {
private static final int MAX_RADIUS = 30_000_000;
private WordList wordList;
private WordCoordsCodec converter;
@Override
protected void onInit() {
Path wordListFile = getPlugin().getDataFolder().toPath().resolve("storage/wordlist.txt");
if (Files.notExists(wordListFile)) {
try {
saveDefaultWordList(wordListFile);
} catch (IOException e) {
throw new RuntimeException("Failed to save default word list", e);
}
}
try {
this.wordList = WordList.fromFile(wordListFile);
} catch (IOException e) {
throw new RuntimeException("Failed to load word list", e);
}
this.converter = new WordCoordsCodec(this.wordList);
registerCommand("wordcoords", this);
registerEvents(this);
}
private void saveDefaultWordList(Path wordListFile) throws IOException {
try (InputStream is = getPlugin().getResource("wordlist.txt")) {
assert is != null;
Files.copy(is, wordListFile);
}
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
int x = 0, z = 0;
boolean encode = false; // means encode pos to words
args = String.join(" ", args)
.replaceAll("[^\\p{L}\\p{N}\\s]", " ")
.trim()
.split(" +");
if (args.length == 1 && args[0].isEmpty())
args = new String[0]; // empty split fix
DebugLogger.fine("Args: %s %d", String.join(", ", args), args.length);
if (args.length == 0) {
if (!(sender instanceof Player player)) {
sender.sendMessage(Language.getString("wordCoordsPlayerOnly"));
return true;
}
x = player.getLocation().getBlockX();
z = player.getLocation().getBlockZ();
encode = true;
} else {
if (Character.isDigit(args[0].codePointAt(0))) {
if (args.length > 1) {
try {
double dx = Double.parseDouble(args[0]);
double dz = Double.parseDouble(args[args.length > 2 ? 2 : 1]);
if (dx > MAX_RADIUS || dx < -MAX_RADIUS || dz > MAX_RADIUS || dz < -MAX_RADIUS) {
sender.spigot().sendMessage(Language.getComponent("wordCoordsOutOfRange", ChatColor.RED));
return true;
}
x = (int) dx;
z = (int) dz;
encode = true;
} catch (NumberFormatException ignored) { }
} else {
sender.spigot().sendMessage(Language.getComponent("wordCoordsProvideZ", ChatColor.RED));
return true;
}
}
}
if (encode) {
encodeAndSend(x, z, sender);
} else {
decodeAndSend(args, sender);
}
return true;
}
private void encodeAndSend(int x, int z, CommandSender sender) {
String[] words = converter.encodeWords(x, z);
String encoded = "///" + String.join(".", words);
BaseComponent[] components = new ComponentBuilder()
.append("%d, %d -> ".formatted(x, z))
.color(ChatColor.GRAY)
.append(encoded)
.color(ChatColor.AQUA) // TODO improve color
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, encoded))
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(Language.getString("clickToCopy"))))
.create();
sender.spigot().sendMessage(components);
}
private void decodeAndSend(String[] words, CommandSender sender) {
int x, z;
try {
int[] xz = converter.decodeCoords(words);
x = xz[0];
z = xz[1];
} catch (NoSuchElementException e) {
sender.spigot().sendMessage(Language.getComponent("wordCoordsInvalidWord", ChatColor.RED, e.getMessage()));
return;
}
String encoded = "///" + String.join(".", words);
BaseComponent[] components = new ComponentBuilder()
.append(encoded + " -> ")
.color(ChatColor.GRAY)
.append("%d, %d".formatted(x, z))
.color(ChatColor.AQUA) // TODO improve color
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, "%d, %d".formatted(x, z)))
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(Language.getString("clickToCopy"))))
.append(" ±8")
.color(ChatColor.GRAY)
.create();
sender.spigot().sendMessage(components);
}
@EventHandler
public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) {
if (!event.getMessage().startsWith("///")) return;
event.getPlayer().performCommand("wordcoords " + event.getMessage().substring(3));
event.setCancelled(true);
}
}

View file

@ -0,0 +1,62 @@
/*
* 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() && !s.startsWith("#"))
.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.codec;
import eu.m724.tweaks.DebugLogger;
import eu.m724.tweaks.module.wordcoords.WordList;
import java.util.NoSuchElementException;
import java.util.Arrays;
public class DecoderWordsToCoords {
private final WordList wordList;
private final int bitsPerWord;
public DecoderWordsToCoords(WordList wordList) {
this.wordList = wordList;
this.bitsPerWord = wordList.getBitsPerWord();
}
public int[] decodeCoords(String[] words) throws NoSuchElementException {
int[] wordIndexes = new int[words.length];
for (int i=0; i<words.length; i++) {
wordIndexes[i] = wordList.getWordIndex(words[i]);
if (wordIndexes[i] == -1)
throw new NoSuchElementException(words[i]);
}
return decodeCoords(wordIndexes);
}
public int[] decodeCoords(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 = decodeCombined(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 wordIndex : wordIndexes) {
combinedValue <<= bitsPerWord;
combinedValue |= wordIndex;
}
return combinedValue;
}
private int[] decodeCombined(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,113 @@
/*
* Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
package eu.m724.tweaks.module.wordcoords.codec;
import eu.m724.tweaks.DebugLogger;
import eu.m724.tweaks.module.wordcoords.WordList;
import java.util.Arrays;
public class EncoderCoordsToWords {
private final WordList wordList;
private final int bitsPerWord;
public EncoderCoordsToWords(WordList wordList) {
this.wordList = wordList;
this.bitsPerWord = wordList.getBitsPerWord();
}
public String[] encodeWords(int xCoord, int zCoord) {
int chunkX = Math.floorDiv(xCoord, 16);
int chunkZ = Math.floorDiv(zCoord, 16);
DebugLogger.finer("Chunk: %d, %d", chunkX, chunkZ);
// Calculate minimum bits required per coordinate based on range
int bitsRequiredPerCoordinate = findBitsRequiredPerCoordinate(chunkX, chunkZ);
int minTotalBits = bitsRequiredPerCoordinate * 2;
DebugLogger.finer("Min bits required per coordinate: %d (total: %d)", bitsRequiredPerCoordinate, minTotalBits);
// Calculate words required, ensuring total bits is sufficient and even
int wordsRequired = 0;
int actualTotalBits = 0;
if (minTotalBits > 0) { // Avoid division by zero if bitsPerWord is 0, or log(0)
wordsRequired = Math.ceilDiv(minTotalBits, bitsPerWord);
actualTotalBits = wordsRequired * bitsPerWord;
// Ensure total bits is sufficient
while (actualTotalBits < minTotalBits) {
wordsRequired++;
actualTotalBits = wordsRequired * bitsPerWord;
}
}
// Final bits per coordinate based on words
bitsRequiredPerCoordinate = actualTotalBits / 2;
DebugLogger.finer("Final Words required: %d", wordsRequired);
DebugLogger.finer("Final Bits required: %d (per coord: %d)", actualTotalBits, bitsRequiredPerCoordinate);
int encodedX = encodeCoord(chunkX, bitsRequiredPerCoordinate);
int encodedZ = encodeCoord(chunkZ, bitsRequiredPerCoordinate);
DebugLogger.finer("Encoded coordinates: %d, %d", encodedX, encodedZ);
long combinedValue = ((long) encodedX << bitsRequiredPerCoordinate) | encodedZ;
DebugLogger.finer("Combined value: %d", combinedValue);
int[] wordIndexes = combinedValueToWordIndexes(combinedValue, wordsRequired);
DebugLogger.finer("Word indexes: %s", Arrays.toString(wordIndexes));
return wordList.getWords(wordIndexes);
}
/** Calculates the minimum number of bits required to represent the coordinate
* using the encoding scheme (offset + coord) & mask, such that the coordinate
* fits within the range [-(1 << (bits - 1)), (1 << (bits - 1)) - 1]. */
private int findBitsRequiredPerCoordinate(int x, int z) {
int maxVal = Math.max(x, z);
int minVal = Math.min(x, z);
// Determine the required positive magnitude for the encoding range's positive side.
// We need `(1 << (bits - 1)) >= max(maxVal + 1, -minVal)`
int requiredPositiveMagnitude = Math.max(maxVal + 1, -minVal);
if (requiredPositiveMagnitude <= 0) {
requiredPositiveMagnitude = 1; // Ensure it's at least 1 if coords are 0 or -1.
}
int p;
if (requiredPositiveMagnitude == 1) {
p = 0;
} else {
p = 32 - Integer.numberOfLeadingZeros(requiredPositiveMagnitude - 1);
}
return p + 1;
}
private int encodeCoord(int coord, int bitsRequiredPerCoordinate) {
// Bitmask and offset for positive integer conversion
int coordinateMask = (1 << bitsRequiredPerCoordinate) - 1;
int coordinateOffset = 1 << (bitsRequiredPerCoordinate - 1);
// Encode coordinates with offset into positive range
return (coordinateOffset + coord) & coordinateMask;
}
private int[] combinedValueToWordIndexes(long combinedValue, int wordsRequired) {
int bitsRequired = wordsRequired * bitsPerWord;
// Break into word indexes
int[] wordIndexes = new int[wordsRequired];
int currentIndex = wordsRequired; // Start filling from the end of the array
for (int remainingBits = bitsRequired; remainingBits > 0; remainingBits -= bitsPerWord) {
int wordMask = (1 << bitsPerWord) - 1;
wordIndexes[--currentIndex] = (int) (combinedValue & wordMask);
combinedValue >>= bitsPerWord;
}
return wordIndexes;
}
}

View file

@ -8,7 +8,7 @@ package eu.m724.tweaks.module.worldborder;
import eu.m724.tweaks.module.TweaksModule; import eu.m724.tweaks.module.TweaksModule;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import org.bukkit.craftbukkit.v1_21_R3.CraftWorld; import org.bukkit.craftbukkit.v1_21_R4.CraftWorld;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldLoadEvent; import org.bukkit.event.world.WorldLoadEvent;

View file

@ -8,7 +8,7 @@ api-version: 1.21.1
softdepend: [ProtocolLib] softdepend: [ProtocolLib]
libraries: libraries:
- eu.m724:mstats-spigot:0.1.0 - eu.m724:mstats-spigot:0.1.2
commands: commands:
chat: chat:
@ -43,6 +43,10 @@ commands:
durabilityalert: durabilityalert:
description: Durability alert toggle description: Durability alert toggle
permission: tweaks724.durabilityalert permission: tweaks724.durabilityalert
wordcoords:
description: Word to coords conversion
permission: tweaks724.wordcoords
aliases: [woco, wc, w3w]
permissions: permissions:
tweaks724.chatmanage: tweaks724.chatmanage:
@ -63,6 +67,8 @@ permissions:
default: false default: false
tweaks724.durabilityalert: tweaks724.durabilityalert:
default: true default: true
tweaks724.wordcoords:
default: true
7weaks724.ignore.this: 7weaks724.ignore.this:
description: "Internal, not for use. ${project.spigot.version}" description: "Internal, not for use. ${project.spigot.version}"

View file

@ -30,12 +30,24 @@ chatAlreadyHere = You're already in this room.
authKickWrongKey = You're connecting to the wrong server address. You must connect to the one you're registered to. authKickWrongKey = You're connecting to the wrong server address. You must connect to the one you're registered to.
# If force is enabled and player is not registered. Changing this reveals you're using this plugin # If force is enabled and player is not registered. Changing this reveals you're using this plugin
authKickUnregistered = You are not whitelisted on this server! authKickUnregistered = You are not whitelisted on this server!
authKickError = An error occured. Please try again. If this persists, contact an administrator. authKickError = An error occurred. Please try again. If this persists, contact an administrator.
redstoneGatewayItem = Redstone gateway redstoneGatewayItem = Redstone gateway
clickToCopy = Click to copy to clipboard clickToCopy = Click to copy
clickToExecuteCommand = Click to execute command clickToExecuteCommand = Click to execute command
durabilityEnabled = Enabled durability alert durabilityEnabled = Enabled durability alert
durabilityDisabled = Disabled durability alert durabilityDisabled = Disabled durability alert
# When console executes /wordcoords without arguments
wordCoordsPlayerOnly = Only players can execute this command without arguments.
wordCoordsOutOfRange = Those coordinates are invalid.
wordCoordsInvalidWord = Invalid word or coordinate: "%s"
wordCoordsProvideZ = Please provide the Z coordinate.
# /pomodoro
pomodoroStopped = Pomodoro stopped. Restart it with /%s start
pomodoroStart = Start pomodoro with /%s start
pomodoroShortBreak = Short break
pomodoroLongBreak = Long break

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,10 @@
#!/bin/sh #!/bin/sh
# FILENAME=nms_1_21_1+3+4+5.tar.zst
# Copyright (C) 2025 Minecon724 CHECKSUM=0ea6267ce39213ddb0d6a7669d8021283350bb56de0d65f2e9fddd3c85337c5fbd204272b07c7e8532c6eaef46c3a47a39bac183abd6f4cfa7b171e08b4d7029
# Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
# in the project root for the full license text.
#
FILENAME=MS4yMS4xLTQK.tar.zst
curl -O https://36ab09b1.m724.eu/$FILENAME curl -O https://36ab09b1.m724.eu/$FILENAME
if [ "$(sha512sum $FILENAME)" = "475b931b6dde126aafd3f959bd02e122aa3c671ad11e83cbe1a9c9ea771a424f203381aa31e4ab40052dae1bfbe96c61daa81add1afab46dd423a5f038d68a6b MS4yMS4xLTQK.tar.zst" ]; then if [ "$(sha512sum $FILENAME)" = "$CHECKSUM $FILENAME" ]; then
tar -xaf $FILENAME -C "$1" tar -xaf $FILENAME -C "$1"
rm $FILENAME rm $FILENAME
else else