Protect owned pets from lethal damage on Forge

This commit is contained in:
Frederik Spirgi
2026-10-12 09:35:41 +02:00
parent b82489f9d3
commit 59ccc9e106
10 changed files with 286 additions and 12 deletions
@@ -37,26 +37,27 @@ public class HelpUpHandler {
return helpUpAttempts.values().stream().anyMatch(attempt -> attempt.helperId.equals(helperId));
}
public static void startHelpUp(ServerPlayer helper) {
if (!Config.COMMON.helpUpEnabled.get()) return;
public static boolean startHelpUp(ServerPlayer helper) {
if (!Config.COMMON.helpUpEnabled.get()) return false;
if (KnockedManager.isKnocked(helper)) {
helper.sendSystemMessage(Component.translatable("message.knockedback.cannot_help_while_knocked"));
return;
return false;
}
if (PlayerExecutionHandler.isExecuting(helper.getUUID())) return;
if (isHelping(helper.getUUID())) return;
if (PlayerExecutionHandler.isExecuting(helper.getUUID())) return false;
if (isHelping(helper.getUUID()) || PetKnockdownHandler.isHelping(helper.getUUID())) return false;
Player target = findNearestKnocked(helper);
if (target == null) return;
if (target == null) return false;
UUID knockedId = target.getUUID();
if (CarryManager.isBeingCarried(knockedId)) {
helper.sendSystemMessage(Component.translatable("message.knockedback.cannot_help_carried_player"));
return;
return false;
}
if (helpUpAttempts.containsKey(knockedId)) return;
if (NpcHelpUpHandler.isBeingHelped(knockedId)) return;
if (helpUpAttempts.containsKey(knockedId)) return false;
if (NpcHelpUpHandler.isBeingHelped(knockedId)) return false;
helpUpAttempts.put(knockedId, new HelpUpAttempt(helper.getUUID()));
helper.sendSystemMessage(Component.translatable("message.knockedback.help_up_started", target.getDisplayName()));
target.sendSystemMessage(Component.translatable("message.knockedback.being_helped", helper.getDisplayName()));
return true;
}
public static void cancelHelpUp(UUID knockedId) {
@@ -26,9 +26,12 @@ public class HelpUpKnockedPacket {
ServerPlayer helper = ctx.get().getSender();
if (helper == null) return;
if (msg.helping) {
HelpUpHandler.startHelpUp(helper);
if (!HelpUpHandler.startHelpUp(helper)) {
PetKnockdownHandler.startHelpUp(helper);
}
} else {
HelpUpHandler.cancelHelpUpByHelper(helper.getUUID());
PetKnockdownHandler.cancelHelpUpByHelper(helper.getUUID());
}
});
ctx.get().setPacketHandled(true);
@@ -121,7 +121,8 @@ public final class NpcHelpUpHandler {
}
private static boolean isValidHelper(Mob mob, ServerPlayer player) {
if (mob == null || !mob.isAlive() || mob.level() != player.level()) return false;
if (mob == null || !mob.isAlive() || mob.level() != player.level()
|| PetKnockdownHandler.isKnocked(mob)) 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);
@@ -0,0 +1,232 @@
package net.fretux.knockedback;
import net.fretux.knockedback.config.Config;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.OwnableEntity;
import net.minecraft.world.entity.TamableAnimal;
import net.minecraft.world.phys.AABB;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.living.LivingDeathEvent;
import net.minecraftforge.event.entity.living.LivingEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.event.entity.player.AttackEntityEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.network.PacketDistributor;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.server.ServerLifecycleHooks;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Mod.EventBusSubscriber(modid = KnockedBack.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public final class PetKnockdownHandler {
private static final String TIMER_KEY = "knockedback.pet_remaining";
private static final double HELP_RANGE = 2.0D;
private static final Map<UUID, HelpAttempt> HELP_ATTEMPTS = new HashMap<>();
private PetKnockdownHandler() {}
private static final class HelpAttempt {
final UUID helperId;
int remaining;
HelpAttempt(UUID helperId) {
this.helperId = helperId;
this.remaining = Config.COMMON.helpUpTime.get();
}
}
public static boolean isKnocked(LivingEntity entity) {
return entity instanceof Mob && entity.getPersistentData().getInt(TIMER_KEY) > 0;
}
private static boolean isProtectedPet(LivingEntity entity) {
if (!Config.COMMON.petKnockdownEnabled.get() || !(entity instanceof Mob)) return false;
if (entity instanceof TamableAnimal pet && pet.isTame() && pet.getOwnerUUID() != null) return true;
if (entity instanceof OwnableEntity owned && owned.getOwnerUUID() != null) return true;
var id = ForgeRegistries.ENTITY_TYPES.getKey(entity.getType());
return id != null && Config.COMMON.protectedEntityTypes.get().contains(id.toString());
}
@SubscribeEvent
public static void onDeath(LivingDeathEvent event) {
LivingEntity entity = event.getEntity();
if (entity.level().isClientSide || event.getSource().is(KnockedBackDamageTags.BYPASS_KNOCKDOWN)
|| !isProtectedPet(entity)) return;
entity.setHealth(1.0F);
entity.getPersistentData().putInt(TIMER_KEY, Config.COMMON.petKnockdownDuration.get());
if (entity instanceof Mob mob) {
mob.setTarget(null);
mob.getNavigation().stop();
}
notifyOwner(entity, "message.knockedback.pet_knocked");
event.setCanceled(true);
}
@SubscribeEvent
public static void onHurt(LivingHurtEvent event) {
LivingEntity entity = event.getEntity();
if (entity.level().isClientSide || !isKnocked(entity)) return;
if (event.getSource().is(KnockedBackDamageTags.BYPASS_KNOCKDOWN)) {
clearKnockdown(entity);
return;
}
entity.getPersistentData().putInt(TIMER_KEY, Config.COMMON.petKnockdownDuration.get());
event.setCanceled(true);
}
@SubscribeEvent
public static void onLivingTick(LivingEvent.LivingTickEvent event) {
LivingEntity entity = event.getEntity();
if (entity.level().isClientSide || !isKnocked(entity)) return;
if (entity instanceof Mob mob) {
mob.setTarget(null);
mob.getNavigation().stop();
mob.setDeltaMovement(0, mob.getDeltaMovement().y(), 0);
}
if (HELP_ATTEMPTS.containsKey(entity.getUUID())) return;
CompoundTag data = entity.getPersistentData();
int remaining = data.getInt(TIMER_KEY) - 1;
if (remaining > 0) {
data.putInt(TIMER_KEY, remaining);
} else {
clearKnockdown(entity);
entity.setHealth(Math.min(entity.getMaxHealth(), Config.COMMON.petRecoveryHealth.get().floatValue()));
notifyOwner(entity, "message.knockedback.pet_recovered");
}
}
private static void clearKnockdown(LivingEntity entity) {
entity.getPersistentData().remove(TIMER_KEY);
cancelHelpUp(entity.getUUID());
}
private static void notifyOwner(LivingEntity entity, String messageKey) {
if (!(entity instanceof OwnableEntity owned) || owned.getOwnerUUID() == null) return;
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
if (server == null) return;
ServerPlayer owner = server.getPlayerList().getPlayer(owned.getOwnerUUID());
if (owner != null) owner.sendSystemMessage(Component.translatable(messageKey, entity.getDisplayName()));
}
public static boolean isHelping(UUID helperId) {
return HELP_ATTEMPTS.values().stream().anyMatch(attempt -> attempt.helperId.equals(helperId));
}
public static void startHelpUp(ServerPlayer helper) {
if (!Config.COMMON.helpUpEnabled.get() || !Config.COMMON.petHelpUpEnabled.get()
|| KnockedManager.isKnocked(helper) || HelpUpHandler.isHelping(helper.getUUID())
|| PlayerExecutionHandler.isExecuting(helper.getUUID()) || isHelping(helper.getUUID())) return;
AABB box = helper.getBoundingBox().inflate(HELP_RANGE);
Mob pet = helper.level().getEntitiesOfClass(Mob.class, box).stream()
.filter(entity -> isKnocked(entity) && !HELP_ATTEMPTS.containsKey(entity.getUUID())
&& helper.distanceToSqr(entity) <= HELP_RANGE * HELP_RANGE)
.min(Comparator.comparingDouble(helper::distanceToSqr)).orElse(null);
if (pet == null) return;
HELP_ATTEMPTS.put(pet.getUUID(), new HelpAttempt(helper.getUUID()));
helper.sendSystemMessage(Component.translatable("message.knockedback.help_up_started", pet.getDisplayName()));
}
public static void cancelHelpUpByHelper(UUID helperId) {
for (Map.Entry<UUID, HelpAttempt> entry : new ArrayList<>(HELP_ATTEMPTS.entrySet())) {
if (entry.getValue().helperId.equals(helperId)) cancelHelpUp(entry.getKey());
}
}
private static void cancelHelpUp(UUID petId) {
HelpAttempt attempt = HELP_ATTEMPTS.remove(petId);
if (attempt == null) return;
ServerPlayer helper = findPlayer(attempt.helperId);
if (helper != null) clearProgress(helper);
}
@SubscribeEvent
public static void onServerTick(TickEvent.ServerTickEvent event) {
if (event.phase != TickEvent.Phase.END || !event.side.isServer()) return;
for (Map.Entry<UUID, HelpAttempt> entry : new ArrayList<>(HELP_ATTEMPTS.entrySet())) {
UUID petId = entry.getKey();
HelpAttempt attempt = entry.getValue();
Mob pet = findMob(petId);
ServerPlayer helper = findPlayer(attempt.helperId);
if (pet == null || helper == null || !isKnocked(pet) || KnockedManager.isKnocked(helper)
|| pet.level() != helper.level() || helper.distanceToSqr(pet) > HELP_RANGE * HELP_RANGE) {
cancelHelpUp(petId);
continue;
}
attempt.remaining--;
NetworkHandler.CHANNEL.send(PacketDistributor.PLAYER.with(() -> helper),
new ExecutionProgressPacket(attempt.remaining, Config.COMMON.helpUpTime.get(),
ExecutionProgressPacket.Action.HELP_UP, pet.getDisplayName().getString(), null));
if (attempt.remaining <= 0) {
HELP_ATTEMPTS.remove(petId);
pet.getPersistentData().remove(TIMER_KEY);
pet.setHealth(Math.min(pet.getMaxHealth(), pet.getHealth() + Config.COMMON.helpUpHealAmount.get().floatValue()));
clearProgress(helper);
KnockedbackAdvancements.recordHelpUp(helper);
helper.sendSystemMessage(Component.translatable("message.knockedback.you_helped_up", pet.getDisplayName()));
notifyOwner(pet, "message.knockedback.pet_recovered");
}
}
}
@SubscribeEvent
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
if (event.side.isServer() && event.phase == TickEvent.Phase.END
&& event.player instanceof ServerPlayer helper && isHelping(helper.getUUID())) {
helper.setDeltaMovement(0, 0, 0);
helper.setSprinting(false);
}
}
@SubscribeEvent
public static void onAttack(AttackEntityEvent event) {
if (event.getEntity() instanceof ServerPlayer helper && isHelping(helper.getUUID())) event.setCanceled(true);
}
@SubscribeEvent
public static void onHelperHurt(LivingHurtEvent event) {
if (event.getEntity() instanceof ServerPlayer helper) cancelHelpUpByHelper(helper.getUUID());
}
@SubscribeEvent
public static void onLogout(PlayerEvent.PlayerLoggedOutEvent event) {
cancelHelpUpByHelper(event.getEntity().getUUID());
}
@SubscribeEvent
public static void onRespawn(PlayerEvent.PlayerRespawnEvent event) {
cancelHelpUpByHelper(event.getEntity().getUUID());
}
private static Mob findMob(UUID id) {
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
if (server == null) return null;
for (ServerLevel level : server.getAllLevels()) {
Entity entity = level.getEntity(id);
if (entity instanceof Mob mob) return mob;
}
return null;
}
private static ServerPlayer findPlayer(UUID id) {
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
return server == null ? null : server.getPlayerList().getPlayer(id);
}
private static void clearProgress(ServerPlayer player) {
NetworkHandler.CHANNEL.send(PacketDistributor.PLAYER.with(() -> player),
new ExecutionProgressPacket(0, 1, ExecutionProgressPacket.Action.NONE, null));
}
}
@@ -34,6 +34,11 @@ public class Config {
public final ForgeConfigSpec.DoubleValue npcHelpUpSearchRange;
public final ForgeConfigSpec.DoubleValue npcThreatRange;
public final ForgeConfigSpec.ConfigValue<List<? extends String>> npcHelpUpAllowlist;
public final ForgeConfigSpec.BooleanValue petKnockdownEnabled;
public final ForgeConfigSpec.IntValue petKnockdownDuration;
public final ForgeConfigSpec.DoubleValue petRecoveryHealth;
public final ForgeConfigSpec.BooleanValue petHelpUpEnabled;
public final ForgeConfigSpec.ConfigValue<List<? extends String>> protectedEntityTypes;
public Common(ForgeConfigSpec.Builder builder) {
builder.push("general");
@@ -124,6 +129,30 @@ public class Config {
.comment("If true, Totems of Undying prevent entering the knocked state. Default: true.")
.define("totemPreventsKnockdown", true);
builder.pop().push("pets");
petKnockdownEnabled = builder
.comment("If true, owned pets and listed follower types survive lethal combat damage. Default: true.")
.define("petKnockdownEnabled", true);
petKnockdownDuration = builder
.comment("How long (in ticks) a pet remains knocked before recovering. Default: 400 (20 seconds).")
.defineInRange("petKnockdownDuration", 400, 20, 1200);
petRecoveryHealth = builder
.comment("Health restored to a pet after automatic recovery. Default: 4.0 (2 hearts).")
.defineInRange("petRecoveryHealth", 4.0D, 1.0D, 100.0D);
petHelpUpEnabled = builder
.comment("If true, a player can hold the help-up key near a knocked pet to rescue it early. Default: true.")
.define("petHelpUpEnabled", true);
protectedEntityTypes = builder
.comment("Additional entity type ids to protect, for modded followers without vanilla ownership data.",
"Every mob of a listed type is protected, so list only intended follower types.",
"Example: [\"modid:follower\"]")
.defineListAllowEmpty("protectedEntityTypes", List.of(), o -> o instanceof String);
builder.pop();
}
}
@@ -24,6 +24,8 @@
"message.knockedback.been_dropped": "Du wurdest von %1$s abgesetzt",
"message.knockedback.you_executed": "Du hast %1$s hingerichtet!",
"message.knockedback.you_were_executed": "Du wurdest von %1$s hingerichtet!",
"message.knockedback.pet_knocked": "%s wurde niedergeschlagen. Halte H in der N\u00e4he gedr\u00fcckt, um zu helfen.",
"message.knockedback.pet_recovered": "%s hat sich erholt.",
"overlay.knockedback.execution_progress": "Hinrichten",
"overlay.knockedback.help_up_progress": "Aufhelfen",
"overlay.knockedback.executing_target": "%s hinrichten...",
@@ -24,6 +24,8 @@
"message.knockedback.you_were_helped_up": "You were helped up by %s!",
"message.knockedback.you_executed": "You executed %s!",
"message.knockedback.you_were_executed": "You were executed by %s!",
"message.knockedback.pet_knocked": "%s was knocked down. Hold H nearby to help them recover.",
"message.knockedback.pet_recovered": "%s recovered from knockdown.",
"overlay.knockedback.execution_progress": "Executing",
"overlay.knockedback.help_up_progress": "Helping up",
"overlay.knockedback.executing_target": "Executing %s...",