├── settings.gradle
├── Images
├── Commands.png
├── Features.png
├── Overview.png
├── Support.png
├── Template.png
├── MinerTrack.png
├── Permissions.png
├── Configuration.png
├── Installation.png
├── Requirements.png
└── MinerTrack_64x.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .github
├── ISSUE_TEMPLATE
│ ├── Feature_Request_CN.md
│ ├── Feature_Request.md
│ ├── BUG_Report_CN.md
│ ├── bug报告--folia-.md
│ ├── BUG_Report.md
│ └── bug-report--folia-.md
└── workflows
│ └── AutoBuild_CI_dev.yml
├── .gitignore
├── .settings
├── org.eclipse.buildship.core.prefs
└── org.eclipse.jdt.core.prefs
├── src
└── main
│ ├── java
│ └── link
│ │ └── star_dust
│ │ └── MinerTrack
│ │ ├── FoliaCheck.java
│ │ ├── listeners
│ │ ├── LogCacheListener.java
│ │ └── MiningDetectionExtension.java
│ │ ├── Notifier.java
│ │ ├── utils
│ │ └── LogViewerUtils.java
│ │ ├── hooks
│ │ ├── CustomJsonWebHook.java
│ │ └── DiscordWebHook.java
│ │ ├── managers
│ │ ├── LanguageManager.java
│ │ ├── ConfigManager.java
│ │ ├── UpdateManager.java
│ │ └── ViolationManager.java
│ │ ├── MinerTrack.java
│ │ └── commands
│ │ └── MinerTrackCommand.java
│ └── resources
│ ├── plugin.yml
│ ├── Translations
│ ├── zh_cn.yml
│ └── fr.yml
│ ├── language.yml
│ └── config.yml
├── .classpath
├── .project
├── gradlew.bat
├── README-zh_hans.md
├── README.md
├── gradlew
└── LICENSE
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'MinerTrack'
2 |
--------------------------------------------------------------------------------
/Images/Commands.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/At87668/MinerTrack/HEAD/Images/Commands.png
--------------------------------------------------------------------------------
/Images/Features.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/At87668/MinerTrack/HEAD/Images/Features.png
--------------------------------------------------------------------------------
/Images/Overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/At87668/MinerTrack/HEAD/Images/Overview.png
--------------------------------------------------------------------------------
/Images/Support.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/At87668/MinerTrack/HEAD/Images/Support.png
--------------------------------------------------------------------------------
/Images/Template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/At87668/MinerTrack/HEAD/Images/Template.png
--------------------------------------------------------------------------------
/Images/MinerTrack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/At87668/MinerTrack/HEAD/Images/MinerTrack.png
--------------------------------------------------------------------------------
/Images/Permissions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/At87668/MinerTrack/HEAD/Images/Permissions.png
--------------------------------------------------------------------------------
/Images/Configuration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/At87668/MinerTrack/HEAD/Images/Configuration.png
--------------------------------------------------------------------------------
/Images/Installation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/At87668/MinerTrack/HEAD/Images/Installation.png
--------------------------------------------------------------------------------
/Images/Requirements.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/At87668/MinerTrack/HEAD/Images/Requirements.png
--------------------------------------------------------------------------------
/Images/MinerTrack_64x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/At87668/MinerTrack/HEAD/Images/MinerTrack_64x.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/At87668/MinerTrack/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Feature_Request_CN.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 功能请求
3 | about: 为这个插件提出一个想法
4 | title: "请求标题"
5 | labels: "➕Enhancement"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### 请求标题
11 |
12 | #### 想要的功能
13 |
14 | [描述]
15 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Feature_Request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest an idea for this plugin.
4 | title: "Request Title"
5 | labels: "➕Enhancement"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Request Title
11 |
12 | #### Wanted Feature
13 |
14 | [Description]
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle
2 | .gradle/
3 | !gradle/wrapper/gradle-wrapper.jar
4 | !**/src/main/**/build/
5 | !**/src/test/**/build/
6 | build/
7 | bin/
8 |
9 | # Wiki
10 | wiki/
11 |
12 | # VS Code
13 | .vscode/
14 | *.code-workspace
15 |
16 | # Cache
17 | *.class
18 |
19 | # Script
20 | *.py
21 |
22 | # Changes Log
23 | Updates.md
24 |
25 | # Eclipse
26 | .classpath
27 | .project
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG_Report_CN.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: BUG报告
3 | about: 如果你想报告一个BUG, 选择这个
4 | title: "[Minecraft版本] 问题标题"
5 | labels: "❌BUG"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### [Minecraft版本] 问题标题
11 |
12 | - [x] 已在Paper测试
13 | - [x] 已在仅有此插件的情况下测试
14 | #### 描述
15 |
16 | [描述]
17 |
18 | #### 服务器日志
19 |
20 | [使用类似 [Github Gist](https://gist.github.com/) 或 [PasteBin](https://pastebin.com/) 的平台]
21 |
22 | #### 安装的插件
23 | [使用 "/plugin"获取]
24 |
25 | #### 插件版本
26 | [插件版本]
27 |
28 | #### Minecraft版本
29 | [Minecraft版本]
30 |
31 | #### 服务端类型
32 | [如 Spigot 或 Paper]
33 |
34 | #### 如何复现
35 | 1. 步骤 1
36 | 1. 步骤 2
37 | 1. 步骤 ...
38 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug报告--folia-.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: BUG报告 (Folia)
3 | about: 如果你想报告一个BUG, 选择这个
4 | title: "[Minecraft版本] 问题标题"
5 | labels: "❌BUG, \U0001F343Folia"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### [Minecraft版本] 问题标题
11 |
12 | - [x] 已在Folia测试
13 | - [x] 已在仅有此插件的情况下测试
14 | #### 描述
15 |
16 | [描述]
17 |
18 | #### 服务器日志
19 |
20 | [使用类似 [Github Gist](https://gist.github.com/) 或 [PasteBin](https://pastebin.com/) 的平台]
21 |
22 | #### 安装的插件
23 | [使用 "/plugin"获取]
24 |
25 | #### 插件版本
26 | [插件版本]
27 |
28 | #### Minecraft版本
29 | [Minecraft版本]
30 |
31 | #### 服务端类型
32 | [如 Spigot 或 Paper]
33 |
34 | #### 如何复现
35 | 1. 步骤 1
36 | 1. 步骤 2
37 | 1. 步骤 ...
38 |
--------------------------------------------------------------------------------
/.settings/org.eclipse.buildship.core.prefs:
--------------------------------------------------------------------------------
1 | arguments=--init-script C\:\\Users\\Administrator\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.50.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\init\\init.gradle --init-script C\:\\Users\\Administrator\\AppData\\Roaming\\Code\\User\\globalStorage\\redhat.java\\1.50.0\\config_win\\org.eclipse.osgi\\58\\0\\.cp\\gradle\\protobuf\\init.gradle
2 | auto.sync=false
3 | build.scans.enabled=false
4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
5 | connection.project.dir=
6 | eclipse.preferences.version=1
7 | gradle.user.home=
8 | java.home=C\:/Program Files/Java/zulu17.46.19-ca-jdk17.0.9-win_x64
9 | jvm.arguments=
10 | offline.mode=false
11 | override.workspace.settings=true
12 | show.console.view=true
13 | show.executions.view=true
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG_Report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: BUG Report
3 | about: If you want to report a bug, select this.
4 | title: "[Minecraft Version] Issue Title"
5 | labels: "❌BUG"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### [Minecraft Version] Issue Title
11 |
12 | - [x] Tested on Paper
13 | - [x] Tested with this plugin only
14 | #### Description
15 |
16 | [Description]
17 |
18 | #### Server Log
19 |
20 | [Using [Github Gist](https://gist.github.com/) or [PasteBin](https://pastebin.com/) like.]
21 |
22 | #### Installed Plugins
23 | [Use "/plugin" to get.]
24 |
25 | #### Plugin Version
26 | [Plugin's version.]
27 |
28 | #### Minecraft Version
29 | [Minecraft's version.]
30 |
31 | #### Server Software
32 | [Server software name like Spigot or Paper.]
33 |
34 | #### How to reproduce
35 | 1. Step 1
36 | 1. Step 2
37 | 1. Step ...
38 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report--folia-.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: BUG Report (Folia)
3 | about: If you want to report a bug, select this.
4 | title: "[Minecraft Version] Issue Title"
5 | labels: "❌BUG, \U0001F343Folia"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### [Minecraft Version] Issue Title
11 |
12 | - [x] Tested on Folia
13 | - [x] Tested with this plugin only
14 | #### Description
15 |
16 | [Description]
17 |
18 | #### Server Log
19 |
20 | [Using [Github Gist](https://gist.github.com/) or [PasteBin](https://pastebin.com/) like.]
21 |
22 | #### Installed Plugins
23 | [Use "/plugin" to get.]
24 |
25 | #### Plugin Version
26 | [Plugin's version.]
27 |
28 | #### Minecraft Version
29 | [Minecraft's version.]
30 |
31 | #### Server Software
32 | [Server software name like Spigot or Paper.]
33 |
34 | #### How to reproduce
35 | 1. Step 1
36 | 1. Step 2
37 | 1. Step ...
38 |
--------------------------------------------------------------------------------
/src/main/java/link/star_dust/MinerTrack/FoliaCheck.java:
--------------------------------------------------------------------------------
1 | /**
2 | * DON'T REMOVE THIS
3 | *
4 | * /MinerTrack/src/main/java/link/star_dust/MinerTrack/FoliaCheck.java
5 | *
6 | * MinerTrack Source Code - Public under GPLv3 license
7 | * Original Author: Author87668
8 | * Contributors: Author87668
9 | *
10 | * DON'T REMOVE THIS
11 | **/
12 | package link.star_dust.MinerTrack;
13 |
14 | public class FoliaCheck {
15 | private static Boolean isFolia = null;
16 |
17 | public static boolean isFolia() {
18 | if (isFolia == null) {
19 | try {
20 | Class.forName("io.papermc.paper.threadedregions.scheduler.RegionScheduler");
21 | isFolia = true;
22 | } catch (ClassNotFoundException e) {
23 | isFolia = false;
24 | }
25 | }
26 | return isFolia;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | PlayerDataRollback
4 | Project PlayerDataRollback created by Buildship.
5 |
6 |
7 |
8 |
9 | org.eclipse.jdt.core.javabuilder
10 |
11 |
12 |
13 |
14 | org.eclipse.buildship.core.gradleprojectbuilder
15 |
16 |
17 |
18 |
19 |
20 | org.eclipse.jdt.core.javanature
21 | org.eclipse.buildship.core.gradleprojectnature
22 |
23 |
24 |
25 | 1739358334330
26 |
27 | 30
28 |
29 | org.eclipse.core.resources.regexFilterMatcher
30 | node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/main/java/link/star_dust/MinerTrack/listeners/LogCacheListener.java:
--------------------------------------------------------------------------------
1 | /**
2 | * DON'T REMOVE THIS
3 | *
4 | * /MinerTrack/src/main/java/link/star_dust/MinerTrack/listeners/LogCacheListener.java
5 | *
6 | * MinerTrack Source Code - Public under GPLv3 license
7 | * Original Author: Author87668
8 | * Contributors: Author87668
9 | *
10 | * DON'T REMOVE THIS
11 | **/
12 | package link.star_dust.MinerTrack.listeners;
13 |
14 | import org.bukkit.event.EventHandler;
15 | import org.bukkit.event.Listener;
16 | import org.bukkit.event.player.PlayerQuitEvent;
17 | import org.bukkit.entity.Player;
18 | import link.star_dust.MinerTrack.commands.MinerTrackCommand;
19 |
20 | public class LogCacheListener implements Listener {
21 | private final MinerTrackCommand command;
22 |
23 | public LogCacheListener(MinerTrackCommand command) {
24 | this.command = command;
25 | }
26 |
27 | @EventHandler
28 | public void onPlayerQuit(PlayerQuitEvent event) {
29 | Player player = event.getPlayer();
30 | command.clearLogCache(player);
31 | }
32 | }
--------------------------------------------------------------------------------
/.settings/org.eclipse.jdt.core.prefs:
--------------------------------------------------------------------------------
1 | eclipse.preferences.version=1
2 | org.eclipse.jdt.core.classpath.outputOverlappingAnotherSource=ignore
3 | org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
4 | org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull
5 | org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=javax.annotation.ParametersAreNonnullByDefault
6 | org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable
7 | org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
8 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
9 | org.eclipse.jdt.core.compiler.compliance=17
10 | org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning
11 | org.eclipse.jdt.core.compiler.problem.nullReference=warning
12 | org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning
13 | org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
14 | org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
15 | org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled
16 | org.eclipse.jdt.core.compiler.source=17
17 |
--------------------------------------------------------------------------------
/src/main/java/link/star_dust/MinerTrack/Notifier.java:
--------------------------------------------------------------------------------
1 | /**
2 | * DON'T REMOVE THIS
3 | *
4 | * /MinerTrack/src/main/java/link/star_dust/MinerTrack/Notifier.java
5 | *
6 | * MinerTrack Source Code - Public under GPLv3 license
7 | * Original Author: Author87668
8 | * Contributors: Author87668
9 | *
10 | * DON'T REMOVE THIS
11 | **/
12 | package link.star_dust.MinerTrack;
13 |
14 | import link.star_dust.MinerTrack.managers.LanguageManager;
15 |
16 | import java.util.Arrays;
17 |
18 | import org.bukkit.Bukkit;
19 | import org.bukkit.ChatColor;
20 | import org.bukkit.command.Command;
21 | import org.bukkit.command.CommandSender;
22 | import org.bukkit.entity.Player;
23 |
24 | public class Notifier {
25 | private final MinerTrack plugin;
26 | private final LanguageManager lang;
27 |
28 | public Notifier(MinerTrack plugin) {
29 | this.plugin = plugin;
30 | this.lang = plugin.getLanguageManager();
31 | }
32 |
33 | public void kickPlayer(Player player, String reason) {
34 | player.kickPlayer(reason);
35 | }
36 |
37 |
38 | public void sendNotifyMessage(String messageContent) {
39 | // Define prefixes and add color codes
40 | String prefix = ChatColor.translateAlternateColorCodes('&', "&8[&9&lMiner&c&lTrack&8]&r ");
41 | String formattedMessage = prefix + ChatColor.translateAlternateColorCodes('&', messageContent);
42 |
43 | // Send message to players with permissions
44 | for (Player player : Bukkit.getOnlinePlayers()) {
45 | if (player.hasPermission("minertrack.notify")) {
46 | player.sendMessage(formattedMessage);
47 | }
48 | }
49 |
50 | // Send message to console
51 | Bukkit.getConsoleSender().sendMessage(formattedMessage);
52 | }
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/src/main/resources/plugin.yml:
--------------------------------------------------------------------------------
1 | name: MinerTrack
2 | version: 1.9.0-beta
3 | main: link.star_dust.MinerTrack.MinerTrack
4 | authors: [Author87668]
5 | contributors: [Author87668, Zhang12334]
6 | website: https://modrinth.com/plugin/minertrack
7 | api-version: 1.18
8 | folia-supported: true
9 | default-permission: false
10 | description: The Advanced Anti-Xray that you need, have you ever dreamed of a powerful anti-xray?
11 | commands:
12 | minertrack:
13 | description: Plugin's main command
14 | aliases: [mt, mtrack]
15 | usage: "/minertrack "
16 | permission: minertrack.use
17 | permissions:
18 | minertrack.use:
19 | description: Permission to access the root command "/mtrack"
20 | default: op
21 | minertrack.check:
22 | description: Allow player to use /mtrack check
23 | default: op
24 | minertrack.kick:
25 | description: Allow player to use /mtrack kick
26 | default: op
27 | minertrack.help:
28 | description: Allow player to use /mtrack help
29 | default: op
30 | minertrack.reset:
31 | description: Allow player to use /mtrack reset
32 | default: op
33 | minertrack.sendnotify:
34 | description: Allow player to use /mtrack notify
35 | default: op
36 | minertrack.verbose:
37 | description: Allow player to use /mtrack verbose
38 | default: op
39 | minertrack.reload:
40 | description: Allow player to use /mtrack reload
41 | default: op
42 | minertrack.bypass:
43 | description: Bypass the X-Ray detection
44 | default: op
45 | minertrack.notify:
46 | description: Receive notify information and verbose information
47 | default: op
48 | minertrack.checkupdate:
49 | description: Allow player to receive update information and use /mtrack update
50 | default: op
51 | minertrack.logs:
52 | description: Allow player to use /mtrack logs
53 | default: op
54 |
--------------------------------------------------------------------------------
/src/main/java/link/star_dust/MinerTrack/utils/LogViewerUtils.java:
--------------------------------------------------------------------------------
1 | /**
2 | * DON'T REMOVE THIS
3 | *
4 | * /MinerTrack/src/main/java/link/star_dust/MinerTrack/utils/LogViewerUtils.java
5 | *
6 | * MinerTrack Source Code - Public under GPLv3 license
7 | * Original Author: Author87668
8 | * Contributors: Author87668
9 | *
10 | * DON'T REMOVE THIS
11 | **/
12 | package link.star_dust.MinerTrack.utils;
13 |
14 | import org.bukkit.command.CommandSender;
15 | import java.io.File;
16 | import java.io.IOException;
17 | import java.nio.charset.StandardCharsets;
18 | import java.nio.file.Files;
19 | import java.util.List;
20 | import java.util.Map;
21 | import java.util.concurrent.ConcurrentHashMap;
22 |
23 | public class LogViewerUtils {
24 | public static class LogCache {
25 | public String logName;
26 | public List lines;
27 | public int totalPages;
28 | public int currentPage;
29 | public LogCache(String logName, List lines, int totalPages, int currentPage) {
30 | this.logName = logName;
31 | this.lines = lines;
32 | this.totalPages = totalPages;
33 | this.currentPage = currentPage;
34 | }
35 | }
36 |
37 | private static final Map logCacheMap = new ConcurrentHashMap<>();
38 |
39 | public static LogCache getCache(CommandSender sender) {
40 | return logCacheMap.get(sender);
41 | }
42 | public static void putCache(CommandSender sender, LogCache cache) {
43 | logCacheMap.put(sender, cache);
44 | }
45 | public static void clearCache(CommandSender sender) {
46 | logCacheMap.remove(sender);
47 | }
48 |
49 | public static List readLogFile(File logFile) throws IOException {
50 | return Files.readAllLines(logFile.toPath(), StandardCharsets.UTF_8);
51 | }
52 |
53 | public static int getTotalPages(int totalLines, int perPage) {
54 | return (int) Math.ceil((double) totalLines / perPage);
55 | }
56 |
57 | public static int[] getPageRange(int totalLines, int page, int perPage) {
58 | int start = (page - 1) * perPage;
59 | int end = Math.min(start + perPage, totalLines);
60 | return new int[]{start, end};
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/.github/workflows/AutoBuild_CI_dev.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub.
2 | # They are provided by a third-party and are governed by
3 | # separate terms of service, privacy policy, and support
4 | # documentation.
5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
6 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
7 |
8 | name: MinerTrack Dev Build CI
9 |
10 | on:
11 | push:
12 | branches: [ "dev" ]
13 | pull_request:
14 | branches: [ "dev" ]
15 |
16 | jobs:
17 | build:
18 |
19 | runs-on: ubuntu-latest
20 | permissions:
21 | contents: read
22 |
23 | steps:
24 | - uses: actions/checkout@v4
25 | - name: Set up JDK 17
26 | uses: actions/setup-java@v4
27 | with:
28 | java-version: '17'
29 | distribution: 'temurin'
30 |
31 | # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies.
32 | # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md
33 | - name: Setup Gradle
34 | uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
35 |
36 | - name: Build with Gradle Wrapper
37 | run: ./gradlew shadowJar
38 |
39 | - name: 'Upload Artifact'
40 | uses: actions/upload-artifact@v4
41 | with:
42 | name: MinerTrack-dev-${{ github.sha }}
43 | path: ./build/libs/*.jar
44 | retention-days: 90
45 |
46 | #- name: Coveralls Check
47 | # uses: coverallsapp/github-action@v2
48 |
49 | # NOTE: The Gradle Wrapper is the default and recommended way to run Gradle (https://docs.gradle.org/current/userguide/gradle_wrapper.html).
50 | # If your project does not have the Gradle Wrapper configured, you can use the following configuration to run Gradle with a specified version.
51 | #
52 | # - name: Setup Gradle
53 | # uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
54 | # with:
55 | # gradle-version: '8.9'
56 | #
57 | # - name: Build with Gradle 8.9
58 | # run: gradle build
59 |
60 | # dependency-submission:
61 | #
62 | # runs-on: ubuntu-latest
63 | # permissions:
64 | # contents: write
65 | #
66 | # steps:
67 | # - uses: actions/checkout@v4
68 | # - name: Set up JDK 17
69 | # uses: actions/setup-java@v4
70 | # with:
71 | # java-version: '17'
72 | # distribution: 'temurin'
73 | #
74 | # # Generates and submits a dependency graph, enabling Dependabot Alerts for all project dependencies.
75 | # # See: https://github.com/gradle/actions/blob/main/dependency-submission/README.md
76 | # - name: Generate and submit dependency graph
77 | # uses: gradle/actions/dependency-submission@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
78 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if %ERRORLEVEL% equ 0 goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if %ERRORLEVEL% equ 0 goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | set EXIT_CODE=%ERRORLEVEL%
84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
86 | exit /b %EXIT_CODE%
87 |
88 | :mainEnd
89 | if "%OS%"=="Windows_NT" endlocal
90 |
91 | :omega
92 |
--------------------------------------------------------------------------------
/src/main/java/link/star_dust/MinerTrack/hooks/CustomJsonWebHook.java:
--------------------------------------------------------------------------------
1 | /**
2 | * DON'T REMOVE THIS
3 | *
4 | * /MinerTrack/src/main/java/link/star_dust/MinerTrack/hooks/CustomJsonWebHook.java
5 | *
6 | * MinerTrack Source Code - Public under GPLv3 license
7 | * Original Author: Author87668
8 | * Contributors: Author87668, Zhang12334
9 | *
10 | * DON'T REMOVE THIS
11 | **/
12 |
13 | package link.star_dust.MinerTrack.hooks;
14 |
15 | import link.star_dust.MinerTrack.MinerTrack;
16 | import org.apache.hc.client5.http.classic.methods.HttpPost;
17 | import org.apache.hc.core5.http.io.entity.StringEntity;
18 | import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
19 | import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
20 | import org.apache.hc.client5.http.impl.classic.HttpClients;
21 | import org.bukkit.Bukkit;
22 |
23 | import java.time.LocalDateTime;
24 | import java.time.format.DateTimeFormatter;
25 | import java.util.Map;
26 | import java.nio.charset.StandardCharsets;
27 |
28 | public class CustomJsonWebHook {
29 | private final MinerTrack plugin;
30 | private final String webHookUrl;
31 | private final String jsonFormat;
32 |
33 | public CustomJsonWebHook(MinerTrack plugin, String webHookUrl, String jsonFormat) {
34 | this.plugin = plugin;
35 | this.webHookUrl = webHookUrl;
36 | this.jsonFormat = jsonFormat;
37 | }
38 |
39 | public void sendMessage(Map placeholders) {
40 | Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
41 | try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
42 | HttpPost post = new HttpPost(webHookUrl);
43 | post.setHeader("Content-Type", "application/json; charset=UTF-8");
44 |
45 | // Add timestamp
46 | placeholders.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
47 |
48 | // Replace holders
49 | String jsonPayload = jsonFormat;
50 | for (Map.Entry entry : placeholders.entrySet()) {
51 | jsonPayload = jsonPayload.replace("%" + entry.getKey() + "%", entry.getValue());
52 | }
53 |
54 | // Debug: log webhook URL and payload preview
55 | //plugin.getLogger().info("[CustomJsonWebHook] Sending to URL: " + webHookUrl + " payload: " + jsonPayload);
56 |
57 | post.setEntity(new StringEntity(jsonPayload, StandardCharsets.UTF_8));
58 |
59 | try (CloseableHttpResponse response = httpClient.execute(post)) {
60 | int statusCode = response.getCode();
61 | if (statusCode != 200 && statusCode != 204) {
62 | plugin.getLogger().warning("Failed to send custom JSON webhook, Response Code: " + statusCode);
63 | }
64 | }
65 |
66 | } catch (Exception e) {
67 | plugin.getLogger().severe("Error while sending custom JSON webhook: " + e.getMessage());
68 | e.printStackTrace();
69 | }
70 | });
71 | }
72 | }
--------------------------------------------------------------------------------
/src/main/resources/Translations/zh_cn.yml:
--------------------------------------------------------------------------------
1 | #### MinerTrack Config - language.yml ##################################################
2 | #
3 | # 你好! 欢迎使用 MinerTrack anti-xray。这是 language.yml 文件,
4 | # 用于定义 anti-xray 插件显示的语言内容。你可以在这里自定义插件显示的所有消息。
5 | #
6 | # 有用的链接:
7 | # 获取更新: https://modrinth.com/plugin/minertrack
8 | # 查看源码: https://github.com/At87668/MinerTrack
9 | # 提交问题: https://github.com/At87668/MinerTrack/issues
10 | # 提交合并请求: https://github.com/At87668/MinerTrack/pulls
11 | # 查阅 Wiki: https://minertrack.pages.dev/wiki/
12 | # 加入 Discord: https://discord.gg/MzTea2W9cb
13 | #
14 | # 不管怎样,感谢你使用 MinerTrack anti-xray!❤
15 | #
16 | #####################################################################################
17 |
18 | # 在此定义消息前缀
19 | prefix: '&8[&9&lMiner&c&lTrack&8]&r '
20 |
21 | # 定义当玩家因使用 X-Ray 被踢出时的公告消息
22 | # 占位符: %player% %reason%
23 | kick-format: '&b%player% &7因涉嫌使用 X-Ray 被踢出服务器! &8(%reason%)'
24 |
25 | # 定义详细模式(verbose)消息格式
26 | # 占位符: %player% %vl% %add_vl% %block_type% %count% %vein_count% %world% %pos_x% %pos_y% %pos_z%
27 | verbose-format: '&7%player% 未通过 &fX-Ray &7检测 &7违规等级: %vl%&b(+%add_vl%) &7方块: &6%block_type%&ex%count%&7(矿脉&ax%vein_count%&7)&7 世界: %world% 位置: X%pos_x% Y%pos_y% Z%pos_z%'
28 |
29 | # 定义日志格式
30 | # 占位符: %year% %month% %day% %hour% %minute% %second% %player% %vl% %add_vl% %block_type% %count% %vein_count% %world% %pos_x% %pos_y% %pos_z%
31 | log-format: '%year%-%month%-%day% %hour%:%minute%:%second% | %player% 未通过 X-Ray 检测 违规等级: %vl%(+%add_vl%) 方块: %block_type%x%count%(矿脉x%vein_count%) 世界: %world% 位置: X%pos_x% Y%pos_y% Z%pos_z%'
32 |
33 | # 当玩家被踢出时,是否广播消息?
34 | kick-broadcast: true
35 |
36 | help:
37 | - "&8----[&9&lMiner&c&lTrack &6帮助&8]-----------"
38 | - ""
39 | - "&f/mtrack notify <消息> &7- 向工作人员发送警报"
40 | - "&f/mtrack verbose &7- 开启详细模式,当玩家违规等级上升时,向启用该模式的工作人员发送通知"
41 | - "&f/mtrack check <玩家> &7- 查看玩家的违规历史记录"
42 | - "&f/mtrack reset <玩家> &7- 重置玩家的违规记录"
43 | - "&f/mtrack help &7- 显示插件帮助信息"
44 | - "&f/mtrack update &7- 检查更新"
45 | - "&f/mtrack kick <玩家> <原因> &7- 以指定原因踢出玩家"
46 | - "&f/mtrack logs <日志文件名.log> &7- 查看 MinerTrack 日志文件"
47 |
48 | no-permission: "&c你没有权限使用此命令。"
49 |
50 | usage-notify: "&c用法: /mtrack notify <消息>"
51 | usage-check: "&c用法: /mtrack check <玩家>"
52 | usage-reset: "&c用法: /mtrack reset <玩家>"
53 | usage-kick: "&c用法: /mtrack kick <玩家> <原因>"
54 | usage-logs: "&c用法: /mtrack logs <日志文件名.log>"
55 |
56 | # 占位符: {player}
57 | reset-success: "&a成功重置 {player} 的违规记录。"
58 |
59 | # 占位符: {player} {level}
60 | violation-level: "&e{player} 的违规等级为 {level}。"
61 |
62 | # 占位符: {player}
63 | player-not-found: "&c未找到玩家 {player}。"
64 |
65 | config-reloaded: "&a配置与语言文件已成功重载。"
66 |
67 | unknown-command: "&c未知子命令。使用 /mtrack help 查看命令列表。"
68 |
69 | verbose-enable: "&7详细模式已&a启用&7!"
70 | verbose-disable: "&7详细模式已&c禁用&7!"
71 |
72 | # 日志查看器每页显示行数
73 | log-viewer-lines-per-page: 10
74 | log-viewer-header: "&7---- &9&lMiner&c&lTrack &b日志 &7({current_page}/{max_page}) ----"
75 | log-viewer-logs-color: "&7"
76 | log-viewer-not-found: "&c未找到日志文件 {log_file}。"
77 | log-viewer-not-log-file: "&c只能查看日志文件。"
78 | log-viewer-cant-read: "&c读取日志失败。"
79 | log-viewer-page-invalid: "&c页码超出范围 (1 - {max_page})。"
80 | log-viewer-page-nan: "&c页码必须为数字"
81 | log-viewer-next-page: "&6使用 &a/mtrack logs page {next_page} &6查看下一页。"
82 | log-viewer-empty: "&7无日志条目可显示。"
83 |
84 | # 占位符: %latest_version%
85 | update:
86 | alpha-available: '&c新 Alpha 版本 %latest_version% 现已可用!'
87 | beta-available: '&e新 Beta 版本 %latest_version% 现已可用!'
88 | stable-available: '&a新稳定版本 %latest_version% 现已可用!'
89 | using-latest: '&2你正在使用最新版本。'
90 | check-failed: '&c检查更新失败。'
--------------------------------------------------------------------------------
/src/main/resources/language.yml:
--------------------------------------------------------------------------------
1 | #### MinerTrack Config - language.yml ##################################################
2 | #
3 | # Hi, welcome to use MinerTrack anti-xray, this is language.yml, which is used
4 | # to define the display language of anti-xray. Here you can define what message
5 | # the anti-xray displays.
6 | #
7 | # Useful Links:
8 | # Get Update: https://modrinth.com/plugin/minertrack
9 | # View Source: https://github.com/At87668/MinerTrack
10 | # Report Issue: https://github.com/At87668/MinerTrack/issues
11 | # Pull Requset: https://github.com/At87668/MinerTrack/pulls
12 | # Visit Wiki: https://minertrack.pages.dev/wiki/
13 | # Join Discord: https://discord.gg/MzTea2W9cb
14 | #
15 | # Anyway, thanks for using MinerTrack anti-xray! ❤
16 | #
17 | #####################################################################################
18 |
19 | # You can define the message prefix here
20 | prefix: '&8[&9&lMiner&c&lTrack&8]&r '
21 |
22 | # Define the announcement message after the player is kicked out by anti-xraying
23 | # Placeholders: %player% %reason%
24 | kick-format: '&b%player% &7kicked out of the server for allegedly using X-Ray! &8(%reason%)'
25 |
26 | # Define the verbose message format
27 | # Placeholders: %player% %vl% %add_vl% %block_type% %count% %vein_count% %world% %pos_x% %pos_y% %pos_z%
28 | verbose-format: '&7%player% failed &fX-Ray &7check &7VL:%vl%&b(+%add_vl%) &7Block:&6%block_type%&ex%count%&7(Vein&ax%vein_count%&7) &7World: %world% Pos: X%pos_x% Y%pos_y% Z%pos_z%'
29 |
30 | # Define the log format
31 | # Placeholders: %year% %month% %day% %hour% %minute% %second% %player% %vl% %add_vl% %block_type% %count% %vein_count% %world% %pos_x% %pos_y% %pos_z%
32 | log-format: '%year%-%month%-%day% %hour%:%minute%:%second% | %player% failed X-Ray check VL:%vl%(+%add_vl%) Block:%block_type%x%count%(Veinx%vein_count%) World: %world% Pos: X%pos_x% Y%pos_y% Z%pos_z%'
33 |
34 | # Should MinerTrack broadcast a message when a player is kicked?
35 | kick-broadcast: true
36 |
37 | help:
38 | - "&8----[&9&lMiner&c&lTrack &6Help&8]-----------"
39 | - ""
40 | - "&f/mtrack notify &7- Send alerts to staff"
41 | - "&f/mtrack verbose &7- Turn on Detailed Mode, and send a notification to the staff who have Detailed Mode enabled whenever the player's VL increases"
42 | - "&f/mtrack check &7- Check player's history of violations"
43 | - "&f/mtrack reset &7- Reset player's violation record"
44 | - "&f/mtrack help &7- Get plugin's help"
45 | - "&f/mtrack update &7- Check update"
46 | - "&f/mtrack kick &7- Kick the player with a specified reason"
47 | - "&f/mtrack logs &7- View a MinerTrack log file"
48 |
49 | no-permission: "&cYou do not have permission to use this command."
50 |
51 | usage-notify: "&cUsage: /mtrack notify "
52 | usage-check: "&cUsage: /mtrack check "
53 | usage-reset: "&cUsage: /mtrack reset "
54 | usage-kick: "&cUsage: /mtrack kick "
55 | usage-logs: "&cUsage: /mtrack logs "
56 |
57 | # Placeholders: {player}
58 | reset-success: "&aSuccessfully reset {player}'s violation record."
59 |
60 | # Placeholders: {player} {level}
61 | violation-level: "&e{player}'s violation level is {level}."
62 |
63 | # Placeholders: {player}
64 | player-not-found: "&cPlayer {player} not found."
65 |
66 | config-reloaded: "&aConfiguration and Language reloaded successfully."
67 |
68 | unknown-command: "&cUnknown subcommand. Use /mtrack help for a list of commands."
69 |
70 | verbose-enable: "&7Verbose Mode &aEnabled&7!"
71 | verbose-disable: "&7Verbose Mode &cDisabled&7!"
72 |
73 | # Placeholders: {current_page} {max_page} {log_file} {next_page}
74 | log-viewer-lines-per-page: 10
75 | log-viewer-header: "&7---- &9&lMiner&c&lTrack &bLogs &7({current_page}/{max_page}) ----"
76 | log-viewer-logs-color: "&7"
77 | log-viewer-not-found: "&cLog file {log_file} not found."
78 | log-viewer-not-log-file: "&cOnly can view log file."
79 | log-viewer-cant-read: "&cFailed to read logs."
80 | log-viewer-page-invalid: "&cPage number out of range (1 - {max_page})"
81 | log-viewer-page-nan: "&cPage number must be a number"
82 | log-viewer-next-page: "&6Use &a/mtrack logs page {next_page} &6to view next page."
83 | log-viewer-empty: "&7No log entries to display."
84 |
85 | # Placeholders: %latest_version%
86 | update:
87 | alpha-available: '&cNew alpha version %latest_version% now available!'
88 | beta-available: '&eNew beta version %latest_version% now available!'
89 | stable-available: '&aNew stable version %latest_version% now available!'
90 | using-latest: '&2You are using the latest version.'
91 | check-failed: '&cFailed to check for updates.'
--------------------------------------------------------------------------------
/src/main/resources/Translations/fr.yml:
--------------------------------------------------------------------------------
1 | # ### MinerTrack Config - language.yml ##################################################
2 | #
3 | # Bonjour et bienvenue sur MinerTrack anti-xray, ceci est le fichier language.yml,
4 | # utilisé pour définir la langue d’affichage de l’anti-xray. Ici, vous pouvez définir
5 | # les messages que l’anti-xray affichera.
6 | #
7 | # Liens utiles :
8 | # Obtenir les mises à jour : https://modrinth.com/plugin/minertrack
9 | # Voir le code source : https://github.com/At87668/MinerTrack
10 | # Signaler un problème : https://github.com/At87668/MinerTrack/issues
11 | # Pull Request : https://github.com/At87668/MinerTrack/pulls
12 | # Consulter le Wiki : https://minertrack.pages.dev/wiki/
13 | # Rejoindre le Discord : https://discord.gg/MzTea2W9cb
14 | #
15 | # Quoi qu’il en soit, merci d’utiliser MinerTrack anti-xray ! ❤
16 | #
17 | # ####################################################################################
18 |
19 | # Vous pouvez définir ici le préfixe des messages
20 | prefix: '&8[&9&lMiner&c&lTrack&8]&r '
21 |
22 | # Message d’annonce après qu’un joueur a été expulsé pour usage suspect de X-Ray
23 | # Placeholders : %player% %reason%
24 | kick-format: '&b%player% &7a été expulsé du serveur pour utilisation suspecte de X-Ray ! &8(%reason%)'
25 |
26 | # Format du message détaillé (verbose)
27 | # Placeholders : %player% %vl% %add_vl% %block_type% %count% %vein_count% %world% %pos_x% %pos_y% %pos_z%
28 | verbose-format: '&7%player% a échoué au &fcontrôle X-Ray &7VL:%vl%&b(+%add_vl%) &7Bloc:&6%block_type%&ex%count%&7(Veine&ax%vein_count%&7)
29 | &7Monde : %world% Pos : X%pos_x% Y%pos_y% Z%pos_z%'
30 |
31 | # Format des logs
32 | # Placeholders : %year% %month% %day% %hour% %minute% %second% %player% %vl% %add_vl% %block_type% %count% %vein_count% %world% %pos_x% %pos_y% %pos_z%
33 | log-format: '%year%-%month%-%day% %hour%:%minute%:%second% | %player% a échoué au contrôle X-Ray
34 | VL:%vl%(+%add_vl%) Bloc:%block_type%x%count%(Veinex%vein_count%) Monde : %world%
35 | Pos : X%pos_x% Y%pos_y% Z%pos_z%'
36 |
37 | # MinerTrack doit-il diffuser un message lorsqu’un joueur est expulsé ?
38 | kick-broadcast: true
39 |
40 | help:
41 | - '&8----[&9&lMiner&c&lTrack &6Aide&8]-----------'
42 | - ''
43 | - '&f/mtrack notify &7- Envoyer une alerte au staff'
44 | - '&f/mtrack verbose &7- Activer le mode détaillé et envoyer une notification au staff
45 | ayant le mode détaillé activé à chaque augmentation du VL d’un joueur'
46 | - '&f/mtrack check &7- Consulter l’historique des infractions d’un joueur'
47 | - '&f/mtrack reset &7- Réinitialiser les infractions d’un joueur'
48 | - '&f/mtrack help &7- Afficher l’aide du plugin'
49 | - '&f/mtrack update &7- Vérifier les mises à jour'
50 | - '&f/mtrack kick &7- Expulser un joueur avec une raison spécifiée'
51 | - '&f/mtrack logs &7- Consulter un fichier de logs MinerTrack'
52 |
53 | no-permission: '&cVous n’avez pas la permission d’utiliser cette commande.'
54 |
55 | usage-notify: '&cUtilisation : /mtrack notify '
56 | usage-check: '&cUtilisation : /mtrack check '
57 | usage-reset: '&cUtilisation : /mtrack reset '
58 | usage-kick: '&cUtilisation : /mtrack kick '
59 | usage-logs: '&cUtilisation : /mtrack logs '
60 |
61 | # Placeholders : {player}
62 | reset-success: '&aLe niveau d’infraction de {player} a été réinitialisé avec succès.'
63 |
64 | # Placeholders : {player} {level}
65 | violation-level: '&eLe niveau d’infraction de {player} est de {level}.'
66 |
67 | # Placeholders : {player}
68 | player-not-found: '&cJoueur {player} introuvable.'
69 |
70 | config-reloaded: '&aConfiguration et langue rechargées avec succès.'
71 |
72 | unknown-command: '&cSous-commande inconnue. Utilisez /mtrack help pour la liste des commandes.'
73 |
74 | verbose-enable: '&7Mode Verbose &aActivé&7!'
75 | verbose-disable: '&7Mode Verbose &cDésactivé&7!'
76 |
77 | # Placeholders : {current_page} {max_page} {log_file} {next_page}
78 | log-viewer-lines-per-page: 10
79 | log-viewer-header: '&7---- &9&lMiner&c&lTrack &bLogs &7({current_page}/{max_page})
80 | ----'
81 | log-viewer-logs-color: '&7'
82 | log-viewer-not-found: '&cFichier de log {log_file} introuvable.'
83 | log-viewer-not-log-file: '&cSeuls les fichiers de logs peuvent être consultés.'
84 | log-viewer-cant-read: '&cImpossible de lire les logs.'
85 | log-viewer-page-invalid: '&cNuméro de page hors limites (1 - {max_page})'
86 | log-viewer-page-nan: '&cLe numéro de page doit être un nombre'
87 | log-viewer-next-page: '&6Utilisez &a/mtrack logs page {next_page} &6pour voir la page suivante.'
88 | log-viewer-empty: '&7Aucune entrée de log à afficher.'
89 |
90 | # Placeholders : %latest_version%
91 | update:
92 | alpha-available: '&cUne nouvelle version alpha %latest_version% est disponible !'
93 | beta-available: '&eUne nouvelle version bêta %latest_version% est disponible !'
94 | stable-available: '&aUne nouvelle version stable %latest_version% est disponible !'
95 | using-latest: '&2Vous utilisez la dernière version.'
96 | check-failed: '&cÉchec de la vérification des mises à jour.'
97 |
--------------------------------------------------------------------------------
/src/main/java/link/star_dust/MinerTrack/listeners/MiningDetectionExtension.java:
--------------------------------------------------------------------------------
1 | /**
2 | * DON'T REMOVE THIS
3 | *
4 | * /MinerTrack/src/main/java/link/star_dust/MinerTrack/listeners/MiningDetectionExtension.java
5 | *
6 | * MinerTrack Source Code - Public under GPLv3 license
7 | * Original Author: Author87668
8 | * Contributors: Author87668
9 | *
10 | * DON'T REMOVE THIS
11 | **/
12 |
13 | package link.star_dust.MinerTrack.listeners;
14 |
15 | import org.bukkit.Bukkit;
16 | import org.bukkit.Location;
17 | import org.bukkit.Material;
18 | import org.bukkit.World;
19 | import org.bukkit.block.Block;
20 | import org.bukkit.entity.Player;
21 | import org.bukkit.event.EventHandler;
22 | import org.bukkit.event.Listener;
23 | import org.bukkit.event.block.BlockBreakEvent;
24 |
25 | import link.star_dust.MinerTrack.MinerTrack;
26 |
27 | import java.util.*;
28 |
29 | public class MiningDetectionExtension implements Listener {
30 |
31 | private final Map> miningHistory = new HashMap<>();
32 | private final Map playerSuspicionMap = new HashMap<>();
33 | private final MinerTrack plugin;
34 |
35 | public MiningDetectionExtension(MinerTrack plugin) {
36 | this.plugin = plugin;
37 | }
38 |
39 | public void register() {
40 | Bukkit.getPluginManager().registerEvents(this, plugin);
41 | //plugin.getLogger().info("ExtensionLoader > MiningDetectionExtension loaded!");
42 | }
43 |
44 | private static class MiningFeature {
45 | int airBlocks;
46 | int solidBlocks;
47 |
48 | MiningFeature(int airBlocks, int solidBlocks) {
49 | this.airBlocks = airBlocks;
50 | this.solidBlocks = solidBlocks;
51 | }
52 | }
53 |
54 | @EventHandler
55 | public void onBlockBreak(BlockBreakEvent event) {
56 | Player player = event.getPlayer();
57 | Block block = event.getBlock();
58 |
59 | analyzeMiningPattern(player, block);
60 | }
61 |
62 | private void analyzeMiningPattern(Player player, Block block) {
63 | World world = block.getWorld();
64 | if (world == null) return;
65 |
66 | Location location = block.getLocation();
67 | int airBlocks = 0, solidBlocks = 0;
68 |
69 | // Analyze surrounding blocks
70 | for (int x = -1; x <= 1; x++) {
71 | for (int y = -1; y <= 1; y++) {
72 | for (int z = -1; z <= 1; z++) {
73 | if (x == 0 && y == 0 && z == 0) continue; // Ignore center block
74 | Block nearby = world.getBlockAt(location.clone().add(x, y, z));
75 | if (nearby.getType() == Material.AIR && nearby.getType() == Material.CAVE_AIR) {
76 | airBlocks++;
77 | } else if (nearby.getType().isSolid()) {
78 | solidBlocks++;
79 | }
80 | }
81 | }
82 | }
83 |
84 | // Preserve mine features
85 | UUID playerId = player.getUniqueId();
86 | MiningFeature currentFeature = new MiningFeature(airBlocks, solidBlocks);
87 | miningHistory.computeIfAbsent(playerId, k -> new ArrayList<>()).add(currentFeature);
88 |
89 | // keep window size of history to 5
90 | List history = miningHistory.get(playerId);
91 | if (history.size() > 5) {
92 | history.remove(0);
93 | }
94 |
95 | // Calculate historical feature averages
96 | int totalAir = 0, totalSolid = 0;
97 | for (MiningFeature feature : history) {
98 | totalAir += feature.airBlocks;
99 | totalSolid += feature.solidBlocks;
100 | }
101 | int averageAir = totalAir / history.size();
102 | int averageSolid = totalSolid / history.size();
103 |
104 | // Adjust suspicious values based on feature trends
105 | int suspicionLevel = playerSuspicionMap.getOrDefault(playerId, 0);
106 | if (averageSolid >= 7 && averageAir <= 1) {
107 | // road
108 | suspicionLevel += 5;
109 | } else if (averageAir >= 6) {
110 | // cave
111 | suspicionLevel -= 5;
112 | } else {
113 | // normal
114 | suspicionLevel += 1;
115 | }
116 |
117 | // update suspicious values
118 | playerSuspicionMap.put(playerId, suspicionLevel);
119 |
120 | // Output debug info
121 | plugin.getLogger().info(player.getName() + " -> Air: " + averageAir + ", Solid: " + averageSolid + ", Suspicion: " + suspicionLevel);
122 | }
123 |
124 | // Interface to obtain suspicious values
125 | public int getSuspicionLevel(UUID playerId) {
126 | return playerSuspicionMap.getOrDefault(playerId, 0);
127 | }
128 |
129 | public int getSuspicionLevel(Player player) {
130 | UUID playerID = Bukkit.getOfflinePlayer(player.getName()).getUniqueId();
131 | return getSuspicionLevel(playerID);
132 | }
133 |
134 | public void resetSuspicionLevel(Player player) {
135 | UUID playerID = Bukkit.getOfflinePlayer(player.getName()).getUniqueId();
136 | playerSuspicionMap.put(playerID, 0);
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/main/java/link/star_dust/MinerTrack/hooks/DiscordWebHook.java:
--------------------------------------------------------------------------------
1 | /**
2 | * DON'T REMOVE THIS
3 | *
4 | * /MinerTrack/src/main/java/link/star_dust/MinerTrack/hooks/DiscordWebHook.java
5 | *
6 | * MinerTrack Source Code - Public under GPLv3 license
7 | * Original Author: Author87668
8 | * Contributors: Author87668, Zhang12334
9 | *
10 | * DON'T REMOVE THIS
11 | **/
12 | package link.star_dust.MinerTrack.hooks;
13 |
14 | import com.google.gson.Gson;
15 | import com.google.gson.annotations.SerializedName;
16 | import link.star_dust.MinerTrack.MinerTrack;
17 |
18 | import javax.net.ssl.*;
19 | import java.security.cert.X509Certificate;
20 | import java.nio.charset.StandardCharsets;
21 |
22 | import org.apache.hc.client5.http.classic.methods.HttpPost;
23 | import org.apache.hc.core5.http.io.entity.StringEntity;
24 | import org.apache.hc.core5.http.ContentType;
25 | import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
26 | import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
27 | import org.apache.hc.client5.http.impl.classic.HttpClients;
28 | import org.bukkit.Bukkit;
29 | import link.star_dust.MinerTrack.FoliaCheck;
30 |
31 | public class DiscordWebHook {
32 |
33 | private final MinerTrack plugin;
34 | private final String webHookUrl;
35 | private final Gson gson = new Gson();
36 |
37 | public DiscordWebHook(MinerTrack plugin, String webHookUrl) {
38 | this.plugin = plugin;
39 | this.webHookUrl = webHookUrl;
40 | }
41 |
42 | /**
43 | * Sends a simple text message to the Discord WebHook.
44 | *
45 | * @param content The message content to send.
46 | */
47 | public void sendMessage(String content) {
48 | send(new Payload(content));
49 | }
50 |
51 | /**
52 | * Sends an embed message to the Discord WebHook.
53 | *
54 | * @param embed The embed payload to send.
55 | */
56 | public void sendEmbed(Embed embed) {
57 | send(new Payload(embed));
58 | }
59 |
60 | /**
61 | * Sends a payload to the Discord WebHook asynchronously.
62 | *
63 | * @param payload The payload to send.
64 | */
65 | private void send(Payload payload) {
66 | Runnable task = () -> {
67 | try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
68 | HttpPost post = new HttpPost(webHookUrl);
69 | post.setHeader("Content-Type", "application/json; charset=UTF-8");
70 |
71 | String jsonPayload = gson.toJson(payload);
72 | // Debug: log webhook URL and payload preview
73 | //plugin.getLogger().info("[DiscordWebHook] Sending to URL: " + webHookUrl + " payload: " + jsonPayload);
74 | StringEntity entity = new StringEntity(jsonPayload, ContentType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8));
75 | post.setEntity(entity);
76 |
77 | try (CloseableHttpResponse response = httpClient.execute(post)) {
78 | int statusCode = response.getCode();
79 | if (statusCode != 200 && statusCode != 204) {
80 | plugin.getLogger().warning("Failed to send message to Discord WebHook. Response Code: " + statusCode);
81 | }
82 | }
83 |
84 | } catch (Exception e) {
85 | plugin.getLogger().severe("Error while sending message to Discord WebHook: " + e.getMessage());
86 | e.printStackTrace();
87 | }
88 | };
89 |
90 | // On Folia the Bukkit async scheduler may throw UnsupportedOperationException when
91 | // called from certain region threads, so use a plain thread instead. Fall back to
92 | // Bukkit async scheduling on non-Folia servers for compatibility.
93 | if (FoliaCheck.isFolia()) {
94 | Thread t = new Thread(task, "MinerTrack-DiscordWebHook");
95 | t.setDaemon(true);
96 | t.start();
97 | } else {
98 | Bukkit.getScheduler().runTaskAsynchronously(plugin, task);
99 | }
100 | }
101 |
102 | /**
103 | * Represents the payload structure for a WebHook message.
104 | */
105 | public static class Payload {
106 | @SerializedName("content")
107 | private final String content;
108 |
109 | @SerializedName("embeds")
110 | private final Embed[] embeds;
111 |
112 | public Payload(String content) {
113 | this.content = content;
114 | this.embeds = null;
115 | }
116 |
117 | public Payload(Embed embed) {
118 | this.content = null;
119 | this.embeds = new Embed[]{embed};
120 | }
121 | }
122 |
123 | /**
124 | * Represents an embed structure for Discord messages.
125 | */
126 | public static class Embed {
127 | @SerializedName("title")
128 | private final String title;
129 |
130 | @SerializedName("description")
131 | private final String description;
132 |
133 | @SerializedName("color")
134 | private final int color;
135 |
136 | public Embed(String title, String description, int color) {
137 | this.title = title;
138 | this.description = description;
139 | this.color = color;
140 | }
141 | }
142 | }
143 |
144 |
--------------------------------------------------------------------------------
/src/main/java/link/star_dust/MinerTrack/managers/LanguageManager.java:
--------------------------------------------------------------------------------
1 | /**
2 | * DON'T REMOVE THIS
3 | *
4 | * /MinerTrack/src/main/java/link/star_dust/MinerTrack/managers/LanguageManager.java
5 | *
6 | * MinerTrack Source Code - Public under GPLv3 license
7 | * Original Author: Author87668
8 | * Contributors: Author87668
9 | *
10 | * DON'T REMOVE THIS
11 | **/
12 | package link.star_dust.MinerTrack.managers;
13 |
14 | import org.bukkit.ChatColor;
15 | import org.bukkit.configuration.InvalidConfigurationException;
16 | import org.bukkit.configuration.file.YamlConfiguration;
17 | import org.jetbrains.annotations.NotNull;
18 |
19 | import link.star_dust.MinerTrack.MinerTrack;
20 |
21 | import java.io.File;
22 | import java.io.IOException;
23 | import java.io.InputStream;
24 | import java.io.InputStreamReader;
25 | import java.util.List;
26 | import java.util.stream.Collectors;
27 |
28 | public class LanguageManager {
29 | private final MinerTrack plugin;
30 | private YamlConfiguration languageConfig;
31 | private final File languageFile;
32 | private static LanguageManager instance;
33 |
34 | public LanguageManager(MinerTrack plugin) {
35 | this.plugin = plugin;
36 | this.languageFile = new File(plugin.getDataFolder(), "language.yml");
37 | loadLanguageFile();
38 | }
39 |
40 | public void loadLanguageFile() {
41 | // Save default language file if it doesn't exist
42 | if (!languageFile.exists()) {
43 | plugin.saveResource("language.yml", false);
44 | }
45 |
46 | // Load the language file
47 | languageConfig = YamlConfiguration.loadConfiguration(languageFile);
48 |
49 | // Load defaults from the resource file using InputStreamReader
50 | try (InputStream defaultStream = plugin.getResource("language.yml")) {
51 | if (defaultStream != null) {
52 | YamlConfiguration defaultConfig = YamlConfiguration.loadConfiguration(new InputStreamReader(defaultStream));
53 | languageConfig.setDefaults(defaultConfig);
54 | languageConfig.options().copyDefaults(true);
55 | }
56 | } catch (IOException e) {
57 | plugin.getLogger().severe("Could not load default language configuration: " + e.getMessage());
58 | }
59 |
60 | // Save only the custom values back to file
61 | saveCustomLanguageFile();
62 | }
63 |
64 | public static LanguageManager getInstance(MinerTrack plugin) {
65 | if (instance == null) {
66 | instance = new LanguageManager(plugin);
67 | }
68 | return instance;
69 | }
70 |
71 | public void reloadLanguage() {
72 | reloadLanguageFile();
73 | }
74 |
75 | public String getKickMessage(String playerName) {
76 | return applyColors(getMessage("kick-format").replace("%player%", playerName));
77 | }
78 |
79 | public String getPrefix() {
80 | return applyColors(getMessage("prefix"));
81 | }
82 |
83 | public List getHelpMessages() {
84 | List helpMessages = languageConfig.getStringList("help");
85 | return helpMessages.stream().map(this::applyColors).collect(Collectors.toList());
86 | }
87 |
88 | public String getPrefixedMessage(String key) {
89 | return getPrefix() + " " + applyColors(getMessage(key));
90 | }
91 |
92 | public String getMessage(String path) {
93 | return languageConfig.getString(path);
94 | }
95 |
96 | public String getColoredMessage(String path) {
97 | return applyColors(languageConfig.getString(path));
98 | }
99 |
100 | public String applyColors(String message) {
101 | return ChatColor.translateAlternateColorCodes('&', message);
102 | }
103 |
104 | public String getLogFormat() {
105 | return getMessage("log-format");
106 | }
107 |
108 | public boolean isKickBroadcastEnabled() {
109 | return languageConfig.getBoolean("kick-broadcast", true);
110 | }
111 |
112 | public String logNotFound(String log_name) {
113 | return applyColors(getMessage("log-not-found").replace("{log_file}", log_name));
114 | }
115 |
116 | public int getLogViewerLinesPerPage() {
117 | return languageConfig.getInt("log-viewer-lines-per-page", 10);
118 | }
119 |
120 | // Save only custom values
121 | private void saveCustomLanguageFile() {
122 | try {
123 | languageConfig.save(languageFile);
124 | } catch (IOException e) {
125 | plugin.getLogger().severe("Could not save custom language configuration to " + languageFile.getName() + ": " + e.getMessage());
126 | }
127 | }
128 |
129 | // Reload the language file
130 | public void reloadLanguageFile() {
131 | try {
132 | languageConfig.load(languageFile);
133 | } catch (IOException | InvalidConfigurationException e) {
134 | plugin.getLogger().severe("Could not reload language configuration: " + e.getMessage());
135 | }
136 | }
137 |
138 | public String getPrefixedMessageWithDefault(String key, String defaultMessage) {
139 | String message = getMessage(key);
140 |
141 | if (message == null || message.isEmpty()) {
142 | message = defaultMessage;
143 | }
144 |
145 | return applyColors(getPrefix() + message);
146 | }
147 | }
--------------------------------------------------------------------------------
/README-zh_hans.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ## MinerTrack Anti-XRay
4 |
5 | [](https://modrinth.com/plugin/minertrack) [](https://www.spigotmc.org/resources/120562/) [](https://hangar.papermc.io/Author87668/MinerTrack) [](https://www.curseforge.com/minecraft/bukkit-plugins/minertrack)
6 |
7 | [](https://github.com/At87668/MinerTrack/actions/workflows/AutoBuild_CI_dev.yml) [](https://gitHub.com/At87668/MinerTrack/releases/) [](https://gitHub.com/At87668/MinerTrack/issues/) [](https://gitHub.com/At87668/MinerTrack/commit/) [](https://github.com/At87668/MinerTrack/blob/main/LICENSE)
8 |
9 | 
10 |
11 | [](https://discord.gg/MzTea2W9cb)
12 |
13 | [English](./README.md) | [简体中文](./README-zh_hans.md)
14 |
15 | 
16 |
17 | **MinerTrack** 是一款全新的反矿透插件,它使用挖矿行为分析而不是隐藏矿石的反矿透方式
18 |
19 | *本插件的工作方式?*
20 |
21 | 为了更精准的判断矿透行为, MinerTrack 使用了 **多维度数据** 进行分析, 例如挖掘矿石稀有度、玩家一段时间内挖矿数量、玩家前往矿石的路径
22 |
23 | *那么, 玩家还能使用矿透进行作弊吗?*
24 |
25 | 能, 但并不完全, 使用矿透的玩家面临着两个选择:
26 | 1. 开着矿透, 但挖掘与正常玩家相同数量的矿石
27 | 2. 卸载矿透, 不然被抓
28 |
29 | *为什么要选择 MinerTrack?*
30 |
31 | 与 Paper 的 Anti-Xray 和 Orebfuscator 相比:
32 | - MinerTrack 更为轻量
33 | - 使用算法检测, 更加先进
34 | - 可以检测 Xray 使用者,如何处理由你决定
35 |
36 | 与其他的反矿透插件相比:
37 | - 更新迅速, 支持最新游戏版本
38 | - 已在 20 人在线服务器中测试,几乎没有误判(默认配置)
39 | - 完全免费
40 |
41 | 看看用户们怎么说:
42 |
43 |
44 | 评价
45 |
46 | 
47 |
48 | 
49 |
50 | 
51 |
52 |
53 | 
54 |
55 | * 检测使用 Xray 的玩家
56 | * 当玩家违规等级达到阈值时自动处理
57 | * 当玩家行为正常时自动降低违规等级
58 | * 分析玩家挖矿路径判断是否使用 Xray
59 | * 玩家在洞穴中正常挖矿不会被误判
60 | * 高度可配置的检测配置文件
61 |
62 | ---
63 |
64 | 
65 |
66 | * `/mtrack notify <消息>` - 向管理发送通知
67 | * `/mtrack verbose` - 开启详细模式,玩家违规等级变动时提示管理
68 | * `/mtrack check <玩家>` - 查看某个玩家的违规记录
69 | * `/mtrack reset <玩家>` - 重置某个玩家的违规记录
70 | * `/mtrack help` - 查看插件帮助
71 | * `/mtrack kick <玩家> <理由>` - 踢出玩家并说明原因
72 | * `/mtrack reload` - 重新加载插件配置
73 | * `/mtrack update` - 检查插件更新
74 | * `/mtrack logs <日志名.log>` - 查看 MinerTrack 生成的日志文件
75 |
76 | ---
77 |
78 | 
79 |
80 | * `minertrack.bypass` - 绕过 Xray 检测
81 | * `minertrack.notify` - 接收警告与详细信息
82 | * `minertrack.checkupdate` - 接收更新信息并使用 `/mtrack update`
83 | * `minertrack.use` - 使用 `/mtrack` 根命令
84 | * `minertrack.check` - 使用 `/mtrack check`
85 | * `minertrack.kick` - 使用 `/mtrack kick`
86 | * `minertrack.logs` - 使用 `/mtrack logs`
87 | * `minertrack.help` - 使用 `/mtrack help`
88 | * `minertrack.reset` - 使用 `/mtrack reset`
89 | * `minertrack.sendnotify` - 使用 `/mtrack notify`
90 | * `minertrack.verbose` - 使用 `/mtrack verbose`
91 | * `minertrack.reload` - 使用 `/mtrack reload`
92 |
93 | ---
94 |
95 | 
96 |
97 | 1. 从 SpigotMC 下载最新版 **MinerTrack**
98 | 2. 把 `.jar` 文件放入 `plugins` 文件夹
99 | 3. 重启服务器生成配置文件与相关数据
100 |
101 | ---
102 |
103 | 
104 |
105 | * Java 17 或更高版本
106 | * 仅支持 Paper、Purpur、Folia 或兼容分支(支持 1.18 及以上)*请注意, 本插件不支持 Spigot!*
107 |
108 | ---
109 |
110 | 
111 |
112 | 如果你遇到任何问题,或者有建议功能,可以通过 SpigotMC 联系我们,或在 GitHub 提交 issue
113 |
114 | ---
115 |
116 | * **查看源码**: [https://github.com/At87668/MinerTrack](https://github.com/At87668/MinerTrack)
117 | * **提交问题**: [https://github.com/At87668/MinerTrack/issues](https://github.com/At87668/MinerTrack/issues)
118 | * **提交Pull Request**: [https://github.com/At87668/MinerTrack/pulls](https://github.com/At87668/MinerTrack/pulls)
119 | * **访问 Wiki**: [https://minertrack.pages.dev/wiki/](https://minertrack.pages.dev/wiki/)
120 | * **加入 Discord**: [https://discord.gg/MzTea2W9cb](https://discord.gg/MzTea2W9cb)
121 |
122 | [](https://bstats.org/plugin/bukkit/MinerTrack/23790)
123 |
124 | ---
125 |
126 | ### *如果您想要支持我的工作, 可以订阅我的 [Patreon](https://www.patreon.com/Author87668/join) 会员资格.*
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ## MinerTrack Anti-XRay
4 |
5 | [](https://modrinth.com/plugin/minertrack) [](https://www.spigotmc.org/resources/120562/) [](https://hangar.papermc.io/Author87668/MinerTrack) [](https://www.curseforge.com/minecraft/bukkit-plugins/minertrack)
6 |
7 | [](https://github.com/At87668/MinerTrack/actions/workflows/AutoBuild_CI_dev.yml) [](https://gitHub.com/At87668/MinerTrack/releases/) [](https://gitHub.com/At87668/MinerTrack/issues/) [](https://gitHub.com/At87668/MinerTrack/commit/) [](https://github.com/At87668/MinerTrack/blob/main/LICENSE)
8 |
9 | 
10 |
11 | [](https://discord.gg/MzTea2W9cb)
12 |
13 | [English](./README.md) | [简体中文](./README-zh_hans.md)
14 |
15 | 
16 |
17 | **MinerTrack** is a plugin that will really help you catch those naughty players using Xray on your server in a **different way** from other Anti-Xray plugins. This plugin doesn't hide ores, as hiding ores requires a lot of resources.
18 |
19 | *So how does it work?*
20 |
21 | To catch Xray, MinerTrack uses an **advanced algorithm** that combines **several factors** like ore scarcity, the amount of ores mined in a certain period, the player’s path to ores, and many other aspects.
22 |
23 | *But can't people still use Xray?*
24 |
25 | Yes and no. Indeed, a player using Xray has two options:
26 | 1. Keep using Xray but behave like a normal player to avoid being caught. They can't get more ores than a normal player or they’ll be caught by MinerTrack.
27 | 2. Uninstall their Xray or be caught.
28 |
29 | *Why choose MinerTrack over other AntiXray options?*
30 |
31 | Compared to Paper Anti-XRay and Orebfuscator:
32 | - MinerTrack is lightweight.
33 | - MinerTrack's engine is innovative.
34 | - MinerTrack detects Xray users, leaving it to you to decide on sanctions.
35 |
36 | Compared to other AntiXray solutions:
37 | - MinerTrack supports the latest Minecraft version.
38 |
39 | - MinerTrack has been tested on servers with about 20 simultaneous players, with very few false positives (*default config*).
40 |
41 | - MinerTrack is free.
42 |
43 | Look what users have to say:
44 |
45 |
46 | Assessments
47 |
48 | 
49 |
50 | 
51 |
52 | 
53 |
54 |
55 | 
56 |
57 | - Detect XRayer
58 | - Automatically handle cases when a player's X-Ray violation level reaches a threshold
59 | - Automatically reduce the violation level when the player's behavior normalizes
60 | - Analyze player mining paths to detect X-Ray usage
61 | - When the player is mining in a cave, they will not be detected incorrectly
62 | - Highly configurable profiles
63 |
64 | 
65 |
66 | - `/mtrack notify ` - Send alerts to staff
67 | - `/mtrack verbose` - Enable Detailed Mode and notify staff with it enabled whenever a player’s violation level increases
68 | - `/mtrack check ` - Check a player’s violation history
69 | - `/mtrack reset ` - Reset a player's violation record
70 | - `/mtrack help` - Get plugin help
71 | - `/mtrack kick ` - Kick a player with a specific reason
72 | - `/mtrack reload` - Reload the plugin’s configuration
73 | - `/mtrack update` - Check for plugin updates
74 | - `/mtrack logs ` - View a MinerTrack log file
75 |
76 | 
77 |
78 | - `minertrack.bypass` - Bypass X-Ray detection
79 | - `minertrack.notify` - Receive notifications and verbose information
80 | - `minertrack.checkupdate` - Receive update information and use `/mtrack update`
81 | - `minertrack.use` - Access the root command `/mtrack`
82 | - `minertrack.check` - Use `/mtrack check`
83 | - `minertrack.kick` - Use `/mtrack kick`
84 | - `minertrack.logs` - Use `/mtrack logs`
85 | - `minertrack.help` - Use `/mtrack help`
86 | - `minertrack.reset` - Use `/mtrack reset`
87 | - `minertrack.sendnotify` - Use `/mtrack notify`
88 | - `minertrack.verbose` - Use `/mtrack verbose`
89 | - `minertrack.reload` - Use `/mtrack reload`
90 |
91 | 
92 |
93 | 1. Download the latest version of **MinerTrack** from SpigotMC.
94 | 2. Place the .jar file into the plugins folder.
95 | 3. Restart the server to generate the configuration and necessary files.
96 |
97 | 
98 |
99 | - Java 17 or higher
100 | - Paper, Purpur, Folia or compatible forks (1.18 or newer) *Not Spigot!*
101 |
102 | 
103 |
104 | If you encounter any issues or have suggestions for new features, feel free to reach out via Discord or open an issue on the plugin’s GitHub repository.
105 |
106 |
107 |
108 | **View Source: https://github.com/At87668/MinerTrack**
109 |
110 | **Report Issue: https://github.com/At87668/MinerTrack/issues**
111 |
112 | **Pull Requset: https://github.com/At87668/MinerTrack/pulls**
113 |
114 | **Visit Wiki: https://minertrack.pages.dev/wiki/**
115 |
116 | **Join Discord: https://discord.gg/MzTea2W9cb**
117 |
118 |
119 |
120 | [](https://bstats.org/plugin/bukkit/MinerTrack/23790)
121 |
122 | ---
123 |
124 | ### *If you want to support my work, you can subscribe my [Patreon](https://www.patreon.com/Author87668/join) membership.*
125 |
--------------------------------------------------------------------------------
/src/main/resources/config.yml:
--------------------------------------------------------------------------------
1 | #### MinerTrack Anti-Xray Configuration - config.yml ##############################
2 | #
3 | # Welcome to MinerTrack anti-xray configuration file.
4 | # This file defines global settings specifically optimized for anti-xray detection.
5 | #
6 | # Useful Links:
7 | # Get Update: https://modrinth.com/plugin/minertrack
8 | # View Source: https://github.com/At87668/MinerTrack
9 | # Report Issue: https://github.com/At87668/MinerTrack/issues
10 | # Pull Requset: https://github.com/At87668/MinerTrack/pulls
11 | # Visit Wiki: https://minertrack.pages.dev/wiki/
12 | # Join Discord: https://discord.gg/MzTea2W9cb
13 | #
14 | ##################################################################################
15 |
16 | # Check for updates on startup (recommended for latest optimizations).
17 | check_update: true
18 | # Available options: stable/beta/alpha. Using the pre-release version will receive updates from more stable versions.
19 | check_update_channel: "stable"
20 |
21 | # Visual effect when a player is kicked using /minertrack kick (default: true)
22 | kick_strike_lightning: true
23 |
24 | # Log Violations (default: true)
25 | # Logs all detections in /plugins/MinerTrack/logs for easier monitoring.
26 | log_file: true
27 |
28 | # Auto-delete outdated logs after specified days (-1 to disable auto-deletion)
29 | delete_time: 30
30 |
31 | # Deny Bypass Permission - force checks on all players, including Operators.
32 | disable_bypass_permission: false
33 |
34 | # Discord WebHook
35 | # Placeholders: %player% %player_uuid% %player_vl% %ore_type% %ore_count% %mined_veins% %pos_x% %pos_y% %pos_z% %timestamp%
36 | DiscordWebHook:
37 | enable: false
38 | WebHookURL: ''
39 | vl-required: 4 # If a player's violation level is greater than this value, an alert is sent.
40 | vl-add-message:
41 | color: 0xFF5733
42 | title: "X-Ray Alert"
43 | text:
44 | - 'Player Name: %player%'
45 | - 'Player UUID: %player_uuid%'
46 | - 'Player Violation Level: %player_vl%'
47 | - ''
48 | - 'Mining Ore: %ore_type%x%ore_count%'
49 | - 'Mined Veins: %mined_veins%'
50 | - ''
51 | - 'Pos: %pos_x% %pos_y% %pos_z%'
52 | # If the custom Json format below is enabled, this Json body format will be used to send to the WebHookURL configured above
53 | custom-json:
54 | enable: false
55 | format: |
56 | {
57 | "title": "X-Ray Alert",
58 | "data": {
59 | "player": {
60 | "name": "%player%",
61 | "uuid": "%player_uuid%",
62 | "violation_level": %player_vl%
63 | },
64 | "mining": {
65 | "ore_type": "%ore_type%",
66 | "ore_count": %ore_count%,
67 | "mined_veins": %mined_veins%
68 | },
69 | "location": {
70 | "x": %pos_x%,
71 | "y": %pos_y%,
72 | "z": %pos_z%
73 | },
74 | "timestamp": "%timestamp%"
75 | }
76 | }
77 |
78 | #############################################################################################################################
79 | ## ## This plugin in order to capture XRayer quickly, might generate false positives for players who are lucky! ##
80 | ## Warn ## Be careful not to set the VL threshold too low! ##
81 | ## ## Don't detect for common ores either, you should only detect for rare ores! ##
82 | #############################################################################################################################
83 |
84 | #############################################################################################################################
85 | ## ## Default configuration may be slow on the first check-out. ##
86 | ## Info ## It will be more accurate afterwards, ##
87 | ## ## Therefore, it is recommended to adjust the configuration to suit your needs. ##
88 | #############################################################################################################################
89 |
90 | xray:
91 | enable: true # Enable X-ray detection
92 |
93 | # At what height should I start detecting X-Ray?
94 | # Worlds that are not listed do not have XRay detection.
95 | worlds:
96 | world:
97 | enable: true
98 | max-height: 32
99 | world_nether:
100 | enable: true
101 | max-height: 128
102 | world_the_end:
103 | enable: false
104 | all_unnamed_world:
105 | enable: false
106 |
107 | # The type of ore a player has to mine before being able to be checked
108 | # List of allowed materials: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html
109 | rare-ores:
110 | - ANCIENT_DEBRIS
111 | - EMERALD_ORE
112 | - DEEPSLATE_EMERALD_ORE
113 | - DIAMOND_ORE
114 | - DEEPSLATE_DIAMOND_ORE
115 |
116 | # Mined length record limit (Unit: block)
117 | max_path_length: 500
118 |
119 | # Path record is reset after the player VL is 0 {n} minutes.
120 | trace_remove: 15
121 |
122 | max_vein_distance: 5 # When two veins are close together, VL is not increased.
123 |
124 | # VL is increased as the player digs the number of veins
125 | veinCountThreshold: 2
126 |
127 | # Small vein detection size
128 | # If a vein contains <= this number of blocks, the plugin uses a more sensitive
129 | # comparison strategy (cluster overlap/min distance) to decide whether it's a new vein.
130 | # Default: 4
131 | small_vein_detection_size: 4
132 |
133 | path-detection:
134 | turn-count-threshold: 10 # Maximum number of turns threshold
135 | branch-count-threshold: 6 # Maximum number of branches
136 | y-change-threshold: 4 # Maximum Y-axis change threshold
137 | y-change-threshold-add-required: 3 # If y pos transformation exceeds this value, the threshold is increased
138 |
139 | # Natural Detection
140 | natural-detection:
141 | enable: true
142 |
143 | # Satisfying the conditions will think they are in a natural.
144 | cave:
145 | air-threshold: 14 # When the number of air blocks reaches several, the player is considered to be in a cave?
146 | CaveAirMultiplier: 5 # If cave air is found, regarded as how many ordinary air?
147 | detection-range: 3 # 3 = 7x7x7, 2 = 5x5x5
148 | check_skip_vl: true # VL add of whether or not to skip natural behavior
149 |
150 | # Prevents players from manually creating air to bypass detection.
151 | air-monitor:
152 | enable: true # Enable or disable
153 | min-path-length: 10 # Minimum digging path required to perform this detection.
154 | air-ratio-threshold: 0.3 # Air percentage threshold (e.g., 0.3 for 30%).
155 | violation-increase: 1 # The value of the increase in rating after each triggered violation.
156 | violation-threshold: 5 # The number of ratings reached should be flagged as suspicion of deliberate creation of artificial air.
157 | remove-time: 20 # How long to remove this record once (in minutes)
158 | sea:
159 | check-running-water: false # Should we check running water?
160 | water-threshold: 14
161 | detection-range: 3
162 | check_skip_vl: true
163 | lava-sea:
164 | lava-threshold: 14
165 | detection-range: 3
166 | check_skip_vl: true
167 |
168 | # Violation level decay settings (Unit: minute)
169 | decay:
170 | interval: 3 # The interval between decay tasks (in minutes)
171 | amount: 1 # The value of each linear falloff
172 | use_factor: false # Whether to enable nonlinear attenuation
173 | factor: 0.9 # Nonlinear decay scale (only works when use_factor=true)
174 |
175 | # Commands executed at specified violation thresholds
176 | commands:
177 | 2: 'minertrack notify &f%player% &7triggered &fX-Ray &7detection [&cVL: 2&7]'
178 | 4: 'minertrack notify &f%player% &7triggered &fX-Ray &7detection [&cVL: 4&7]'
179 | 5:
180 | - 'minertrack kick %player% X-Ray'
181 | - 'minertrack notify &f%player% &7was kicked for &fX-Ray'
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Stop when "xargs" is not available.
209 | if ! command -v xargs >/dev/null 2>&1
210 | then
211 | die "xargs is not available"
212 | fi
213 |
214 | # Use "xargs" to parse quoted args.
215 | #
216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
217 | #
218 | # In Bash we could simply go:
219 | #
220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
221 | # set -- "${ARGS[@]}" "$@"
222 | #
223 | # but POSIX shell has neither arrays nor command substitution, so instead we
224 | # post-process each arg (as a line of input to sed) to backslash-escape any
225 | # character that might be a shell metacharacter, then use eval to reverse
226 | # that process (while maintaining the separation between arguments), and wrap
227 | # the whole thing up as a single "set" statement.
228 | #
229 | # This will of course break if any of these variables contains a newline or
230 | # an unmatched quote.
231 | #
232 |
233 | eval "set -- $(
234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
235 | xargs -n1 |
236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
237 | tr '\n' ' '
238 | )" '"$@"'
239 |
240 | exec "$JAVACMD" "$@"
241 |
--------------------------------------------------------------------------------
/src/main/java/link/star_dust/MinerTrack/MinerTrack.java:
--------------------------------------------------------------------------------
1 | /**
2 | * DON'T REMOVE THIS
3 | *
4 | * /MinerTrack/src/main/java/link/star_dust/MinerTrack/MinerTrack.java
5 | *
6 | * MinerTrack Source Code - Public under GPLv3 license
7 | * Original Author: Author87668
8 | * Contributors: Author87668
9 | *
10 | * DON'T REMOVE THIS
11 | **/
12 | package link.star_dust.MinerTrack;
13 |
14 | import java.io.BufferedReader;
15 | import java.io.IOException;
16 | import java.io.InputStreamReader;
17 | import java.net.HttpURLConnection;
18 | import java.net.URL;
19 | import java.util.Arrays;
20 | import java.util.HashSet;
21 | import java.util.Set;
22 | import java.util.UUID;
23 | import java.util.regex.Matcher;
24 | import java.util.regex.Pattern;
25 |
26 | import org.bstats.bukkit.Metrics;
27 | import org.bukkit.Bukkit;
28 | import org.bukkit.ChatColor;
29 | import org.bukkit.command.Command;
30 | import org.bukkit.command.CommandSender;
31 | import org.bukkit.command.ConsoleCommandSender;
32 | import org.bukkit.entity.Player;
33 | import org.bukkit.event.EventHandler;
34 | import org.bukkit.event.player.PlayerJoinEvent;
35 | import org.bukkit.event.server.ServerLoadEvent;
36 | import org.bukkit.plugin.java.JavaPlugin;
37 | import org.bukkit.event.Listener;
38 | import org.json.JSONObject;
39 |
40 | import link.star_dust.MinerTrack.managers.ConfigManager;
41 | import link.star_dust.MinerTrack.managers.LanguageManager;
42 | import link.star_dust.MinerTrack.managers.UpdateManager;
43 | import link.star_dust.MinerTrack.managers.ViolationManager;
44 | import net.md_5.bungee.api.chat.BaseComponent;
45 | import link.star_dust.MinerTrack.listeners.MiningDetectionExtension;
46 | import link.star_dust.MinerTrack.listeners.MiningListener;
47 | import link.star_dust.MinerTrack.listeners.LogCacheListener;
48 | import link.star_dust.MinerTrack.commands.MinerTrackCommand;
49 |
50 | public class MinerTrack extends JavaPlugin implements Listener {
51 | private ConfigManager configManager;
52 | private LanguageManager languageManager;
53 | private ViolationManager violationManager;
54 | private Notifier notifier;
55 | private final Set verbosePlayers = new HashSet<>();
56 | private boolean verboseConsole = false;
57 | private UpdateManager updateManager;
58 | private String ColoredVersion;
59 | public MiningDetectionExtension miningDetectionExtension;
60 | private MinerTrackCommand minerTrackCommand;
61 | // Keep a reference to the listener so other managers can request per-player cleanup
62 | private MiningListener miningListener;
63 |
64 | @Override
65 | public void onEnable() {
66 | saveDefaultConfig();
67 | configManager = new ConfigManager(this);
68 | languageManager = new LanguageManager(this);
69 | violationManager = new ViolationManager(this);
70 | notifier = new Notifier(this);
71 | updateManager = new UpdateManager(this);
72 | //miningDetectionExtension = new MiningDetectionExtension(this);
73 | //miningDetectionExtension.register();
74 |
75 | int pluginId = 23790;
76 | new Metrics(this, pluginId);
77 |
78 | registerCommands();
79 | registerListeners();
80 | getCommand("minertrack").setExecutor(new MinerTrackCommand(this));
81 |
82 | if (getConfigManager().updateCheck()) {
83 | boolean isHasNewerVersion = updateManager.isHasNewerVersion();
84 |
85 | if(isHasNewerVersion) {
86 | ColoredVersion = "&cv" + getDescription().getVersion();
87 | } else {
88 | ColoredVersion = "&av" + getDescription().getVersion();
89 | }
90 | } else {
91 | ColoredVersion = "&av" + getDescription().getVersion();
92 | }
93 |
94 | getServer().getConsoleSender().sendMessage(applyColors("&8----[&9&lMiner&c&lTrack " + ColoredVersion + " &8]-----------"));
95 | getServer().getConsoleSender().sendMessage(applyColors("&9&lMiner&c&lTrack &4&oAnti-XRay &aEnabled!"));
96 | getServer().getConsoleSender().sendMessage(applyColors(""));
97 | getServer().getConsoleSender().sendMessage(applyColors("&7Authors: Author87668"));
98 | getServer().getConsoleSender().sendMessage(applyColors("&7Original Author: Author87668"));
99 | getServer().getConsoleSender().sendMessage(applyColors("&7Contributors: Author87668, Zhang12334"));
100 | getServer().getConsoleSender().sendMessage(applyColors(""));
101 | getServer().getConsoleSender().sendMessage(applyColors("&a&oThanks for your use!"));
102 | getServer().getConsoleSender().sendMessage(applyColors("&8-----------------------------------------"));
103 |
104 | /*if (getConfigManager().updateCheck()) {
105 | checkForUpdates(null);
106 | } else {
107 | return;
108 | }*/
109 | }
110 |
111 | @EventHandler
112 | public void onServerLoad(ServerLoadEvent event) {
113 | if (getConfigManager().updateCheck()) {
114 | getLogger().info("Server has finished loading. Checking for updates...");
115 | checkForUpdates(null);
116 | } else {
117 | return;
118 | }
119 | }
120 |
121 | public String applyColors(String message) {
122 | return ChatColor.translateAlternateColorCodes('&', message);
123 | }
124 |
125 | private void registerCommands() {
126 | minerTrackCommand = new MinerTrackCommand(this);
127 | getCommand("mtrack").setExecutor(minerTrackCommand);
128 | getCommand("mtrack").setTabCompleter(minerTrackCommand);
129 | }
130 |
131 | private void registerListeners() {
132 | Bukkit.getPluginManager().registerEvents(this, this);
133 | miningListener = new MiningListener(this);
134 | Bukkit.getPluginManager().registerEvents(miningListener, this);
135 | Bukkit.getPluginManager().registerEvents(new LogCacheListener(minerTrackCommand), this);
136 | }
137 |
138 | public MiningListener getMiningListener() {
139 | return miningListener;
140 | }
141 |
142 | public ConfigManager getConfigManager() {
143 | return configManager;
144 | }
145 |
146 | public LanguageManager getLanguageManager() {
147 | return languageManager;
148 | }
149 |
150 | public Notifier getNotifier() {
151 | return notifier;
152 | }
153 |
154 | public ViolationManager getViolationManager() {
155 | return violationManager;
156 | }
157 |
158 | public Set getVerbosePlayers() {
159 | return verbosePlayers;
160 | }
161 |
162 | public boolean isVerboseConsoleEnabled() {
163 | return verboseConsole;
164 | }
165 |
166 | public void toggleVerboseMode(CommandSender sender) {
167 | String enableMessage = getLanguageManager().getPrefixedMessage("verbose-enable");
168 | String disableMessage = getLanguageManager().getPrefixedMessage("verbose-disable");
169 |
170 | if (sender instanceof Player) {
171 | Player player = (Player) sender;
172 | UUID playerId = player.getUniqueId();
173 |
174 | if (verbosePlayers.contains(playerId)) {
175 | verbosePlayers.remove(playerId);
176 | player.sendMessage(disableMessage);
177 | } else {
178 | verbosePlayers.add(playerId);
179 | player.sendMessage(enableMessage);
180 | }
181 | } else if (sender instanceof ConsoleCommandSender) {
182 | verboseConsole = !verboseConsole;
183 |
184 | if (verboseConsole) {
185 | sender.sendMessage(enableMessage);
186 | } else {
187 | sender.sendMessage(disableMessage);
188 | }
189 | }
190 | }
191 |
192 |
193 | @EventHandler
194 | public void onPlayerJoin(PlayerJoinEvent event) {
195 | if (!configManager.updateCheck()) {
196 | return;
197 | }
198 |
199 | Player player = event.getPlayer();
200 |
201 | if (player.hasPermission("minertrack.checkupdate") && updateManager.isHasNewerVersion()) {
202 | BaseComponent[] updateComponent = updateManager.getUpdateMessageComponent();
203 | if (updateComponent != null) {
204 | player.spigot().sendMessage(updateComponent);
205 | } // Call UpdateManager for player-specific check
206 | //updateManager.checkForUpdates(player);
207 | }
208 | }
209 |
210 | public void checkForUpdates(CommandSender sender) {
211 | updateManager.checkForUpdates(sender);
212 | }
213 |
214 | @Override
215 | public void onDisable() {
216 | getServer().getConsoleSender().sendMessage(applyColors("&8----[&9&lMiner&c&lTrack " + ColoredVersion + " &8]-----------"));
217 | getServer().getConsoleSender().sendMessage(applyColors("&9&lMiner&c&lTrack &4&oAnti-XRay &cDisabled!"));
218 | getServer().getConsoleSender().sendMessage(applyColors(""));
219 | getServer().getConsoleSender().sendMessage(applyColors("&7Authors: Author87668"));
220 | getServer().getConsoleSender().sendMessage(applyColors("&7Original Author: Author87668"));
221 | getServer().getConsoleSender().sendMessage(applyColors("&7Contributors: Author87668, Zhang12334"));
222 | getServer().getConsoleSender().sendMessage(applyColors(""));
223 | getServer().getConsoleSender().sendMessage(applyColors("&a&oGood bye!"));
224 | getServer().getConsoleSender().sendMessage(applyColors("&8-----------------------------------------"));
225 | }
226 | }
--------------------------------------------------------------------------------
/src/main/java/link/star_dust/MinerTrack/managers/ConfigManager.java:
--------------------------------------------------------------------------------
1 | /**
2 | * DON'T REMOVE THIS
3 | *
4 | * /MinerTrack/src/main/java/link/star_dust/MinerTrack/managers/ConfigManager.java
5 | *
6 | * MinerTrack Source Code - Public under GPLv3 license
7 | * Original Author: Author87668
8 | * Contributors: Author87668
9 | *
10 | * DON'T REMOVE THIS
11 | **/
12 | package link.star_dust.MinerTrack.managers;
13 |
14 | import org.bukkit.configuration.ConfigurationSection;
15 | import org.bukkit.configuration.InvalidConfigurationException;
16 | import org.bukkit.configuration.file.FileConfiguration;
17 | import org.bukkit.configuration.file.YamlConfiguration;
18 | import org.jetbrains.annotations.Nullable;
19 |
20 | import link.star_dust.MinerTrack.MinerTrack;
21 |
22 | import java.io.*;
23 | import java.util.List;
24 | import java.util.Set;
25 |
26 | public class ConfigManager {
27 | private final MinerTrack plugin;
28 | private final File configFile;
29 | private final FileConfiguration config;
30 |
31 | public ConfigManager(MinerTrack plugin) {
32 | this.plugin = plugin;
33 | this.configFile = new File(plugin.getDataFolder(), "config.yml");
34 |
35 | // Load existing config
36 | this.config = YamlConfiguration.loadConfiguration(configFile);
37 |
38 | // Load defaults and merge only missing keys
39 | try (InputStream defaultStream = plugin.getResource("config.yml")) {
40 | if (defaultStream != null) {
41 | YamlConfiguration defaultConfig = YamlConfiguration.loadConfiguration(new InputStreamReader(defaultStream));
42 | mergeConfigurations(config, defaultConfig, ""); // Recursive merge
43 | saveConfig(); // Save back merged configuration
44 | }
45 | } catch (IOException e) {
46 | plugin.getLogger().severe("Error loading default configuration: " + e.getMessage());
47 | }
48 | }
49 |
50 | /**
51 | * Recursively merges default configuration into the current configuration.
52 | *
53 | * @param currentConfig The current configuration to merge into.
54 | * @param defaultConfig The default configuration to merge from.
55 | */
56 | private void mergeConfigurations(ConfigurationSection currentConfig, ConfigurationSection defaultConfig, String currentPath) {
57 | Set whitelistKeys = Set.of(
58 | "check_update",
59 | "check_update_channel",
60 | "kick_strike_lightning",
61 | "log_file",
62 | "delete_time",
63 | "disable_bypass_permission",
64 | "DiscordWebHook",
65 | "DiscordWebHook.enable",
66 | "DiscordWebHook.WebHookURL",
67 | "DiscordWebHook.vl-required",
68 | "DiscordWebHook.vl-add-message",
69 | "DiscordWebHook.vl-add-message.color",
70 | "DiscordWebHook.vl-add-message.title",
71 | "DiscordWebHook.vl-add-message.text",
72 | "xray",
73 | "xray.enable",
74 | "xray.worlds",
75 | "xray.worlds.world",
76 | "xray.worlds.all_unnamed_world",
77 | "xray.worlds.all_unnamed_world.enable",
78 | "xray.rare-ores",
79 | "xray.max_path_length",
80 | "xray.trace_remove",
81 | "xray.max_vein_distance",
82 | "xray.veinCountThreshold",
83 | "xray.path-detection",
84 | "xray.path-detection.turn-count-threshold",
85 | "xray.path-detection.branch-count-threshold",
86 | "xray.path-detection.y-change-threshold",
87 | "xray.path-detection.y-change-threshold-add-required",
88 | "xray.natural-detection",
89 | "xray.natural-detection.enable",
90 | "xray.natural-detection.cave",
91 | "xray.natural-detection.cave.air-threshold",
92 | "xray.natural-detection.cave.CaveAirMultiplier",
93 | "xray.natural-detection.cave.detection-range",
94 | "xray.natural-detection.cave.check_skip_vl",
95 | "xray.natural-detection.cave.air-monitor",
96 | "xray.natural-detection.cave.air-monitor.enable",
97 | "xray.natural-detection.cave.air-monitor.min-path-length",
98 | "xray.natural-detection.cave.air-monitor.air-ratio-threshold",
99 | "xray.natural-detection.cave.air-monitor.violation-increase",
100 | "xray.natural-detection.cave.air-monitor.violation-threshold",
101 | "xray.natural-detection.cave.air-monitor.remove-time",
102 | "xray.natural-detection.sea",
103 | "xray.natural-detection.sea.check-running-water",
104 | "xray.natural-detection.sea.water-threshold",
105 | "xray.natural-detection.sea.detection-range",
106 | "xray.natural-detection.sea.check_skip_vl",
107 | "xray.natural-detection.lava-sea",
108 | "xray.natural-detection.lava-sea.lava-threshold",
109 | "xray.natural-detection.lava-sea.detection-range",
110 | "xray.natural-detection.lava-sea.check_skip_vl",
111 | "xray.small_vein_detection_size",
112 | "xray.decay",
113 | "xray.decay.interval",
114 | "xray.decay.amount",
115 | "xray.decay.use_factor",
116 | "xray.decay.factor",
117 | "explosion",
118 | "explosion.entity-explode-check",
119 | "explosion.explosion_retention_time",
120 | "explosion.base_vl_rate",
121 | "explosion.suspicious_hit_rate",
122 | "commands"
123 | );
124 |
125 | for (String key : defaultConfig.getKeys(false)) {
126 | String fullKeyPath = (currentPath.isEmpty() ? "" : currentPath + ".") + key;
127 | if (!whitelistKeys.contains(fullKeyPath)) {
128 | continue; // Skip keys not in the whitelist
129 | }
130 |
131 | if (currentConfig.contains(key)) {
132 | Object currentValue = currentConfig.get(key);
133 | Object defaultValue = defaultConfig.get(key);
134 |
135 | // Recurse for nested sections
136 | if (currentValue instanceof ConfigurationSection && defaultValue instanceof ConfigurationSection) {
137 | mergeConfigurations(
138 | (ConfigurationSection) currentValue,
139 | (ConfigurationSection) defaultValue,
140 | fullKeyPath
141 | );
142 | }
143 | } else {
144 | // Add missing key
145 | currentConfig.set(key, defaultConfig.get(key));
146 | }
147 | }
148 | }
149 |
150 |
151 | /**
152 | * Saves the current configuration back to the file while preserving structure.
153 | */
154 | public void saveConfig() {
155 | try {
156 | config.save(configFile);
157 | } catch (IOException e) {
158 | plugin.getLogger().severe("Could not save configuration to " + configFile.getName() + ": " + e.getMessage());
159 | }
160 | }
161 |
162 | /**
163 | * Reloads the configuration from the file.
164 | */
165 | public void reloadConfig() {
166 | try {
167 | config.load(configFile);
168 | } catch (IOException | InvalidConfigurationException e) {
169 | plugin.getLogger().severe("Error reloading configuration: " + e.getMessage());
170 | }
171 | }
172 |
173 | // Add your getter methods here, for example:
174 | public boolean isDenyBypassPermissionEnabled() {
175 | return config.getBoolean("disable_bypass_permission", false);
176 | }
177 |
178 | public boolean isKickStrikeLightning() {
179 | return config.getBoolean("xray.kick-strike-lightning", true);
180 | }
181 |
182 | public List getRareOres() {
183 | return config.getStringList("xray.rare-ores");
184 | }
185 |
186 | public int getVeinCountThreshold() {
187 | return config.getInt("xray.veinCountThreshold", 3);
188 | }
189 |
190 | public int getTurnCountThreshold() {
191 | return config.getInt("xray.path-detection.turn-count-threshold", 10);
192 | }
193 |
194 | public int getBranchCountThreshold() {
195 | return config.getInt("xray.path-detection.branch-count-threshold", 6);
196 | }
197 |
198 | public int getYChangeThreshold() {
199 | return config.getInt("xray.path-detection.y-change-threshold", 4);
200 | }
201 |
202 | public int getWorldMaxHeight(String worldName) {
203 | ConfigurationSection xraySection = config.getConfigurationSection("xray.worlds");
204 | if (xraySection == null || !xraySection.isConfigurationSection(worldName)) {
205 | //plugin.getLogger().warning("Max height configuration for world " + worldName + " not found. Defaulting to no height limit.");
206 | return config.getInt("xray.worlds.all_unnamed_world.max-height", -1);
207 | }
208 | return xraySection.getInt(worldName + ".max-height", -1);
209 | }
210 |
211 | public boolean isWorldDetectionEnabled(String worldName) {
212 | ConfigurationSection worldsSection = config.getConfigurationSection("xray.worlds");
213 | if (worldsSection == null || !worldsSection.isConfigurationSection(worldName)) {
214 | return config.getBoolean("xray.worlds.all_unnamed_world.enable", false);
215 | }
216 | return worldsSection.getBoolean(worldName + ".enable", false);
217 | }
218 |
219 | public boolean DisableBypass() {
220 | return config.getBoolean("disable_bypass_permission", false);
221 | }
222 |
223 | public String getCommandForThreshold(int threshold) {
224 | ConfigurationSection commandsSection = config.getConfigurationSection("xray.commands");
225 | if (commandsSection != null && commandsSection.contains(String.valueOf(threshold))) {
226 | return commandsSection.getString(String.valueOf(threshold));
227 | }
228 | return null;
229 | }
230 |
231 | public int getMaxVeinDistance() {
232 | return config.getInt("xray.max_vein_distance", 5);
233 | }
234 |
235 | public int getSmallVeinSize() {
236 | return config.getInt("xray.small_vein_detection_size", 4);
237 | }
238 |
239 | public boolean getNaturalEnable() {
240 | return config.getBoolean("xray.natural-detection.enable", true);
241 | }
242 |
243 | public int getCaveBypassAirThreshold() {
244 | return config.getInt("xray.natural-detection.cave.air-threshold", 14);
245 | }
246 |
247 | public int getCaveAirMultiplier() {
248 | return config.getInt("xray.natural-detection.cave.CaveAirMultiplier", 5);
249 | }
250 |
251 | public int getCaveDetectionRange() {
252 | return config.getInt("xray.natural-detection.cave.detection-range", 3);
253 | }
254 |
255 | public boolean isCaveSkipVL() {
256 | return config.getBoolean("xray.natural-detection.cave.check_skip_vl", true);
257 | }
258 |
259 | public boolean isRunningWaterCheckEnabled() {
260 | return config.getBoolean("xray.natural-detection.sea.check-running-water", false);
261 | }
262 |
263 | public int getWaterThreshold() {
264 | return config.getInt("xray.natural-detection.sea.water-threshold", 14);
265 | }
266 |
267 | public int getWaterDetectionRange() {
268 | return config.getInt("xray.natural-detection.sea.detection-range", 3);
269 | }
270 |
271 | public boolean isSeaSkipVL() {
272 | return config.getBoolean("xray.natural-detection.sea.check_skip_vl", true);
273 | }
274 |
275 | public int getLavaThreshold() {
276 | return config.getInt("xray.natural-detection.lava-sea.lava-threshold", 14);
277 | }
278 |
279 | public int getLavaDetectionRange() {
280 | return config.getInt("xray.natural-detection.lava-sea.detection-range", 3);
281 | }
282 |
283 | public boolean isLavaSeaSkipVL() {
284 | return config.getBoolean("xray.natural-detection.lava-sea.check_skip_vl", true);
285 | }
286 |
287 | public int traceBackLength() {
288 | return config.getInt("xray.trace_back_length", 10);
289 | }
290 |
291 | public boolean updateCheck() {
292 | return config.getBoolean("check_update", true);
293 | }
294 |
295 | public String updateCheckChannel() {
296 | return config.getString("check_update_channel", "stable");
297 | }
298 |
299 | public int getSuspicionThreshold() {
300 | return config.getInt("xray.mine.suspicionThreshold", 100);
301 | }
302 |
303 | public String WebHookURL() {
304 | return config.getString("DiscordWebHook.WebHookURL");
305 | }
306 |
307 | public boolean WebHookEnable() {
308 | return config.getBoolean("DiscordWebHook.enable", false);
309 | }
310 |
311 | public int WebHookColor() {
312 | return config.getInt("DiscordWebHook.vl-add-message.color", 0xFF5733);
313 | }
314 |
315 | public String WebHookTitle() {
316 | return config.getString("DiscordWebHook.vl-add-message.title");
317 | }
318 |
319 | public List WebHookText() {
320 | return config.getStringList("DiscordWebHook.vl-add-message.text");
321 | }
322 |
323 | public int WebHookVLRequired() {
324 | return config.getInt("DiscordWebHook.vl-required");
325 | }
326 |
327 | public boolean isCustomJsonEnabled() {
328 | return config.getBoolean("DiscordWebHook.custom-json.enable", false);
329 | }
330 |
331 | public String getCustomJsonFormat() {
332 | return config.getString("DiscordWebHook.custom-json.format", "");
333 | }
334 |
335 | public int getYPosChangeThresholdAddRequired() {
336 | return config.getInt("xray.path-detection.y-change-threshold-add-required", 3);
337 | }
338 |
339 | public int AirMonitorVLT() {
340 | return plugin.getConfig().getInt("xray.natural-detection.cave.air-monitor.violation-threshold", 5);
341 | }
342 | }
343 |
344 |
345 |
346 |
--------------------------------------------------------------------------------
/src/main/java/link/star_dust/MinerTrack/managers/UpdateManager.java:
--------------------------------------------------------------------------------
1 | /**
2 | * DON'T REMOVE THIS
3 | *
4 | * /MinerTrack/src/main/java/link/star_dust/MinerTrack/managers/UpdateManager.java
5 | *
6 | * MinerTrack Source Code - Public under GPLv3 license
7 | * Original Author: Author87668
8 | * Contributors: Author87668
9 | *
10 | * DON'T REMOVE THIS
11 | **/
12 | package link.star_dust.MinerTrack.managers;
13 |
14 | import link.star_dust.MinerTrack.MinerTrack;
15 | import net.md_5.bungee.api.chat.BaseComponent;
16 | import net.md_5.bungee.api.chat.ClickEvent;
17 | import net.md_5.bungee.api.chat.HoverEvent;
18 | import net.md_5.bungee.api.chat.TextComponent;
19 | import org.bukkit.Bukkit;
20 | import org.bukkit.command.CommandSender;
21 | import org.bukkit.entity.Player;
22 | import org.json.JSONArray;
23 | import org.json.JSONObject;
24 |
25 | import java.io.BufferedReader;
26 | import java.io.IOException;
27 | import java.io.InputStreamReader;
28 | import java.net.HttpURLConnection;
29 | import java.net.URL;
30 |
31 | public class UpdateManager {
32 | private final MinerTrack plugin;
33 | private String latestVersion;
34 | private final String currentVersion;
35 | private String downloadUrl;
36 |
37 | public UpdateManager(MinerTrack plugin) {
38 | this.plugin = plugin;
39 | this.currentVersion = plugin.getDescription().getVersion();
40 |
41 | if (plugin.getConfigManager().updateCheck()) {
42 | fetchLatestVersionFromModrinth();
43 | } else {
44 | this.latestVersion = null;
45 | }
46 | }
47 |
48 | public void checkForUpdates(CommandSender sender) {
49 | fetchLatestVersionFromModrinth();
50 |
51 | if (latestVersion == null) {
52 | String errorMessage = plugin.getLanguageManager().getPrefixedMessageWithDefault(
53 | "update.check-failed",
54 | "&cFailed to check for updates."
55 | );
56 | sendMessage(sender, errorMessage);
57 | return;
58 | }
59 |
60 | if (shouldConsiderAsUpdate(latestVersion, currentVersion)) {
61 | sendUpdateMessage(sender, latestVersion);
62 | } else {
63 | String upToDateMessage = plugin.getLanguageManager().getPrefixedMessageWithDefault(
64 | "update.using-latest",
65 | "&2You are using the latest version."
66 | );
67 | sendMessage(sender, upToDateMessage);
68 | }
69 | }
70 |
71 | private void fetchLatestVersionFromModrinth() {
72 | try {
73 | URL url = new URL("https://api.modrinth.com/v2/project/minertrack/version");
74 | HttpURLConnection conn = (HttpURLConnection) url.openConnection();
75 | conn.setRequestMethod("GET");
76 | conn.setRequestProperty("User-Agent", "MinerTrack Update Checker");
77 | conn.setConnectTimeout(5000);
78 | conn.setReadTimeout(5000);
79 |
80 | BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
81 | StringBuilder response = new StringBuilder();
82 | String line;
83 |
84 | while ((line = reader.readLine()) != null) {
85 | response.append(line);
86 | }
87 | reader.close();
88 |
89 | JSONArray versions = new JSONArray(response.toString());
90 | if (versions.length() > 0) {
91 | JSONObject latest = versions.getJSONObject(0);
92 | this.latestVersion = latest.getString("version_number");
93 | this.downloadUrl = "https://modrinth.com/plugin/minertrack/version/" + this.latestVersion;
94 | } else {
95 | plugin.getLogger().warning("No versions found on Modrinth.");
96 | this.latestVersion = null;
97 | this.downloadUrl = null;
98 | }
99 |
100 | } catch (IOException | org.json.JSONException e) {
101 | plugin.getLogger().warning("Failed to check for updates from Modrinth: " + e.getMessage());
102 | this.latestVersion = null;
103 | this.downloadUrl = null;
104 | }
105 | }
106 |
107 | private boolean shouldConsiderAsUpdate(String latestVersion, String currentVersion) {
108 | if (latestVersion == null || currentVersion == null) {
109 | return false;
110 | }
111 |
112 | String channel = plugin.getConfigManager().updateCheckChannel().toLowerCase();
113 | Version latest = parseVersion(latestVersion);
114 | Version current = parseVersion(currentVersion);
115 |
116 | int mainCompare = compareVersionNumbers(latest.mainParts, current.mainParts);
117 | if (mainCompare < 0) {
118 | return false;
119 | }
120 | if (mainCompare > 0) {
121 | return isVersionAllowedByChannel(latest, channel);
122 | }
123 |
124 | if (latest.preReleaseTag == null && current.preReleaseTag == null) {
125 | return false;
126 | }
127 |
128 | if (latest.preReleaseTag == null && current.preReleaseTag != null) {
129 | return isVersionAllowedByChannel(latest, channel);
130 | }
131 |
132 | if (latest.preReleaseTag != null) {
133 | if (latest.isNewerPreReleaseThan(current)) {
134 | return isVersionAllowedByChannel(latest, channel);
135 | }
136 | }
137 |
138 | return false;
139 | }
140 |
141 | private boolean isVersionAllowedByChannel(Version version, String channel) {
142 | if (version.preReleaseTag == null) {
143 | return true;
144 | }
145 |
146 | String tag = version.preReleaseTag.toLowerCase();
147 | if ("alpha".equals(channel)) {
148 | return true;
149 | } else if ("beta".equals(channel)) {
150 | return tag.startsWith("beta");
151 | } else { // stable or unknown
152 | return false;
153 | }
154 | }
155 |
156 | private static class Version {
157 | final int[] mainParts;
158 | final String preReleaseTag;
159 |
160 | Version(int[] mainParts, String preReleaseTag) {
161 | this.mainParts = mainParts;
162 | this.preReleaseTag = preReleaseTag;
163 | }
164 |
165 | boolean isNewerPreReleaseThan(Version other) {
166 | boolean thisStable = this.preReleaseTag == null;
167 | boolean otherStable = other.preReleaseTag == null;
168 |
169 | if (thisStable && !otherStable) return true;
170 | if (!thisStable && otherStable) return false;
171 | if (thisStable && otherStable) return false;
172 |
173 | return comparePreRelease(this.preReleaseTag, other.preReleaseTag) > 0;
174 | }
175 | }
176 |
177 | private Version parseVersion(String versionStr) {
178 | versionStr = versionStr.replaceFirst("^v", "");
179 | String[] parts = versionStr.split("-", 2);
180 | String main = parts[0];
181 | String pre = parts.length > 1 ? parts[1] : null;
182 |
183 | String[] mainSplit = main.split("\\.");
184 | int[] mainParts = new int[mainSplit.length];
185 | for (int i = 0; i < mainSplit.length; i++) {
186 | mainParts[i] = parsePositiveInt(mainSplit[i], 0);
187 | }
188 |
189 | return new Version(mainParts, pre);
190 | }
191 |
192 | private int compareVersionNumbers(int[] a, int[] b) {
193 | int len = Math.max(a.length, b.length);
194 | for (int i = 0; i < len; i++) {
195 | int va = i < a.length ? a[i] : 0;
196 | int vb = i < b.length ? b[i] : 0;
197 | if (va != vb) {
198 | return Integer.compare(va, vb);
199 | }
200 | }
201 | return 0;
202 | }
203 |
204 | private static int comparePreRelease(String a, String b) {
205 | if (a == null && b == null) return 0;
206 | if (a == null) return 1;
207 | if (b == null) return -1;
208 |
209 | String aLower = a.toLowerCase();
210 | String bLower = b.toLowerCase();
211 |
212 | boolean aIsAlpha = aLower.startsWith("alpha");
213 | boolean aIsBeta = aLower.startsWith("beta");
214 | boolean bIsAlpha = bLower.startsWith("alpha");
215 | boolean bIsBeta = bLower.startsWith("beta");
216 |
217 | if (aIsBeta && bIsAlpha) return 1;
218 | if (aIsAlpha && bIsBeta) return -1;
219 | if ((aIsBeta || aIsAlpha) != (bIsBeta || bIsAlpha)) {
220 | return a.compareTo(b); // fallback
221 | }
222 |
223 | int numA = extractNumericSuffix(a);
224 | int numB = extractNumericSuffix(b);
225 | if (numA != numB) {
226 | return Integer.compare(numA, numB);
227 | }
228 | return a.compareTo(b); // fallback
229 | }
230 |
231 | private static int extractNumericSuffix(String tag) {
232 | String[] parts = tag.split("[^0-9]+");
233 | for (int i = parts.length - 1; i >= 0; i--) {
234 | if (!parts[i].isEmpty()) {
235 | try {
236 | return Integer.parseInt(parts[i]);
237 | } catch (NumberFormatException ignored) {}
238 | }
239 | }
240 | return 0;
241 | }
242 |
243 | private static int parsePositiveInt(String str, int defaultValue) {
244 | try {
245 | int val = Integer.parseInt(str.trim());
246 | return val >= 0 ? val : defaultValue;
247 | } catch (NumberFormatException e) {
248 | return defaultValue;
249 | }
250 | }
251 |
252 | public boolean isHasNewerVersion() {
253 | return latestVersion != null && shouldConsiderAsUpdate(latestVersion, currentVersion);
254 | }
255 |
256 | public BaseComponent[] getUpdateMessageComponent() {
257 | if (latestVersion == null || !shouldConsiderAsUpdate(latestVersion, currentVersion)) {
258 | return null;
259 | }
260 |
261 | String messageKey;
262 | if (latestVersion.contains("-beta")) {
263 | messageKey = "update.beta-available";
264 | } else if (latestVersion.contains("-alpha")) {
265 | messageKey = "update.alpha-available";
266 | } else {
267 | messageKey = "update.stable-available";
268 | }
269 |
270 | String defaultMessage;
271 | if (latestVersion.contains("-beta")) {
272 | defaultMessage = "&eNew beta version %latest_version% now available!";
273 | } else if (latestVersion.contains("-alpha")) {
274 | defaultMessage = "&cNew alpha version %latest_version% now available!";
275 | } else {
276 | defaultMessage = "&aNew stable version %latest_version% now available!";
277 | }
278 |
279 | String baseMessage = plugin.getLanguageManager().getPrefixedMessageWithDefault(messageKey, defaultMessage)
280 | .replace("%latest_version%", latestVersion);
281 |
282 | TextComponent component = new TextComponent(plugin.getLanguageManager().applyColors(baseMessage));
283 | if (downloadUrl != null) {
284 | component.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, downloadUrl));
285 | component.setHoverEvent(new HoverEvent(
286 | HoverEvent.Action.SHOW_TEXT,
287 | new BaseComponent[] {
288 | new TextComponent(plugin.getLanguageManager().applyColors("&f" + latestVersion + ": " + downloadUrl))
289 | }
290 | ));
291 | }
292 |
293 | return new BaseComponent[]{component};
294 | }
295 |
296 | private void sendMessage(CommandSender sender, String message) {
297 | String coloredMessage = plugin.getLanguageManager().applyColors(message);
298 | if (sender != null) {
299 | sender.sendMessage(coloredMessage);
300 | } else {
301 | Bukkit.getConsoleSender().sendMessage(coloredMessage);
302 | }
303 | }
304 |
305 | private void sendUpdateMessage(CommandSender sender, String latestVersion) {
306 | String messageKey;
307 | if (latestVersion.contains("-beta")) {
308 | messageKey = "update.beta-available";
309 | } else if (latestVersion.contains("-alpha")) {
310 | messageKey = "update.alpha-available";
311 | } else {
312 | messageKey = "update.stable-available";
313 | }
314 |
315 | String defaultMessage;
316 | if (latestVersion.contains("-beta")) {
317 | defaultMessage = "&eNew beta version %latest_version% now available!";
318 | } else if (latestVersion.contains("-alpha")) {
319 | defaultMessage = "&cNew alpha version %latest_version% now available!";
320 | } else {
321 | defaultMessage = "&aNew stable version %latest_version% now available!";
322 | }
323 |
324 | String baseMessage = plugin.getLanguageManager().getPrefixedMessageWithDefault(messageKey, defaultMessage)
325 | .replace("%latest_version%", latestVersion);
326 |
327 | TextComponent component = new TextComponent(plugin.getLanguageManager().applyColors(baseMessage));
328 | if (downloadUrl != null) {
329 | component.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, downloadUrl));
330 | component.setHoverEvent(new HoverEvent(
331 | HoverEvent.Action.SHOW_TEXT,
332 | new TextComponent[] {
333 | new TextComponent(plugin.getLanguageManager().applyColors("&f" + latestVersion + ": " + downloadUrl))
334 | }
335 | ));
336 | }
337 |
338 | if (sender instanceof Player) {
339 | ((Player) sender).spigot().sendMessage(component);
340 | } else {
341 | Bukkit.getConsoleSender().sendMessage(plugin.getLanguageManager().applyColors(baseMessage));
342 | }
343 | }
344 | }
--------------------------------------------------------------------------------
/src/main/java/link/star_dust/MinerTrack/commands/MinerTrackCommand.java:
--------------------------------------------------------------------------------
1 | /**
2 | * DON'T REMOVE THIS
3 | *
4 | * /MinerTrack/src/main/java/link/star_dust/MinerTrack/commands/MinerTrackCommand.java
5 | *
6 | * MinerTrack Source Code - Public under GPLv3 license
7 | * Original Author: Author87668
8 | * Contributors: Author87668
9 | *
10 | * DON'T REMOVE THIS
11 | **/
12 | package link.star_dust.MinerTrack.commands;
13 |
14 | import link.star_dust.MinerTrack.FoliaCheck;
15 | import link.star_dust.MinerTrack.MinerTrack;
16 | import link.star_dust.MinerTrack.managers.LanguageManager;
17 | import link.star_dust.MinerTrack.utils.LogViewerUtils;
18 |
19 | import org.bukkit.Bukkit;
20 | import org.bukkit.command.Command;
21 | import org.bukkit.command.CommandExecutor;
22 | import org.bukkit.command.CommandSender;
23 | import org.bukkit.command.TabCompleter;
24 | import org.bukkit.command.ConsoleCommandSender;
25 | import org.bukkit.entity.Player;
26 |
27 | import io.papermc.paper.threadedregions.scheduler.RegionScheduler;
28 |
29 | import java.io.File;
30 | import java.io.IOException;
31 | import java.nio.charset.StandardCharsets;
32 | import java.nio.file.Files;
33 | import java.util.ArrayList;
34 | import java.util.Arrays;
35 | import java.util.Comparator;
36 | import java.util.List;
37 | import java.util.Map;
38 | import java.util.concurrent.ConcurrentHashMap;
39 |
40 | public class MinerTrackCommand implements CommandExecutor, TabCompleter {
41 | private final MinerTrack plugin;
42 |
43 | public MinerTrackCommand(MinerTrack plugin) {
44 | this.plugin = plugin;
45 | }
46 |
47 | @SuppressWarnings("deprecation")
48 | @Override
49 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
50 | if (!(sender instanceof Player) || sender instanceof ConsoleCommandSender) {
51 | // pass
52 | } else if (!sender.hasPermission("minertrack.use")) {
53 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("no-permission"));
54 | return true;
55 | }
56 |
57 | if (args.length == 0 || args[0].equalsIgnoreCase("help")) {
58 | if (!sender.hasPermission("minertrack.help")) {
59 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("no-permission"));
60 | return true;
61 | }
62 | List helpMessages = plugin.getLanguageManager().getHelpMessages();
63 | for (String message : helpMessages) {
64 | sender.sendMessage(plugin.getLanguageManager().applyColors(message));
65 | }
66 | return true;
67 | }
68 |
69 | switch (args[0].toLowerCase()) {
70 | case "notify":
71 | if (!sender.hasPermission("minertrack.sendnotify")) {
72 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("no-permission"));
73 | return true;
74 | }
75 | if (args.length < 2) {
76 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("usage-notify"));
77 | return true;
78 | }
79 | String messageContent = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
80 | plugin.getNotifier().sendNotifyMessage(messageContent);
81 | break;
82 |
83 | case "verbose":
84 | if (!sender.hasPermission("minertrack.verbose")) {
85 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("no-permission"));
86 | return true;
87 | }
88 | plugin.toggleVerboseMode(sender);
89 | break;
90 |
91 | case "check":
92 | if (!sender.hasPermission("minertrack.check")) {
93 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("no-permission"));
94 | return true;
95 | }
96 | if (args.length < 2) {
97 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("usage-check"));
98 | return true;
99 | }
100 | Player target = plugin.getServer().getPlayer(args[1]);
101 | if (target != null) {
102 | int violationLevel = plugin.getViolationManager().getViolationLevel(target);
103 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("violation-level")
104 | .replace("{player}", target.getName())
105 | .replace("{level}", String.valueOf(violationLevel)));
106 | } else {
107 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("player-not-found")
108 | .replace("{player}", args[1]));
109 | }
110 | break;
111 |
112 | case "reset":
113 | if (!sender.hasPermission("minertrack.reset")) {
114 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("no-permission"));
115 | return true;
116 | }
117 | if (args.length < 2) {
118 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("usage-reset"));
119 | return true;
120 | }
121 | Player targetToReset = plugin.getServer().getPlayer(args[1]);
122 | if (targetToReset != null) {
123 | plugin.getViolationManager().resetViolationLevel(targetToReset);
124 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("reset-success")
125 | .replace("{player}", args[1]));
126 | } else {
127 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("player-not-found")
128 | .replace("{player}", args[1]));
129 | }
130 | break;
131 |
132 | case "kick":
133 | if (!sender.hasPermission("minertrack.kick")) {
134 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("no-permission"));
135 | return true;
136 | }
137 | if (args.length < 3) {
138 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("usage-kick"));
139 | return true;
140 | }
141 |
142 | Player playerToKick = plugin.getServer().getPlayer(args[1]);
143 | if (playerToKick != null) {
144 | String reason = String.join(" ", Arrays.copyOfRange(args, 2, args.length));
145 |
146 | if (plugin.getConfigManager().isKickStrikeLightning()) {
147 | if (FoliaCheck.isFolia()) {
148 | // Use Folia's region-based scheduler to execute the task safely
149 | Bukkit.getRegionScheduler().execute(plugin, playerToKick.getLocation(), () -> {
150 | try {
151 | playerToKick.getWorld().strikeLightningEffect(playerToKick.getLocation());
152 | } catch (Exception e) {
153 | plugin.getLogger().severe("Failed to strike lightning effect on Folia: " + e.getMessage());
154 | e.printStackTrace(); // Log the full stack trace for debugging
155 | }
156 | });
157 | } else {
158 | // Non-Folia servers can execute the task directly
159 | try {
160 | playerToKick.getWorld().strikeLightningEffect(playerToKick.getLocation());
161 | } catch (Exception e) {
162 | plugin.getLogger().severe("Failed to strike lightning effect: " + e.getMessage());
163 | e.printStackTrace(); // Log the full stack trace for debugging
164 | }
165 | }
166 | }
167 |
168 | if (plugin.getLanguageManager().isKickBroadcastEnabled()) {
169 | String kickMessage = plugin.getLanguageManager().getPrefixedMessage("kick-format")
170 | .replace("%player%", playerToKick.getName())
171 | .replace("%reason%", reason);
172 | plugin.getServer().broadcastMessage(kickMessage);
173 | }
174 |
175 | plugin.getNotifier().kickPlayer(playerToKick, reason);
176 |
177 | } else {
178 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("player-not-found")
179 | .replace("{player}", args[1]));
180 | }
181 | break;
182 |
183 | case "reload":
184 | if (!sender.hasPermission("minertrack.reload")) {
185 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("no-permission"));
186 | return true;
187 | }
188 | plugin.getConfigManager().reloadConfig();
189 | plugin.getLanguageManager().loadLanguageFile();
190 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("config-reloaded"));
191 | break;
192 |
193 | case "update":
194 | if (!sender.hasPermission("minertrack.checkupdate")) {
195 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("no-permission"));
196 | return true;
197 | }
198 | plugin.checkForUpdates(sender);
199 | break;
200 |
201 | case "logs":
202 | if (!sender.hasPermission("minertrack.logs")) {
203 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("no-permission"));
204 | return true;
205 | }
206 | if (args.length == 2) {
207 | String logName = args[1];
208 | if (!logName.endsWith(".log")) {
209 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("log-viewer-not-log-file"));
210 | return true;
211 | }
212 | File logDir = new File(plugin.getDataFolder(), "logs");
213 | File logFile = new File(logDir, logName);
214 | if (!logFile.exists() || !logFile.isFile()) {
215 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("log-viewer-not-found").replace("{log_file}", logName));
216 | return true;
217 | }
218 | try {
219 | List lines = LogViewerUtils.readLogFile(logFile);
220 | List reversedLines = new ArrayList<>(lines);
221 | java.util.Collections.reverse(reversedLines);
222 | int perPage = plugin.getLanguageManager().getLogViewerLinesPerPage();
223 | int totalPages = LogViewerUtils.getTotalPages(reversedLines.size(), perPage);
224 | int page = 1;
225 | LogViewerUtils.LogCache cache = new LogViewerUtils.LogCache(logName, reversedLines, totalPages, page);
226 | LogViewerUtils.putCache(sender, cache);
227 | sendLogPage(sender, cache, perPage);
228 | } catch (IOException e) {
229 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("log-viewer-cant-read"));
230 | }
231 | return true;
232 | } else if (args.length == 3 && args[1].equalsIgnoreCase("page")) {
233 | LogViewerUtils.LogCache cache = LogViewerUtils.getCache(sender);
234 | if (cache == null) {
235 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("usage-logs"));
236 | return true;
237 | }
238 | int page;
239 | try {
240 | page = Integer.parseInt(args[2]);
241 | } catch (NumberFormatException ex) {
242 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("log-viewer-page-nan"));
243 | return true;
244 | }
245 | if (page < 1 || page > cache.totalPages) {
246 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("log-viewer-page-invalid").replace("{max_page}", String.valueOf(cache.totalPages)));
247 | return true;
248 | }
249 | cache.currentPage = page;
250 | sendLogPage(sender, cache, plugin.getLanguageManager().getLogViewerLinesPerPage());
251 | return true;
252 | }
253 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("usage-logs"));
254 | break;
255 |
256 | default:
257 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("unknown-command"));
258 | break;
259 | }
260 | return true;
261 | }
262 |
263 | private void sendLogPage(CommandSender sender, LogViewerUtils.LogCache cache, int perPage) {
264 | int totalLines = cache.lines.size();
265 | int page = cache.currentPage;
266 | int totalPages = cache.totalPages;
267 | int[] range = LogViewerUtils.getPageRange(totalLines, page, perPage);
268 | int start = range[0];
269 | int end = range[1];
270 | String header = plugin.getLanguageManager().getColoredMessage("log-viewer-header")
271 | .replace("{current_page}", String.valueOf(page))
272 | .replace("{max_page}", String.valueOf(totalPages))
273 | .replace("{log_file}", cache.logName);
274 | sender.sendMessage(header);
275 | if (start >= end) {
276 | sender.sendMessage(plugin.getLanguageManager().getPrefixedMessage("log-viewer-empty"));
277 | return;
278 | }
279 | for (int i = start; i < end; i++) {
280 | sender.sendMessage(plugin.getLanguageManager().getColoredMessage("log-viewer-logs-color") + cache.lines.get(i));
281 | }
282 |
283 | if (page < totalPages) {
284 | sender.sendMessage("");
285 | sender.sendMessage(plugin.getLanguageManager().getColoredMessage("log-viewer-next-page").replace("{next_page}", String.valueOf(page + 1)));
286 | }
287 | }
288 |
289 | @Override
290 | public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
291 | List completions = new ArrayList<>();
292 | if (args.length == 1) {
293 | completions.addAll(Arrays.asList("help", "notify", "verbose", "check", "reset", "kick", "reload", "update", "logs"));
294 | } else if (args.length == 2) {
295 | if (args[0].equalsIgnoreCase("check") || args[0].equalsIgnoreCase("reset") || args[0].equalsIgnoreCase("kick")) {
296 | for (Player player : plugin.getServer().getOnlinePlayers()) {
297 | completions.add(player.getName());
298 | }
299 | } else if (args[0].equalsIgnoreCase("logs")) {
300 | File logDir = new File(plugin.getDataFolder(), "logs");
301 | File[] files = logDir.listFiles((dir, name) -> name.endsWith(".log"));
302 | if (files != null) {
303 | Arrays.sort(files, Comparator.comparing(File::getName).reversed());
304 | int max = Math.min(10, files.length);
305 | for (int i = 0; i < max; i++) {
306 | completions.add(files[i].getName());
307 | }
308 | }
309 | }
310 | }
311 |
312 | return completions;
313 | }
314 |
315 | public void clearLogCache(CommandSender sender) {
316 | LogViewerUtils.clearCache(sender);
317 | }
318 | }
--------------------------------------------------------------------------------
/src/main/java/link/star_dust/MinerTrack/managers/ViolationManager.java:
--------------------------------------------------------------------------------
1 | /**
2 | * DON'T REMOVE THIS
3 | *
4 | * /MinerTrack/src/main/java/link/star_dust/MinerTrack/managers/ViolationManager.java
5 | *
6 | * MinerTrack Source Code - Public under GPLv3 license
7 | * Original Author: Author87668
8 | * Contributors: Author87668
9 | *
10 | * DON'T REMOVE THIS
11 | **/
12 | package link.star_dust.MinerTrack.managers;
13 |
14 | import link.star_dust.MinerTrack.FoliaCheck;
15 | import link.star_dust.MinerTrack.MinerTrack;
16 | import link.star_dust.MinerTrack.listeners.MiningListener;
17 | import link.star_dust.MinerTrack.hooks.DiscordWebHook;
18 | import link.star_dust.MinerTrack.hooks.CustomJsonWebHook;
19 |
20 | import org.bukkit.Bukkit;
21 | import org.bukkit.Location;
22 | import org.bukkit.entity.Player;
23 | import org.bukkit.plugin.Plugin;
24 |
25 | import java.io.File;
26 | import java.io.FileWriter;
27 | import java.io.IOException;
28 | import java.text.SimpleDateFormat;
29 | import java.time.LocalDate;
30 | import java.time.LocalDateTime;
31 | import java.time.format.DateTimeFormatter;
32 | import java.time.temporal.ChronoField;
33 | import java.util.ArrayList;
34 | import java.util.HashMap;
35 | import java.util.HashSet;
36 | import java.util.List;
37 | import java.util.Locale;
38 | import java.util.Map;
39 | import java.util.UUID;
40 | import org.bukkit.scheduler.BukkitRunnable;
41 | import org.bukkit.scheduler.BukkitTask;
42 | import java.util.function.Consumer;
43 |
44 | import io.papermc.paper.threadedregions.scheduler.RegionScheduler;
45 | import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
46 |
47 | public class ViolationManager {
48 | private final MinerTrack plugin;
49 | private final static Map violationLevels = new HashMap<>();
50 | private final Map vlZeroTimestamp = new HashMap<>();
51 | private final Map vlChangedTimestamp = new HashMap<>();
52 | private final Map vlDecayTasks = new HashMap<>();
53 |
54 | private String currentLogFileName;
55 |
56 | public ViolationManager(MinerTrack plugin) {
57 | this.plugin = plugin;
58 | this.currentLogFileName = generateLogFileName();
59 | int interval = 20 * 60; // Scheduling interval (unit: tick)
60 |
61 | if (FoliaCheck.isFolia()) {
62 | try {
63 | Class> schedulerClass = Class.forName("org.bukkit.Bukkit");
64 | Object scheduler = schedulerClass.getMethod("getGlobalRegionScheduler").invoke(null);
65 | scheduler.getClass().getMethod("runAtFixedRate",
66 | Plugin.class,
67 | Class.forName("java.util.function.Consumer"),
68 | long.class,
69 | long.class
70 | ).invoke(scheduler, plugin, (Consumer