Compare commits

..

No commits in common. "master" and "tweaks-0.1.14" have entirely different histories.

35 changed files with 456 additions and 3203 deletions

View file

@ -13,8 +13,6 @@ 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 Normal file
View file

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

6
.idea/copyright/gpl3.xml generated Normal file
View file

@ -0,0 +1,6 @@
<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>

7
.idea/copyright/profiles_settings.xml generated Normal file
View file

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

4
.idea/misc.xml generated
View file

@ -8,5 +8,7 @@
</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 Normal file
View file

@ -0,0 +1,124 @@
<?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
View file

@ -1,110 +0,0 @@
<?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.](docs/BUILDING.md) - To use modules marked <sup><sub>N</sub></sup>, you must use a JAR [made for the exact server version.](/Minecon724/tweaks724/src/branch/master/docs/BUILDING.md)
# Features # 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 four directions and stuff like beds, lodestones, death pos (TODO) etc. Holding a compass shows a bar with 4 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 are five players on the server. A night is 10 minutes long. \ There's 5 players on the server. A night is 10 minutes long. \
Each player can instantly skip 2 minutes of the night at any time, even if others aren't sleeping 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,30 +85,26 @@ 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 controlled over the internet. \ Adds a "gateway" item that are controlled over internet. \
[RETSTONE.md for more info](docs/RETSTONE.md) [RETSTONE.md for more info](/Minecon724/tweaks724/src/branch/master/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, by command or HTTP request. Quickly kills (terminates) the server on trigger, via command or HTTP request.
[KILLSWITCH.md for more info](docs/KILLSWITCH.md) [KILLSWITCH.md for more info](/Minecon724/tweaks724/src/branch/master/docs/KILLSWITCH.md)
### Swing through grasses ### Swing through grass
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` - display player ping <sup><sub>P</sub></sup> \ - `/ping` - displays 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 joins That allows for more precision (decimal places) and to get the ping immediately after a player join

View file

@ -1,34 +1,32 @@
Killswitch immediately stops (kills) the server. Killswitch immediately stops the server.
### Warning ### Warning
This terminates the server process (not the OS), meaning it's **like you pulled the power cable.** \ This terminates the server process, meaning it's like you'd pull the power. \
You lose some progress (since the last auto save), or in the worst case, **data gets corrupted.** So you will lose some progress (since the last auto save), or worst case your world (or other data) gets corrupted.
Terminal froze after kill? Do `reset` Terminal froze after kill? `reset`
### By command ### Over a 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** with a permission plugin—it's not automatically assigned, not even to OPs. You must grant the permission manually, like with a permission plugin - it's not automatically assigned even to OPs.
### Over the internet ### Over HTTP
**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. \ HTTP is insecure, meaning others *could* 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. \ 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 every use. Or regenerate the key after every usage.
Make a GET request to `/kill/<secret key>`: Make a GET request to `/key/<base64 encoded key>`
Example:
``` ```
GET http://127.0.0.1:57932/kill/lNwANMSZhLiTWhNxSoqQ5Q== https://127.0.0.1:57932/key/lNwANMSZhLiTWhNxSoqQ5Q==
|_ endpoint _| |_ secret key _| |_ server address _| |_ base64 encoded key _|
``` ```
There is no response; the connection is closed immediately, no matter what.[^4] The response is a 404 no matter what. Either way, you will notice that the server has stopped.
The key is in `plugins/Tweaks724/killswitch secret key.txt`. You can provide your own key. The key should be plaintext (not bytes). The key is generated to `plugins/Tweaks724/storage/killswitch key` \
To use it with HTTP server, encode it to base64.
The ratelimit is one request per 2 minutes. **Do not send requests when blocked, it resets the timer!** Rate limit is 1 request / 5 minutes
[^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.16-SNAPSHOT</version> <version>0.1.14</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_R4</project.craftbukkit.version> <project.craftbukkit.version>v1_21_R3</project.craftbukkit.version>
<project.minecraft.version>1.21.5</project.minecraft.version> <project.minecraft.version>1.21.4</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_R4" value="${project.craftbukkit.version}" dir="src/main"> <replace token="v1_21_R3" 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_R4" dir="src/main"> <replace token="${project.craftbukkit.version}" value="v1_21_R3" 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> <!-- oldest supported version --> <version>1.21.1-R0.1-SNAPSHOT</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.2</version> <version>0.1.0</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>HEAD</tag> <tag>tweaks-0.1.14</tag>
</scm> </scm>
</project> </project>

View file

@ -10,11 +10,8 @@ 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) {
@ -38,16 +35,20 @@ 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;
if (!logger.isLoggable(level)) { var caller = Thread.currentThread().getStackTrace()[3].getClassName();
return;
if (caller.equals(TweaksModule.class.getName())) {
var pcaller = Thread.currentThread().getStackTrace()[4].getClassName();
if (pcaller.endsWith("Module"))
caller = pcaller;
} }
message = message.formatted(format); if (caller.startsWith("eu.m724.tweaks."))
caller = caller.substring(15);
if (logger.getLevel().intValue() <= Level.FINE.intValue()) { message = "[" + caller + "] " + message.formatted(format);
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)
@ -60,30 +61,5 @@ 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,7 +27,6 @@ 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;
@ -158,8 +157,6 @@ 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()) {
@ -167,7 +164,7 @@ public class TweaksPlugin extends MStatsPlugin {
mStats(1); mStats(1);
} }
DebugLogger.fine("Took %.3f milliseconds", (System.nanoTime() - start) / 1000000.0); DebugLogger.fine("Took %.3f milliseconds".formatted((System.nanoTime() - start) / 1000000.0));
} }
private String getTargetVersion() { private String getTargetVersion() {

View file

@ -21,60 +21,32 @@ 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 module " + name); DebugLogger.finer("Initializing " + 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
@ -83,16 +55,9 @@ public abstract class TweaksModule {
} }
}); });
DebugLogger.finer("Registered outgoing packet listener: " + packetType.name()); DebugLogger.finer("Registered packet send: " + 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
@ -101,27 +66,14 @@ public abstract class TweaksModule {
} }
}); });
DebugLogger.finer("Registered incoming packet listener: " + packetType.name()); DebugLogger.finer("Registered packet receive: " + 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_R4.CraftRegistry; import org.bukkit.craftbukkit.v1_21_R3.CraftRegistry;
import org.bukkit.craftbukkit.v1_21_R4.entity.CraftPlayer; import org.bukkit.craftbukkit.v1_21_R3.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,59 +6,63 @@
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 { public class KillswitchModule extends TweaksModule implements CommandExecutor, HttpHandler {
private String loadSecret(Path file) { private final Ratelimit ratelimit = new Ratelimit();
String secret;
if (Files.exists(file)) { private byte[] secret;
private String secretEncoded;
private void loadKey(File file) {
if (file.exists()) {
try { try {
secret = Files.readString(file); this.secret = Files.readAllBytes(file.toPath());
} 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 {
try {
byte[] buf = new byte[16]; byte[] buf = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(buf);
secret = Base64.getEncoder().encodeToString(buf); try {
Files.writeString(file, secret); SecureRandom.getInstanceStrong().nextBytes(buf);
Files.write(file.toPath(), buf);
} catch (IOException | NoSuchAlgorithmException e) { } catch (IOException | NoSuchAlgorithmException e) {
throw new RuntimeException("Generating killswitch key", e); throw new RuntimeException("Generating killswitch key", e);
} }
DebugLogger.info("Killswitch secret key generated and saved to %s", file); this.secret = buf;
DebugLogger.info("Killswitch key generated and saved to " + file.getPath());
} }
return secret; this.secretEncoded = Base64.getEncoder().encodeToString(secret);
} }
@Override @Override
protected void onInit() { protected void onInit() {
registerCommand("servkill", this); registerCommand("servkill", this);
if (getConfig().killswitchListen() == null) { if (getConfig().killswitchListen() != null) {
return; loadKey(new File(getPlugin().getDataFolder(), "storage/killswitch key"));
}
String secret = loadSecret(getPlugin().getDataFolder().toPath().resolve("killswitch secret key.txt")); ratelimit.runTaskTimerAsynchronously(getPlugin(), 0, 20 * 300);
var listenAddress = getConfig().killswitchListen().split(":"); var listenAddress = getConfig().killswitchListen().split(":");
InetSocketAddress bindAddress; InetSocketAddress bindAddress;
@ -68,23 +72,47 @@ public class KillswitchModule extends TweaksModule implements CommandExecutor {
bindAddress = new InetSocketAddress(listenAddress[0], Integer.parseInt(listenAddress[1])); bindAddress = new InetSocketAddress(listenAddress[0], Integer.parseInt(listenAddress[1]));
} }
new KillswitchSecureHttpServer(secret, this::kill).start(bindAddress); try {
HttpServer server = HttpServer.create(bindAddress, 0);
DebugLogger.fine("HTTP server started"); server.createContext("/", this);
server.setExecutor(null);
server.start();
DebugLogger.fine("server started");
} catch (IOException e) {
throw new RuntimeException("Starting HTTP server", e);
}
}
} }
private void kill() { 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

@ -0,0 +1,26 @@
/*
* Copyright (C) 2025 Minecon724
* Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
* in the project root for the full license text.
*/
package eu.m724.tweaks.module.killswitch;
import org.bukkit.scheduler.BukkitRunnable;
import java.net.InetAddress;
import java.util.HashSet;
import java.util.Set;
public class Ratelimit extends BukkitRunnable {
private Set<InetAddress> requests = new HashSet<>();
boolean submitRequest(InetAddress address) {
return requests.add(address);
}
@Override
public void run() {
requests = new HashSet<>();
}
}

View file

@ -1,84 +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.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

@ -1,37 +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.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,77 +6,70 @@
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 intervalsDone = 0; private int pomodori = 0;
private boolean breaktime = false; private boolean isBreak = false;
private long currentIntervalStartedAtNanos = -1; // this is for both break and not break
private long intervalStart = -1;
public int getIntervalsDone() { /**
return intervalsDone; * 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 boolean isBreaktime() { */
return breaktime; public int getPomodori() {
} return pomodori;
public boolean isLongBreak() {
return (intervalsDone + 1) % PomodoroModule.INTERVALS_BEFORE_LONG_BREAK == 0;
} }
/** /**
* @return The time the current interval or break started, in milliseconds * When did the current interval start<br>
* Or when did the break start
*
* @see PlayerPomodoro#isBreak()
*/ */
public long getCurrentIntervalStartedAtNanos() { public long getIntervalStart() {
return currentIntervalStartedAtNanos; return intervalStart;
} }
public int getCurrentIntervalDurationSeconds() { public int getCycleDurationSeconds() {
if (breaktime) { return isBreak ? (pomodori < 3 ? 300 : 1200) : 1500;
if (isLongBreak()) {
return PomodoroModule.LONG_BREAK_DURATION_SECONDS;
} }
return PomodoroModule.SHORT_BREAK_DURATION_SECONDS; public long getRemainingSeconds(long now) {
} else { return getCycleDurationSeconds() - (now - getIntervalStart()) / 1000000000;
return PomodoroModule.INTERVAL_DURATION_SECONDS;
}
} }
public long getCurrentIntervalDurationNanos() { /**
return TimeUnit.SECONDS.toNanos(getCurrentIntervalDurationSeconds()); * Is it a break currently
*/
public boolean isBreak() {
return isBreak;
} }
public long getCurrentIntervalRemainingNanos(long nowNanos) { public boolean isCycleComplete() {
long elapsed = nowNanos - getCurrentIntervalStartedAtNanos(); return intervalStart + getCycleDurationSeconds() * 1000000000L < System.nanoTime();
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.intervalsDone = 0; this.pomodori = 0;
this.breaktime = false; this.isBreak = false;
this.currentIntervalStartedAtNanos = System.nanoTime(); this.intervalStart = System.nanoTime();
} }
/** /**
* Completes a cycle * Completes a cycle
*/ */
public void startBreakOrNextInterval() { public void next() {
if (breaktime) { // from break to interval if (isBreak) { // from break to interval
this.intervalsDone++; this.pomodori++;
this.intervalsDone %= PomodoroModule.INTERVALS_BEFORE_LONG_BREAK; this.pomodori %= 4;
} }
this.currentIntervalStartedAtNanos = System.nanoTime(); this.intervalStart = System.nanoTime();
breaktime = !breaktime; isBreak = !isBreak;
} }
} }

View file

@ -1,29 +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 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,9 +6,6 @@
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;
@ -21,34 +18,25 @@ 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 = PlayerPomodoroTracker.get(player); PlayerPomodoro pomodoro = Pomodoros.get(player);
long now = System.nanoTime();
if (pomodoro != null) { if (pomodoro != null) {
if ("stop".equals(action)) { if ("stop".equals(action)) {
PlayerPomodoroTracker.remove(player); Pomodoros.remove(player);
sender.spigot().sendMessage(Language.getComponent("pomodoroStopped", ChatColor.GREEN, label)); sender.sendMessage("Pomodoro disabled");
} else { } else {
if (pomodoro.isIntervalComplete(now)) { if (pomodoro.isCycleComplete()) {
pomodoro.startBreakOrNextInterval(); pomodoro.next();
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 = PlayerPomodoroTracker.create(player); pomodoro = Pomodoros.create(player);
pomodoro.start(); pomodoro.start();
sender.spigot().sendMessage(Pomodoros.formatTimer(pomodoro, pomodoro.getCycleDurationSeconds()));
sender.spigot().sendMessage(PomodoroModule.formatTimer(pomodoro, now));
} else { } else {
// TODO help? sender.sendMessage("Start pomodoro with /pom start");
sender.spigot().sendMessage(Language.getComponent("pomodoroStart", ChatColor.GOLD, label));
} }
} }

View file

@ -7,68 +7,56 @@
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.PlayerLoginEvent; import org.bukkit.event.player.*;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
public class PomodoroListener implements Listener { public class PomodoroListener implements Listener {
private final TweaksConfig config = TweaksConfig.getConfig(); private final boolean force = TweaksConfig.getConfig().pomodoroForce();
@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 = PlayerPomodoroTracker.get(player); PlayerPomodoro pomodoro = Pomodoros.get(player);
if (pomodoro == null) return; if (pomodoro == null) return;
long now = System.nanoTime(); long remaining = pomodoro.getRemainingSeconds(System.nanoTime());
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (pomodoro.isBreaktime()) { if (pomodoro.isBreak()) {
if (remaining > 0 && config.pomodoroForce()) { if (pomodoro.isCycleComplete()) {
player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText()); pomodoro.next();
} else { } else {
pomodoro.startBreakOrNextInterval(); if (force) {
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 = PlayerPomodoroTracker.get(player); PlayerPomodoro pomodoro = Pomodoros.get(player);
if (pomodoro == null) return; if (pomodoro == null) return;
if (pomodoro.isBreaktime()) return; if (!pomodoro.isBreak() && pomodoro.isCycleComplete()) {
pomodoro.next();
long now = System.nanoTime();
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (remaining <= 0) {
pomodoro.startBreakOrNextInterval();
} }
} }
@EventHandler @EventHandler
public void onPlayerToggleSneak(PlayerToggleSneakEvent event) { public void onPlayerMove(PlayerMoveEvent event) {
// Sneaking ends break
if (!event.isSneaking()) return;
Player player = event.getPlayer(); Player player = event.getPlayer();
PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player); PlayerPomodoro timer = Pomodoros.get(player);
if (pomodoro == null) return; if (timer == null) return;
if (!pomodoro.isBreaktime()) return; if (timer.isBreak() && timer.getRemainingSeconds(System.nanoTime()) <= 0)
timer.next(); // resume timer if break ended
long now = System.nanoTime();
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
if (remaining <= 0) {
pomodoro.startBreakOrNextInterval();
}
} }
} }

View file

@ -6,23 +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 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());
@ -30,66 +16,4 @@ 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,49 +9,34 @@ 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 TweaksConfig config = TweaksConfig.getConfig(); private final boolean force = TweaksConfig.getConfig().pomodoroForce();
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();
PlayerPomodoroTracker.timers.forEach((uuid, pomodoro) -> { Bukkit.getOnlinePlayers().forEach(player -> {
long remainingNanos = pomodoro.getCurrentIntervalRemainingNanos(now); PlayerPomodoro pomodoro = Pomodoros.get(player);
long remainingSecs = TimeUnit.NANOSECONDS.toSeconds(remainingNanos); if (pomodoro == null) return;
// TODO optimize? long remaining = pomodoro.getRemainingSeconds(now);
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, PomodoroModule.formatTimer(pomodoro, now)); player.spigot().sendMessage(ChatMessageType.ACTION_BAR, Pomodoros.formatTimer(pomodoro, remaining));
if (remainingNanos <= 0) { if (remaining <= 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) {
if (remainingSecs < -PomodoroModule.KICK_DELAY_SECONDS && config.pomodoroForce()) { plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
pomodoro.startBreakOrNextInterval(); pomodoro.next();
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

@ -0,0 +1,71 @@
/*
* 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

@ -1,45 +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.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

@ -1,180 +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.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

@ -1,62 +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.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

@ -1,82 +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.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

@ -1,113 +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.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_R4.CraftWorld; import org.bukkit.craftbukkit.v1_21_R3.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.2 - eu.m724:mstats-spigot:0.1.0
commands: commands:
chat: chat:
@ -43,10 +43,6 @@ 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:
@ -67,8 +63,6 @@ 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,24 +30,12 @@ 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 occurred. Please try again. If this persists, contact an administrator. authKickError = An error occured. Please try again. If this persists, contact an administrator.
redstoneGatewayItem = Redstone gateway redstoneGatewayItem = Redstone gateway
clickToCopy = Click to copy clickToCopy = Click to copy to clipboard
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,10 +1,15 @@
#!/bin/sh #!/bin/sh
FILENAME=nms_1_21_1+3+4+5.tar.zst #
CHECKSUM=0ea6267ce39213ddb0d6a7669d8021283350bb56de0d65f2e9fddd3c85337c5fbd204272b07c7e8532c6eaef46c3a47a39bac183abd6f4cfa7b171e08b4d7029 # 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
curl -O https://36ab09b1.m724.eu/$FILENAME curl -O https://36ab09b1.m724.eu/$FILENAME
if [ "$(sha512sum $FILENAME)" = "$CHECKSUM $FILENAME" ]; then if [ "$(sha512sum $FILENAME)" = "475b931b6dde126aafd3f959bd02e122aa3c671ad11e83cbe1a9c9ea771a424f203381aa31e4ab40052dae1bfbe96c61daa81add1afab46dd423a5f038d68a6b MS4yMS4xLTQK.tar.zst" ]; then
tar -xaf $FILENAME -C "$1" tar -xaf $FILENAME -C "$1"
rm $FILENAME rm $FILENAME
else else