Compare commits
19 commits
tweaks-0.1
...
master
Author | SHA1 | Date | |
---|---|---|---|
1c19a713d8 | |||
30bd73e5be | |||
2773275e0b | |||
670cbd9635 | |||
ba7f1aa226 | |||
e67c187f4e | |||
2213ebe3cf | |||
2c835a4eab | |||
352807483c | |||
e3f34ec46f | |||
a8e67dbe26 | |||
4f752bef61 | |||
41583939ac | |||
22bb2d16d2 | |||
![]() |
598902ef33 | ||
![]() |
7cd334f4a2 | ||
![]() |
b421b48e51 | ||
![]() |
684e31bf07 | ||
![]() |
cf654fcb42 |
35 changed files with 3203 additions and 456 deletions
.forgejo/workflows
.idea
README.mddocs
pom.xmlsrc/main
java/eu/m724/tweaks
DebugLogger.javaTweaksPlugin.java
module
resources
tools
|
@ -13,6 +13,8 @@ jobs:
|
||||||
- name: Download NMS
|
- name: Download NMS
|
||||||
run: ./tools/download_nms.sh ~
|
run: ./tools/download_nms.sh ~
|
||||||
|
|
||||||
|
- name: Build for 1.21.5
|
||||||
|
run: ./mvnw package -Dproject.minecraft.version=1.21.5 -Dproject.craftbukkit.version=v1_21_R4
|
||||||
|
|
||||||
- name: Build for 1.21.4
|
- name: Build for 1.21.4
|
||||||
run: ./mvnw package -Dproject.minecraft.version=1.21.4 -Dproject.craftbukkit.version=v1_21_R3
|
run: ./mvnw package -Dproject.minecraft.version=1.21.4 -Dproject.craftbukkit.version=v1_21_R3
|
||||||
|
|
3
.idea/.gitignore
generated
vendored
3
.idea/.gitignore
generated
vendored
|
@ -1,3 +0,0 @@
|
||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
6
.idea/copyright/gpl3.xml
generated
6
.idea/copyright/gpl3.xml
generated
|
@ -1,6 +0,0 @@
|
||||||
<component name="CopyrightManager">
|
|
||||||
<copyright>
|
|
||||||
<option name="notice" value="Copyright (C) &#36;originalComment.match("Copyright \(c\) (\d+)", 1, "-", "&#36;today.year")&#36;today.year Minecon724 Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text." />
|
|
||||||
<option name="myName" value="gpl3" />
|
|
||||||
</copyright>
|
|
||||||
</component>
|
|
7
.idea/copyright/profiles_settings.xml
generated
7
.idea/copyright/profiles_settings.xml
generated
|
@ -1,7 +0,0 @@
|
||||||
<component name="CopyrightManager">
|
|
||||||
<settings default="gpl3">
|
|
||||||
<module2copyright>
|
|
||||||
<element module="All" copyright="gpl3" />
|
|
||||||
</module2copyright>
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
4
.idea/misc.xml
generated
4
.idea/misc.xml
generated
|
@ -8,7 +8,5 @@
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK" />
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
|
||||||
</component>
|
|
||||||
</project>
|
</project>
|
124
.idea/uiDesigner.xml
generated
124
.idea/uiDesigner.xml
generated
|
@ -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
110
.idea/workspace.xml
generated
Normal 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">{
|
||||||
|
"customColor": "",
|
||||||
|
"associatedIndex": 1
|
||||||
|
}</component>
|
||||||
|
<component name="ProjectId" id="2xdUuJ0x0gqOBEPQKKoh4gRpUH9" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent">{
|
||||||
|
"keyToString": {
|
||||||
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"git-widget-placeholder": "master"
|
||||||
|
}
|
||||||
|
}</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>
|
28
README.md
28
README.md
|
@ -14,7 +14,7 @@ Stuff no<sub><sup>t many</sup></sub> other plugins do.
|
||||||
Dependencies:
|
Dependencies:
|
||||||
- **1.21.1 and newer**
|
- **1.21.1 and newer**
|
||||||
- [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/)
|
- [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/)
|
||||||
- To use modules marked <sup><sub>N</sub></sup>, you must use a JAR [made for the exact server version.](/Minecon724/tweaks724/src/branch/master/docs/BUILDING.md)
|
- To use modules marked <sup><sub>N</sub></sup>, you must use a JAR [made for the exact server version.](docs/BUILDING.md)
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ Random MOTD for every ping
|
||||||
`/chatmanage` - create, delete, modify etc. (`tweaks724.chatmanage`)
|
`/chatmanage` - create, delete, modify etc. (`tweaks724.chatmanage`)
|
||||||
|
|
||||||
### Compass
|
### Compass
|
||||||
Holding a compass shows a bar with 4 directions and stuff like beds, lodestones, death pos (TODO) etc.
|
Holding a compass shows a bar with four directions and stuff like beds, lodestones, death pos (TODO) etc.
|
||||||
|
|
||||||
### Pomodoro
|
### Pomodoro
|
||||||
Self-discipline with a pomodoro timer that's actually forced
|
Self-discipline with a pomodoro timer that's actually forced
|
||||||
|
@ -65,7 +65,7 @@ Hardcore hearts by chance
|
||||||
Sleeping doesn't skip night, but speeds it up. The more players, the faster it goes.
|
Sleeping doesn't skip night, but speeds it up. The more players, the faster it goes.
|
||||||
- Instant sleep \
|
- Instant sleep \
|
||||||
One can instantly skip, but only a part of the night. \
|
One can instantly skip, but only a part of the night. \
|
||||||
There's 5 players on the server. A night is 10 minutes long. \
|
There are five players on the server. A night is 10 minutes long. \
|
||||||
Each player can instantly skip 2 minutes of the night at any time, even if others aren't sleeping
|
Each player can instantly skip 2 minutes of the night at any time, even if others aren't sleeping
|
||||||
- Heal \
|
- Heal \
|
||||||
Sleeping heals
|
Sleeping heals
|
||||||
|
@ -85,26 +85,30 @@ Issue messages that the player needs to read to keep playing, and that make an a
|
||||||
`/emergencyalerts` (`tweaks724.emergencyalerts`)
|
`/emergencyalerts` (`tweaks724.emergencyalerts`)
|
||||||
|
|
||||||
### Remote redstone
|
### Remote redstone
|
||||||
Adds a "gateway" item that are controlled over internet. \
|
Adds a "gateway" item controlled over the internet. \
|
||||||
[RETSTONE.md for more info](/Minecon724/tweaks724/src/branch/master/docs/RETSTONE.md)
|
[RETSTONE.md for more info](docs/RETSTONE.md)
|
||||||
|
|
||||||
### Knockback
|
### Knockback
|
||||||
Control knockback dealt by entities
|
Control knockback dealt by entities
|
||||||
|
|
||||||
### Kill switch
|
### Kill switch
|
||||||
Quickly kills (terminates) the server on trigger, via command or HTTP request.
|
Quickly kills (terminates) the server on trigger, by command or HTTP request.
|
||||||
[KILLSWITCH.md for more info](/Minecon724/tweaks724/src/branch/master/docs/KILLSWITCH.md)
|
[KILLSWITCH.md for more info](docs/KILLSWITCH.md)
|
||||||
|
|
||||||
### Swing through grass
|
### Swing through grasses
|
||||||
Self-explanatory
|
Self-explanatory
|
||||||
|
|
||||||
### Durability alert
|
### Durability alert
|
||||||
Self-explanatory too. \
|
Self-explanatory too.
|
||||||
For simplicity, there's no configuration. Control with `tweaks724.durabilityalert`
|
|
||||||
|
### WordCoords
|
||||||
|
Converts coords to words so remembering is easier.
|
||||||
|
|
||||||
|
`/wordcoords` (`tweaks724.wordcoords`)
|
||||||
|
|
||||||
### Utility commands
|
### Utility commands
|
||||||
|
|
||||||
- `/ping` - displays player ping <sup><sub>P</sub></sup> \
|
- `/ping` - display player ping <sup><sub>P</sub></sup> \
|
||||||
**Ping is calculated by the plugin**. \
|
**Ping is calculated by the plugin**. \
|
||||||
That allows for more precision (decimal places) and to get the ping immediately after a player join
|
That allows for more precision (decimal places) and to get the ping immediately after a player joins
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,34 @@
|
||||||
Killswitch immediately stops the server.
|
Killswitch immediately stops (kills) the server.
|
||||||
|
|
||||||
### Warning
|
### Warning
|
||||||
This terminates the server process, meaning it's like you'd pull the power. \
|
This terminates the server process (not the OS), meaning it's **like you pulled the power cable.** \
|
||||||
So you will lose some progress (since the last auto save), or worst case your world (or other data) gets corrupted.
|
You lose some progress (since the last auto save), or in the worst case, **data gets corrupted.**
|
||||||
|
|
||||||
Terminal froze after kill? `reset`
|
Terminal froze after kill? Do `reset`
|
||||||
|
|
||||||
### Over a command
|
### By command
|
||||||
No key is required. \
|
No key is required. \
|
||||||
`/servkill` is the command. Permission: `tweaks724.servkill`. \
|
`/servkill` is the command. Permission: `tweaks724.servkill`. \
|
||||||
You must grant the permission manually, like with a permission plugin - it's not automatically assigned even to OPs.
|
**You must grant the permission manually** with a permission plugin—it's not automatically assigned, not even to OPs.
|
||||||
|
|
||||||
### Over HTTP
|
### Over the internet
|
||||||
HTTP is insecure, meaning others *could* intercept your request to the server and get your key. \
|
**HTTP is insecure.** The secret key travels in plain text, meaning whoever oversees the path[^1] to your server *could*[^2] intercept your request to the server and get your key. \
|
||||||
To encrypt, put this behind a proxy to get HTTPS or a VPN (directly to the server, not a commercial VPN) \
|
I recommend putting this behind a controlled[^3] VPN, or an HTTPS proxy with good access control. \
|
||||||
Or regenerate the key after every usage.
|
Or regenerate the key every use.
|
||||||
|
|
||||||
Make a GET request to `/key/<base64 encoded key>`
|
Make a GET request to `/kill/<secret key>`:
|
||||||
|
|
||||||
Example:
|
|
||||||
```
|
```
|
||||||
https://127.0.0.1:57932/key/lNwANMSZhLiTWhNxSoqQ5Q==
|
GET http://127.0.0.1:57932/kill/lNwANMSZhLiTWhNxSoqQ5Q==
|
||||||
|_ server address _| |_ base64 encoded key _|
|
|_ endpoint _| |_ secret key _|
|
||||||
```
|
```
|
||||||
|
|
||||||
The response is a 404 no matter what. Either way, you will notice that the server has stopped.
|
There is no response; the connection is closed immediately, no matter what.[^4]
|
||||||
|
|
||||||
The key is generated to `plugins/Tweaks724/storage/killswitch key` \
|
The key is in `plugins/Tweaks724/killswitch secret key.txt`. You can provide your own key. The key should be plaintext (not bytes).
|
||||||
To use it with HTTP server, encode it to base64.
|
|
||||||
|
|
||||||
Rate limit is 1 request / 5 minutes
|
The ratelimit is one request per 2 minutes. **Do not send requests when blocked, it resets the timer!**
|
||||||
|
|
||||||
|
[^1]: Typically you, then it's your ISP, maybe its upstream, then the internet backbone (various entities), then your hosting, and finally your server. That's how the Internet works!
|
||||||
|
[^2]: Though unlikely
|
||||||
|
[^3]: Direct to the server, think WireGuard. Not a commercial offering!
|
||||||
|
[^4]: *Stealth*. You will notice that the server has stopped.
|
16
pom.xml
16
pom.xml
|
@ -10,15 +10,15 @@
|
||||||
|
|
||||||
<groupId>eu.m724</groupId>
|
<groupId>eu.m724</groupId>
|
||||||
<artifactId>tweaks</artifactId>
|
<artifactId>tweaks</artifactId>
|
||||||
<version>0.1.14</version>
|
<version>0.1.16-SNAPSHOT</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|
||||||
<project.craftbukkit.version>v1_21_R3</project.craftbukkit.version>
|
<project.craftbukkit.version>v1_21_R4</project.craftbukkit.version>
|
||||||
<project.minecraft.version>1.21.4</project.minecraft.version>
|
<project.minecraft.version>1.21.5</project.minecraft.version>
|
||||||
<project.spigot.version>${project.minecraft.version}-R0.1-SNAPSHOT</project.spigot.version>
|
<project.spigot.version>${project.minecraft.version}-R0.1-SNAPSHOT</project.spigot.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
</goals>
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<target>
|
<target>
|
||||||
<replace token="v1_21_R3" value="${project.craftbukkit.version}" dir="src/main">
|
<replace token="v1_21_R4" value="${project.craftbukkit.version}" dir="src/main">
|
||||||
<include name="**/*.java" />
|
<include name="**/*.java" />
|
||||||
</replace>
|
</replace>
|
||||||
</target>
|
</target>
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
</goals>
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<target>
|
<target>
|
||||||
<replace token="${project.craftbukkit.version}" value="v1_21_R3" dir="src/main">
|
<replace token="${project.craftbukkit.version}" value="v1_21_R4" dir="src/main">
|
||||||
<include name="**/*.java" />
|
<include name="**/*.java" />
|
||||||
</replace>
|
</replace>
|
||||||
</target>
|
</target>
|
||||||
|
@ -128,7 +128,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.spigotmc</groupId>
|
<groupId>org.spigotmc</groupId>
|
||||||
<artifactId>spigot-api</artifactId>
|
<artifactId>spigot-api</artifactId>
|
||||||
<version>1.21.1-R0.1-SNAPSHOT</version>
|
<version>1.21.1-R0.1-SNAPSHOT</version> <!-- oldest supported version -->
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
@ -147,7 +147,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>eu.m724</groupId>
|
<groupId>eu.m724</groupId>
|
||||||
<artifactId>mstats-spigot</artifactId>
|
<artifactId>mstats-spigot</artifactId>
|
||||||
<version>0.1.0</version>
|
<version>0.1.2</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
@ -168,6 +168,6 @@
|
||||||
|
|
||||||
<scm>
|
<scm>
|
||||||
<developerConnection>scm:git:git@git.m724.eu:Minecon724/tweaks724.git</developerConnection>
|
<developerConnection>scm:git:git@git.m724.eu:Minecon724/tweaks724.git</developerConnection>
|
||||||
<tag>tweaks-0.1.14</tag>
|
<tag>HEAD</tag>
|
||||||
</scm>
|
</scm>
|
||||||
</project>
|
</project>
|
|
@ -10,8 +10,11 @@ import eu.m724.tweaks.module.TweaksModule;
|
||||||
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
public class DebugLogger {
|
public class DebugLogger {
|
||||||
|
private DebugLogger() {}
|
||||||
static Logger logger;
|
static Logger logger;
|
||||||
|
|
||||||
public static void info(String message, Object... format) {
|
public static void info(String message, Object... format) {
|
||||||
|
@ -35,20 +38,16 @@ public class DebugLogger {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void log(Level level, String message, Object... format) {
|
private static void log(Level level, String message, Object... format) {
|
||||||
if (logger.getLevel().intValue() > level.intValue()) return;
|
|
||||||
|
|
||||||
var caller = Thread.currentThread().getStackTrace()[3].getClassName();
|
if (!logger.isLoggable(level)) {
|
||||||
|
return;
|
||||||
if (caller.equals(TweaksModule.class.getName())) {
|
|
||||||
var pcaller = Thread.currentThread().getStackTrace()[4].getClassName();
|
|
||||||
if (pcaller.endsWith("Module"))
|
|
||||||
caller = pcaller;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (caller.startsWith("eu.m724.tweaks."))
|
message = message.formatted(format);
|
||||||
caller = caller.substring(15);
|
|
||||||
|
|
||||||
message = "[" + caller + "] " + message.formatted(format);
|
if (logger.getLevel().intValue() <= Level.FINE.intValue()) {
|
||||||
|
message = "[" + getCaller() + "] " + message;
|
||||||
|
}
|
||||||
|
|
||||||
if (level.intValue() < Level.INFO.intValue()) { // levels below info are never logged even if set for some reason
|
if (level.intValue() < Level.INFO.intValue()) { // levels below info are never logged even if set for some reason
|
||||||
// colors text gray (cyan is close to gray)
|
// colors text gray (cyan is close to gray)
|
||||||
|
@ -61,5 +60,30 @@ public class DebugLogger {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log(level, message);
|
logger.log(level, message);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getCaller() {
|
||||||
|
String caller = Thread.currentThread().getStackTrace()[4].getClassName();
|
||||||
|
|
||||||
|
// TweaksModule has helper functions that log, we want to label the logs
|
||||||
|
if (caller.equals(TweaksModule.class.getName())) {
|
||||||
|
String pcaller = Thread.currentThread().getStackTrace()[5].getClassName();
|
||||||
|
if (pcaller.endsWith("Module"))
|
||||||
|
caller = pcaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caller.startsWith("eu.m724.tweaks.")) {
|
||||||
|
caller = caller.substring(15);
|
||||||
|
|
||||||
|
String[] packages = caller.split("\\.");
|
||||||
|
|
||||||
|
caller = IntStream.range(0, packages.length - 1)
|
||||||
|
.mapToObj(i -> packages[i].substring(0, 2))
|
||||||
|
.collect(Collectors.joining(".")) + "." + packages[packages.length - 1];
|
||||||
|
// TODO leading dot or no dot?
|
||||||
|
}
|
||||||
|
|
||||||
|
return caller;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import eu.m724.tweaks.module.redstone.RedstoneModule;
|
||||||
import eu.m724.tweaks.module.sleep.SleepModule;
|
import eu.m724.tweaks.module.sleep.SleepModule;
|
||||||
import eu.m724.tweaks.module.swing.SwingModule;
|
import eu.m724.tweaks.module.swing.SwingModule;
|
||||||
import eu.m724.tweaks.module.updater.UpdaterModule;
|
import eu.m724.tweaks.module.updater.UpdaterModule;
|
||||||
|
import eu.m724.tweaks.module.wordcoords.WordCoordsModule;
|
||||||
import eu.m724.tweaks.module.worldborder.WorldBorderExpandModule;
|
import eu.m724.tweaks.module.worldborder.WorldBorderExpandModule;
|
||||||
import eu.m724.tweaks.module.worldborder.WorldBorderHideModule;
|
import eu.m724.tweaks.module.worldborder.WorldBorderHideModule;
|
||||||
|
|
||||||
|
@ -157,6 +158,8 @@ public class TweaksPlugin extends MStatsPlugin {
|
||||||
|
|
||||||
TweaksModule.init(DurabilityModule.class);
|
TweaksModule.init(DurabilityModule.class);
|
||||||
|
|
||||||
|
TweaksModule.init(WordCoordsModule.class);
|
||||||
|
|
||||||
/* end modules */
|
/* end modules */
|
||||||
|
|
||||||
if (config.metrics()) {
|
if (config.metrics()) {
|
||||||
|
@ -164,7 +167,7 @@ public class TweaksPlugin extends MStatsPlugin {
|
||||||
mStats(1);
|
mStats(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
DebugLogger.fine("Took %.3f milliseconds".formatted((System.nanoTime() - start) / 1000000.0));
|
DebugLogger.fine("Took %.3f milliseconds", (System.nanoTime() - start) / 1000000.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getTargetVersion() {
|
private String getTargetVersion() {
|
||||||
|
|
|
@ -21,32 +21,60 @@ import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public abstract class TweaksModule {
|
public abstract class TweaksModule {
|
||||||
|
/**
|
||||||
|
* Called on module initialize.
|
||||||
|
*/
|
||||||
protected abstract void onInit();
|
protected abstract void onInit();
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
var name = getClass().getSimpleName();
|
var name = getClass().getSimpleName();
|
||||||
DebugLogger.finer("Initializing " + name);
|
DebugLogger.finer("Initializing module " + name);
|
||||||
|
|
||||||
long start = System.nanoTime();
|
long start = System.nanoTime();
|
||||||
|
|
||||||
this.onInit();
|
this.onInit();
|
||||||
|
|
||||||
long end = System.nanoTime();
|
long end = System.nanoTime();
|
||||||
|
|
||||||
DebugLogger.fine("Initialized %s in %d µs", name, (end - start) / 1000);
|
DebugLogger.fine("Initialized %s in %d µs", name, (end - start) / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the plugin instance.
|
||||||
|
*
|
||||||
|
* @return The plugin instance
|
||||||
|
*/
|
||||||
protected TweaksPlugin getPlugin() {
|
protected TweaksPlugin getPlugin() {
|
||||||
return TweaksPlugin.getInstance();
|
return TweaksPlugin.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the plugin config.
|
||||||
|
*
|
||||||
|
* @return The plugin config
|
||||||
|
*/
|
||||||
protected TweaksConfig getConfig() {
|
protected TweaksConfig getConfig() {
|
||||||
return TweaksConfig.getConfig();
|
return TweaksConfig.getConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an event listener.
|
||||||
|
*
|
||||||
|
* @param listener The event listener
|
||||||
|
*/
|
||||||
protected void registerEvents(Listener listener) {
|
protected void registerEvents(Listener listener) {
|
||||||
DebugLogger.finer("Registered listener: " + listener.getClass().getName());
|
|
||||||
getPlugin().getServer().getPluginManager().registerEvents(listener, getPlugin());
|
getPlugin().getServer().getPluginManager().registerEvents(listener, getPlugin());
|
||||||
|
|
||||||
|
DebugLogger.finer("Registered event listener: " + listener.getClass().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an OUTGOING packet listener.
|
||||||
|
* Priority is {@link ListenerPriority}.NORMAL.
|
||||||
|
*
|
||||||
|
* @param packetType The {@link PacketType} to listen for
|
||||||
|
* @param consumer The consumer that will be called when the packet is received.
|
||||||
|
*/
|
||||||
protected void onPacketSend(PacketType packetType, Consumer<PacketEvent> consumer) {
|
protected void onPacketSend(PacketType packetType, Consumer<PacketEvent> consumer) {
|
||||||
ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
|
ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
|
||||||
@Override
|
@Override
|
||||||
|
@ -55,9 +83,16 @@ public abstract class TweaksModule {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
DebugLogger.finer("Registered packet send: " + packetType.name());
|
DebugLogger.finer("Registered outgoing packet listener: " + packetType.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an INCOMING packet listener.
|
||||||
|
* Priority is {@link ListenerPriority}.NORMAL.
|
||||||
|
*
|
||||||
|
* @param packetType The {@link PacketType} to listen for
|
||||||
|
* @param consumer The consumer that will be called when the packet is received.
|
||||||
|
*/
|
||||||
protected void onPacketReceive(PacketType packetType, Consumer<PacketEvent> consumer) {
|
protected void onPacketReceive(PacketType packetType, Consumer<PacketEvent> consumer) {
|
||||||
ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
|
ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
|
||||||
@Override
|
@Override
|
||||||
|
@ -66,14 +101,27 @@ public abstract class TweaksModule {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
DebugLogger.finer("Registered packet receive: " + packetType.name());
|
DebugLogger.finer("Registered incoming packet listener: " + packetType.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a command.
|
||||||
|
*
|
||||||
|
* @param command The command
|
||||||
|
* @param executor The command executor
|
||||||
|
*/
|
||||||
protected void registerCommand(String command, CommandExecutor executor) {
|
protected void registerCommand(String command, CommandExecutor executor) {
|
||||||
getPlugin().getCommand(command).setExecutor(executor);
|
getPlugin().getCommand(command).setExecutor(executor);
|
||||||
DebugLogger.finer("Registered command: " + command);
|
DebugLogger.finer("Registered command: " + command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a module.
|
||||||
|
*
|
||||||
|
* @param clazz The class of the initialized module
|
||||||
|
* @return The module instance
|
||||||
|
* @param <T> The type of the initialized module
|
||||||
|
*/
|
||||||
public static <T extends TweaksModule> T init(Class<T> clazz) {
|
public static <T extends TweaksModule> T init(Class<T> clazz) {
|
||||||
T module;
|
T module;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -14,8 +14,8 @@ import net.md_5.bungee.api.chat.TextComponent;
|
||||||
import net.md_5.bungee.api.chat.TranslatableComponent;
|
import net.md_5.bungee.api.chat.TranslatableComponent;
|
||||||
import net.md_5.bungee.chat.ComponentSerializer;
|
import net.md_5.bungee.chat.ComponentSerializer;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import org.bukkit.craftbukkit.v1_21_R3.CraftRegistry;
|
import org.bukkit.craftbukkit.v1_21_R4.CraftRegistry;
|
||||||
import org.bukkit.craftbukkit.v1_21_R3.entity.CraftPlayer;
|
import org.bukkit.craftbukkit.v1_21_R4.entity.CraftPlayer;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
|
|
|
@ -6,63 +6,59 @@
|
||||||
|
|
||||||
package eu.m724.tweaks.module.killswitch;
|
package eu.m724.tweaks.module.killswitch;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
|
||||||
import com.sun.net.httpserver.HttpHandler;
|
|
||||||
import com.sun.net.httpserver.HttpServer;
|
|
||||||
import eu.m724.tweaks.DebugLogger;
|
import eu.m724.tweaks.DebugLogger;
|
||||||
import eu.m724.tweaks.module.TweaksModule;
|
import eu.m724.tweaks.module.TweaksModule;
|
||||||
|
import eu.m724.tweaks.module.killswitch.server.KillswitchSecureHttpServer;
|
||||||
import org.bukkit.command.Command;
|
import org.bukkit.command.Command;
|
||||||
import org.bukkit.command.CommandExecutor;
|
import org.bukkit.command.CommandExecutor;
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
public class KillswitchModule extends TweaksModule implements CommandExecutor, HttpHandler {
|
public class KillswitchModule extends TweaksModule implements CommandExecutor {
|
||||||
private final Ratelimit ratelimit = new Ratelimit();
|
private String loadSecret(Path file) {
|
||||||
|
String secret;
|
||||||
|
|
||||||
private byte[] secret;
|
if (Files.exists(file)) {
|
||||||
private String secretEncoded;
|
|
||||||
|
|
||||||
private void loadKey(File file) {
|
|
||||||
if (file.exists()) {
|
|
||||||
try {
|
try {
|
||||||
this.secret = Files.readAllBytes(file.toPath());
|
secret = Files.readString(file);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("Reading killswitch key", e);
|
throw new RuntimeException("Reading killswitch key", e);
|
||||||
}
|
}
|
||||||
DebugLogger.fine("Loaded key");
|
DebugLogger.fine("Loaded key");
|
||||||
} else {
|
} else {
|
||||||
byte[] buf = new byte[16];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
byte[] buf = new byte[16];
|
||||||
SecureRandom.getInstanceStrong().nextBytes(buf);
|
SecureRandom.getInstanceStrong().nextBytes(buf);
|
||||||
Files.write(file.toPath(), buf);
|
|
||||||
|
secret = Base64.getEncoder().encodeToString(buf);
|
||||||
|
Files.writeString(file, secret);
|
||||||
} catch (IOException | NoSuchAlgorithmException e) {
|
} catch (IOException | NoSuchAlgorithmException e) {
|
||||||
throw new RuntimeException("Generating killswitch key", e);
|
throw new RuntimeException("Generating killswitch key", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.secret = buf;
|
DebugLogger.info("Killswitch secret key generated and saved to %s", file);
|
||||||
DebugLogger.info("Killswitch key generated and saved to " + file.getPath());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.secretEncoded = Base64.getEncoder().encodeToString(secret);
|
return secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onInit() {
|
protected void onInit() {
|
||||||
registerCommand("servkill", this);
|
registerCommand("servkill", this);
|
||||||
|
|
||||||
if (getConfig().killswitchListen() != null) {
|
if (getConfig().killswitchListen() == null) {
|
||||||
loadKey(new File(getPlugin().getDataFolder(), "storage/killswitch key"));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ratelimit.runTaskTimerAsynchronously(getPlugin(), 0, 20 * 300);
|
String secret = loadSecret(getPlugin().getDataFolder().toPath().resolve("killswitch secret key.txt"));
|
||||||
|
|
||||||
var listenAddress = getConfig().killswitchListen().split(":");
|
var listenAddress = getConfig().killswitchListen().split(":");
|
||||||
InetSocketAddress bindAddress;
|
InetSocketAddress bindAddress;
|
||||||
|
@ -72,47 +68,23 @@ public class KillswitchModule extends TweaksModule implements CommandExecutor, H
|
||||||
bindAddress = new InetSocketAddress(listenAddress[0], Integer.parseInt(listenAddress[1]));
|
bindAddress = new InetSocketAddress(listenAddress[0], Integer.parseInt(listenAddress[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
new KillswitchSecureHttpServer(secret, this::kill).start(bindAddress);
|
||||||
HttpServer server = HttpServer.create(bindAddress, 0);
|
|
||||||
server.createContext("/", this);
|
DebugLogger.fine("HTTP server started");
|
||||||
server.setExecutor(null);
|
|
||||||
server.start();
|
|
||||||
DebugLogger.fine("server started");
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Starting HTTP server", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void kill() {
|
private void kill() {
|
||||||
|
DebugLogger.info("Killing server on request");
|
||||||
|
|
||||||
Runtime.getRuntime().halt(0);
|
Runtime.getRuntime().halt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||||
|
DebugLogger.fine("Kill requested by: %s", sender.getName());
|
||||||
|
sender.sendMessage("Killing server");
|
||||||
|
|
||||||
kill();
|
kill();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handle(HttpExchange exchange) throws IOException {
|
|
||||||
exchange.sendResponseHeaders(404, -1);
|
|
||||||
|
|
||||||
var path = exchange.getRequestURI().getPath();
|
|
||||||
if (!path.startsWith("/key/")) return;
|
|
||||||
|
|
||||||
var address = exchange.getRemoteAddress().getAddress();
|
|
||||||
if (!ratelimit.submitRequest(address)) {
|
|
||||||
DebugLogger.fine(address + " is ratelimited");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var key = path.substring(5);
|
|
||||||
if (key.equals(secretEncoded)) {
|
|
||||||
DebugLogger.fine("Got a request with valid key");
|
|
||||||
kill();
|
|
||||||
} else {
|
|
||||||
DebugLogger.fine("Got a request with invalid key");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<>();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,70 +6,77 @@
|
||||||
|
|
||||||
package eu.m724.tweaks.module.pomodoro;
|
package eu.m724.tweaks.module.pomodoro;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class PlayerPomodoro {
|
public class PlayerPomodoro {
|
||||||
private int pomodori = 0;
|
private int intervalsDone = 0;
|
||||||
|
|
||||||
private boolean isBreak = false;
|
private boolean breaktime = false;
|
||||||
// this is for both break and not break
|
private long currentIntervalStartedAtNanos = -1;
|
||||||
private long intervalStart = -1;
|
|
||||||
|
|
||||||
/**
|
public int getIntervalsDone() {
|
||||||
* A "pomodoro" is the 25-minute cycle you take breaks after<br>
|
return intervalsDone;
|
||||||
* This returns how many cycles already elapsed, so if this is the first cycle this is 0<br>
|
}
|
||||||
* The break after the "pomodoro," so if it's breaktime after the first "pomodoro" it stays at 0
|
|
||||||
*/
|
public boolean isBreaktime() {
|
||||||
public int getPomodori() {
|
return breaktime;
|
||||||
return pomodori;
|
}
|
||||||
|
|
||||||
|
public boolean isLongBreak() {
|
||||||
|
return (intervalsDone + 1) % PomodoroModule.INTERVALS_BEFORE_LONG_BREAK == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When did the current interval start<br>
|
* @return The time the current interval or break started, in milliseconds
|
||||||
* Or when did the break start
|
|
||||||
*
|
|
||||||
* @see PlayerPomodoro#isBreak()
|
|
||||||
*/
|
*/
|
||||||
public long getIntervalStart() {
|
public long getCurrentIntervalStartedAtNanos() {
|
||||||
return intervalStart;
|
return currentIntervalStartedAtNanos;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCycleDurationSeconds() {
|
public int getCurrentIntervalDurationSeconds() {
|
||||||
return isBreak ? (pomodori < 3 ? 300 : 1200) : 1500;
|
if (breaktime) {
|
||||||
|
if (isLongBreak()) {
|
||||||
|
return PomodoroModule.LONG_BREAK_DURATION_SECONDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getRemainingSeconds(long now) {
|
return PomodoroModule.SHORT_BREAK_DURATION_SECONDS;
|
||||||
return getCycleDurationSeconds() - (now - getIntervalStart()) / 1000000000;
|
} else {
|
||||||
|
return PomodoroModule.INTERVAL_DURATION_SECONDS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public long getCurrentIntervalDurationNanos() {
|
||||||
* Is it a break currently
|
return TimeUnit.SECONDS.toNanos(getCurrentIntervalDurationSeconds());
|
||||||
*/
|
|
||||||
public boolean isBreak() {
|
|
||||||
return isBreak;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCycleComplete() {
|
public long getCurrentIntervalRemainingNanos(long nowNanos) {
|
||||||
return intervalStart + getCycleDurationSeconds() * 1000000000L < System.nanoTime();
|
long elapsed = nowNanos - getCurrentIntervalStartedAtNanos();
|
||||||
|
return getCurrentIntervalDurationNanos() - elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isIntervalComplete(long nowNanos) {
|
||||||
|
return currentIntervalStartedAtNanos + getCurrentIntervalDurationNanos() < nowNanos;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets and starts the timer
|
* Resets and starts the timer
|
||||||
*/
|
*/
|
||||||
public void start() {
|
public void start() {
|
||||||
this.pomodori = 0;
|
this.intervalsDone = 0;
|
||||||
this.isBreak = false;
|
this.breaktime = false;
|
||||||
this.intervalStart = System.nanoTime();
|
this.currentIntervalStartedAtNanos = System.nanoTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Completes a cycle
|
* Completes a cycle
|
||||||
*/
|
*/
|
||||||
public void next() {
|
public void startBreakOrNextInterval() {
|
||||||
if (isBreak) { // from break to interval
|
if (breaktime) { // from break to interval
|
||||||
this.pomodori++;
|
this.intervalsDone++;
|
||||||
this.pomodori %= 4;
|
this.intervalsDone %= PomodoroModule.INTERVALS_BEFORE_LONG_BREAK;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.intervalStart = System.nanoTime();
|
this.currentIntervalStartedAtNanos = System.nanoTime();
|
||||||
isBreak = !isBreak;
|
breaktime = !breaktime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,9 @@
|
||||||
|
|
||||||
package eu.m724.tweaks.module.pomodoro;
|
package eu.m724.tweaks.module.pomodoro;
|
||||||
|
|
||||||
|
import eu.m724.tweaks.Language;
|
||||||
|
import eu.m724.tweaks.config.TweaksConfig;
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
import org.bukkit.command.Command;
|
import org.bukkit.command.Command;
|
||||||
import org.bukkit.command.CommandExecutor;
|
import org.bukkit.command.CommandExecutor;
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
|
@ -18,25 +21,34 @@ public class PomodoroCommands implements CommandExecutor {
|
||||||
Player player = (Player) sender;
|
Player player = (Player) sender;
|
||||||
String action = args.length > 0 ? args[0] : null;
|
String action = args.length > 0 ? args[0] : null;
|
||||||
|
|
||||||
PlayerPomodoro pomodoro = Pomodoros.get(player);
|
PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
|
||||||
|
|
||||||
|
long now = System.nanoTime();
|
||||||
|
|
||||||
if (pomodoro != null) {
|
if (pomodoro != null) {
|
||||||
if ("stop".equals(action)) {
|
if ("stop".equals(action)) {
|
||||||
Pomodoros.remove(player);
|
PlayerPomodoroTracker.remove(player);
|
||||||
sender.sendMessage("Pomodoro disabled");
|
sender.spigot().sendMessage(Language.getComponent("pomodoroStopped", ChatColor.GREEN, label));
|
||||||
} else {
|
} else {
|
||||||
if (pomodoro.isCycleComplete()) {
|
if (pomodoro.isIntervalComplete(now)) {
|
||||||
pomodoro.next();
|
pomodoro.startBreakOrNextInterval();
|
||||||
|
|
||||||
|
if (pomodoro.isBreaktime() && TweaksConfig.getConfig().pomodoroForce()) {
|
||||||
|
player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText());
|
||||||
}
|
}
|
||||||
sender.spigot().sendMessage(Pomodoros.formatTimer(pomodoro, pomodoro.getRemainingSeconds(System.nanoTime())));
|
}
|
||||||
|
|
||||||
|
sender.spigot().sendMessage(PomodoroModule.formatTimer(pomodoro, now));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ("start".equals(action)) {
|
if ("start".equals(action)) {
|
||||||
pomodoro = Pomodoros.create(player);
|
pomodoro = PlayerPomodoroTracker.create(player);
|
||||||
pomodoro.start();
|
pomodoro.start();
|
||||||
sender.spigot().sendMessage(Pomodoros.formatTimer(pomodoro, pomodoro.getCycleDurationSeconds()));
|
|
||||||
|
sender.spigot().sendMessage(PomodoroModule.formatTimer(pomodoro, now));
|
||||||
} else {
|
} else {
|
||||||
sender.sendMessage("Start pomodoro with /pom start");
|
// TODO help?
|
||||||
|
sender.spigot().sendMessage(Language.getComponent("pomodoroStart", ChatColor.GOLD, label));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,56 +7,68 @@
|
||||||
package eu.m724.tweaks.module.pomodoro;
|
package eu.m724.tweaks.module.pomodoro;
|
||||||
|
|
||||||
import eu.m724.tweaks.config.TweaksConfig;
|
import eu.m724.tweaks.config.TweaksConfig;
|
||||||
import net.md_5.bungee.api.chat.ComponentBuilder;
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
import org.bukkit.event.player.*;
|
import org.bukkit.event.player.PlayerLoginEvent;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
import org.bukkit.event.player.PlayerToggleSneakEvent;
|
||||||
|
|
||||||
public class PomodoroListener implements Listener {
|
public class PomodoroListener implements Listener {
|
||||||
private final boolean force = TweaksConfig.getConfig().pomodoroForce();
|
private final TweaksConfig config = TweaksConfig.getConfig();
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onPlayerLogin(PlayerLoginEvent event) {
|
public void onPlayerLogin(PlayerLoginEvent event) {
|
||||||
|
// Joining ends break
|
||||||
Player player = event.getPlayer();
|
Player player = event.getPlayer();
|
||||||
PlayerPomodoro pomodoro = Pomodoros.get(player);
|
PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
|
||||||
if (pomodoro == null) return;
|
if (pomodoro == null) return;
|
||||||
|
|
||||||
long remaining = pomodoro.getRemainingSeconds(System.nanoTime());
|
long now = System.nanoTime();
|
||||||
|
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
|
||||||
|
|
||||||
if (pomodoro.isBreak()) {
|
if (pomodoro.isBreaktime()) {
|
||||||
if (pomodoro.isCycleComplete()) {
|
if (remaining > 0 && config.pomodoroForce()) {
|
||||||
pomodoro.next();
|
player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText());
|
||||||
} else {
|
} else {
|
||||||
if (force) {
|
pomodoro.startBreakOrNextInterval();
|
||||||
event.getPlayer().kickPlayer(
|
|
||||||
new ComponentBuilder()
|
|
||||||
.append(Pomodoros.formatTimer(pomodoro, remaining))
|
|
||||||
.build().toLegacyText()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||||
|
// Quitting starts break
|
||||||
Player player = event.getPlayer();
|
Player player = event.getPlayer();
|
||||||
PlayerPomodoro pomodoro = Pomodoros.get(player);
|
PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
|
||||||
if (pomodoro == null) return;
|
if (pomodoro == null) return;
|
||||||
|
|
||||||
if (!pomodoro.isBreak() && pomodoro.isCycleComplete()) {
|
if (pomodoro.isBreaktime()) return;
|
||||||
pomodoro.next();
|
|
||||||
|
long now = System.nanoTime();
|
||||||
|
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
pomodoro.startBreakOrNextInterval();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onPlayerMove(PlayerMoveEvent event) {
|
public void onPlayerToggleSneak(PlayerToggleSneakEvent event) {
|
||||||
Player player = event.getPlayer();
|
// Sneaking ends break
|
||||||
PlayerPomodoro timer = Pomodoros.get(player);
|
if (!event.isSneaking()) return;
|
||||||
if (timer == null) return;
|
|
||||||
|
|
||||||
if (timer.isBreak() && timer.getRemainingSeconds(System.nanoTime()) <= 0)
|
Player player = event.getPlayer();
|
||||||
timer.next(); // resume timer if break ended
|
PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player);
|
||||||
|
if (pomodoro == null) return;
|
||||||
|
|
||||||
|
if (!pomodoro.isBreaktime()) return;
|
||||||
|
|
||||||
|
long now = System.nanoTime();
|
||||||
|
long remaining = pomodoro.getCurrentIntervalRemainingNanos(now);
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
pomodoro.startBreakOrNextInterval();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,23 @@
|
||||||
|
|
||||||
package eu.m724.tweaks.module.pomodoro;
|
package eu.m724.tweaks.module.pomodoro;
|
||||||
|
|
||||||
|
import eu.m724.tweaks.Language;
|
||||||
|
import eu.m724.tweaks.config.TweaksConfig;
|
||||||
import eu.m724.tweaks.module.TweaksModule;
|
import eu.m724.tweaks.module.TweaksModule;
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.md_5.bungee.api.chat.BaseComponent;
|
||||||
|
import net.md_5.bungee.api.chat.ComponentBuilder;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class PomodoroModule extends TweaksModule {
|
public class PomodoroModule extends TweaksModule {
|
||||||
|
static final int INTERVAL_DURATION_SECONDS = 10;
|
||||||
|
static final int SHORT_BREAK_DURATION_SECONDS = (int) TimeUnit.MINUTES.toSeconds(5);
|
||||||
|
static final int LONG_BREAK_DURATION_SECONDS = (int) TimeUnit.MINUTES.toSeconds(20);
|
||||||
|
static final int INTERVALS_BEFORE_LONG_BREAK = 4;
|
||||||
|
|
||||||
|
static final int KICK_DELAY_SECONDS = 60;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onInit() {
|
protected void onInit() {
|
||||||
registerEvents(new PomodoroListener());
|
registerEvents(new PomodoroListener());
|
||||||
|
@ -16,4 +30,66 @@ public class PomodoroModule extends TweaksModule {
|
||||||
|
|
||||||
registerCommand("pomodoro", new PomodoroCommands());
|
registerCommand("pomodoro", new PomodoroCommands());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a formatted timer for a player.
|
||||||
|
*
|
||||||
|
* @param pomodoro the player's {@link PlayerPomodoro} instance
|
||||||
|
* @param nowNanos unix now timestamp in nanoseconds
|
||||||
|
* @return the timer as {@link BaseComponent}
|
||||||
|
*/
|
||||||
|
static BaseComponent formatTimer(PlayerPomodoro pomodoro, long nowNanos) {
|
||||||
|
ComponentBuilder builder = new ComponentBuilder();
|
||||||
|
|
||||||
|
long remainingNanos = pomodoro.getCurrentIntervalRemainingNanos(nowNanos);
|
||||||
|
long remainingSeconds = TimeUnit.NANOSECONDS.toSeconds(remainingNanos);
|
||||||
|
|
||||||
|
if (pomodoro.isBreaktime()) {
|
||||||
|
if (pomodoro.isLongBreak()) {
|
||||||
|
builder.append(Language.getComponent("pomodoroLongBreak", ChatColor.LIGHT_PURPLE));
|
||||||
|
} else {
|
||||||
|
builder.append(Language.getComponent("pomodoroShortBreak", ChatColor.LIGHT_PURPLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingNanos > 0) {
|
||||||
|
builder.append(" %02d:%02d".formatted(remainingSeconds / 60, remainingSeconds % 60))
|
||||||
|
.color(ChatColor.GOLD);
|
||||||
|
} else {
|
||||||
|
builder.append(" 00:00")
|
||||||
|
.color(ChatColor.GREEN);
|
||||||
|
|
||||||
|
if (!TweaksConfig.getConfig().pomodoroForce()) {
|
||||||
|
builder.append( "/pom").color(ChatColor.GOLD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (remainingNanos > 0) {
|
||||||
|
builder
|
||||||
|
.append("%02d:%02d".formatted(remainingSeconds / 60, remainingSeconds % 60))
|
||||||
|
.color(ChatColor.GRAY);
|
||||||
|
} else {
|
||||||
|
builder
|
||||||
|
.append("00:00")
|
||||||
|
.color(remainingSeconds % 2 == 0 ? ChatColor.RED : ChatColor.GRAY);
|
||||||
|
|
||||||
|
if (!TweaksConfig.getConfig().pomodoroForce()) {
|
||||||
|
builder.append( "/pom").color(ChatColor.GOLD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (int i=0; i<INTERVALS_BEFORE_LONG_BREAK; i++) {
|
||||||
|
ChatColor color = ChatColor.GRAY;
|
||||||
|
if (i == pomodoro.getIntervalsDone()) {
|
||||||
|
color = ChatColor.LIGHT_PURPLE;
|
||||||
|
} else if (i > pomodoro.getIntervalsDone()) {
|
||||||
|
color = ChatColor.DARK_GRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.append(" o").color(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,34 +9,49 @@ package eu.m724.tweaks.module.pomodoro;
|
||||||
import eu.m724.tweaks.TweaksPlugin;
|
import eu.m724.tweaks.TweaksPlugin;
|
||||||
import eu.m724.tweaks.config.TweaksConfig;
|
import eu.m724.tweaks.config.TweaksConfig;
|
||||||
import net.md_5.bungee.api.ChatMessageType;
|
import net.md_5.bungee.api.ChatMessageType;
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.Sound;
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
import org.bukkit.scheduler.BukkitRunnable;
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class PomodoroRunnable extends BukkitRunnable {
|
public class PomodoroRunnable extends BukkitRunnable {
|
||||||
private final boolean force = TweaksConfig.getConfig().pomodoroForce();
|
private final TweaksConfig config = TweaksConfig.getConfig();
|
||||||
private final Plugin plugin = TweaksPlugin.getInstance(); // used only to kick
|
private final Plugin plugin = TweaksPlugin.getInstance(); // used only to kick
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
long now = System.nanoTime();
|
long now = System.nanoTime();
|
||||||
|
|
||||||
Bukkit.getOnlinePlayers().forEach(player -> {
|
PlayerPomodoroTracker.timers.forEach((uuid, pomodoro) -> {
|
||||||
PlayerPomodoro pomodoro = Pomodoros.get(player);
|
long remainingNanos = pomodoro.getCurrentIntervalRemainingNanos(now);
|
||||||
if (pomodoro == null) return;
|
long remainingSecs = TimeUnit.NANOSECONDS.toSeconds(remainingNanos);
|
||||||
|
|
||||||
long remaining = pomodoro.getRemainingSeconds(now);
|
// TODO optimize?
|
||||||
|
Player player = plugin.getServer().getPlayer(uuid);
|
||||||
|
|
||||||
|
if (player != null && player.isOnline()) {
|
||||||
// TODO make not always on
|
// TODO make not always on
|
||||||
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, Pomodoros.formatTimer(pomodoro, remaining));
|
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, PomodoroModule.formatTimer(pomodoro, now));
|
||||||
|
|
||||||
if (remaining <= 0) {
|
if (remainingNanos <= 0) {
|
||||||
player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_FALL, 1.0f, 0.5f);
|
player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_FALL, 1.0f, 0.5f);
|
||||||
if (remaining < -60 && force) {
|
|
||||||
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {
|
if (remainingSecs < -PomodoroModule.KICK_DELAY_SECONDS && config.pomodoroForce()) {
|
||||||
pomodoro.next();
|
pomodoro.startBreakOrNextInterval();
|
||||||
player.kickPlayer(Pomodoros.formatTimer(pomodoro, pomodoro.getRemainingSeconds(now)).toLegacyText());
|
|
||||||
});
|
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () ->
|
||||||
|
player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (remainingNanos <= 0) {
|
||||||
|
// Start break automatically if the player is offline
|
||||||
|
if (!pomodoro.isBreaktime()) {
|
||||||
|
pomodoro.startBreakOrNextInterval();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
62
src/main/java/eu/m724/tweaks/module/wordcoords/WordList.java
Normal file
62
src/main/java/eu/m724/tweaks/module/wordcoords/WordList.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ package eu.m724.tweaks.module.worldborder;
|
||||||
|
|
||||||
import eu.m724.tweaks.module.TweaksModule;
|
import eu.m724.tweaks.module.TweaksModule;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import org.bukkit.craftbukkit.v1_21_R3.CraftWorld;
|
import org.bukkit.craftbukkit.v1_21_R4.CraftWorld;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
import org.bukkit.event.world.WorldLoadEvent;
|
import org.bukkit.event.world.WorldLoadEvent;
|
||||||
|
|
|
@ -8,7 +8,7 @@ api-version: 1.21.1
|
||||||
softdepend: [ProtocolLib]
|
softdepend: [ProtocolLib]
|
||||||
|
|
||||||
libraries:
|
libraries:
|
||||||
- eu.m724:mstats-spigot:0.1.0
|
- eu.m724:mstats-spigot:0.1.2
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
chat:
|
chat:
|
||||||
|
@ -43,6 +43,10 @@ commands:
|
||||||
durabilityalert:
|
durabilityalert:
|
||||||
description: Durability alert toggle
|
description: Durability alert toggle
|
||||||
permission: tweaks724.durabilityalert
|
permission: tweaks724.durabilityalert
|
||||||
|
wordcoords:
|
||||||
|
description: Word to coords conversion
|
||||||
|
permission: tweaks724.wordcoords
|
||||||
|
aliases: [woco, wc, w3w]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
tweaks724.chatmanage:
|
tweaks724.chatmanage:
|
||||||
|
@ -63,6 +67,8 @@ permissions:
|
||||||
default: false
|
default: false
|
||||||
tweaks724.durabilityalert:
|
tweaks724.durabilityalert:
|
||||||
default: true
|
default: true
|
||||||
|
tweaks724.wordcoords:
|
||||||
|
default: true
|
||||||
|
|
||||||
7weaks724.ignore.this:
|
7weaks724.ignore.this:
|
||||||
description: "Internal, not for use. ${project.spigot.version}"
|
description: "Internal, not for use. ${project.spigot.version}"
|
||||||
|
|
|
@ -30,12 +30,24 @@ chatAlreadyHere = You're already in this room.
|
||||||
authKickWrongKey = You're connecting to the wrong server address. You must connect to the one you're registered to.
|
authKickWrongKey = You're connecting to the wrong server address. You must connect to the one you're registered to.
|
||||||
# If force is enabled and player is not registered. Changing this reveals you're using this plugin
|
# If force is enabled and player is not registered. Changing this reveals you're using this plugin
|
||||||
authKickUnregistered = You are not whitelisted on this server!
|
authKickUnregistered = You are not whitelisted on this server!
|
||||||
authKickError = An error occured. Please try again. If this persists, contact an administrator.
|
authKickError = An error occurred. Please try again. If this persists, contact an administrator.
|
||||||
|
|
||||||
redstoneGatewayItem = Redstone gateway
|
redstoneGatewayItem = Redstone gateway
|
||||||
|
|
||||||
clickToCopy = Click to copy to clipboard
|
clickToCopy = Click to copy
|
||||||
clickToExecuteCommand = Click to execute command
|
clickToExecuteCommand = Click to execute command
|
||||||
|
|
||||||
durabilityEnabled = Enabled durability alert
|
durabilityEnabled = Enabled durability alert
|
||||||
durabilityDisabled = Disabled durability alert
|
durabilityDisabled = Disabled durability alert
|
||||||
|
|
||||||
|
# When console executes /wordcoords without arguments
|
||||||
|
wordCoordsPlayerOnly = Only players can execute this command without arguments.
|
||||||
|
wordCoordsOutOfRange = Those coordinates are invalid.
|
||||||
|
wordCoordsInvalidWord = Invalid word or coordinate: "%s"
|
||||||
|
wordCoordsProvideZ = Please provide the Z coordinate.
|
||||||
|
|
||||||
|
# /pomodoro
|
||||||
|
pomodoroStopped = Pomodoro stopped. Restart it with /%s start
|
||||||
|
pomodoroStart = Start pomodoro with /%s start
|
||||||
|
pomodoroShortBreak = Short break
|
||||||
|
pomodoroLongBreak = Long break
|
2054
src/main/resources/wordlist.txt
Normal file
2054
src/main/resources/wordlist.txt
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,15 +1,10 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
FILENAME=nms_1_21_1+3+4+5.tar.zst
|
||||||
# Copyright (C) 2025 Minecon724
|
CHECKSUM=0ea6267ce39213ddb0d6a7669d8021283350bb56de0d65f2e9fddd3c85337c5fbd204272b07c7e8532c6eaef46c3a47a39bac183abd6f4cfa7b171e08b4d7029
|
||||||
# Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
|
|
||||||
# in the project root for the full license text.
|
|
||||||
#
|
|
||||||
|
|
||||||
FILENAME=MS4yMS4xLTQK.tar.zst
|
|
||||||
|
|
||||||
curl -O https://36ab09b1.m724.eu/$FILENAME
|
curl -O https://36ab09b1.m724.eu/$FILENAME
|
||||||
if [ "$(sha512sum $FILENAME)" = "475b931b6dde126aafd3f959bd02e122aa3c671ad11e83cbe1a9c9ea771a424f203381aa31e4ab40052dae1bfbe96c61daa81add1afab46dd423a5f038d68a6b MS4yMS4xLTQK.tar.zst" ]; then
|
if [ "$(sha512sum $FILENAME)" = "$CHECKSUM $FILENAME" ]; then
|
||||||
tar -xaf $FILENAME -C "$1"
|
tar -xaf $FILENAME -C "$1"
|
||||||
rm $FILENAME
|
rm $FILENAME
|
||||||
else
|
else
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue