mirror of
https://github.com/Shiewk/BlockHistory3.git
synced 2026-04-28 04:24:17 +02:00
Initial Commit (v3.0.0)
This commit is contained in:
+65
@@ -0,0 +1,65 @@
|
|||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
out/
|
||||||
|
.idea_modules/
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.ctxt
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.nar
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
hs_err_pid*
|
||||||
|
*~
|
||||||
|
.fuse_hidden*
|
||||||
|
.directory
|
||||||
|
.Trash-*
|
||||||
|
.nfs*
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Icon
|
||||||
|
._*
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
*.stackdump
|
||||||
|
[Dd]esktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
*.lnk
|
||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
gradle-app.setting
|
||||||
|
.gradletasknamecache
|
||||||
|
**/build/
|
||||||
|
run/
|
||||||
|
runs/
|
||||||
|
gradle-wrapper.jar
|
||||||
|
./gradlew
|
||||||
|
./gradlew.bat
|
||||||
|
gradlew
|
||||||
|
gradlew.bat
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# BlockHistory
|
||||||
|
|
||||||
|
A simple but effective Paper Minecraft plugin that allows you to find griefers on your server more easily.
|
||||||
|
|
||||||
|
This plugin can help you find griefers more easily. The plugin saves when a block is broken or placed. That way you can easily see who broke or placed blocks.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Install the plugin on your server, and it will start to track every block's history.
|
||||||
|
|
||||||
|
### Block history command
|
||||||
|
Then, if you need to view the history of a block, run **[/blockhistory history \<x> \<y> \<z>](https://github.com/Shiewk/BlockHistory3/blob/main/docs/commands.md)**. This will display the history of the block. It should look like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This way, you can easily see changes like someone destroying your builds etc.
|
||||||
|
|
||||||
|
### Stats command
|
||||||
|
|
||||||
|
You can use the **[/blockhistory stats](https://github.com/Shiewk/BlockHistory3/blob/main/docs/commands.md)** command to see statistics about the plugin's disk usage and uptime. This is what the command's output looks like:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Supported interactions
|
||||||
|
Currently, the following block interactions are supported:
|
||||||
|
- Placing blocks
|
||||||
|
- Breaking blocks
|
||||||
|
- Buckets
|
||||||
|
- Exploding with end crystal / TNT / Creeper / Respawn Anchor / Bed
|
||||||
|
- Sign changes
|
||||||
|
- Opening containers
|
||||||
|
|
||||||
|
## Further documentation
|
||||||
|
- [Command list](https://github.com/Shiewk/BlockHistory3/blob/main/docs/commands.md)
|
||||||
|
- [Permission list](https://github.com/Shiewk/BlockHistory3/blob/main/docs/permissions.md)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
}
|
||||||
|
|
||||||
|
group = 'de.shiewk'
|
||||||
|
version = '3.0.0'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
name = "papermc-repo"
|
||||||
|
url = "https://repo.papermc.io/repository/maven-public/"
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
name = "sonatype"
|
||||||
|
url = "https://oss.sonatype.org/content/groups/public/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly("io.papermc.paper:paper-api:1.21-R0.1-SNAPSHOT")
|
||||||
|
}
|
||||||
|
|
||||||
|
def targetJavaVersion = 21
|
||||||
|
java {
|
||||||
|
def javaVersion = JavaVersion.toVersion(targetJavaVersion)
|
||||||
|
sourceCompatibility = javaVersion
|
||||||
|
targetCompatibility = javaVersion
|
||||||
|
if (JavaVersion.current() < javaVersion) {
|
||||||
|
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile).configureEach {
|
||||||
|
options.encoding = 'UTF-8'
|
||||||
|
|
||||||
|
if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) {
|
||||||
|
options.release.set(targetJavaVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processResources {
|
||||||
|
def props = [version: version]
|
||||||
|
inputs.properties props
|
||||||
|
filteringCharset 'UTF-8'
|
||||||
|
filesMatching('plugin.yml') {
|
||||||
|
expand props
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# BlockHistory commands
|
||||||
|
| Command | Description | Permission |
|
||||||
|
|-----------------------------------------------|-------------------------------------------------------------------------------------|------------------------------|
|
||||||
|
| /blockhistory stats | Shows stats about the plugin. | blockhistory.command.stats |
|
||||||
|
| /blockhistory history \<x> \<y> \<z> | Shows the history of the block at the specified coordinates. | blockhistory.command.history |
|
||||||
|
| /blockhistory history \<x> \<y> \<z> \<world> | Shows the history of the block at the specified coordinates in the specified world. | blockhistory.command.history |
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# BlockHistory Permissions
|
||||||
|
| Permission | Description | Default | Child permissions |
|
||||||
|
|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------|---------------------------|
|
||||||
|
| blockhistory.command.stats | Allows the player to use /blockhistory stats | Only server operators | blockhistory.command.root |
|
||||||
|
| blockhistory.command.history | Allows the player to use /blockhistory history | Only server operators | blockhistory.command.root |
|
||||||
|
| blockhistory.command.root | Allows the player to access the root /blockhistory command node (you can't do anything with this unless you have one of the other permissions as well) | Only server operators | *None* |
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'BlockHistory3'
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package de.shiewk.blockhistory.v3;
|
||||||
|
|
||||||
|
import de.shiewk.blockhistory.v3.command.BlockHistoryCommand;
|
||||||
|
import de.shiewk.blockhistory.v3.listener.BlockListener;
|
||||||
|
import io.papermc.paper.command.brigadier.Commands;
|
||||||
|
import io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent;
|
||||||
|
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
|
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static net.kyori.adventure.text.Component.text;
|
||||||
|
|
||||||
|
public final class BlockHistoryPlugin extends JavaPlugin {
|
||||||
|
|
||||||
|
private static ComponentLogger LOGGER = null;
|
||||||
|
private static BlockHistoryPlugin INSTANCE = null;
|
||||||
|
|
||||||
|
public static final TextColor COLOR_PRIMARY = TextColor.color(0xff8500),
|
||||||
|
COLOR_SECONDARY = TextColor.color(0xff0011),
|
||||||
|
COLOR_FAIL = TextColor.color(0xCF0000);
|
||||||
|
|
||||||
|
public static final Component CHAT_PREFIX = text("BlockHistory \u00BB ", COLOR_SECONDARY);
|
||||||
|
|
||||||
|
private HistoryManager historyManager;
|
||||||
|
private StatManager statManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoad() {
|
||||||
|
INSTANCE = this;
|
||||||
|
LOGGER = getComponentLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
statManager = new StatManager();
|
||||||
|
Path saveDirectory = Path.of(getDataFolder().getPath(), "history");
|
||||||
|
try {
|
||||||
|
Files.createDirectories(saveDirectory);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
historyManager = new HistoryManager(LOGGER, saveDirectory, statManager);
|
||||||
|
|
||||||
|
getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, this::registerCommands);
|
||||||
|
|
||||||
|
Bukkit.getScheduler().scheduleSyncRepeatingTask(this, BlockListener::clearCache, 6000, 6000);
|
||||||
|
|
||||||
|
listen(new BlockListener());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerCommands(@NotNull ReloadableRegistrarEvent<Commands> event) {
|
||||||
|
Commands commands = event.registrar();
|
||||||
|
commands.register(new BlockHistoryCommand().getCommandNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void listen(Listener listener){
|
||||||
|
Bukkit.getPluginManager().registerEvents(listener, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
historyManager.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public HistoryManager getHistoryManager() {
|
||||||
|
return historyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StatManager getStatManager() {
|
||||||
|
return statManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BlockHistoryPlugin instance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ComponentLogger logger() {
|
||||||
|
return LOGGER;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package de.shiewk.blockhistory.v3;
|
||||||
|
|
||||||
|
import de.shiewk.blockhistory.v3.exception.LowDiskSpaceException;
|
||||||
|
import de.shiewk.blockhistory.v3.history.BlockHistoryElement;
|
||||||
|
import de.shiewk.blockhistory.v3.history.BlockHistorySearchCallback;
|
||||||
|
import de.shiewk.blockhistory.v3.util.BlockHistoryFileNames;
|
||||||
|
import de.shiewk.blockhistory.v3.util.NamedLoggingThreadFactory;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
public final class HistoryManager {
|
||||||
|
|
||||||
|
private final Path saveDirectory;
|
||||||
|
private final Logger logger;
|
||||||
|
private final StatManager statManager;
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor writeExecutor;
|
||||||
|
private final ThreadPoolExecutor readExecutor;
|
||||||
|
|
||||||
|
HistoryManager(Logger logger, Path saveDirectory, StatManager statManager) {
|
||||||
|
this.saveDirectory = saveDirectory;
|
||||||
|
this.logger = logger;
|
||||||
|
this.statManager = statManager;
|
||||||
|
AtomicInteger threadNumber = new AtomicInteger();
|
||||||
|
String threadName = "BlockHistoryIO";
|
||||||
|
int threadPriority = 2;
|
||||||
|
writeExecutor = new ThreadPoolExecutor(
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
new ArrayBlockingQueue<>(0xff),
|
||||||
|
new NamedLoggingThreadFactory(
|
||||||
|
threadName,
|
||||||
|
threadPriority,
|
||||||
|
logger,
|
||||||
|
"BlockHistory write I/O",
|
||||||
|
threadNumber
|
||||||
|
)
|
||||||
|
);
|
||||||
|
readExecutor = new ThreadPoolExecutor(
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
30,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
new ArrayBlockingQueue<>(3),
|
||||||
|
new NamedLoggingThreadFactory(
|
||||||
|
threadName,
|
||||||
|
threadPriority,
|
||||||
|
logger,
|
||||||
|
"BlockHistory read I/O",
|
||||||
|
threadNumber
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getSaveDirectory() {
|
||||||
|
return saveDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
void shutdown() {
|
||||||
|
logger.info("Shutting down I/O executor...");
|
||||||
|
long n = System.nanoTime();
|
||||||
|
readExecutor.shutdown();
|
||||||
|
writeExecutor.shutdown();
|
||||||
|
try {
|
||||||
|
if (!readExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
logger.warn("Read executor shutdown timed out after {}ms", (System.nanoTime() - n) / 1000000);
|
||||||
|
}
|
||||||
|
if (!writeExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||||
|
logger.warn("Read executor shutdown timed out after {}ms", (System.nanoTime() - n) / 1000000);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e){
|
||||||
|
logger.warn("Thread interrupted while waiting for shutdown");
|
||||||
|
}
|
||||||
|
logger.info("Shutdown finished ({}ms)", (System.nanoTime() - n) / 1000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Void> addHistoryElement(BlockHistoryElement element){
|
||||||
|
Objects.requireNonNull(element);
|
||||||
|
return CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
byte[] saveData = element.saveData();
|
||||||
|
writeToDisk(
|
||||||
|
BlockHistoryFileNames.encode(saveDirectory, element.getLocation()),
|
||||||
|
saveData
|
||||||
|
);
|
||||||
|
statManager.bytesWritten(saveData.length);
|
||||||
|
statManager.elementWritten();
|
||||||
|
} catch (LowDiskSpaceException e) {
|
||||||
|
BlockHistoryPlugin.logger().warn("Free disk space is too low to safely write further history elements: {} bytes", e.getFreeBytes());
|
||||||
|
} catch (Exception e) {
|
||||||
|
|
||||||
|
StringWriter strw = new StringWriter();
|
||||||
|
e.printStackTrace(new PrintWriter(strw));
|
||||||
|
|
||||||
|
BlockHistoryPlugin.logger().warn("Exception while writing history element:");
|
||||||
|
for (String s : strw.toString().split("\n")) {
|
||||||
|
BlockHistoryPlugin.logger().warn(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, writeExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeToDisk(Path path, byte[] saveData) throws LowDiskSpaceException, IOException {
|
||||||
|
long usableDiskSpace = getUsableDiskSpace();
|
||||||
|
if (usableDiskSpace < 8192){
|
||||||
|
throw new LowDiskSpaceException(usableDiskSpace);
|
||||||
|
}
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Files.write(path, saveData, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUsableDiskSpace() throws IOException {
|
||||||
|
return Files.getFileStore(saveDirectory).getUsableSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Void> searchAsync(BlockHistorySearchCallback callback, World world, int x, int y, int z) {
|
||||||
|
return CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
this.search(callback, world, x, y, z);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
callback.onNoFilePresent(e);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}, readExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void search(BlockHistorySearchCallback callback, World world, int x, int y, int z) throws IOException {
|
||||||
|
Location location = new Location(world, x, y, z);
|
||||||
|
Path path = BlockHistoryFileNames.encode(saveDirectory, location);
|
||||||
|
try (FileInputStream fin = new FileInputStream(path.toFile())){
|
||||||
|
try (DataInputStream dataIn = new DataInputStream(fin)){
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
int b = dataIn.readUnsignedByte();
|
||||||
|
BlockHistoryElement element = BlockHistoryElement.read(b, dataIn, world, location.getChunk().getX(), location.getChunk().getZ());
|
||||||
|
if (element.x() == x && element.y() == y && element.z() == z){
|
||||||
|
callback.onElementFound(element);
|
||||||
|
}
|
||||||
|
} catch (EOFException e) {
|
||||||
|
// done reading
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package de.shiewk.blockhistory.v3;
|
||||||
|
|
||||||
|
public final class StatManager {
|
||||||
|
|
||||||
|
private final long startTime = System.nanoTime();
|
||||||
|
private long elementsWritten = 0;
|
||||||
|
private long bytesWritten = 0;
|
||||||
|
|
||||||
|
StatManager(){}
|
||||||
|
|
||||||
|
void elementWritten(){
|
||||||
|
elementsWritten++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bytesWritten(int bytes){
|
||||||
|
bytesWritten += bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getElementsWritten() {
|
||||||
|
return elementsWritten;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getStartTime() {
|
||||||
|
return startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimeMsSinceStart(){
|
||||||
|
return (System.nanoTime() - getStartTime()) / 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimeSecondsSinceStart(){
|
||||||
|
return getTimeMsSinceStart() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getElementsWrittenPerSecond() {
|
||||||
|
return Math.round((float) getElementsWritten() / getTimeSecondsSinceStart() * 10f) / 10f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getBytesWritten() {
|
||||||
|
return bytesWritten;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getBytesWrittenPerSecond() {
|
||||||
|
return Math.round((float) getBytesWritten() / getTimeSecondsSinceStart() * 10f) / 10f;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package de.shiewk.blockhistory.v3.command;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.Command;
|
||||||
|
import com.mojang.brigadier.context.CommandContext;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||||
|
import de.shiewk.blockhistory.v3.BlockHistoryPlugin;
|
||||||
|
import de.shiewk.blockhistory.v3.StatManager;
|
||||||
|
import de.shiewk.blockhistory.v3.history.BlockHistoryElement;
|
||||||
|
import de.shiewk.blockhistory.v3.history.BlockHistorySearchCallback;
|
||||||
|
import de.shiewk.blockhistory.v3.util.CommandUtil;
|
||||||
|
import de.shiewk.blockhistory.v3.util.PlayerUtil;
|
||||||
|
import de.shiewk.blockhistory.v3.util.UnitUtil;
|
||||||
|
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||||
|
import io.papermc.paper.command.brigadier.argument.ArgumentTypes;
|
||||||
|
import io.papermc.paper.command.brigadier.argument.resolvers.BlockPositionResolver;
|
||||||
|
import io.papermc.paper.math.BlockPosition;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static de.shiewk.blockhistory.v3.BlockHistoryPlugin.*;
|
||||||
|
import static io.papermc.paper.command.brigadier.Commands.argument;
|
||||||
|
import static io.papermc.paper.command.brigadier.Commands.literal;
|
||||||
|
import static net.kyori.adventure.text.Component.*;
|
||||||
|
|
||||||
|
public final class BlockHistoryCommand {
|
||||||
|
|
||||||
|
public @NotNull LiteralCommandNode<CommandSourceStack> getCommandNode() {
|
||||||
|
return literal("blockhistory")
|
||||||
|
.requires(CommandUtil.requirePermission("blockhistory.command.root"))
|
||||||
|
.then(literal("stats")
|
||||||
|
.requires(CommandUtil.requirePermission("blockhistory.command.stats"))
|
||||||
|
.executes(this::statsCommand)
|
||||||
|
)
|
||||||
|
.then(literal("history")
|
||||||
|
.requires(CommandUtil.requirePermission("blockhistory.command.history"))
|
||||||
|
.then(argument("location", ArgumentTypes.blockPosition())
|
||||||
|
.executes(this::historyCommand)
|
||||||
|
.then(argument("world", ArgumentTypes.world())
|
||||||
|
.executes(this::historyCommand)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int statsCommand(CommandContext<CommandSourceStack> context) {
|
||||||
|
CommandSender sender = context.getSource().getSender();
|
||||||
|
StatManager statManager = BlockHistoryPlugin.instance().getStatManager();
|
||||||
|
long millisSinceStart = statManager.getTimeMsSinceStart();
|
||||||
|
sender.sendMessage(CHAT_PREFIX.append(
|
||||||
|
text("The plugin has started up ", COLOR_PRIMARY)
|
||||||
|
.append(text(UnitUtil.formatTime(millisSinceStart), COLOR_SECONDARY))
|
||||||
|
.append(text(" ago."))
|
||||||
|
));
|
||||||
|
sender.sendMessage(CHAT_PREFIX.append(
|
||||||
|
text("So far, we have written ", COLOR_PRIMARY)
|
||||||
|
.append(text(statManager.getElementsWritten(), COLOR_SECONDARY))
|
||||||
|
.append(text(" history elements to disk. "))
|
||||||
|
.append(text("(%s elements per second)".formatted(statManager.getElementsWrittenPerSecond()), NamedTextColor.GRAY))
|
||||||
|
));
|
||||||
|
sender.sendMessage(CHAT_PREFIX.append(
|
||||||
|
text("These elements have a total size of ", COLOR_PRIMARY)
|
||||||
|
.append(text(UnitUtil.formatDataSize(statManager.getBytesWritten()), COLOR_SECONDARY))
|
||||||
|
.append(text(". "))
|
||||||
|
.append(text("(%s per second)".formatted(UnitUtil.formatDataSize(statManager.getBytesWrittenPerSecond())), NamedTextColor.GRAY))
|
||||||
|
));
|
||||||
|
try {
|
||||||
|
long usableDiskSpace = instance().getHistoryManager().getUsableDiskSpace();
|
||||||
|
sender.sendMessage(CHAT_PREFIX.append(
|
||||||
|
text("There are ", COLOR_PRIMARY)
|
||||||
|
.append(text(UnitUtil.formatDataSize(usableDiskSpace), COLOR_SECONDARY))
|
||||||
|
.append(text(" of disk space available."))
|
||||||
|
));
|
||||||
|
} catch (IOException e) {
|
||||||
|
sender.sendMessage(CHAT_PREFIX.append(
|
||||||
|
text("Failed to get usable disk space", COLOR_FAIL)
|
||||||
|
));
|
||||||
|
StringWriter strw = new StringWriter();
|
||||||
|
e.printStackTrace(new PrintWriter(strw));
|
||||||
|
|
||||||
|
BlockHistoryPlugin.logger().warn("Exception while getting usable disk space:");
|
||||||
|
for (String s : strw.toString().split("\n")) {
|
||||||
|
BlockHistoryPlugin.logger().warn(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int historyCommand(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
|
||||||
|
CommandSourceStack stack = context.getSource();
|
||||||
|
CommandSender sender = stack.getSender();
|
||||||
|
BlockPosition blockPosition = context.getArgument("location", BlockPositionResolver.class).resolve(stack);
|
||||||
|
World world;
|
||||||
|
try {
|
||||||
|
world = context.getArgument("world", World.class);
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
world = CommandUtil.getPlayerOf(stack, "You need to provide a world to search").getWorld();
|
||||||
|
}
|
||||||
|
int blockZ = blockPosition.blockZ();
|
||||||
|
int blockY = blockPosition.blockY();
|
||||||
|
int blockX = blockPosition.blockX();
|
||||||
|
sender.sendMessage(CHAT_PREFIX.append(text("Searching in world %s at x=%s y=%s z=%s, please wait...\n".formatted(world.key(), blockX, blockY, blockZ), COLOR_PRIMARY)));
|
||||||
|
try {
|
||||||
|
World searchedWorld = world;
|
||||||
|
long n = System.nanoTime();
|
||||||
|
AtomicInteger foundElements = new AtomicInteger(0);
|
||||||
|
BlockHistoryPlugin.instance().getHistoryManager().searchAsync(
|
||||||
|
new BlockHistoryCommandCallback(sender, foundElements),
|
||||||
|
world,
|
||||||
|
blockX,
|
||||||
|
blockY,
|
||||||
|
blockZ
|
||||||
|
).whenComplete((unused, throwable) -> {
|
||||||
|
if (throwable != null){
|
||||||
|
StringWriter strw = new StringWriter();
|
||||||
|
throwable.printStackTrace(new PrintWriter(strw));
|
||||||
|
|
||||||
|
BlockHistoryPlugin.logger().warn("Exception while searching block at world {} x {} y {} z {}:", searchedWorld, blockX, blockY, blockZ);
|
||||||
|
for (String s : strw.toString().split("\n")) {
|
||||||
|
BlockHistoryPlugin.logger().warn(s);
|
||||||
|
}
|
||||||
|
sender.sendMessage(CHAT_PREFIX.append(text("An error occurred while searching, please check the server console.\n", COLOR_FAIL)));
|
||||||
|
} else {
|
||||||
|
int elementsFound = foundElements.get();
|
||||||
|
sender.sendMessage((elementsFound > 0 ? newline() : empty()).append(CHAT_PREFIX.append(text("Search completed in %s ms, %s elements found".formatted((System.nanoTime() - n) / 1000000, elementsFound), COLOR_PRIMARY))));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (RejectedExecutionException e) {
|
||||||
|
sender.sendMessage(text("The searching system is currently too busy, please try again later.", COLOR_FAIL));
|
||||||
|
}
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record BlockHistoryCommandCallback(CommandSender sender, AtomicInteger foundElements) implements BlockHistorySearchCallback {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onElementFound(BlockHistoryElement element) {
|
||||||
|
sender.sendMessage(CHAT_PREFIX.append(element.toComponent(PlayerUtil::playerName)));
|
||||||
|
foundElements.getAndIncrement();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNoFilePresent(FileNotFoundException e) {
|
||||||
|
BlockHistoryPlugin.logger().info("No file present");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package de.shiewk.blockhistory.v3.exception;
|
||||||
|
|
||||||
|
public class LowDiskSpaceException extends Exception {
|
||||||
|
|
||||||
|
private final long freeBytes;
|
||||||
|
|
||||||
|
public LowDiskSpaceException(long freeBytes){
|
||||||
|
super("Free disk space is too low to safely execute this action: " + freeBytes + " bytes");
|
||||||
|
this.freeBytes = freeBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFreeBytes() {
|
||||||
|
return freeBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package de.shiewk.blockhistory.v3.history;
|
||||||
|
|
||||||
|
import de.shiewk.blockhistory.v3.util.UnitUtil;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.TextComponent;
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import static de.shiewk.blockhistory.v3.BlockHistoryPlugin.COLOR_PRIMARY;
|
||||||
|
import static de.shiewk.blockhistory.v3.BlockHistoryPlugin.COLOR_SECONDARY;
|
||||||
|
import static net.kyori.adventure.text.Component.text;
|
||||||
|
import static net.kyori.adventure.text.Component.translatable;
|
||||||
|
|
||||||
|
public record BlockHistoryElement(
|
||||||
|
@NotNull World world,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int z,
|
||||||
|
@NotNull BlockHistoryType type,
|
||||||
|
long timestamp,
|
||||||
|
@Nullable UUID player,
|
||||||
|
@NotNull Material material,
|
||||||
|
byte[] additionalData
|
||||||
|
) {
|
||||||
|
|
||||||
|
|
||||||
|
public BlockHistoryElement(
|
||||||
|
@NotNull World world,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int z,
|
||||||
|
@NotNull BlockHistoryType type,
|
||||||
|
long timestamp,
|
||||||
|
@Nullable UUID player,
|
||||||
|
@NotNull Material material,
|
||||||
|
byte @Nullable [] additionalData
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(world, "world");
|
||||||
|
Objects.requireNonNull(type, "type");
|
||||||
|
Objects.requireNonNull(material, "material");
|
||||||
|
this.world = world;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
this.type = type;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.player = player;
|
||||||
|
this.material = material;
|
||||||
|
this.additionalData = Objects.requireNonNullElse(additionalData, new byte[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.LONG);
|
||||||
|
|
||||||
|
public Component toComponent(Function<UUID, Component> playerNameResolver) {
|
||||||
|
Component playerName = playerNameResolver.apply(player);
|
||||||
|
Date date = Date.from(Instant.ofEpochMilli(timestamp));
|
||||||
|
TextComponent component = text("Block ", COLOR_PRIMARY)
|
||||||
|
.append(translatable(material, COLOR_SECONDARY))
|
||||||
|
.append(text(" was "))
|
||||||
|
.append(text(type.displayName, COLOR_SECONDARY))
|
||||||
|
.append(text(" by "))
|
||||||
|
.append(playerName.colorIfAbsent(COLOR_SECONDARY))
|
||||||
|
.append(text(" at "))
|
||||||
|
.append(text(dateFormat.format(date)));
|
||||||
|
if (additionalData.length > 0 && type == BlockHistoryType.SIGN){
|
||||||
|
component = component.append(
|
||||||
|
text(": \"")
|
||||||
|
.append(MiniMessage.miniMessage().deserialize(new String(additionalData)))
|
||||||
|
.append(text("\""))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BlockHistoryElement read(int dataVersion, DataInputStream dataIn, World world, int chunkX, int chunkZ) throws IOException {
|
||||||
|
return switch (dataVersion) {
|
||||||
|
case 0 -> readv0(dataIn, world, chunkX, chunkZ);
|
||||||
|
default -> throw new IllegalStateException("Unknown data version: " + dataVersion);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockHistoryElement readv0(DataInputStream dataIn, World world, int chunkX, int chunkZ) throws IOException {
|
||||||
|
byte chunkRelativeX = dataIn.readByte();
|
||||||
|
int realX = chunkX < 0 ? (chunkX+1) * 16 - (16 - chunkRelativeX) : chunkX * 16 + chunkRelativeX;
|
||||||
|
byte chunkRelativeZ = dataIn.readByte();
|
||||||
|
int realZ = chunkZ < 0 ? (chunkZ+1) * 16 - (16 - chunkRelativeZ) : chunkZ * 16 + chunkRelativeZ;
|
||||||
|
int y = dataIn.readInt();
|
||||||
|
BlockHistoryType type = BlockHistoryType.values()[dataIn.readInt()];
|
||||||
|
long timestamp = dataIn.readLong();
|
||||||
|
UUID player;
|
||||||
|
if (dataIn.readBoolean()){
|
||||||
|
player = new UUID(dataIn.readLong(), dataIn.readLong());
|
||||||
|
} else player = null;
|
||||||
|
String materialName = dataIn.readUTF();
|
||||||
|
Material material = Material.getMaterial(materialName);
|
||||||
|
if (material == null){
|
||||||
|
throw new IllegalStateException("Material " + materialName + " not found");
|
||||||
|
}
|
||||||
|
int additionalLength = dataIn.readInt();
|
||||||
|
byte[] additionalData;
|
||||||
|
if (additionalLength > 0){
|
||||||
|
additionalData = dataIn.readNBytes(additionalLength);
|
||||||
|
} else {
|
||||||
|
additionalData = new byte[0];
|
||||||
|
}
|
||||||
|
return new BlockHistoryElement(
|
||||||
|
world, realX, y, realZ, type, timestamp, player, material, additionalData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] saveData() throws IOException {
|
||||||
|
ByteArrayOutputStream arrayOut = new ByteArrayOutputStream();
|
||||||
|
try (DataOutputStream dataOut = new DataOutputStream(arrayOut)) {
|
||||||
|
dataOut.writeByte(0); // data version
|
||||||
|
dataOut.writeByte(UnitUtil.getBlockChunkLocation(x));
|
||||||
|
dataOut.writeByte(UnitUtil.getBlockChunkLocation(z));
|
||||||
|
dataOut.writeInt(y);
|
||||||
|
dataOut.writeInt(type.ordinal());
|
||||||
|
dataOut.writeLong(timestamp);
|
||||||
|
boolean writePlayerData = player != null;
|
||||||
|
dataOut.writeBoolean(writePlayerData);
|
||||||
|
if (writePlayerData){
|
||||||
|
dataOut.writeLong(player.getMostSignificantBits());
|
||||||
|
dataOut.writeLong(player.getLeastSignificantBits());
|
||||||
|
}
|
||||||
|
dataOut.writeUTF(material.name());
|
||||||
|
dataOut.writeInt(additionalData.length);
|
||||||
|
dataOut.flush();
|
||||||
|
}
|
||||||
|
arrayOut.write(additionalData);
|
||||||
|
arrayOut.flush();
|
||||||
|
return arrayOut.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getLocation() {
|
||||||
|
return new Location(world, x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.shiewk.blockhistory.v3.history;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
|
||||||
|
public interface BlockHistorySearchCallback {
|
||||||
|
|
||||||
|
void onElementFound(BlockHistoryElement element);
|
||||||
|
|
||||||
|
void onNoFilePresent(FileNotFoundException e);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package de.shiewk.blockhistory.v3.history;
|
||||||
|
|
||||||
|
public enum BlockHistoryType {
|
||||||
|
PLACE("PLACED"),
|
||||||
|
BREAK("BROKEN"),
|
||||||
|
EMPTY_BUCKET("PLACED USING BUCKET"),
|
||||||
|
FILL_BUCKET("PICKED UP USING BUCKET"),
|
||||||
|
EXPLODE_END_CRYSTAL("EXPLODED USING END CRYSTAL"),
|
||||||
|
EXPLODE_TNT("EXPLODED USING TNT"),
|
||||||
|
EXPLODE_CREEPER("EXPLODED USING CREEPER"),
|
||||||
|
EXPLODE_BLOCK("EXPLODED USING BLOCK"),
|
||||||
|
SIGN("CHANGED"),
|
||||||
|
CHEST_OPEN("OPENED");
|
||||||
|
|
||||||
|
public final String displayName;
|
||||||
|
|
||||||
|
BlockHistoryType(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package de.shiewk.blockhistory.v3.listener;
|
||||||
|
|
||||||
|
import de.shiewk.blockhistory.v3.BlockHistoryPlugin;
|
||||||
|
import de.shiewk.blockhistory.v3.history.BlockHistoryElement;
|
||||||
|
import de.shiewk.blockhistory.v3.history.BlockHistoryType;
|
||||||
|
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.BlockFace;
|
||||||
|
import org.bukkit.block.Container;
|
||||||
|
import org.bukkit.block.data.type.Bed;
|
||||||
|
import org.bukkit.entity.*;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.*;
|
||||||
|
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||||
|
import org.bukkit.event.entity.EntityExplodeEvent;
|
||||||
|
import org.bukkit.event.player.PlayerBucketEmptyEvent;
|
||||||
|
import org.bukkit.event.player.PlayerBucketFillEvent;
|
||||||
|
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class BlockListener implements Listener {
|
||||||
|
|
||||||
|
private static final Object2ObjectOpenHashMap<UUID, UUID> igniters = new Object2ObjectOpenHashMap<>();
|
||||||
|
private static final Object2ObjectOpenHashMap<Block, UUID> blocks = new Object2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
|
public static void clearCache() {
|
||||||
|
igniters.clear();
|
||||||
|
blocks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||||
|
public void onBlockBreak(BlockBreakEvent event){
|
||||||
|
final Block block = event.getBlock();
|
||||||
|
createAndAddEntry(BlockHistoryType.BREAK, event.getPlayer(), block);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||||
|
public void onBlockPlace(BlockPlaceEvent event){
|
||||||
|
final Block block = event.getBlock();
|
||||||
|
createAndAddEntry(BlockHistoryType.PLACE, event.getPlayer(), block);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||||
|
public void onBucketEmptied(PlayerBucketEmptyEvent event){
|
||||||
|
Bukkit.getScheduler().scheduleSyncDelayedTask(BlockHistoryPlugin.instance(), () -> {
|
||||||
|
final Block block = event.getBlock();
|
||||||
|
createAndAddEntry(BlockHistoryType.EMPTY_BUCKET, event.getPlayer(), block);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||||
|
public void onBucketFilled(PlayerBucketFillEvent event){
|
||||||
|
final Block block = event.getBlock();
|
||||||
|
createAndAddEntry(BlockHistoryType.FILL_BUCKET, event.getPlayer(), block);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||||
|
public void onEntityExplode(EntityExplodeEvent event){
|
||||||
|
Entity entity = event.getEntity();
|
||||||
|
switch (entity) {
|
||||||
|
case TNTPrimed primed -> {
|
||||||
|
for (Block block : event.blockList()) {
|
||||||
|
createAndAddEntry(BlockHistoryType.EXPLODE_TNT, primed.getSource() instanceof Player player ? player : null, block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Creeper creeper -> {
|
||||||
|
final UUID igniter = igniters.remove(creeper.getUniqueId());
|
||||||
|
for (Block block : event.blockList()) {
|
||||||
|
createAndAddEntry(BlockHistoryType.EXPLODE_CREEPER, igniter, block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case EnderCrystal crystal -> {
|
||||||
|
final UUID igniter = igniters.remove(crystal.getUniqueId());
|
||||||
|
for (Block block : event.blockList()) {
|
||||||
|
createAndAddEntry(BlockHistoryType.EXPLODE_END_CRYSTAL, igniter, block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||||
|
public void onBlockExplode(BlockExplodeEvent event){
|
||||||
|
Block explodedBlock = event.getBlock();
|
||||||
|
UUID exploder = blocks.remove(explodedBlock);
|
||||||
|
BlockHistoryType type = BlockHistoryType.EXPLODE_BLOCK;
|
||||||
|
for (Block block : event.blockList()) {
|
||||||
|
createAndAddEntry(type, exploder, block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||||
|
public void onEntityDamagedByEntity(EntityDamageByEntityEvent event){
|
||||||
|
if (event.getDamager() instanceof Player player){
|
||||||
|
if (event.getEntity() instanceof EnderCrystal enderCrystal){
|
||||||
|
igniters.put(enderCrystal.getUniqueId(), player.getUniqueId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent event){
|
||||||
|
if (event.getRightClicked() instanceof Creeper creeper){
|
||||||
|
if (event.getPlayer().getEquipment().getItemInMainHand().getAmount() > 0 || event.getPlayer().getEquipment().getItemInOffHand().getAmount() > 0){
|
||||||
|
igniters.put(creeper.getUniqueId(), event.getPlayer().getUniqueId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onPlayerInteract(PlayerInteractEvent event){
|
||||||
|
if (event.getAction() == Action.RIGHT_CLICK_BLOCK){
|
||||||
|
final Block clickedBlock = event.getClickedBlock();
|
||||||
|
if (clickedBlock.getType() == Material.RESPAWN_ANCHOR) {
|
||||||
|
blocks.put(clickedBlock, event.getPlayer().getUniqueId());
|
||||||
|
} else if (clickedBlock.getBlockData() instanceof Bed bed){
|
||||||
|
final BlockFace facing = bed.getFacing();
|
||||||
|
if (bed.getPart() == Bed.Part.FOOT){
|
||||||
|
Block actualBlock = new Location(clickedBlock.getWorld(), clickedBlock.getX() + (facing.getModX()), clickedBlock.getY() + (facing.getModY()), clickedBlock.getZ() + (facing.getModZ())).getBlock();
|
||||||
|
if (actualBlock.getBlockData() instanceof Bed){
|
||||||
|
blocks.put(actualBlock, event.getPlayer().getUniqueId());
|
||||||
|
} else {
|
||||||
|
BlockHistoryPlugin.logger().warn("Error: Bed does not exist");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
blocks.put(clickedBlock, event.getPlayer().getUniqueId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onSignChange(SignChangeEvent event){
|
||||||
|
final Player player = event.getPlayer();
|
||||||
|
final Block block = event.getBlock();
|
||||||
|
final Location blockLocation = block.getLocation();
|
||||||
|
final StringBuilder signData = new StringBuilder();
|
||||||
|
final MiniMessage serializer = MiniMessage.miniMessage();
|
||||||
|
for (Component line : event.lines()) {
|
||||||
|
if (!signData.isEmpty()){
|
||||||
|
signData.append(" ");
|
||||||
|
}
|
||||||
|
signData.append(serializer.serialize(line));
|
||||||
|
}
|
||||||
|
BlockHistoryPlugin.instance().getHistoryManager().addHistoryElement(new BlockHistoryElement(
|
||||||
|
block.getWorld(),
|
||||||
|
blockLocation.getBlockX(),
|
||||||
|
blockLocation.getBlockY(),
|
||||||
|
blockLocation.getBlockZ(),
|
||||||
|
BlockHistoryType.SIGN,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
player.getUniqueId(),
|
||||||
|
block.getType(),
|
||||||
|
signData.toString().getBytes()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addEntryToManager(BlockHistoryElement element){
|
||||||
|
BlockHistoryPlugin.instance().getHistoryManager().addHistoryElement(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createAndAddEntry(BlockHistoryType type, Player player, Block block) {
|
||||||
|
createAndAddEntry(type, player == null ? null : player.getUniqueId(), block);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createAndAddEntry(BlockHistoryType type, UUID player, Block block) {
|
||||||
|
addEntryToManager(new BlockHistoryElement(
|
||||||
|
block.getWorld(),
|
||||||
|
block.getX(),
|
||||||
|
block.getY(),
|
||||||
|
block.getZ(),
|
||||||
|
type,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
player,
|
||||||
|
block.getType(),
|
||||||
|
null
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||||
|
public void onContainerOpen(PlayerInteractEvent event){
|
||||||
|
if (event.hasBlock() && event.getAction().isRightClick()){
|
||||||
|
final Block block = event.getClickedBlock();
|
||||||
|
if (block.getState() instanceof Container){
|
||||||
|
createAndAddEntry(BlockHistoryType.CHEST_OPEN, event.getPlayer(), block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package de.shiewk.blockhistory.v3.util;
|
||||||
|
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public final class BlockHistoryFileNames {
|
||||||
|
private BlockHistoryFileNames(){}
|
||||||
|
|
||||||
|
private static char base32encode(short i){
|
||||||
|
return switch (i){
|
||||||
|
case 0 -> '0';
|
||||||
|
case 1 -> '1';
|
||||||
|
case 2 -> '2';
|
||||||
|
case 3 -> '3';
|
||||||
|
case 4 -> '4';
|
||||||
|
case 5 -> '5';
|
||||||
|
case 6 -> '6';
|
||||||
|
case 7 -> '7';
|
||||||
|
case 8 -> '8';
|
||||||
|
case 9 -> '9';
|
||||||
|
case 10 -> 'a';
|
||||||
|
case 11 -> 'b';
|
||||||
|
case 12 -> 'c';
|
||||||
|
case 13 -> 'd';
|
||||||
|
case 14 -> 'e';
|
||||||
|
case 15 -> 'f';
|
||||||
|
case 16 -> 'g';
|
||||||
|
case 17 -> 'h';
|
||||||
|
case 18 -> 'i';
|
||||||
|
case 19 -> 'j';
|
||||||
|
case 20 -> 'k';
|
||||||
|
case 21 -> 'l';
|
||||||
|
case 22 -> 'm';
|
||||||
|
case 23 -> 'n';
|
||||||
|
case 24 -> 'o';
|
||||||
|
case 25 -> 'p';
|
||||||
|
case 26 -> 'q';
|
||||||
|
case 27 -> 'r';
|
||||||
|
case 28 -> 's';
|
||||||
|
case 29 -> 't';
|
||||||
|
case 30 -> 'u';
|
||||||
|
case 31 -> 'v';
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: " + i);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Path encode(Path parentDirectory, Location location){
|
||||||
|
// encoded string is 13 characters long
|
||||||
|
World world = location.getWorld();
|
||||||
|
Chunk chunk = location.getChunk();
|
||||||
|
int chunkX = chunk.getX();
|
||||||
|
int chunkZ = chunk.getZ();
|
||||||
|
// 20 bytes
|
||||||
|
long packed = 0;
|
||||||
|
|
||||||
|
packed |= chunkX & 0b1111111111111111111L;
|
||||||
|
if (chunkX < 0) packed |= 0b10000000000000000000;
|
||||||
|
|
||||||
|
packed |= (chunkZ & 0b1111111111111111111L) << 20;
|
||||||
|
if (chunkZ < 0) packed |= 0b1000000000000000000000000000000000000000L;
|
||||||
|
|
||||||
|
String encodedChunkFileName = new String(new char[]{
|
||||||
|
base32encode((short) (packed >> 35 & 0b11111)),
|
||||||
|
base32encode((short) (packed >> 30 & 0b11111)),
|
||||||
|
base32encode((short) (packed >> 25 & 0b11111)),
|
||||||
|
base32encode((short) (packed >> 20 & 0b11111)),
|
||||||
|
base32encode((short) (packed >> 15 & 0b11111)),
|
||||||
|
base32encode((short) (packed >> 10 & 0b11111)),
|
||||||
|
base32encode((short) (packed >> 5 & 0b11111)),
|
||||||
|
base32encode((short) (packed & 0b11111))
|
||||||
|
});
|
||||||
|
|
||||||
|
return Path.of(parentDirectory.toString(),
|
||||||
|
world.getWorldFolder().getName(),
|
||||||
|
encodedChunkFileName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package de.shiewk.blockhistory.v3.util;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||||
|
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||||
|
import io.papermc.paper.command.brigadier.MessageComponentSerializer;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public final class CommandUtil {
|
||||||
|
private CommandUtil(){}
|
||||||
|
|
||||||
|
public static @NotNull Predicate<CommandSourceStack> requirePermission(String permission) {
|
||||||
|
return stack -> stack.getSender().hasPermission(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NotNull Player getPlayerOf(CommandSourceStack stack) throws CommandSyntaxException {
|
||||||
|
return getPlayerOf(stack, "Only players can execute this (sub)command");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NotNull Player getPlayerOf(CommandSourceStack stack, String errorMessage) throws CommandSyntaxException {
|
||||||
|
if (stack.getSender() instanceof Player player){
|
||||||
|
return player;
|
||||||
|
} else throw new CommandSyntaxException(
|
||||||
|
new SimpleCommandExceptionType(null),
|
||||||
|
MessageComponentSerializer.message().serialize(
|
||||||
|
Component.text(errorMessage, NamedTextColor.RED)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package de.shiewk.blockhistory.v3.util;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
public final class NamedLoggingThreadFactory implements ThreadFactory {
|
||||||
|
|
||||||
|
private final String threadName;
|
||||||
|
private final int threadPriority;
|
||||||
|
private final Logger logger;
|
||||||
|
private final String label;
|
||||||
|
private final AtomicInteger threadNumber;
|
||||||
|
|
||||||
|
public NamedLoggingThreadFactory(String threadName, int threadPriority, Logger logger, String label, AtomicInteger threadNumber) {
|
||||||
|
this.threadName = threadName;
|
||||||
|
this.threadPriority = threadPriority;
|
||||||
|
this.logger = logger;
|
||||||
|
this.label = label;
|
||||||
|
this.threadNumber = threadNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Thread newThread(@NotNull Runnable r) {
|
||||||
|
Thread thread = new Thread(r);
|
||||||
|
thread.setName(threadName + "-" + threadNumber.incrementAndGet());
|
||||||
|
thread.setPriority(threadPriority);
|
||||||
|
logger.info("Created new thread for {}: {}", label, thread);
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package de.shiewk.blockhistory.v3.util;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static net.kyori.adventure.text.Component.text;
|
||||||
|
|
||||||
|
public final class PlayerUtil {
|
||||||
|
private PlayerUtil(){}
|
||||||
|
|
||||||
|
|
||||||
|
public static String offlinePlayerName(UUID uuid){
|
||||||
|
if (uuid == null) return "Unknown Player";
|
||||||
|
OfflinePlayer player = Bukkit.getOfflinePlayer(uuid);
|
||||||
|
return player.getName() == null ? uuid.toString() : player.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component playerName(UUID uuid) {
|
||||||
|
Player player;
|
||||||
|
if ((player = Bukkit.getPlayer(uuid)) != null){
|
||||||
|
return player.displayName();
|
||||||
|
} else {
|
||||||
|
return text(offlinePlayerName(uuid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package de.shiewk.blockhistory.v3.util;
|
||||||
|
|
||||||
|
public final class UnitUtil {
|
||||||
|
private UnitUtil(){}
|
||||||
|
|
||||||
|
public static String formatTime(long totalMillis){
|
||||||
|
long millis = totalMillis % 1000;
|
||||||
|
long seconds = totalMillis / 1000 % 60;
|
||||||
|
long minutes = totalMillis / 60000 % 60;
|
||||||
|
long hours = totalMillis / 3600000 % 24;
|
||||||
|
long days = totalMillis / 86400000;
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
|
if (days != 0){
|
||||||
|
builder.append(days).append("d ");
|
||||||
|
}
|
||||||
|
if (hours != 0){
|
||||||
|
builder.append(hours).append("h ");
|
||||||
|
}
|
||||||
|
if (minutes != 0){
|
||||||
|
builder.append(minutes).append("m ");
|
||||||
|
}
|
||||||
|
if (seconds != 0){
|
||||||
|
builder.append(seconds).append("s ");
|
||||||
|
}
|
||||||
|
if (builder.isEmpty() || millis != 0){
|
||||||
|
builder.append(millis).append("ms");
|
||||||
|
}
|
||||||
|
return builder.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatDataSize(double bytes) {
|
||||||
|
String[] suffixes = new String[]{"KiB", "MiB", "GiB"};
|
||||||
|
String suffix = "bytes";
|
||||||
|
int i = -1;
|
||||||
|
while (bytes > 1024 && ++i < suffixes.length){
|
||||||
|
bytes /= 1024;
|
||||||
|
suffix = suffixes[i];
|
||||||
|
}
|
||||||
|
return Math.floor(bytes * 10d) / 10d + " " + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte getBlockChunkLocation(int x){
|
||||||
|
byte b = (byte) (x % 16);
|
||||||
|
if (b < 0) b = (byte) (16 + b);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: BlockHistory
|
||||||
|
version: '${version}'
|
||||||
|
main: de.shiewk.blockhistory.v3.BlockHistoryPlugin
|
||||||
|
api-version: '1.21'
|
||||||
|
prefix: BlockHistory
|
||||||
|
load: STARTUP
|
||||||
|
authors: [ Shiewk ]
|
||||||
|
description: BlockHistory is a Paper plugin that helps you to find griefers more easily.
|
||||||
|
permissions:
|
||||||
|
blockhistory.command.root:
|
||||||
|
default: op
|
||||||
|
description: Player can access root BlockHistory command node
|
||||||
|
blockhistory.command.stats:
|
||||||
|
default: op
|
||||||
|
description: Player can view the BlockHistory stats
|
||||||
|
children:
|
||||||
|
blockhistory.command.root: true
|
||||||
|
blockhistory.command.history:
|
||||||
|
default: op
|
||||||
|
description: Player can search the history of a block
|
||||||
|
children:
|
||||||
|
blockhistory.command.root: true
|
||||||
Reference in New Issue
Block a user