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
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
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>
</option>
</component>
<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>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK" />
</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:
- **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)
- To use modules marked <sup><sub>N</sub></sup>, you must use a JAR [made for the exact server version.](docs/BUILDING.md)
# Features
@ -44,7 +44,7 @@ Random MOTD for every ping
`/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.
Holding a compass shows a bar with four directions and stuff like beds, lodestones, death pos (TODO) etc.
### Pomodoro
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.
- 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. \
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
- Heal \
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`)
### Remote redstone
Adds a "gateway" item that are controlled over internet. \
[RETSTONE.md for more info](/Minecon724/tweaks724/src/branch/master/docs/RETSTONE.md)
Adds a "gateway" item controlled over the internet. \
[RETSTONE.md for more info](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)
Quickly kills (terminates) the server on trigger, by command or HTTP request.
[KILLSWITCH.md for more info](docs/KILLSWITCH.md)
### Swing through grass
### Swing through grasses
Self-explanatory
### Durability alert
Self-explanatory too. \
For simplicity, there's no configuration. Control with `tweaks724.durabilityalert`
Self-explanatory too.
### WordCoords
Converts coords to words so remembering is easier.
`/wordcoords` (`tweaks724.wordcoords`)
### 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**. \
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
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.
This terminates the server process (not the OS), meaning it's **like you pulled the power cable.** \
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. \
`/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
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.
### Over the internet
**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. \
I recommend putting this behind a controlled[^3] VPN, or an HTTPS proxy with good access control. \
Or regenerate the key every use.
Make a GET request to `/key/<base64 encoded key>`
Example:
Make a GET request to `/kill/<secret key>`:
```
https://127.0.0.1:57932/key/lNwANMSZhLiTWhNxSoqQ5Q==
|_ server address _| |_ base64 encoded key _|
GET http://127.0.0.1:57932/kill/lNwANMSZhLiTWhNxSoqQ5Q==
|_ 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` \
To use it with HTTP server, encode it to base64.
The key is in `plugins/Tweaks724/killswitch secret key.txt`. You can provide your own key. The key should be plaintext (not bytes).
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>
<artifactId>tweaks</artifactId>
<version>0.1.14</version>
<version>0.1.16-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.craftbukkit.version>v1_21_R3</project.craftbukkit.version>
<project.minecraft.version>1.21.4</project.minecraft.version>
<project.craftbukkit.version>v1_21_R4</project.craftbukkit.version>
<project.minecraft.version>1.21.5</project.minecraft.version>
<project.spigot.version>${project.minecraft.version}-R0.1-SNAPSHOT</project.spigot.version>
</properties>
@ -44,7 +44,7 @@
</goals>
<configuration>
<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" />
</replace>
</target>
@ -58,7 +58,7 @@
</goals>
<configuration>
<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" />
</replace>
</target>
@ -128,7 +128,7 @@
<dependency>
<groupId>org.spigotmc</groupId>
<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>
</dependency>
<dependency>
@ -147,7 +147,7 @@
<dependency>
<groupId>eu.m724</groupId>
<artifactId>mstats-spigot</artifactId>
<version>0.1.0</version>
<version>0.1.2</version>
<scope>provided</scope>
</dependency>
<dependency>
@ -168,6 +168,6 @@
<scm>
<developerConnection>scm:git:git@git.m724.eu:Minecon724/tweaks724.git</developerConnection>
<tag>tweaks-0.1.14</tag>
<tag>HEAD</tag>
</scm>
</project>

View file

@ -10,8 +10,11 @@ import eu.m724.tweaks.module.TweaksModule;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class DebugLogger {
private DebugLogger() {}
static Logger logger;
public static void info(String message, Object... format) {
@ -35,20 +38,16 @@ public class DebugLogger {
}
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 (!logger.isLoggable(level)) {
return;
}
if (caller.startsWith("eu.m724.tweaks."))
caller = caller.substring(15);
message = message.formatted(format);
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
// colors text gray (cyan is close to gray)
@ -61,5 +60,30 @@ public class DebugLogger {
}
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.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;
@ -157,6 +158,8 @@ public class TweaksPlugin extends MStatsPlugin {
TweaksModule.init(DurabilityModule.class);
TweaksModule.init(WordCoordsModule.class);
/* end modules */
if (config.metrics()) {
@ -164,7 +167,7 @@ public class TweaksPlugin extends MStatsPlugin {
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() {

View file

@ -21,32 +21,60 @@ import java.lang.reflect.InvocationTargetException;
import java.util.function.Consumer;
public abstract class TweaksModule {
/**
* Called on module initialize.
*/
protected abstract void onInit();
void init() {
var name = getClass().getSimpleName();
DebugLogger.finer("Initializing " + name);
DebugLogger.finer("Initializing module " + name);
long start = System.nanoTime();
this.onInit();
long end = System.nanoTime();
DebugLogger.fine("Initialized %s in %d µs", name, (end - start) / 1000);
}
/**
* Gets the plugin instance.
*
* @return The plugin instance
*/
protected TweaksPlugin getPlugin() {
return TweaksPlugin.getInstance();
}
/**
* Gets the plugin config.
*
* @return The plugin config
*/
protected TweaksConfig getConfig() {
return TweaksConfig.getConfig();
}
/**
* Registers an event listener.
*
* @param listener The event listener
*/
protected void registerEvents(Listener listener) {
DebugLogger.finer("Registered listener: " + listener.getClass().getName());
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) {
ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
@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) {
ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
@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) {
getPlugin().getCommand(command).setExecutor(executor);
DebugLogger.finer("Registered command: " + command);
}
/**
* Initializes a module.
*
* @param clazz The class of the initialized module
* @return The module instance
* @param <T> The type of the initialized module
*/
public static <T extends TweaksModule> T init(Class<T> clazz) {
T module;
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.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.craftbukkit.v1_21_R4.CraftRegistry;
import org.bukkit.craftbukkit.v1_21_R4.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;

View file

@ -6,63 +6,59 @@
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 eu.m724.tweaks.module.killswitch.server.KillswitchSecureHttpServer;
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.nio.file.Path;
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();
public class KillswitchModule extends TweaksModule implements CommandExecutor {
private String loadSecret(Path file) {
String secret;
private byte[] secret;
private String secretEncoded;
private void loadKey(File file) {
if (file.exists()) {
if (Files.exists(file)) {
try {
this.secret = Files.readAllBytes(file.toPath());
secret = Files.readString(file);
} catch (IOException e) {
throw new RuntimeException("Reading killswitch key", e);
}
DebugLogger.fine("Loaded key");
} else {
byte[] buf = new byte[16];
try {
byte[] buf = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(buf);
Files.write(file.toPath(), buf);
secret = Base64.getEncoder().encodeToString(buf);
Files.writeString(file, secret);
} catch (IOException | NoSuchAlgorithmException e) {
throw new RuntimeException("Generating killswitch key", e);
}
this.secret = buf;
DebugLogger.info("Killswitch key generated and saved to " + file.getPath());
DebugLogger.info("Killswitch secret key generated and saved to %s", file);
}
this.secretEncoded = Base64.getEncoder().encodeToString(secret);
return secret;
}
@Override
protected void onInit() {
registerCommand("servkill", this);
if (getConfig().killswitchListen() != null) {
loadKey(new File(getPlugin().getDataFolder(), "storage/killswitch key"));
if (getConfig().killswitchListen() == null) {
return;
}
ratelimit.runTaskTimerAsynchronously(getPlugin(), 0, 20 * 300);
String secret = loadSecret(getPlugin().getDataFolder().toPath().resolve("killswitch secret key.txt"));
var listenAddress = getConfig().killswitchListen().split(":");
InetSocketAddress bindAddress;
@ -72,47 +68,23 @@ public class KillswitchModule extends TweaksModule implements CommandExecutor, H
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);
}
}
new KillswitchSecureHttpServer(secret, this::kill).start(bindAddress);
DebugLogger.fine("HTTP server started");
}
private void kill() {
DebugLogger.info("Killing server on request");
Runtime.getRuntime().halt(0);
}
@Override
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();
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;
import java.util.concurrent.TimeUnit;
public class PlayerPomodoro {
private int pomodori = 0;
private int intervalsDone = 0;
private boolean isBreak = false;
// this is for both break and not break
private long intervalStart = -1;
private boolean breaktime = false;
private long currentIntervalStartedAtNanos = -1;
/**
* A "pomodoro" is the 25-minute cycle you take breaks after<br>
* 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 int getPomodori() {
return pomodori;
public int getIntervalsDone() {
return intervalsDone;
}
public boolean isBreaktime() {
return breaktime;
}
public boolean isLongBreak() {
return (intervalsDone + 1) % PomodoroModule.INTERVALS_BEFORE_LONG_BREAK == 0;
}
/**
* When did the current interval start<br>
* Or when did the break start
*
* @see PlayerPomodoro#isBreak()
* @return The time the current interval or break started, in milliseconds
*/
public long getIntervalStart() {
return intervalStart;
public long getCurrentIntervalStartedAtNanos() {
return currentIntervalStartedAtNanos;
}
public int getCycleDurationSeconds() {
return isBreak ? (pomodori < 3 ? 300 : 1200) : 1500;
public int getCurrentIntervalDurationSeconds() {
if (breaktime) {
if (isLongBreak()) {
return PomodoroModule.LONG_BREAK_DURATION_SECONDS;
}
public long getRemainingSeconds(long now) {
return getCycleDurationSeconds() - (now - getIntervalStart()) / 1000000000;
return PomodoroModule.SHORT_BREAK_DURATION_SECONDS;
} else {
return PomodoroModule.INTERVAL_DURATION_SECONDS;
}
}
/**
* Is it a break currently
*/
public boolean isBreak() {
return isBreak;
public long getCurrentIntervalDurationNanos() {
return TimeUnit.SECONDS.toNanos(getCurrentIntervalDurationSeconds());
}
public boolean isCycleComplete() {
return intervalStart + getCycleDurationSeconds() * 1000000000L < System.nanoTime();
public long getCurrentIntervalRemainingNanos(long nowNanos) {
long elapsed = nowNanos - getCurrentIntervalStartedAtNanos();
return getCurrentIntervalDurationNanos() - elapsed;
}
public boolean isIntervalComplete(long nowNanos) {
return currentIntervalStartedAtNanos + getCurrentIntervalDurationNanos() < nowNanos;
}
/**
* Resets and starts the timer
*/
public void start() {
this.pomodori = 0;
this.isBreak = false;
this.intervalStart = System.nanoTime();
this.intervalsDone = 0;
this.breaktime = false;
this.currentIntervalStartedAtNanos = System.nanoTime();
}
/**
* Completes a cycle
*/
public void next() {
if (isBreak) { // from break to interval
this.pomodori++;
this.pomodori %= 4;
public void startBreakOrNextInterval() {
if (breaktime) { // from break to interval
this.intervalsDone++;
this.intervalsDone %= PomodoroModule.INTERVALS_BEFORE_LONG_BREAK;
}
this.intervalStart = System.nanoTime();
isBreak = !isBreak;
this.currentIntervalStartedAtNanos = System.nanoTime();
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;
import eu.m724.tweaks.Language;
import eu.m724.tweaks.config.TweaksConfig;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -18,25 +21,34 @@ public class PomodoroCommands implements CommandExecutor {
Player player = (Player) sender;
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 ("stop".equals(action)) {
Pomodoros.remove(player);
sender.sendMessage("Pomodoro disabled");
PlayerPomodoroTracker.remove(player);
sender.spigot().sendMessage(Language.getComponent("pomodoroStopped", ChatColor.GREEN, label));
} else {
if (pomodoro.isCycleComplete()) {
pomodoro.next();
if (pomodoro.isIntervalComplete(now)) {
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 {
if ("start".equals(action)) {
pomodoro = Pomodoros.create(player);
pomodoro = PlayerPomodoroTracker.create(player);
pomodoro.start();
sender.spigot().sendMessage(Pomodoros.formatTimer(pomodoro, pomodoro.getCycleDurationSeconds()));
sender.spigot().sendMessage(PomodoroModule.formatTimer(pomodoro, now));
} 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;
import eu.m724.tweaks.config.TweaksConfig;
import net.md_5.bungee.api.chat.ComponentBuilder;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.*;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
public class PomodoroListener implements Listener {
private final boolean force = TweaksConfig.getConfig().pomodoroForce();
private final TweaksConfig config = TweaksConfig.getConfig();
@EventHandler
public void onPlayerLogin(PlayerLoginEvent event) {
// Joining ends break
Player player = event.getPlayer();
PlayerPomodoro pomodoro = Pomodoros.get(player);
PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
if (pomodoro == null) return;
long remaining = pomodoro.getRemainingSeconds(System.nanoTime());
long now = System.nanoTime();
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (pomodoro.isBreak()) {
if (pomodoro.isCycleComplete()) {
pomodoro.next();
if (pomodoro.isBreaktime()) {
if (remaining > 0 && config.pomodoroForce()) {
player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText());
} else {
if (force) {
event.getPlayer().kickPlayer(
new ComponentBuilder()
.append(Pomodoros.formatTimer(pomodoro, remaining))
.build().toLegacyText()
);
}
pomodoro.startBreakOrNextInterval();
}
}
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
// Quitting starts break
Player player = event.getPlayer();
PlayerPomodoro pomodoro = Pomodoros.get(player);
PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
if (pomodoro == null) return;
if (!pomodoro.isBreak() && pomodoro.isCycleComplete()) {
pomodoro.next();
if (pomodoro.isBreaktime()) return;
long now = System.nanoTime();
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (remaining <= 0) {
pomodoro.startBreakOrNextInterval();
}
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
Player player = event.getPlayer();
PlayerPomodoro timer = Pomodoros.get(player);
if (timer == null) return;
public void onPlayerToggleSneak(PlayerToggleSneakEvent event) {
// Sneaking ends break
if (!event.isSneaking()) return;
if (timer.isBreak() && timer.getRemainingSeconds(System.nanoTime()) <= 0)
timer.next(); // resume timer if break ended
Player player = event.getPlayer();
PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
if (pomodoro == null) return;
if (!pomodoro.isBreaktime()) return;
long now = System.nanoTime();
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (remaining <= 0) {
pomodoro.startBreakOrNextInterval();
}
}
}

View file

@ -6,9 +6,23 @@
package eu.m724.tweaks.module.pomodoro;
import eu.m724.tweaks.Language;
import eu.m724.tweaks.config.TweaksConfig;
import eu.m724.tweaks.module.TweaksModule;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import java.util.concurrent.TimeUnit;
public class PomodoroModule extends TweaksModule {
static final int INTERVAL_DURATION_SECONDS = 10;
static final int SHORT_BREAK_DURATION_SECONDS = (int) TimeUnit.MINUTES.toSeconds(5);
static final int LONG_BREAK_DURATION_SECONDS = (int) TimeUnit.MINUTES.toSeconds(20);
static final int INTERVALS_BEFORE_LONG_BREAK = 4;
static final int KICK_DELAY_SECONDS = 60;
@Override
protected void onInit() {
registerEvents(new PomodoroListener());
@ -16,4 +30,66 @@ public class PomodoroModule extends TweaksModule {
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.config.TweaksConfig;
import net.md_5.bungee.api.ChatMessageType;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.concurrent.TimeUnit;
public class PomodoroRunnable extends BukkitRunnable {
private final boolean force = TweaksConfig.getConfig().pomodoroForce();
private final TweaksConfig config = TweaksConfig.getConfig();
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;
PlayerPomodoroTracker.timers.forEach((uuid, pomodoro) -> {
long remainingNanos = pomodoro.getCurrentIntervalRemainingNanos(now);
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
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);
if (remaining < -60 && force) {
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
pomodoro.next();
player.kickPlayer(Pomodoros.formatTimer(pomodoro, pomodoro.getRemainingSeconds(now)).toLegacyText());
});
if (remainingSecs < -PomodoroModule.KICK_DELAY_SECONDS && config.pomodoroForce()) {
pomodoro.startBreakOrNextInterval();
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () ->
player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText())
);
}
}
} else {
if (remainingNanos <= 0) {
// Start break automatically if the player is offline
if (!pomodoro.isBreaktime()) {
pomodoro.startBreakOrNextInterval();
}
}
}
});

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 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.Listener;
import org.bukkit.event.world.WorldLoadEvent;

View file

@ -8,7 +8,7 @@ api-version: 1.21.1
softdepend: [ProtocolLib]
libraries:
- eu.m724:mstats-spigot:0.1.0
- eu.m724:mstats-spigot:0.1.2
commands:
chat:
@ -43,6 +43,10 @@ commands:
durabilityalert:
description: Durability alert toggle
permission: tweaks724.durabilityalert
wordcoords:
description: Word to coords conversion
permission: tweaks724.wordcoords
aliases: [woco, wc, w3w]
permissions:
tweaks724.chatmanage:
@ -63,6 +67,8 @@ permissions:
default: false
tweaks724.durabilityalert:
default: true
tweaks724.wordcoords:
default: true
7weaks724.ignore.this:
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.
# 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!
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
clickToCopy = Click to copy to clipboard
clickToCopy = Click to copy
clickToExecuteCommand = Click to execute command
durabilityEnabled = Enabled durability alert
durabilityDisabled = Disabled durability alert
# When console executes /wordcoords without arguments
wordCoordsPlayerOnly = Only players can execute this command without arguments.
wordCoordsOutOfRange = Those coordinates are invalid.
wordCoordsInvalidWord = Invalid word or coordinate: "%s"
wordCoordsProvideZ = Please provide the Z coordinate.
# /pomodoro
pomodoroStopped = Pomodoro stopped. Restart it with /%s start
pomodoroStart = Start pomodoro with /%s start
pomodoroShortBreak = Short break
pomodoroLongBreak = Long break

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,10 @@
#!/bin/sh
#
# 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.
#
FILENAME=MS4yMS4xLTQK.tar.zst
FILENAME=nms_1_21_1+3+4+5.tar.zst
CHECKSUM=0ea6267ce39213ddb0d6a7669d8021283350bb56de0d65f2e9fddd3c85337c5fbd204272b07c7e8532c6eaef46c3a47a39bac183abd6f4cfa7b171e08b4d7029
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"
rm $FILENAME
else