Initial Commit

This commit is contained in:
IM23a-spirgif
2025-02-26 11:40:27 +01:00
commit a2ffe93ac4
26 changed files with 3010 additions and 0 deletions
@@ -0,0 +1,60 @@
package net.fretux.knockedback;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.config.ModConfigEvent;
import net.minecraftforge.registries.ForgeRegistries;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Mod.EventBusSubscriber(modid = KnockedBack.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
public class Config
{
private static final ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder();
private static final ForgeConfigSpec.BooleanValue LOG_DIRT_BLOCK = BUILDER
.comment("Whether to log the dirt block on common setup")
.define("logDirtBlock", true);
private static final ForgeConfigSpec.IntValue MAGIC_NUMBER = BUILDER
.comment("A magic number")
.defineInRange("magicNumber", 42, 0, Integer.MAX_VALUE);
public static final ForgeConfigSpec.ConfigValue<String> MAGIC_NUMBER_INTRODUCTION = BUILDER
.comment("What you want the introduction message to be for the magic number")
.define("magicNumberIntroduction", "The magic number is... ");
// a list of strings that are treated as resource locations for items
private static final ForgeConfigSpec.ConfigValue<List<? extends String>> ITEM_STRINGS = BUILDER
.comment("A list of items to log on common setup.")
.defineListAllowEmpty("items", List.of("minecraft:iron_ingot"), Config::validateItemName);
static final ForgeConfigSpec SPEC = BUILDER.build();
public static boolean logDirtBlock;
public static int magicNumber;
public static String magicNumberIntroduction;
public static Set<Item> items;
private static boolean validateItemName(final Object obj)
{
return obj instanceof final String itemName && ForgeRegistries.ITEMS.containsKey(new ResourceLocation(itemName));
}
@SubscribeEvent
static void onLoad(final ModConfigEvent event)
{
logDirtBlock = LOG_DIRT_BLOCK.get();
magicNumber = MAGIC_NUMBER.get();
magicNumberIntroduction = MAGIC_NUMBER_INTRODUCTION.get();
// convert the list of strings into a set of items
items = ITEM_STRINGS.get().stream()
.map(itemName -> ForgeRegistries.ITEMS.getValue(new ResourceLocation(itemName)))
.collect(Collectors.toSet());
}
}
@@ -0,0 +1,39 @@
package net.fretux.knockedback;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
public class ExecuteKnockedPacket {
public ExecuteKnockedPacket() { }
public static void encode(ExecuteKnockedPacket msg, FriendlyByteBuf buf) {
}
public static ExecuteKnockedPacket decode(FriendlyByteBuf buf) {
return new ExecuteKnockedPacket();
}
public static void handle(ExecuteKnockedPacket msg, Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
ServerPlayer executor = ctx.get().getSender();
if (executor == null) return;
LivingEntity target = findNearestKnockedEntity(executor);
if (target != null) {
PlayerExecutionHandler.executeKnockedPlayer(executor, target);
}
});
ctx.get().setPacketHandled(true);
}
private static LivingEntity findNearestKnockedEntity(ServerPlayer executor) {
return KnockedManager.getKnockedUuids().stream()
.map(uuid -> NetworkHandlerHelper.getPlayerByUuid(executor.getServer(), uuid))
.filter(player -> player != null && player.distanceTo(executor) <= (float) PlayerExecutionHandler.EXECUTION_RANGE)
.findFirst()
.orElse(null);
}
}
@@ -0,0 +1,71 @@
package net.fretux.knockedback;
import com.mojang.logging.LogUtils;
import net.fretux.knockedback.effects.KnockedEffect;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.living.LivingDeathEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import org.slf4j.Logger;
import net.minecraft.world.damagesource.DamageSource;
@Mod(KnockedBack.MOD_ID)
public class KnockedBack {
public static final String MOD_ID = "knockedback";
private static final Logger LOGGER = LogUtils.getLogger();
public KnockedBack() {
ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, Config.SPEC);
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup);
MinecraftForge.EVENT_BUS.register(this);
MinecraftForge.EVENT_BUS.register(new KnockedEffect());
MinecraftForge.EVENT_BUS.register(new MobKillHandler());
MinecraftForge.EVENT_BUS.register(new PlayerExecutionHandler());
}
private void setup(final FMLCommonSetupEvent event) {
NetworkHandler.register();
PlayerExecutionHandler.register();
}
@SubscribeEvent
public void onServerStarting(ServerStartingEvent event) {
LOGGER.info("KnockedBack mod server starting");
}
@SubscribeEvent
public void onLivingHurt(LivingHurtEvent event) {
DamageSource source = event.getSource();
String damageType = source.getMsgId();
if (event.getEntity() instanceof Player player && player.getHealth() - event.getAmount() <= 0) {
if (!(damageType.equals("fall") || damageType.equals("explosion") || damageType.equals("explosion.player") ||
damageType.equals("fire") || damageType.equals("lava") || damageType.equals("onFire") ||
damageType.equals("fireball"))) {
event.setCanceled(true);
KnockedManager.applyKnockedState(player);
}
}
}
@SubscribeEvent
public void onLivingDeath(LivingDeathEvent event) {
if (KnockedManager.isKnocked(event.getEntity())) {
event.setCanceled(true);
}
}
@SubscribeEvent
public void onServerTick(TickEvent.ServerTickEvent event) {
if (event.phase == TickEvent.Phase.END && event.side.isServer()) {
KnockedManager.tickKnockedStates();
}
}
}
@@ -0,0 +1,99 @@
package net.fretux.knockedback;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import java.util.*;
@Mod.EventBusSubscriber
public class KnockedManager {
private static final Map<UUID, Integer> knockedEntities = new HashMap<>();
private static final Set<UUID> grippedEntities = new HashSet<>();
private static final int KNOCKED_DURATION = 400;
public static void applyKnockedState(LivingEntity entity) {
if (!(entity instanceof Player)) {
return;
}
if (!isKnocked(entity)) {
knockedEntities.put(entity.getUUID(), KNOCKED_DURATION);
entity.setHealth(1.0F);
}
}
public static boolean isKnocked(LivingEntity entity) {
return knockedEntities.containsKey(entity.getUUID());
}
public static void removeKnockedState(LivingEntity entity) {
knockedEntities.remove(entity.getUUID());
grippedEntities.remove(entity.getUUID());
MobKillHandler.clearKillAttempt(entity.getUUID());
}
public static void setGripped(LivingEntity entity, boolean isGripped) {
if (isGripped) {
grippedEntities.add(entity.getUUID());
} else {
grippedEntities.remove(entity.getUUID());
}
}
public static Collection<UUID> getKnockedUuids() {
return new HashSet<>(knockedEntities.keySet());
}
public static void tickKnockedStates() {
Iterator<Map.Entry<UUID, Integer>> it = knockedEntities.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<UUID, Integer> entry = it.next();
UUID entityId = entry.getKey();
int timeLeft = entry.getValue();
if (grippedEntities.contains(entityId)) {
continue;
}
int newTime = timeLeft - 1;
if (newTime <= 0) {
it.remove();
} else {
entry.setValue(newTime);
}
}
}
@SubscribeEvent
public static void onLivingHurt(LivingHurtEvent event) {
LivingEntity entity = event.getEntity();
if (!isKnocked(entity)) return;
// Always reset the timer regardless of other conditions
knockedEntities.put(entity.getUUID(), KNOCKED_DURATION);
DamageSource source = event.getSource();
String damageType = source.getMsgId();
// Handle lethal damage cases (fire, explosions, etc.)
if (damageType.equals("fire") || damageType.equals("lava") || damageType.equals("onFire") ||
damageType.equals("explosion") || damageType.equals("explosion.player") || damageType.equals("fireball")) {
entity.setHealth(0.0F);
removeKnockedState(entity);
return;
}
// Handle falling or lethal explosion
if ((damageType.equals("fall") || damageType.equals("explosion") || damageType.equals("explosion.player")) &&
event.getAmount() >= entity.getHealth()) {
entity.setHealth(0.0F);
removeKnockedState(entity);
return;
}
// Still cancel damage for non-lethal hits, but **reset timer**
event.setCanceled(true);
}
}
@@ -0,0 +1,222 @@
package net.fretux.knockedback;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.AABB;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import static net.fretux.knockedback.KnockedManager.removeKnockedState;
import static net.fretux.knockedback.KnockedManager.setGripped;
/**
* Manages the logic for mobs killing knocked players after a 3-second delay,
* which resets if the mob is hit during those 3 seconds.
*/
public class MobKillHandler {
private static final int EXECUTION_DELAY_TICKS = 3 * 20;
/**
* A record of ongoing kill attempts.
* Key = Knocked player's UUID
* Value = An object tracking the mob's UUID & the countdown in ticks.
*/
private static final Map<UUID, KillAttempt> killAttempts = new HashMap<>();
/**
* Data structure to hold which mob is trying to kill a knocked player,
* along with how many ticks remain before execution.
*/
private static class KillAttempt {
private final UUID mobUuid;
private int timeLeft;
public KillAttempt(UUID mobUuid, int timeLeft) {
this.mobUuid = mobUuid;
this.timeLeft = timeLeft;
}
}
/**
* Runs once per server tick; we look for mobs near knocked players
* and handle the execution countdown.
*/
@SubscribeEvent
public void onServerTick(TickEvent.ServerTickEvent event) {
// Only run logic at the END of each tick on the server
if (event.phase == TickEvent.Phase.END && event.side.isServer()) {
tickKillAttempts();
}
}
/**
* Resets the kill timer if the mob about to perform the kill is attacked.
* "If they're hit in those 3 seconds, the timer resets."
*/
@SubscribeEvent
public void onMobHurt(LivingHurtEvent event) {
LivingEntity entity = event.getEntity();
if (!(entity instanceof Mob mob)) {
return;
}
UUID mobUuid = mob.getUUID();
boolean mobWasReset = false;
UUID playerToRelease = null;
for (Map.Entry<UUID, KillAttempt> entry : killAttempts.entrySet()) {
KillAttempt attempt = entry.getValue();
if (attempt.mobUuid.equals(mobUuid)) {
playerToRelease = entry.getKey();
mobWasReset = true;
break; // Break early because we will remove later
}
}
if (playerToRelease != null) {
killAttempts.remove(playerToRelease);
Player player = getPlayerByUuid(playerToRelease);
if (player != null) {
KnockedManager.setGripped(player, false);
mob.setTarget(null);
mob.getNavigation().stop();
}
}
if (mobWasReset) {
System.out.println("Mob hit: Canceling execution attempt and releasing gripped player.");
}
}
public static void clearKillAttempt(UUID playerUuid) {
killAttempts.remove(playerUuid);
}
/**
* Main logic to find mobs near knocked players and manage execution timers.
*/
private void tickKillAttempts() {
Map<UUID, KillAttempt> updatedAttempts = new HashMap<>();
for (UUID knockedPlayerUuid : KnockedManager.getKnockedUuids()) {
Player knockedPlayer = getPlayerByUuid(knockedPlayerUuid);
if (knockedPlayer == null || !knockedPlayer.isAlive()) {
continue;
}
Mob mobInRange = getMobInRange(knockedPlayer);
if (mobInRange == null) {
setGripped(knockedPlayer, false);
continue;
}
setGripped(knockedPlayer, true);
KillAttempt attempt = killAttempts.get(knockedPlayerUuid);
if (attempt == null || !attempt.mobUuid.equals(mobInRange.getUUID())) {
attempt = new KillAttempt(mobInRange.getUUID(), EXECUTION_DELAY_TICKS);
} else {
attempt.timeLeft--;
gripPlayer(knockedPlayer, mobInRange);
if (attempt.timeLeft <= 0) {
executeKnockedPlayer(knockedPlayer, mobInRange);
continue;
}
}
updatedAttempts.put(knockedPlayerUuid, attempt);
}
killAttempts.clear();
killAttempts.putAll(updatedAttempts);
}
/**
* Handles gripping behavior where the mob focuses on the knocked player.
*/
private void gripPlayer(Player knockedPlayer, Mob mob) {
mob.setTarget(knockedPlayer);
mob.getNavigation().stop();
spawnExecutionParticles(knockedPlayer, mob);
}
/**
* Executes the knocked player when the countdown finishes.
*/
private void executeKnockedPlayer(Player knockedPlayer, Mob mob) {
knockedPlayer.setHealth(0.0F);
mob.getNavigation().stop();
removeKnockedState(knockedPlayer);
knockedPlayer.sendSystemMessage(Component.literal(
"You were executed by " + mob.getName().getString() + "!"
));
}
/**
* Find a mob that is within a certain range of the knocked player.
* Only hostile mobs, aggressive neutral mobs, and players are considered.
*/
@Nullable
private Mob getMobInRange(Player knockedPlayer) {
if (knockedPlayer.level() instanceof ServerLevel serverLevel) {
double range = 2.5;
AABB aabb = new AABB(
knockedPlayer.getX() - range, knockedPlayer.getY() - range, knockedPlayer.getZ() - range,
knockedPlayer.getX() + range, knockedPlayer.getY() + range, knockedPlayer.getZ() + range
);
return serverLevel.getEntitiesOfClass(Mob.class, aabb).stream()
.filter(mob -> isHostile(mob) || isAggressiveNeutral(mob))
.findAny()
.orElse(null);
}
return null;
}
/**
* Checks if a mob is hostile (e.g., zombies, skeletons).
*/
private boolean isHostile(Mob mob) {
return mob instanceof net.minecraft.world.entity.monster.Monster;
}
/**
* Checks if a neutral mob is aggressive (e.g., a wolf that has been attacked).
*/
private boolean isAggressiveNeutral(Mob mob) {
if (mob instanceof net.minecraft.world.entity.animal.Wolf wolf) {
return wolf.isAngry();
}
if (mob instanceof net.minecraft.world.entity.monster.EnderMan enderman) {
return enderman.isCreepy();
}
return false;
}
/**
* Helper to retrieve a Player by UUID from the server.
*/
@Nullable
private Player getPlayerByUuid(UUID uuid) {
var server = net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer();
if (server != null) {
return server.getPlayerList().getPlayer(uuid);
}
return null;
}
private void spawnExecutionParticles(Player knockedPlayer, Mob mobInRange) {
if (knockedPlayer.level() instanceof ServerLevel serverLevel) {
serverLevel.sendParticles(
net.minecraft.core.particles.ParticleTypes.ANGRY_VILLAGER,
mobInRange.getX(),
mobInRange.getY() + mobInRange.getBbHeight() / 2,
mobInRange.getZ(),
1, 0.1, 0.2, 0.1, 0.005
);
}
}
}
@@ -0,0 +1,26 @@
package net.fretux.knockedback;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.network.NetworkRegistry;
import net.minecraftforge.network.simple.SimpleChannel;
public class NetworkHandler {
public static final String PROTOCOL_VERSION = "1";
public static SimpleChannel CHANNEL;
public static void register() {
CHANNEL = NetworkRegistry.newSimpleChannel(
new ResourceLocation(KnockedBack.MOD_ID, "main"),
() -> PROTOCOL_VERSION,
PROTOCOL_VERSION::equals,
PROTOCOL_VERSION::equals
);
int id = 0;
CHANNEL.registerMessage(id++,
ExecuteKnockedPacket.class,
ExecuteKnockedPacket::encode,
ExecuteKnockedPacket::decode,
ExecuteKnockedPacket::handle);
}
}
@@ -0,0 +1,15 @@
package net.fretux.knockedback;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import java.util.UUID;
public class NetworkHandlerHelper {
public static ServerPlayer getPlayerByUuid(MinecraftServer server, UUID uuid) {
if (server == null) {
return null;
}
return server.getPlayerList().getPlayer(uuid);
}
}
@@ -0,0 +1,32 @@
package net.fretux.knockedback;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
@Mod.EventBusSubscriber(modid = "knockedback", bus = Mod.EventBusSubscriber.Bus.MOD)
public class PlayerExecutionHandler {
public static final double EXECUTION_RANGE = 2.0;
public static void register() {
MinecraftForge.EVENT_BUS.register(new PlayerExecutionHandler());
}
@SubscribeEvent
public static void onCommonSetup(FMLCommonSetupEvent event) {
register();
}
public static void executeKnockedPlayer(ServerPlayer executor, LivingEntity knockedPlayer) {
KnockedManager.removeKnockedState(knockedPlayer);
knockedPlayer.setHealth(0.0F);
executor.sendSystemMessage(Component.literal("You executed " + knockedPlayer.getName().getString() + "!"));
if (knockedPlayer instanceof ServerPlayer serverPlayer) {
serverPlayer.sendSystemMessage(Component.literal("You were executed by " + executor.getName().getString() + "!"));
}
}
}
@@ -0,0 +1,45 @@
package net.fretux.knockedback.client;
import net.fretux.knockedback.ExecuteKnockedPacket;
import net.fretux.knockedback.NetworkHandler;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.network.chat.Component;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.RegisterKeyMappingsEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.lwjgl.glfw.GLFW;
@Mod.EventBusSubscriber(modid = "knockedback", value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.FORGE)
public class ClientKeyHandler {
public static final KeyMapping EXECUTE_KEY = new KeyMapping(
"key.knockedback.execute",
GLFW.GLFW_KEY_B,
"key.categories.knockedback"
);
// This event still needs to be registered on the MOD bus for key mapping registration.
@Mod.EventBusSubscriber(modid = "knockedback", value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD)
public static class KeyMappingRegistration {
@SubscribeEvent
public static void registerKeyMappings(RegisterKeyMappingsEvent event) {
event.register(EXECUTE_KEY);
}
}
@SubscribeEvent
public static void onClientTick(TickEvent.ClientTickEvent event) {
if (Minecraft.getInstance().screen != null) return;
if (EXECUTE_KEY.consumeClick()) {
if (Minecraft.getInstance().player != null) {
Minecraft.getInstance().player.sendSystemMessage(
Component.literal("Key B pressed! Sending execution request to server.")
);
// Send the packet to the server
NetworkHandler.CHANNEL.sendToServer(new ExecuteKnockedPacket());
}
}
}
}
@@ -0,0 +1,55 @@
package net.fretux.knockedback.effects;
import net.fretux.knockedback.KnockedManager;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.event.entity.living.LivingEvent;
import net.minecraftforge.event.entity.player.AttackEntityEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(modid = "knockedback", bus = Mod.EventBusSubscriber.Bus.FORGE)
public class KnockedEffect {
@SubscribeEvent
public static void onPlayerTick(LivingEvent.LivingTickEvent event) {
if (!(event.getEntity() instanceof Player player)) return;
if (KnockedManager.isKnocked(player)) {
player.setDeltaMovement(0, 0, 0);
player.setSprinting(false);
player.hurtMarked = true;
}
}
@SubscribeEvent
public static void onPlayerInteract(PlayerInteractEvent event) {
if (KnockedManager.isKnocked(event.getEntity())) {
if (event.isCancelable()) {
event.setCanceled(true);
}
}
}
@SubscribeEvent
public static void onBreakBlock(PlayerEvent.BreakSpeed event) {
if (KnockedManager.isKnocked(event.getEntity())) {
event.setCanceled(true);
}
}
@SubscribeEvent
public static void onAttack(AttackEntityEvent event) {
Player player = event.getEntity();
if (KnockedManager.isKnocked(player)) {
event.setCanceled(true);
}
}
@SubscribeEvent
public static void onLivingJump(LivingEvent.LivingJumpEvent event) {
if (event.getEntity() instanceof Player player && KnockedManager.isKnocked(player)) {
event.getEntity().setDeltaMovement(0, 0, 0);
}
}
}