added combat logging kill, and a refined new welcome message

This commit is contained in:
dvorakmortonxd 2026-09-04 15:49:47 +02:00
commit 96fccb1041
62 changed files with 287 additions and 2 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

Binary file not shown.

View file

@ -0,0 +1,2 @@
#Fri Sep 04 09:55:07 CEST 2026
gradle.version=9.6.1

Binary file not shown.

View file

@ -0,0 +1 @@
鋸D<E98BB8>OX"<22>髀▲s蘓<73>

View file

@ -0,0 +1 @@
ÑuWiPº¢Ïnì7a¶—àut

View file

@ -0,0 +1 @@
X&¿I<C2BF>ה<EFBFBD>±@במ¦gK<67>ֱ<>ֵQ©L%

View file

@ -0,0 +1 @@
®=Γps_οΔ‡μ²;m«KRας

View file

@ -0,0 +1 @@
04874f85-d75a-4b9f-91ec-9f227f2a092¹

View file

@ -0,0 +1 @@
341fbea8-17c8-4ff9-abb1-8cf0a8d07b3ś

Binary file not shown.

View file

@ -0,0 +1 @@
4ad0b03c-7d47-4056-990b-3a93c3db11fá

BIN
.gradle/file-system.probe Normal file

Binary file not shown.

View file

8
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

5
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="KubernetesApiProvider"><![CDATA[{}]]></component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View file

@ -6,6 +6,7 @@ A quality-of-life SMP plugin for Paper/Folia.
- **Join messages** — friendly welcome messages, including a first-join message that lists the server's plugins, and a special announcement when the owner logs in.
- **`/nopvp`** — toggle no-PvP mode. While active you cannot damage other players and other players cannot damage you (direct hits and projectiles). While enabled you also glow so everyone can see you're protected. Turning no-PvP *off* starts a **1-hour cooldown** before you can enable it again, and the state persists across restarts so relogging can't bypass it.
- **Anti-combat-logging** — whenever you hit another player or get hit by one, you're tagged as being in combat for **15 seconds** with a chat warning. Logging off while tagged kills you on the spot and announces it to the server.
## Commands
@ -27,9 +28,11 @@ Player no-PvP state and cooldown timestamps are stored in `plugins/qol-tweaks/no
## Building
```bash
./gradlew build
./build.sh
```
This just runs `./gradlew build`, but if the pinned Java toolchain isn't installed it will automatically fall back to whatever JDK you do have (keeping the jar's bytecode compatible). You can also pass extra Gradle args, e.g. `./build.sh clean build`.
The plugin jar is output to `build/libs/`.
## License

101
build.sh Executable file
View file

@ -0,0 +1,101 @@
#!/usr/bin/env bash
#
# Builds the qol-tweaks plugin jar.
#
# The build pins a Java toolchain version in build.gradle.kts. This script
# finds that version on your machine, and if it isn't installed it transparently
# falls back to the highest JDK that is (so `./build.sh` just works).
#
# Usage: ./build.sh [extra gradle args...]
set -euo pipefail
cd "$(dirname "$0")"
# --- 1. which Java version does the build pin? --------------------------------
pinned="$(grep -oE 'JavaLanguageVersion\.of\([0-9]+\)' build.gradle.kts | grep -oE '[0-9]+' | head -1 || true)"
pinned="${pinned:-25}"
# --- 2. find an installed JDK for a given version ------------------------------
java_major() { # $1 = path to java binary; echoes its major version (e.g. 26)
"$1" -version 2>&1 | grep -oE '"([0-9]+)' | head -1 | tr -d '"'
}
find_java_home() { # $1 = version (e.g. 25); echoes a home whose java matches, or fails
local -a candidates=()
if [[ -x /usr/libexec/java_home ]]; then
local mac_home
if mac_home="$(/usr/libexec/java_home -v "$1" 2>/dev/null)"; then
candidates+=("$mac_home")
fi
fi
[[ -n "${JAVA_HOME:-}" ]] && candidates+=("$JAVA_HOME")
local home bin ver
for home in "${candidates[@]}"; do
bin="$home/bin/java"
[[ -x "$bin" ]] || continue
ver="$(java_major "$bin")"
if [[ -n "$ver" && "$ver" == "$1" ]]; then
echo "$home"
return 0
fi
done
return 1
}
# --- 3. pick the JDK to build with --------------------------------------------
selected=""
selected_home=""
for v in "$pinned" 26 25 24 23 22 21 20 17; do
if h="$(find_java_home "$v")"; then
selected="$v"
selected_home="$h"
break
fi
done
if [[ -z "$selected" ]]; then
echo "error: no JDK found. Install any JDK 17+ (e.g. Java 25) and try again." >&2
exit 1
fi
export JAVA_HOME="$selected_home"
export PATH="$JAVA_HOME/bin:$PATH"
gradle_args=()
override_init=""
if [[ "$selected" != "$pinned" ]]; then
echo "note: pinned JDK $pinned not found; building with installed JDK $selected."
override_init="${TMPDIR:-/tmp}/qol-tweaks-toolchain-$pinned-$selected.gradle"
cat > "$override_init" <<EOF
gradle.afterProject { p ->
if (p.plugins.hasPlugin('java')) {
p.extensions.findByName('java').toolchain.languageVersion.set(JavaLanguageVersion.of($selected))
p.tasks.withType(JavaCompile).configureEach {
options.release.set($pinned)
}
}
}
EOF
gradle_args+=(-I "$override_init")
# the init script is external input, so a cached configuration would be stale
gradle_args+=(--no-configuration-cache)
fi
trap '[[ -n "$override_init" ]] && rm -f "$override_init"' EXIT
if [[ $# -eq 0 ]]; then
gradle_args+=(build)
else
gradle_args+=("$@")
fi
echo "Using JDK $selected at $JAVA_HOME"
./gradlew "${gradle_args[@]}"
jar="$(ls -1 build/libs/*.jar 2>/dev/null | grep -Ev '(-sources|-javadoc)\.jar$' | head -1 || true)"
if [[ -n "$jar" ]]; then
echo
echo "Done. Plugin jar: $(pwd)/$jar"
fi

View file

@ -1,5 +1,7 @@
package com.dvorakmortonxd.qoltweaks;
import com.dvorakmortonxd.qoltweaks.combatlog.CombatLogListener;
import com.dvorakmortonxd.qoltweaks.combatlog.CombatLogManager;
import com.dvorakmortonxd.qoltweaks.listeners.JoinListener;
import com.dvorakmortonxd.qoltweaks.nopvp.NoPvPCommand;
import com.dvorakmortonxd.qoltweaks.nopvp.NoPvPListener;
@ -10,14 +12,17 @@ import org.bukkit.plugin.java.JavaPlugin;
public final class QoLtweaks extends JavaPlugin {
private NoPvPManager noPvPManager;
private CombatLogManager combatLogManager;
@Override
public void onEnable() {
// Plugin startup logic
noPvPManager = new NoPvPManager(this);
combatLogManager = new CombatLogManager(this);
getServer().getPluginManager().registerEvents(new JoinListener(), this);
getServer().getPluginManager().registerEvents(new NoPvPListener(noPvPManager), this);
getServer().getPluginManager().registerEvents(new CombatLogListener(this, combatLogManager), this);
PluginCommand noPvPCommand = getCommand("nopvp");
if (noPvPCommand != null) {
@ -31,5 +36,8 @@ public final class QoLtweaks extends JavaPlugin {
if (noPvPManager != null) {
noPvPManager.save();
}
if (combatLogManager != null) {
combatLogManager.cancelNotices();
}
}
}

View file

@ -0,0 +1,63 @@
package com.dvorakmortonxd.qoltweaks.combatlog;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.java.JavaPlugin;
public final class CombatLogListener implements Listener {
private final JavaPlugin plugin;
private final CombatLogManager manager;
public CombatLogListener(JavaPlugin plugin, CombatLogManager manager) {
this.plugin = plugin;
this.manager = manager;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
if (!(event.getEntity() instanceof Player victim)) {
return;
}
Player attacker = resolveAttacker(event.getDamager());
if (attacker == null) {
return;
}
if (attacker.getUniqueId().equals(victim.getUniqueId())) {
return;
}
manager.tag(victim, true);
manager.tag(attacker, false);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
if (!plugin.isEnabled() || player.isDead() || !manager.isTagged(player)) {
return;
}
player.setHealth(0.0);
plugin.getServer().getOnlinePlayers().forEach(other ->
other.sendMessage("§c" + player.getName() + " combat-logged and died!"));
}
private Player resolveAttacker(Entity damager) {
if (damager instanceof Player player) {
return player;
}
if (damager instanceof Projectile projectile && projectile.getShooter() instanceof Player player) {
return player;
}
return null;
}
}

View file

@ -0,0 +1,70 @@
package com.dvorakmortonxd.qoltweaks.combatlog;
import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class CombatLogManager {
public static final long TAG_MILLIS = 15 * 1000L; // 15 seconds
private static final String VICTIM_MESSAGE = "§cYou got hit! Don't log off for 15 seconds or you'll insta-die.";
private static final String ATTACKER_MESSAGE = "§cYou hit a player! Don't log off for 15 seconds or you'll insta-die.";
private static final String SAFE_MESSAGE = "§aYou are no longer in combat.";
private final JavaPlugin plugin;
private final Map<UUID, Long> taggedUntil = new ConcurrentHashMap<>();
private final Map<UUID, ScheduledTask> notices = new ConcurrentHashMap<>();
public CombatLogManager(JavaPlugin plugin) {
this.plugin = plugin;
}
public void tag(Player player, boolean victim) {
UUID uuid = player.getUniqueId();
boolean firstTag = !isTagged(player);
taggedUntil.put(uuid, System.currentTimeMillis() + TAG_MILLIS);
if (firstTag) {
player.sendMessage(victim ? VICTIM_MESSAGE : ATTACKER_MESSAGE);
}
scheduleNotice(player);
}
public boolean isTagged(Player player) {
return getRemainingMillis(player) > 0;
}
public long getRemainingMillis(Player player) {
long until = taggedUntil.getOrDefault(player.getUniqueId(), 0L);
return Math.max(0L, until - System.currentTimeMillis());
}
private void scheduleNotice(Player player) {
UUID uuid = player.getUniqueId();
ScheduledTask previous = notices.remove(uuid);
if (previous != null) {
previous.cancel();
}
long delayTicks = (TAG_MILLIS / 1000L) * 20L;
ScheduledTask task = plugin.getServer().getGlobalRegionScheduler()
.runDelayed(plugin, scheduled -> {
notices.remove(uuid);
if (player.isOnline() && !isTagged(player)) {
player.sendMessage(SAFE_MESSAGE);
}
}, delayTicks);
notices.put(uuid, task);
}
public void cancelNotices() {
for (ScheduledTask task : notices.values()) {
task.cancel();
}
notices.clear();
}
}

View file

@ -10,8 +10,20 @@ public class JoinListener implements Listener {
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
player.sendTitle("§aWelcome to the SMP", "§7on a new world since 4th September 2026", 10, 70, 20);
if (!player.hasPlayedBefore()) {
player.sendMessage("§aWelcome to the SMP, " + player.getName() + "!\nThis is your first time here, so let me tell you whats on here, theres a 3d map of the server at https://bluemap.dvorakmortonxd.dev , theres a voicechat you can optionally install as a mod (Simple Voice Chat), theres a tree chopper plugin you can toggle with /atc, theres veinminer which you can also toggle with /veinminer toggle, theres tradecycle plugin which allows you to cycle thru villagers trades by clicking [F] in any inventory slot while trading and there is ClickVillagers which allows you to pick up villagers into your inventory when shifting and right clicking,\n thats about it, enjoy your game! :3");
player.sendMessage("§aWelcome to the SMP, " + player.getName() + "!");
player.sendMessage("This is your first time here, so let me tell you whats on here:");
player.sendMessage("§7- a 3d map of the server at https://bluemap.dvorakmortonxd.dev");
player.sendMessage("§7- a voicechat you can optionally install as a mod (Simple Voice Chat)");
player.sendMessage("§7- a tree chopper plugin you can toggle with /atc");
player.sendMessage("§7- veinminer, which you can also toggle with /veinminer toggle");
player.sendMessage("§7- tradecycle, which lets you cycle thru villagers trades by pressing [F] in any inventory slot while trading");
player.sendMessage("§7- ClickVillagers, which lets you pick up villagers into your inventory by shift-right-clicking");
player.sendMessage("§7- a /nopvp command to toggle no-pvp mode if you want to build in peace");
player.sendMessage("§cAbuse of /nopvp is bannable. Also, combat logging is NOT a good idea.");
player.sendMessage("§aThats about it, enjoy your game! :3");
event.setJoinMessage("§e" + player.getName() + " has joined the SMP for the first time!");
} else {
if (player.getName().equals("dvorakmortonxd")) {