27 Commits

Author SHA1 Message Date
c009654c5c fix spaces
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 47s
2026-07-22 14:38:35 +00:00
ab2c9bf1d6 Add default value to jdbc url (#11)
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 40s
Co-authored-by: Ardakaz <ardakaz@duck.com>
Reviewed-on: #11
Reviewed-by: Ardakaz <ardakaz@duck.com>
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-07-18 18:25:28 +00:00
7aec608825 add new db tables (#13)
Some checks failed
Build / Explore-Gitea-Actions (push) Has been cancelled
Co-authored-by: ThePindabaas <219561634+ThePindabaas@users.noreply.github.com>
Co-authored-by: Ardakaz <ardakaz@duck.com>
Reviewed-on: #13
Reviewed-by: Ardakaz <ardakaz@duck.com>
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-07-18 18:24:26 +00:00
e980768210 Fix lack of space after animal type (#12)
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 44s
Reviewed-on: #12
Reviewed-by: Ardakaz <ardakaz@duck.com>
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-07-15 12:13:12 +00:00
5064ac5903 Restrict block break if condition (#10)
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 51s
Co-authored-by: ThePindabaas <219561634+ThePindabaas@users.noreply.github.com>
Reviewed-on: #10
Reviewed-by: Ardakaz <ardakaz@duck.com>
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-07-13 15:12:00 +00:00
55881630bc Set up parameterized jdbc connection (#8)
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 42s
Co-authored-by: ThePindabaas <219561634+ThePindabaas@users.noreply.github.com>
Reviewed-on: #8
Reviewed-by: Ardakaz <ardakaz@duck.com>
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-07-03 11:07:53 +00:00
Ardakaz
4cd24ad467 Just organizing a bit and adding comments
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 45s
2026-06-29 10:52:24 +03:00
db3649a2f5 add newline at the end of files (#5)
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 44s
Co-authored-by: ThePindabaas <219561634+ThePindabaas@users.noreply.github.com>
Reviewed-on: #5
Reviewed-by: Ardakaz <ardakaz@duck.com>
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-06-28 11:49:23 +00:00
fec5f78cbc Introduce build pipeline (#4)
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 44s
Co-authored-by: ThePindabaas <219561634+ThePindabaas@users.noreply.github.com>
Reviewed-on: #4
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-06-28 11:23:50 +00:00
7d3363812f Merge pull request 'changed Armorstand Logic' (#3) from armorstand-DeathLogic into main
Reviewed-on: #3
2026-06-27 14:38:18 +00:00
urtypicalblahajlunatic
1329b616f5 debugged and fixed EntityDeathEvent 2026-06-27 16:21:36 +02:00
urtypicalblahajlunatic
4e65e1bcb2 armorstand changed from EntityHitEvent to EntityDeathEvent, SHOULD cause less fake errors 2026-06-27 14:29:49 +02:00
c89d885fd8 Update src/main/java/net/ardakaz/griefalert/GriefAlert.java
2nd try to frickign update this BS
(also removed cancel stuff)
2026-06-27 10:55:30 +00:00
a61436effa Update src/main/java/net/ardakaz/griefalert/GriefAlert.java
removed some unneeded cancel events
2026-06-27 10:48:19 +00:00
ThePindabaas
5913a8060a bump co version + change test to always succeed 2026-06-27 11:45:36 +02:00
48fd811f2a Merge pull request 'add-gradle project' (#1) from add-gradle into main
Reviewed-on: #1
2026-06-27 09:33:29 +00:00
ThePindabaas
4b4063701f add placeholder test file 2026-06-27 11:18:40 +02:00
ThePindabaas
7a971f8188 move yml files under main resources 2026-06-27 11:18:31 +02:00
ThePindabaas
04f894a2f7 move java files to main/java 2026-06-27 11:17:55 +02:00
ThePindabaas
79ab10e9ce setup new gralde project 2026-06-27 11:17:06 +02:00
ThePindabaas
ebf5321604 add dev notes to readme 2026-06-27 11:14:44 +02:00
ThePindabaas
43c49c06ee remove old stuff 2026-06-27 11:14:27 +02:00
ThePindabaas
99d67631ec add gitignore 2026-06-27 11:14:09 +02:00
Ardakaz
8e28ba0b87 Cleaning up 2026-05-19 15:49:03 +03:00
f59c484d6b Upload files to "src/net/ardakaz/griefalert"
added copper block waxing and unwaxing and dusting off logic, (might need a little extra rework because changing its state is considered "replacing" which sorta breaks the logic)

added log stripping detection (same issue as copper blocks)
2026-04-30 09:03:51 +00:00
c3c2113619 Doing some cleaning 2025-11-06 17:37:52 +02:00
da586e55fd Update README.md 2025-08-25 18:54:07 +00:00
19 changed files with 943 additions and 254 deletions

12
.gitattributes vendored Normal file
View File

@@ -0,0 +1,12 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf
# These are Windows script files and should use crlf
*.bat text eol=crlf
# Binary files should be left untouched
*.jar binary

View File

@@ -0,0 +1,16 @@
name: Build
run-name: ${{ gitea.ref }} - ${{ gitea.sha }}
on: [push, workflow_dispatch]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
- run: ./gradlew assemble
- run: ./gradlew test
- run: ./gradlew jar

19
.gitea/workflows/demo.yml Normal file
View File

@@ -0,0 +1,19 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [workflow_dispatch]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.idea
.vscode
.gradle
build
*.iml

View File

@@ -1,3 +1,43 @@
# GriefAlert # GriefAlert
A simple grief alert plugin using CoreProtect API. GriefAlert a simple plugin that alerts the staff when someone is griefing on the server.
**CoreProtect is required for this plugin to work.** DiscordSRV is recommended.
## Features
* Alerts the staff when players break blocks, steal items or kill mobs in other players' builds.
* Alerts go to the chat of ingame staff and optionally Discord.
* Players and coordinates can be excluded from alerts.
## Permissions
* `griefalert.notify` - Gives an ability to view alerts in the chat.
* `griefalert.exclude` - Completely excludes a player from triggering alerts.
* `griefalert.exclude.<player>` - Excludes a player from triggering alerts when touching the specified player's stuff.
* `griefalert.staff` - Ability to use staff commands.
## Commands
* `/griefalert ignore [world] <x> <y> <z>` - Ignores a location from alerts.
* `/griefalert unignore [world] <x> <y> <z>`
* `/griefalert check [world] <x> <y> <z>` - Tells if a location is ignored.
## Discord support
If you have DiscordSRV installed, you can add this to it's alerts.yml:
```yaml
- Trigger: net.ardakaz.griefalert.GriefAlertEvent
Channel: grief-alerts
Content: "${getAlert()}"
```
You'll also have to specify the grief-alerts channel in config.yml.
# Development
* Use `./gradlew clean` to clean the project
* Use `./gradlew assemble` to compile the project
* Use `./gradlew test` to test the project
* Use `./gradlew jar` to create the jar
* Use `./gradlew build` to compile, test and create the jar

43
build.gradle Normal file
View File

@@ -0,0 +1,43 @@
plugins{
id "java"
}
group = "net.ardakaz.griefalert"
version = "0.6"
repositories{
mavenCentral()
maven{
name = "papermc"
url = "https://repo.papermc.io/repository/maven-public/"
}
maven {
name = "core-protect"
url = "https://maven.playpro.com/"
}
}
dependencies {
compileOnly "io.papermc.paper:paper-api:26.1.2.build.72-stable"
compileOnly "net.coreprotect:coreprotect:23.2"
testImplementation(platform('org.junit:junit-bom:5.10.5'))
testImplementation('org.junit.jupiter:junit-jupiter')
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
}
java{
toolchain{
languageVersion = JavaLanguageVersion.of(25)
}
}
tasks.processResources {
filteringCharset = "UTF-8"
}
test {
useJUnitPlatform()
}

7
gradle.properties Normal file
View File

@@ -0,0 +1,7 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties
org.gradle.configuration-cache=true
org.gradle.parallel=true
org.gradle.caching=true

View File

@@ -0,0 +1,2 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

248
gradlew vendored Executable file
View File

@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

93
gradlew.bat vendored Executable file
View File

@@ -0,0 +1,93 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'GriefAlert'

View File

@@ -3,7 +3,6 @@ package net.ardakaz.griefalert;
import net.coreprotect.CoreProtect; import net.coreprotect.CoreProtect;
import net.coreprotect.CoreProtectAPI; import net.coreprotect.CoreProtectAPI;
import net.coreprotect.CoreProtectAPI.ParseResult; import net.coreprotect.CoreProtectAPI.ParseResult;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.ChatColor; import org.bukkit.ChatColor;
import org.bukkit.Location; import org.bukkit.Location;
@@ -11,57 +10,147 @@ import org.bukkit.Material;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter; import org.bukkit.entity.*;
import org.bukkit.entity.AnimalTamer;
import org.bukkit.entity.Cat;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Tameable;
import org.bukkit.entity.Wolf;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.hanging.HangingBreakByEntityEvent; import org.bukkit.event.hanging.HangingBreakByEntityEvent;
import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.PlayerInteractAtEntityEvent; import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.Inventory; import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.entity.ArmorStand;
import java.sql.Connection; import java.sql.*;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.entity.ItemFrame;
public class GriefAlert extends JavaPlugin implements Listener {
public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
private static CoreProtectAPI coreProtectAPI; private static CoreProtectAPI coreProtectAPI;
private Integer identicalAlerts = 1; private Integer identicalAlerts = 1;
private String lastAlert; private String lastAlert;
private Set<Material> EXCLUDED_BLOCKS; private Set<Material> EXCLUDED_BLOCKS;
private Set<InventoryType> VALID_CONTAINERS; private Set<InventoryType> VALID_CONTAINERS;
private String MAP_LINK; private String MAP_LINK;
private Boolean ALLOW_STEALING; private Boolean ALLOW_STEALING;
private Connection connection; private Connection connection;
private final String DB_FILE = "ignored_locations.db";
/*
UTILITY FUNCTIONS
*/
// Block inspector: only the most recent placement counts for ownership.
private static String inspectBlock(Block block, Player player) {
List<String[]> lookup = coreProtectAPI.blockLookup(block, 50000000);
if (lookup == null || lookup.isEmpty()) {
// Natural block
return null;
}
// Find the most recent placement event only
for (String[] result : lookup) {
ParseResult parseResult = coreProtectAPI.parseResult(result);
if (parseResult == null) continue;
if (parseResult.getActionId() == 1 && !parseResult.isRolledBack() && !parseResult.getPlayer().startsWith("#")) {
// If the current player placed it, it's theirs (no alert)
if (parseResult.getPlayer().equals(player.getName())) {
return null;
} else {
return parseResult.getPlayer();
}
}
// If we see a break before a placement, stop (block is gone)
if (parseResult.getActionId() == 0 && !parseResult.isRolledBack()) {
break;
}
}
// No valid placement found
return null;
}
private static String getHumanWorldName(String worldName) {
String world = "";
if (worldName.endsWith("_nether")) {
world = " in the Nether";
} else if (worldName.endsWith("_the_end")) {
world = " in the End";
}
return world;
}
// Sends the alert (or cancels it)
private void alert(String message, String playerName, String mapLink, String target, int x, int y, int z, String world) {
// Exclude trusted people
Player griefer = Bukkit.getPlayer(playerName);
if (griefer.hasPermission("griefalert.exclude") || griefer.hasPermission("griefalert.exclude." + target)) {
return;
}
// Spam limiter
String realAlertMessage = message;
String[] alert1 = null;
if (lastAlert != null) {
alert1 = lastAlert.split(" ");
}
String[] alert2 = message.split(" ");
if (alert1 != null) {
if (alert1[2].equals(alert2[2]) && alert1[5].equals(alert2[5]) && alert1[1].equals("broke") && alert2[1].equals("broke")) {
identicalAlerts += 1;
} else if (Arrays.equals(alert1, alert2)) {
identicalAlerts += 1;
} else {
identicalAlerts = 1;
}
}
if (identicalAlerts == 4) {
message = ChatColor.GRAY + "Same behavior continues.";
mapLink = null;
}
if (identicalAlerts > 4) {
return;
}
// Use direct location check
if (world != null && isLocationIgnored(x, y, z, world)) {
return; // Do not alert if location is ignored
}
// Send an event for external hooks
GriefAlertEvent griefalert_event;
if (MAP_LINK != null && !MAP_LINK.isEmpty() && mapLink != null) {
griefalert_event = new GriefAlertEvent(message + " (" + mapLink + ")");
} else {
griefalert_event = new GriefAlertEvent(message);
}
getServer().getPluginManager().callEvent(griefalert_event);
// Notify staff ingame
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasPermission("griefalert.notify")) {
player.sendMessage(message);
}
}
lastAlert = realAlertMessage;
}
/*
INITIALIZATION
*/
// Init GriefAlert // Init GriefAlert
@Override @Override
@@ -96,21 +185,34 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
public void onDisable() { public void onDisable() {
getLogger().info("GriefAlert has been disabled."); getLogger().info("GriefAlert has been disabled.");
if (connection != null) { if (connection != null) {
try { connection.close(); } catch (SQLException ignored) {} try {
connection.close();
} catch (SQLException ignored) {
}
} }
} }
private void setupDatabase() { private void setupDatabase() {
String jdbcUrl = getConfig().getString("jdbc-url");
try { try {
connection = DriverManager.getConnection("jdbc:sqlite:" + getDataFolder().getAbsolutePath() + "/" + DB_FILE); connection = DriverManager.getConnection(jdbcUrl);
Statement stmt = connection.createStatement(); Statement stmt = connection.createStatement();
stmt.executeUpdate("CREATE TABLE IF NOT EXISTS ignored_locations (x INTEGER, y INTEGER, z INTEGER, world TEXT, PRIMARY KEY (x, y, z, world))"); stmt.executeUpdate(Migrations.CREATE_TABLE_IGNORED_LOCATIONS);
stmt.executeUpdate(Migrations.CREATE_TABLE_PLAYERS);
stmt.executeUpdate(Migrations.CREATE_TABLE_ACTIONS);
stmt.executeUpdate(Migrations.CREATE_TABLE_ALERTS);
stmt.executeUpdate(Migrations.CREATE_TABLE_REPORTS);
stmt.close(); stmt.close();
getLogger().info("Connected to "+jdbcUrl);
} catch (SQLException e) { } catch (SQLException e) {
getLogger().severe("Could not set up SQLite database: " + e.getMessage()); getLogger().severe("Could not set up SQLite database: " + e.getMessage());
} }
} }
/*
LOCATION IGNORING
*/
private boolean isLocationIgnored(int x, int y, int z, String world) { private boolean isLocationIgnored(int x, int y, int z, String world) {
try { try {
PreparedStatement ps = connection.prepareStatement("SELECT 1 FROM ignored_locations WHERE x=? AND y=? AND z=? AND world=?"); PreparedStatement ps = connection.prepareStatement("SELECT 1 FROM ignored_locations WHERE x=? AND y=? AND z=? AND world=?");
@@ -161,6 +263,10 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
} }
} }
/*
EVENT LISTENERS
*/
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
// Block break alerts // Block break alerts
public void onBlockBreak(BlockBreakEvent event) { public void onBlockBreak(BlockBreakEvent event) {
@@ -194,8 +300,7 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
boolean stealing; boolean stealing;
// Event parser for inv // Event parser for inv
if (!(event.getWhoClicked() instanceof Player)) return; if (!(event.getWhoClicked() instanceof Player player)) return;
Player player = (Player) event.getWhoClicked();
Inventory inventory = event.getInventory(); Inventory inventory = event.getInventory();
Inventory clickedInventory = event.getClickedInventory(); Inventory clickedInventory = event.getClickedInventory();
ItemStack item = event.getCurrentItem(); ItemStack item = event.getCurrentItem();
@@ -215,8 +320,7 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
action == InventoryAction.PICKUP_ONE || action == InventoryAction.PICKUP_SOME || action == InventoryAction.PICKUP_ONE || action == InventoryAction.PICKUP_SOME ||
action == InventoryAction.MOVE_TO_OTHER_INVENTORY) && clickedInventory == inventory) { action == InventoryAction.MOVE_TO_OTHER_INVENTORY) && clickedInventory == inventory) {
stealing = true; stealing = true;
} else if (action == InventoryAction.PLACE_ALL || action == InventoryAction.PLACE_SOME || } else if (action == InventoryAction.PLACE_ALL || action == InventoryAction.PLACE_SOME || action == InventoryAction.PLACE_ONE || action == InventoryAction.MOVE_TO_OTHER_INVENTORY) {
action == InventoryAction.PLACE_ONE || (action == InventoryAction.MOVE_TO_OTHER_INVENTORY && clickedInventory != inventory)) {
stealing = false; stealing = false;
} else { } else {
return; return;
@@ -244,17 +348,14 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
// Armor Stand break alerts // Armor Stand break alerts
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void onArmorStandBreak(EntityDamageByEntityEvent event) { public void onArmorStandBreak(EntityDeathEvent event) {
if (!(event.getEntity() instanceof ArmorStand)) return; if (!(event.getEntity() instanceof ArmorStand armorStand)) return;
if (!(event.getDamager() instanceof Player)) return; if(!(event.getDamageSource().getCausingEntity() instanceof Player player)) return;
Player player = (Player) event.getDamager();
ArmorStand armorStand = (ArmorStand) event.getEntity();
int x = armorStand.getLocation().getBlockX(); int x = armorStand.getLocation().getBlockX();
int y = armorStand.getLocation().getBlockY(); int y = armorStand.getLocation().getBlockY();
int z = armorStand.getLocation().getBlockZ(); int z = armorStand.getLocation().getBlockZ();
String worldName = armorStand.getWorld().getName(); String worldName = armorStand.getWorld().getName();
if (isLocationIgnored(x, y, z, worldName)) { if (isLocationIgnored(x, y, z, worldName)) {
event.setCancelled(true);
return; return;
} }
String playerName = player.getName(); String playerName = player.getName();
@@ -263,20 +364,19 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
String message = ChatColor.GRAY + playerName + " broke an armor stand placed by " + target + " at " + x + " " + y + " " + z + getHumanWorldName(worldName); String message = ChatColor.GRAY + playerName + " broke an armor stand placed by " + target + " at " + x + " " + y + " " + z + getHumanWorldName(worldName);
alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName); alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName);
} }
} }
// Armor Stand item interaction alerts // Armor Stand item interaction alerts
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void onArmorStandInteract(PlayerInteractAtEntityEvent event) { public void onArmorStandInteract(PlayerInteractAtEntityEvent event) {
if (!(event.getRightClicked() instanceof ArmorStand)) return; if (!(event.getRightClicked() instanceof ArmorStand armorStand)) return;
Player player = event.getPlayer(); Player player = event.getPlayer();
ArmorStand armorStand = (ArmorStand) event.getRightClicked();
int x = armorStand.getLocation().getBlockX(); int x = armorStand.getLocation().getBlockX();
int y = armorStand.getLocation().getBlockY(); int y = armorStand.getLocation().getBlockY();
int z = armorStand.getLocation().getBlockZ(); int z = armorStand.getLocation().getBlockZ();
String worldName = armorStand.getWorld().getName(); String worldName = armorStand.getWorld().getName();
if (isLocationIgnored(x, y, z, worldName)) { if (isLocationIgnored(x, y, z, worldName)) {
event.setCancelled(true);
return; return;
} }
String target = inspectBlock(armorStand.getLocation().getBlock(), player); String target = inspectBlock(armorStand.getLocation().getBlock(), player);
@@ -287,47 +387,42 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
//org.bukkit.inventory.EquipmentSlot slot = event.getHand(); //org.bukkit.inventory.EquipmentSlot slot = event.getHand();
EntityEquipment equipment = armorStand.getEquipment(); EntityEquipment equipment = armorStand.getEquipment();
ItemStack armorItem = null; ItemStack armorItem = null;
if (event.getClickedPosition() != null) { event.getClickedPosition();// Approximate slot by Y position (not perfect, but Bukkit API is limited so it is what it is :3)
// Approximate slot by Y position (not perfect, but Bukkit API is limited so it is what it is :3)
double yPos = event.getClickedPosition().getY(); double yPos = event.getClickedPosition().getY();
if (yPos > 1.6) armorItem = equipment.getHelmet(); if (yPos > 1.6) armorItem = equipment.getHelmet();
else if (yPos > 1.2) armorItem = equipment.getChestplate(); else if (yPos > 1.2) armorItem = equipment.getChestplate();
else if (yPos > 0.8) armorItem = equipment.getLeggings(); else if (yPos > 0.8) armorItem = equipment.getLeggings();
else if (yPos > 0.1) armorItem = equipment.getBoots(); else if (yPos > 0.1) armorItem = equipment.getBoots();
else armorItem = equipment.getItemInMainHand(); else armorItem = equipment.getItemInMainHand();
} else { boolean handEmpty = handItem.getType() == Material.AIR;
armorItem = equipment.getItemInMainHand();
}
boolean handEmpty = handItem == null || handItem.getType() == Material.AIR;
boolean armorEmpty = armorItem == null || armorItem.getType() == Material.AIR; boolean armorEmpty = armorItem == null || armorItem.getType() == Material.AIR;
String action; String action;
//this works at the instance of interaction before items get moved around //this works at the instance of interaction before items get moved around
if (handEmpty && !armorEmpty) { if (handEmpty && !armorEmpty) {
action = "took " + armorItem.getType().toString(); action = "took " + armorItem.getType();
} else if (!handEmpty && armorEmpty) { } else if (!handEmpty && armorEmpty) {
action = "added " + handItem.getType().toString(); action = "added " + handItem.getType();
} else if (!handEmpty && !armorEmpty) { } else if (!handEmpty) {
action = "swapped " + armorItem.getType().toString() + " with " + handItem.getType().toString(); action = "swapped " + armorItem.getType() + " with " + handItem.getType();
} else { } else {
action = "interacted with"; action = "interacted with";
} }
String message = ChatColor.GRAY + playerName + " " + action + " on an armor stand owned by " + target + " at " + x + " " + y + " " + z + getHumanWorldName(worldName); String message = ChatColor.GRAY + playerName + " " + action + " on an armor stand placed by " + target + " at " + x + " " + y + " " + z + getHumanWorldName(worldName);
alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName); alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName);
} }
} }
//event handler for item frames
// Placing item to an item frame
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if(!(event.getRightClicked() instanceof ItemFrame)) return; if (!(event.getRightClicked() instanceof ItemFrame frame)) return;
Player player = event.getPlayer(); Player player = event.getPlayer();
ItemFrame frame = (ItemFrame) event.getRightClicked();
int x = frame.getLocation().getBlockX(); int x = frame.getLocation().getBlockX();
int y = frame.getLocation().getBlockY(); int y = frame.getLocation().getBlockY();
int z = frame.getLocation().getBlockZ(); int z = frame.getLocation().getBlockZ();
String worldName = frame.getWorld().getName(); String worldName = frame.getWorld().getName();
if (isLocationIgnored(x, y, z, worldName)) { if (isLocationIgnored(x, y, z, worldName)) {
event.setCancelled(true);
return; return;
} }
String target = inspectBlock(frame.getLocation().getBlock(), player); String target = inspectBlock(frame.getLocation().getBlock(), player);
@@ -341,27 +436,22 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
String action; String action;
if (!handEmpty && frameEmpty) { if (!handEmpty && frameEmpty) {
action = "added " + handItem.getType(); action = "added " + handItem.getType();
} } else {
else {
return; return;
} }
String message = ChatColor.GRAY + playerName + " " + action + " an item to an item frame owned by " + target + " at " + x + " " + y + " " + z + getHumanWorldName(worldName); String message = ChatColor.GRAY + playerName + " " + action + " an item to an item frame placed by " + target + " at " + x + " " + y + " " + z + getHumanWorldName(worldName);
alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName); alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName);
} }
} }
// Item frame stealing alerts
//breaking item frame item (uses a different event then placing an item)
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void onItemFrameDamage(EntityDamageByEntityEvent event) { public void onItemFrameDamage(EntityDamageByEntityEvent event) {
if (!(event.getEntity() instanceof ItemFrame)) return; if (!(event.getEntity() instanceof ItemFrame frame)) return;
if (!(event.getDamager() instanceof Player)) return; if (!(event.getDamager() instanceof Player player)) return;
ItemFrame frame = (ItemFrame) event.getEntity();
Player player = (Player) event.getDamager();
ItemStack item = frame.getItem(); ItemStack item = frame.getItem();
String playerName = player.getName(); String playerName = player.getName();
@@ -371,31 +461,27 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
String worldName = frame.getWorld().getName(); String worldName = frame.getWorld().getName();
if (isLocationIgnored(x, y, z, worldName)) { if (isLocationIgnored(x, y, z, worldName)) {
event.setCancelled(true);
return; return;
} }
String target = inspectBlock(frame.getLocation().getBlock(), player); String target = inspectBlock(frame.getLocation().getBlock(), player);
if (target == null || target.equals(player.getName())) return; if (target == null || target.equals(player.getName())) return;
if (item.getType() == Material.AIR) return;
if (item == null || item.getType() == Material.AIR) return;
// At this point, the player is removing the item // At this point, the player is removing the item
String action = "took " + item.getType(); String action = "took " + item.getType();
String message = ChatColor.GRAY + playerName + " " + action + " from item frame owned by " + target + " at " + x + " " + y + " " + z + getHumanWorldName(worldName); String message = ChatColor.GRAY + playerName + " " + action + " from an item frame placed by " + target + " at " + x + " " + y + " " + z + getHumanWorldName(worldName);
alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName); alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName);
} }
// Item frame breaking alerts
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void onItemFrameBreak(HangingBreakByEntityEvent event) { public void onItemFrameBreak(HangingBreakByEntityEvent event) {
if (!(event.getEntity() instanceof ItemFrame)) return; if (!(event.getEntity() instanceof ItemFrame frame)) return;
if (!(event.getRemover() instanceof Player)) return; if (!(event.getRemover() instanceof Player player)) return;
ItemFrame frame = (ItemFrame) event.getEntity();
Player player = (Player) event.getRemover();
String playerName = player.getName(); String playerName = player.getName();
Location loc = frame.getLocation(); Location loc = frame.getLocation();
int x = loc.getBlockX(); int x = loc.getBlockX();
int y = loc.getBlockY(); int y = loc.getBlockY();
@@ -403,21 +489,18 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
String worldName = loc.getWorld().getName(); String worldName = loc.getWorld().getName();
if (isLocationIgnored(x, y, z, worldName)) { if (isLocationIgnored(x, y, z, worldName)) {
event.setCancelled(true);
return; return;
} }
String target = inspectBlock(loc.getBlock(), player); String target = inspectBlock(loc.getBlock(), player);
if (target == null || target.equals(player.getName())) return; if (target == null || target.equals(player.getName())) return;
String action = "broke an item frame"; String action = "broke an item frame";
String message = ChatColor.GRAY + playerName + " " + action + " owned by " + target + " at " + x + " " + y + " " + z + getHumanWorldName(worldName); String message = ChatColor.GRAY + playerName + " " + action + " placed by " + target + " at " + x + " " + y + " " + z + getHumanWorldName(worldName);
alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName); alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName);
} }
// Command handler
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!command.getName().equalsIgnoreCase("griefalert")) return false; if (!command.getName().equalsIgnoreCase("griefalert")) return false;
@@ -425,7 +508,7 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
String sub = args[0].toLowerCase(); String sub = args[0].toLowerCase();
if (sub.equals("ignore")) { if (sub.equals("ignore")) {
if (!sender.hasPermission("griefalert.staff.ignore")) { if (!sender.hasPermission("griefalert.staff.ignore")) {
sender.sendMessage(ChatColor.RED + "You do not have permission."); sender.sendMessage(ChatColor.RED + "You do not have a permission.");
return true; return true;
} }
String world; String world;
@@ -579,85 +662,6 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
return null; return null;
} }
// Sends the alert (or cancels it)
private void alert(String message, String playerName, String mapLink, String target, int x, int y, int z, String world) {
// Exclude trusted people
Player griefer = Bukkit.getPlayer(playerName);
if (griefer.hasPermission("griefalert.exclude") || griefer.hasPermission("griefalert.exclude." + target)) {
return;
}
// Spam limiter
String realAlertMessage = message;
String[] alert1 = null;
if (lastAlert != null) {
alert1 = lastAlert.split(" ");
}
String[] alert2 = message.split(" ");
if (alert1 != null) {
if (alert1[2].equals(alert2[2]) && alert1[5].equals(alert2[5]) && alert1[1].equals("broke") && alert2[1].equals("broke")) {
identicalAlerts += 1;
} else if (Arrays.equals(alert1, alert2)) {
identicalAlerts += 1;
} else {
identicalAlerts = 1;
}
}
if (identicalAlerts == 4) {
message = ChatColor.GRAY + "Same behavior continues.";
mapLink = null;
}
if (identicalAlerts > 4) {
return;
}
// Use direct location check
if (world != null && isLocationIgnored(x, y, z, world)) {
return; // Do not alert if location is ignored
}
// Send an event for external hooks
GriefAlertEvent griefalert_event;
if (MAP_LINK != null && !MAP_LINK.isEmpty() && mapLink != null) {
griefalert_event = new GriefAlertEvent(message + " (" + mapLink + ")");
} else {
griefalert_event = new GriefAlertEvent(message);
}
getServer().getPluginManager().callEvent(griefalert_event);
// Notify staff ingame
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasPermission("griefalert.notify")) {
player.sendMessage(message);
}
}
lastAlert = realAlertMessage;
}
// Block inspector: only the most recent placement counts for ownership.
private static String inspectBlock(Block block, Player player) {
List<String[]> lookup = coreProtectAPI.blockLookup(block, 50000000);
if (lookup == null || lookup.size() == 0) {
// Natural block
return null;
}
// Find the most recent placement event only
for (String[] result : lookup) {
ParseResult parseResult = coreProtectAPI.parseResult(result);
if (parseResult == null) continue;
if (parseResult.getActionId() == 1 && !parseResult.isRolledBack() && !parseResult.getPlayer().startsWith("#")) {
// If the current player placed it, it's theirs (no alert)
if (parseResult.getPlayer().equals(player.getName())) {
return null;
} else {
return parseResult.getPlayer();
}
}
// If we see a break before a placement, stop (block is gone)
if (parseResult.getActionId() == 0) {
break;
}
}
// No valid placement found
return null;
}
//pet alert(dog and cat only) //pet alert(dog and cat only)
@EventHandler @EventHandler
public void onPetKill(EntityDeathEvent event) { public void onPetKill(EntityDeathEvent event) {
@@ -680,11 +684,12 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
String worldName = event.getEntity().getWorld().getName(); String worldName = event.getEntity().getWorld().getName();
String pet = event.getEntity().getType().name(); String pet = event.getEntity().getType().name();
String message = ChatColor.GRAY + killer.getName() + " killed" +" " + pet + " "+ "owned by " + Owner.getName() + " at " + x + " " + y + " " + z + getHumanWorldName(worldName); String message = ChatColor.GRAY + killer.getName() + " killed" + " " + pet + " in " + Owner.getName() + "'s build at " + x + " " + y + " " + z + getHumanWorldName(worldName);
alert(message, killer.getName(), "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", Owner.getName(), x, y, z, worldName); alert(message, killer.getName(), "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", Owner.getName(), x, y, z, worldName);
} }
//farm animal handler
// Mob killing alerts
@EventHandler @EventHandler
public void onFarmAnimalDeath(EntityDeathEvent event) { public void onFarmAnimalDeath(EntityDeathEvent event) {
if (!(event.getEntity().getKiller() instanceof Player killer)) return; if (!(event.getEntity().getKiller() instanceof Player killer)) return;
@@ -708,30 +713,140 @@ public class GriefAlert extends JavaPlugin implements Listener, TabCompleter {
String worldName = event.getEntity().getWorld().getName(); String worldName = event.getEntity().getWorld().getName();
String message = ChatColor.GRAY + killer.getName() + " killed" +" " + typeName + " "+ "owned by " + animalOwner + " at " + x + " " + y + " " + z + getHumanWorldName(worldName); String message = ChatColor.GRAY + killer.getName() + " killed" + " " + typeName + " in " + animalOwner + "'s build at " + x + " " + y + " " + z + getHumanWorldName(worldName);
alert(message, killer.getName(), "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", animalOwner, x, y, z, worldName); alert(message, killer.getName(), "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", animalOwner, x, y, z, worldName);
} }
private static String getHumanWorldName(String worldName) { /*code i hope for copper waxing/unwaxing theres no proper onwax() or onUnWax()
String world = ""; so im kinda gonna try to just check a situation like, if ur holding an axe, left clicking a copper block
and its waxed, ill assume ur unwaxing, and therefor sending an alert
but some of this code was written at 1am so idk how and why im why and howing */
if (worldName.endsWith("_nether")) { // Waxing/unwaxing alerts
world = " in the Nether"; @EventHandler
} public void onBlockClick(PlayerInteractEvent event) {
else if (worldName.endsWith("_the_end")) { if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
world = " in the End";
Player player = event.getPlayer();
Block block = event.getClickedBlock();
ItemStack item = event.getItem();
if (block == null || item == null) return;
Material before = block.getType();
int x = block.getX();
int y = block.getY();
int z = block.getZ();
String world = block.getWorld().getName();
if (isLocationIgnored(x, y, z, world)) {
return;
} }
return world; boolean isCopper = before.name().contains("COPPER");
//if aint copper it can no copper and means no copper affected
if (!isCopper) return;
//for waxing logic might not even be needed
//boolean isHoneycomb = item.getType() == Material.HONEYCOMB;
//for unwax logic and also might not be needed
//boolean isAxe = item.getType().toString().endsWith("_AXE");
String target = inspectBlock(block, player);
if (Objects.equals(target, player.getName())) return;
if (target == null) return;
Bukkit.getScheduler().runTaskLater(this, () -> {
String action;
Material after = block.getType();
if (before == after) return;
boolean wasWaxed = before.name().startsWith("WAXED_");
boolean isWaxed = after.name().startsWith("WAXED_");
if (wasWaxed && !isWaxed) {
action = "Unwaxed";
} else if (!wasWaxed && isWaxed) {
action = "Waxed";
} else if (item != null && item.getType().name().endsWith("_AXE")) {
action = "undusted";
} else {
action = "error 404 check with kleedje30";
} }
String message = ChatColor.GRAY + player.getName() + " " + action + " Copper" + " at " + x + " " + y + " " + z + getHumanWorldName(world);
alert(message, player.getName(), "[Map Link](" + MAP_LINK + "/?worldname=" + world + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, world);
}, 1L);
}
// Log stripping alerts
@EventHandler
public void onLogStrip(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
Player player = event.getPlayer();
Block block = event.getClickedBlock();
ItemStack item = event.getItem();
if (block == null || item == null) return;
Material before = block.getType();
int x = block.getX();
int y = block.getY();
int z = block.getZ();
String world = block.getWorld().getName();
boolean isWood = before.name().contains("LOG");
if (!isWood) return;
if (isLocationIgnored(x, y, z, world)) {
return;
}
String target = inspectBlock(block, player);
if (target == null) return;
if (target.equals(player.getName())) return;
Bukkit.getScheduler().runTaskLater(this, () -> {
String action;
Material after = block.getType();
if (before == after) return;
if (!before.name().contains("STRIPPED") && after.name().contains("STRIPPED")) {
action = "Stripped";
} else {
return;
}
String message = ChatColor.GRAY + player.getName() + action + " a(n) " + before + " at " + x + " " + y + " " + z + getHumanWorldName(world);
alert(message, player.getName(), "[Map Link](" + MAP_LINK + "/?worldname=" + world + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, world);
}, 1L);
}
/*
CoreProtect API
*/
private CoreProtectAPI getCoreProtect() { private CoreProtectAPI getCoreProtect() {
Plugin plugin = getServer().getPluginManager().getPlugin("CoreProtect"); Plugin plugin = getServer().getPluginManager().getPlugin("CoreProtect");
if (plugin == null || !(plugin instanceof CoreProtect)) { if (!(plugin instanceof CoreProtect)) {
return null; return null;
} }

View File

@@ -2,7 +2,6 @@
package net.ardakaz.griefalert; package net.ardakaz.griefalert;
import org.bukkit.Bukkit;
import org.bukkit.event.Event; import org.bukkit.event.Event;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;

View File

@@ -0,0 +1,68 @@
package net.ardakaz.griefalert;
public class Migrations {
public static final String CREATE_TABLE_IGNORED_LOCATIONS =
"CREATE TABLE IF NOT EXISTS ignored_locations (" +
"x INTEGER, " +
"y INTEGER, " +
"z INTEGER, " +
"world TEXT, " +
"PRIMARY KEY (x, y, z, world)" +
")";
public static final String CREATE_TABLE_PLAYERS =
"CREATE TABLE IF NOT EXISTS players (" +
"id TEXT PRIMARY KEY, " +
"names TEXT NOT NULL, " +
"playtime INTEGER NOT NULL DEFAULT 0, " +
"firstseen INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
"lastseen INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP" +
")";
public static final String CREATE_TABLE_ACTIONS =
"CREATE TABLE IF NOT EXISTS actions (" +
"id INTEGER PRIMARY KEY, " +
"player TEXT NOT NULL, " +
"type TEXT NOT NULL, " +
"moderator TEXT NOT NULL, " +
"reason TEXT, " +
"time TEXT, " +
"timestamp INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
"FOREIGN KEY(player) REFERENCES players(id)" +
")";
public static final String CREATE_TABLE_ALERTS =
"CREATE TABLE IF NOT EXISTS alerts (" +
"id INTEGER PRIMARY KEY, " +
"player TEXT NOT NULL, " +
"action TEXT NOT NULL, " +
"target TEXT NOT NULL, " +
"target_amount INTEGER, " +
"target_player TEXT NOT NULL, " +
"x INTEGER NOT NULL, " +
"y INTEGER NOT NULL, " +
"z INTEGER NOT NULL, " +
"world TEXT NOT NULL, " +
"is_flagged INTEGER, " +
"flag_text TEXT, " +
"resolved_by TEXT, " +
"state TEXT NOT NULL DEFAULT 'UNRESOLVED', " +
"timestamp INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
"FOREIGN KEY(player) REFERENCES players(id)" +
")";
public static final String CREATE_TABLE_REPORTS =
"CREATE TABLE IF NOT EXISTS reports (" +
"id INTEGER PRIMARY KEY, " +
"player TEXT NOT NULL, " +
"x INTEGER NOT NULL, " +
"y INTEGER NOT NULL, " +
"z INTEGER NOT NULL, " +
"world TEXT NOT NULL, " +
"text TEXT NOT NULL, " +
"resolved_by TEXT, " +
"timestamp INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
"state TEXT NOT NULL DEFAULT 'UNRESOLVED'" +
")";
}

View File

@@ -1,6 +1,8 @@
# Ignore all stealing? # Ignore all stealing?
allow-stealing: false allow-stealing: false
jdbc-url: "./plugins/GriefAlert/database.db"
# Breaking these blocks don't cause alerts. (Material) # Breaking these blocks don't cause alerts. (Material)
excluded-blocks: excluded-blocks:
- WHEAT - WHEAT

View File

@@ -1,6 +1,6 @@
name: GriefAlert name: GriefAlert
main: net.ardakaz.griefalert.GriefAlert main: net.ardakaz.griefalert.GriefAlert
version: 0.5 version: 0.6
api-version: 1.21 api-version: 1.21
depends: [CoreProtect] depends: [CoreProtect]
softdepend: [DiscordSRV] softdepend: [DiscordSRV]

View File

@@ -0,0 +1,12 @@
package net.ardakaz.griefalert;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class GriefAlertTest {
@Test
public void placeholderTest() {
assertTrue(true);
}
}