implement more config options

This commit is contained in:
2026-07-21 18:48:09 +02:00
parent f30949a269
commit 19a7187bae
8 changed files with 213 additions and 9 deletions
+5
View File
@@ -4,6 +4,11 @@ All notable changes since version 1.0.0 are documented in this file.
## Unreleased ## Unreleased
### Added
- Added separate configuration for mob execution time, executor movement, and knocked-player movement.
- Added configurable NPC help-up behavior for owned pets, friendly villagers, golems, and allowlisted modded NPCs.
- NPC helpers only begin a rescue when they have no hostile target and no living threats remain nearby.
### Fixed ### Fixed
- Improved multiplayer target selection for execute, carry, and help-up interactions so the nearest valid knocked player is selected instead of an arbitrary player. - Improved multiplayer target selection for execute, carry, and help-up interactions so the nearest valid knocked player is selected instead of an arbitrary player.
- Added same-dimension and range validation for ongoing player execution and help-up interactions. - Added same-dimension and range validation for ongoing player execution and help-up interactions.
@@ -53,6 +53,7 @@ public class HelpUpHandler {
return; return;
} }
if (helpUpAttempts.containsKey(knockedId)) return; if (helpUpAttempts.containsKey(knockedId)) return;
if (NpcHelpUpHandler.isBeingHelped(knockedId)) return;
helpUpAttempts.put(knockedId, new HelpUpAttempt(helper.getUUID())); helpUpAttempts.put(knockedId, new HelpUpAttempt(helper.getUUID()));
helper.sendSystemMessage(Component.translatable("message.knockedback.help_up_started", target.getDisplayName())); helper.sendSystemMessage(Component.translatable("message.knockedback.help_up_started", target.getDisplayName()));
target.sendSystemMessage(Component.translatable("message.knockedback.being_helped", helper.getDisplayName())); target.sendSystemMessage(Component.translatable("message.knockedback.being_helped", helper.getDisplayName()));
@@ -56,7 +56,7 @@ public class KnockedManager {
applyKnockedEffects(sp); applyKnockedEffects(sp);
boolean grounded = sp.onGround() || sp.isInWater(); boolean grounded = sp.onGround() || sp.isInWater();
sp.setNoGravity(false); sp.setNoGravity(false);
if (!grounded) { if (!grounded && !Config.COMMON.knockedMovementEnabled.get()) {
sp.setDeltaMovement(0, sp.getDeltaMovement().y() - 0.08, 0); sp.setDeltaMovement(0, sp.getDeltaMovement().y() - 0.08, 0);
} }
} }
@@ -75,6 +75,7 @@ public class KnockedManager {
MobKillHandler.clearKillAttempt(entity.getUUID()); MobKillHandler.clearKillAttempt(entity.getUUID());
PlayerExecutionHandler.cancelExecution(entity.getUUID()); PlayerExecutionHandler.cancelExecution(entity.getUUID());
HelpUpHandler.cancelHelpUp(entity.getUUID()); HelpUpHandler.cancelHelpUp(entity.getUUID());
NpcHelpUpHandler.cancelHelpUp(entity.getUUID());
entity.setPose(Pose.STANDING); entity.setPose(Pose.STANDING);
if (entity instanceof ServerPlayer sp) { if (entity instanceof ServerPlayer sp) {
NetworkHandler.CHANNEL.send( NetworkHandler.CHANNEL.send(
@@ -125,7 +126,8 @@ public class KnockedManager {
} }
if (MobKillHandler.isBeingMobExecuted(playerId) if (MobKillHandler.isBeingMobExecuted(playerId)
|| PlayerExecutionHandler.isBeingPlayerExecuted(playerId) || PlayerExecutionHandler.isBeingPlayerExecuted(playerId)
|| HelpUpHandler.isBeingHelped(playerId)) { || HelpUpHandler.isBeingHelped(playerId)
|| NpcHelpUpHandler.isBeingHelped(playerId)) {
ServerPlayer p = NetworkHandlerHelper.getPlayerByUuid(server, playerId); ServerPlayer p = NetworkHandlerHelper.getPlayerByUuid(server, playerId);
if (p != null) { if (p != null) {
applyKnockedEffects(p); applyKnockedEffects(p);
@@ -216,6 +218,7 @@ public class KnockedManager {
PlayerExecutionHandler.cancelExecution(playerId); PlayerExecutionHandler.cancelExecution(playerId);
PlayerExecutionHandler.cancelExecutionByExecutor(playerId); PlayerExecutionHandler.cancelExecutionByExecutor(playerId);
HelpUpHandler.cancelHelpUp(playerId); HelpUpHandler.cancelHelpUp(playerId);
NpcHelpUpHandler.cancelHelpUp(playerId);
HelpUpHandler.cancelHelpUpByHelper(playerId); HelpUpHandler.cancelHelpUpByHelper(playerId);
CarryManager.stopCarry(playerId); CarryManager.stopCarry(playerId);
CarryManager.stopCarrying(playerId); CarryManager.stopCarrying(playerId);
@@ -22,7 +22,7 @@ import static net.fretux.knockedback.KnockedManager.setGripped;
public class MobKillHandler { public class MobKillHandler {
public static int getExecutionTime() { public static int getExecutionTime() {
return Config.COMMON.executionTime.get(); return Config.COMMON.mobExecutionTime.get();
} }
private static final Map<UUID, KillAttempt> killAttempts = new HashMap<>(); private static final Map<UUID, KillAttempt> killAttempts = new HashMap<>();
@@ -110,7 +110,8 @@ public class MobKillHandler {
continue; continue;
} }
if (PlayerExecutionHandler.isBeingPlayerExecuted(knockedId) if (PlayerExecutionHandler.isBeingPlayerExecuted(knockedId)
|| HelpUpHandler.isBeingHelped(knockedId)) { || HelpUpHandler.isBeingHelped(knockedId)
|| NpcHelpUpHandler.isBeingHelped(knockedId)) {
setGripped(knocked, false); setGripped(knocked, false);
continue; continue;
} }
@@ -144,7 +145,9 @@ public class MobKillHandler {
executeKnockedPlayer(knocked, mob); executeKnockedPlayer(knocked, mob);
continue; continue;
} else { } else {
mob.getNavigation().stop(); if (!Config.COMMON.executorMovementEnabled.get()) {
mob.getNavigation().stop();
}
mob.setTarget(null); mob.setTarget(null);
} }
} }
@@ -0,0 +1,138 @@
package net.fretux.knockedback;
import net.fretux.knockedback.config.Config;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.TamableAnimal;
import net.minecraft.world.entity.animal.IronGolem;
import net.minecraft.world.entity.monster.Monster;
import net.minecraft.world.entity.npc.Villager;
import net.minecraft.world.phys.AABB;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.registries.ForgeRegistries;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.UUID;
@Mod.EventBusSubscriber(modid = KnockedBack.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public final class NpcHelpUpHandler {
private static final double ASSIST_DISTANCE = 2.25D;
private static final Map<UUID, AssistAttempt> ATTEMPTS = new HashMap<>();
private NpcHelpUpHandler() {}
public static boolean isBeingHelped(UUID playerId) {
return ATTEMPTS.containsKey(playerId);
}
public static void cancelHelpUp(UUID playerId) {
AssistAttempt attempt = ATTEMPTS.remove(playerId);
if (attempt != null) {
Mob helper = findMob(attempt.helperId);
if (helper != null) helper.getNavigation().stop();
}
}
@SubscribeEvent
public static void onServerTick(TickEvent.ServerTickEvent event) {
if (event.phase != TickEvent.Phase.END || !event.side.isServer()) return;
if (!Config.COMMON.npcHelpUpEnabled.get()) {
for (UUID playerId : new HashSet<>(ATTEMPTS.keySet())) cancelHelpUp(playerId);
return;
}
for (UUID playerId : KnockedManager.getKnockedUuids()) {
ServerPlayer player = NetworkHandlerHelper.getPlayerByUuid(
net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer(), playerId);
if (player == null || !player.isAlive() || CarryManager.isBeingCarried(playerId)
|| HelpUpHandler.isBeingHelped(playerId)
|| PlayerExecutionHandler.isBeingPlayerExecuted(playerId)
|| MobKillHandler.isBeingMobExecuted(playerId)) {
cancelHelpUp(playerId);
continue;
}
AssistAttempt attempt = ATTEMPTS.get(playerId);
Mob helper = attempt == null ? findHelper(player) : findMob(attempt.helperId);
if (!isValidHelper(helper, player) || hasThreats(player, helper)) {
cancelHelpUp(playerId);
continue;
}
if (attempt == null) {
attempt = new AssistAttempt(helper.getUUID(), Config.COMMON.npcHelpUpTime.get());
ATTEMPTS.put(playerId, attempt);
}
if (helper.distanceToSqr(player) > ASSIST_DISTANCE * ASSIST_DISTANCE) {
helper.getNavigation().moveTo(player, 1.1D);
attempt.timeLeft = Config.COMMON.npcHelpUpTime.get();
} else {
helper.getNavigation().stop();
helper.getLookControl().setLookAt(player, 30.0F, 30.0F);
if (--attempt.timeLeft <= 0) complete(playerId, player, helper);
}
}
ATTEMPTS.keySet().removeIf(id -> !KnockedManager.getKnockedUuids().contains(id));
}
private static void complete(UUID playerId, ServerPlayer player, Mob helper) {
ATTEMPTS.remove(playerId);
helper.getNavigation().stop();
KnockedManager.removeKnockedState(player);
player.heal(Config.COMMON.npcHelpUpHealAmount.get().floatValue());
}
private static Mob findHelper(ServerPlayer player) {
double range = Config.COMMON.npcHelpUpSearchRange.get();
return ((ServerLevel) player.level()).getEntitiesOfClass(Mob.class,
player.getBoundingBox().inflate(range), mob -> isValidHelper(mob, player))
.stream().filter(mob -> !hasThreats(player, mob))
.min(java.util.Comparator.comparingDouble(player::distanceToSqr)).orElse(null);
}
private static boolean isValidHelper(Mob mob, ServerPlayer player) {
if (mob == null || !mob.isAlive() || mob.level() != player.level()) return false;
ResourceLocation id = ForgeRegistries.ENTITY_TYPES.getKey(mob.getType());
if (id == null || !Config.COMMON.npcHelpUpAllowlist.get().contains(id.toString())) return false;
if (mob instanceof TamableAnimal pet) return pet.isTame() && pet.isOwnedBy(player);
if (mob instanceof Villager villager) return villager.getPlayerReputation(player) > 0;
if (mob instanceof IronGolem golem) return golem.isPlayerCreated() || golem.getTarget() != player;
return !(mob instanceof Monster) && mob.getTarget() != player;
}
private static boolean hasThreats(ServerPlayer player, Mob helper) {
if (helper.getTarget() != null && helper.getTarget().isAlive()) return true;
double range = Config.COMMON.npcThreatRange.get();
AABB area = player.getBoundingBox().minmax(helper.getBoundingBox()).inflate(range);
return player.level().getEntitiesOfClass(Mob.class, area).stream().anyMatch(mob ->
mob.isAlive() && mob != helper && (mob instanceof Monster
|| mob.getTarget() == player || mob.getTarget() == helper));
}
private static Mob findMob(UUID id) {
var server = net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer();
if (server == null) return null;
for (ServerLevel level : server.getAllLevels()) {
var entity = level.getEntity(id);
if (entity instanceof Mob mob) return mob;
}
return null;
}
private static final class AssistAttempt {
private final UUID helperId;
private int timeLeft;
private AssistAttempt(UUID helperId, int timeLeft) {
this.helperId = helperId;
this.timeLeft = timeLeft;
}
}
}
@@ -40,7 +40,8 @@ public class PlayerExecutionHandler {
@SubscribeEvent @SubscribeEvent
public static void onPlayerTick(TickEvent.PlayerTickEvent event) { public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
if (event.side.isServer() && event.phase == TickEvent.Phase.END && event.player instanceof ServerPlayer sp && isExecuting(sp.getUUID())) { if (event.side.isServer() && event.phase == TickEvent.Phase.END && event.player instanceof ServerPlayer sp
&& isExecuting(sp.getUUID()) && !Config.COMMON.executorMovementEnabled.get()) {
sp.setDeltaMovement(0, 0, 0); sp.setDeltaMovement(0, 0, 0);
sp.setSprinting(false); sp.setSprinting(false);
} }
@@ -17,6 +17,9 @@ public class Config {
public static class Common { public static class Common {
public final ForgeConfigSpec.IntValue knockedDuration; public final ForgeConfigSpec.IntValue knockedDuration;
public final ForgeConfigSpec.IntValue executionTime; public final ForgeConfigSpec.IntValue executionTime;
public final ForgeConfigSpec.IntValue mobExecutionTime;
public final ForgeConfigSpec.BooleanValue knockedMovementEnabled;
public final ForgeConfigSpec.BooleanValue executorMovementEnabled;
public final ForgeConfigSpec.IntValue helpUpTime; public final ForgeConfigSpec.IntValue helpUpTime;
public final ForgeConfigSpec.DoubleValue helpUpHealAmount; public final ForgeConfigSpec.DoubleValue helpUpHealAmount;
public final ForgeConfigSpec.BooleanValue helpUpEnabled; public final ForgeConfigSpec.BooleanValue helpUpEnabled;
@@ -25,6 +28,12 @@ public class Config {
public final ForgeConfigSpec.ConfigValue<List<? extends String>> mobExecutionAllowlist; public final ForgeConfigSpec.ConfigValue<List<? extends String>> mobExecutionAllowlist;
public final ForgeConfigSpec.ConfigValue<List<? extends String>> knockedPotionEffects; public final ForgeConfigSpec.ConfigValue<List<? extends String>> knockedPotionEffects;
public final ForgeConfigSpec.BooleanValue removeOtherPotionEffectsWhileKnocked; public final ForgeConfigSpec.BooleanValue removeOtherPotionEffectsWhileKnocked;
public final ForgeConfigSpec.BooleanValue npcHelpUpEnabled;
public final ForgeConfigSpec.IntValue npcHelpUpTime;
public final ForgeConfigSpec.DoubleValue npcHelpUpHealAmount;
public final ForgeConfigSpec.DoubleValue npcHelpUpSearchRange;
public final ForgeConfigSpec.DoubleValue npcThreatRange;
public final ForgeConfigSpec.ConfigValue<List<? extends String>> npcHelpUpAllowlist;
public Common(ForgeConfigSpec.Builder builder) { public Common(ForgeConfigSpec.Builder builder) {
builder.push("general"); builder.push("general");
@@ -34,9 +43,21 @@ public class Config {
.defineInRange("knockedDuration", 400, 100, 1200); .defineInRange("knockedDuration", 400, 100, 1200);
executionTime = builder executionTime = builder
.comment("How long (in ticks) it takes to execute a knocked player. Default: 60 (3 seconds).") .comment("How long (in ticks) it takes another player to execute a knocked player. Default: 60 (3 seconds).")
.defineInRange("executionTime", 60, 20, 200); .defineInRange("executionTime", 60, 20, 200);
mobExecutionTime = builder
.comment("How long (in ticks) it takes a mob to execute a knocked player. Default: 60 (3 seconds).")
.defineInRange("mobExecutionTime", 60, 0, 1200);
knockedMovementEnabled = builder
.comment("If true, knocked players retain horizontal movement and can jump. Default: false.")
.define("knockedMovementEnabled", false);
executorMovementEnabled = builder
.comment("If true, players and mobs may move while executing a knocked player. Default: false.")
.define("executorMovementEnabled", false);
helpUpEnabled = builder helpUpEnabled = builder
.comment("If true, players can help knocked players recover by holding the help-up key. Default: true.") .comment("If true, players can help knocked players recover by holding the help-up key. Default: true.")
.define("helpUpEnabled", true); .define("helpUpEnabled", true);
@@ -49,6 +70,34 @@ public class Config {
.comment("How much health a helped-up player receives after recovery. Default: 4.0 (2 hearts).") .comment("How much health a helped-up player receives after recovery. Default: 4.0 (2 hearts).")
.defineInRange("helpUpHealAmount", 4.0D, 1.0D, 20.0D); .defineInRange("helpUpHealAmount", 4.0D, 1.0D, 20.0D);
npcHelpUpEnabled = builder
.comment("If true, friendly configured NPCs can help knocked players recover. Default: true.")
.define("npcHelpUpEnabled", true);
npcHelpUpTime = builder
.comment("How long (in ticks) an NPC must remain beside a player to help them up. Default: 60 (3 seconds).")
.defineInRange("npcHelpUpTime", 60, 1, 1200);
npcHelpUpHealAmount = builder
.comment("How much health an NPC-assisted player receives. Default: 4.0 (2 hearts).")
.defineInRange("npcHelpUpHealAmount", 4.0D, 1.0D, 20.0D);
npcHelpUpSearchRange = builder
.comment("How far friendly NPCs can notice a knocked player. Default: 16 blocks.")
.defineInRange("npcHelpUpSearchRange", 16.0D, 2.0D, 64.0D);
npcThreatRange = builder
.comment("Radius which must be free of living threats before an NPC will help. Default: 12 blocks.")
.defineInRange("npcThreatRange", 12.0D, 1.0D, 64.0D);
npcHelpUpAllowlist = builder
.comment("Entity ids which may help knocked players.",
"Tamed animals must belong to the player; villagers require positive reputation.",
"Other listed mobs, including modded guards, must not be hostile to the player.")
.defineListAllowEmpty("npcHelpUpAllowlist",
List.of("minecraft:wolf", "minecraft:cat", "minecraft:parrot", "minecraft:villager", "minecraft:iron_golem"),
o -> o instanceof String);
mobExecutionEnabled = builder mobExecutionEnabled = builder
.comment("If true, mobs can execute knocked players. Default: true.") .comment("If true, mobs can execute knocked players. Default: true.")
.define("mobExecutionEnabled", true); .define("mobExecutionEnabled", true);
@@ -2,6 +2,7 @@ package net.fretux.knockedback.effects;
import net.fretux.knockedback.KnockedManager; import net.fretux.knockedback.KnockedManager;
import net.fretux.knockedback.CarryManager; import net.fretux.knockedback.CarryManager;
import net.fretux.knockedback.config.Config;
import net.minecraft.world.entity.Pose; import net.minecraft.world.entity.Pose;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraftforge.event.entity.living.LivingEvent; import net.minecraftforge.event.entity.living.LivingEvent;
@@ -21,7 +22,9 @@ public class KnockedEffect {
player.setSprinting(false); player.setSprinting(false);
player.hurtMarked = true; player.hurtMarked = true;
player.setNoGravity(false); player.setNoGravity(false);
player.setDeltaMovement(0, player.getDeltaMovement().y(), 0); if (!Config.COMMON.knockedMovementEnabled.get()) {
player.setDeltaMovement(0, player.getDeltaMovement().y(), 0);
}
if (CarryManager.isBeingCarried(player.getUUID())) { if (CarryManager.isBeingCarried(player.getUUID())) {
if (player.getForcedPose() == Pose.SWIMMING) { if (player.getForcedPose() == Pose.SWIMMING) {
player.setForcedPose(null); player.setForcedPose(null);
@@ -60,7 +63,8 @@ public class KnockedEffect {
@SubscribeEvent @SubscribeEvent
public static void onLivingJump(LivingEvent.LivingJumpEvent event) { public static void onLivingJump(LivingEvent.LivingJumpEvent event) {
if (event.getEntity() instanceof Player player && KnockedManager.isKnocked(player)) { if (event.getEntity() instanceof Player player && KnockedManager.isKnocked(player)
&& !Config.COMMON.knockedMovementEnabled.get()) {
event.getEntity().setDeltaMovement(0, 0, 0); event.getEntity().setDeltaMovement(0, 0, 0);
} }
} }