Skip to content
This repository was archived by the owner on Aug 9, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/main/java/red/jackf/lenientdeath/ClosestSafeBlock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package red.jackf.lenientdeath;

import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.material.Fluids;


public class ClosestSafeBlock {

public static GlobalPos find(ServerLevel level, GlobalPos startPos)
{
var config = LenientDeath.CONFIG.instance().itemResilience;

if (level == null) {
return null; // If there's no world available (e.g., client is null)
}

BlockPos closestPos = null;
double closestDistanceSq = Double.MAX_VALUE; // Initialize to a large value

double immediateProximitySq = 30 * 30; // 30 meters, squared

BlockPos startBlockPos = startPos.pos();
int xCenter = startBlockPos.getX();
int yCenter = startBlockPos.getY();
int zCenter = startBlockPos.getZ();

// Perform a spiral search
for (int radius = 0; radius <= config.closestSafeBlockSearchRange; radius++) {
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
if (Math.abs(dx) != radius && Math.abs(dz) != radius) {
// Skip positions not on the edge of the current radius
continue;
}

for (int dy = -radius; dy <= radius; dy++) { // Include height variation
BlockPos checkPos = new BlockPos(xCenter + dx, yCenter + dy, zCenter + dz);

if (isValidPosition(level, checkPos)) {
double distanceSq = startBlockPos.distSqr(checkPos);

// Return immediately if within immediate proximity
if (distanceSq <= immediateProximitySq) {
return GlobalPos.of(level.dimension(), checkPos.above());
}

// Otherwise, track the closest position found
if (distanceSq < closestDistanceSq) {
closestDistanceSq = distanceSq;
closestPos = checkPos;
}
}
}
}
}
}

// Return the closest valid position found or fallback to spawn
if (closestPos != null) {
return GlobalPos.of(level.dimension(), closestPos.above());
}

return GlobalPos.of(level.dimension(), level.getSharedSpawnPos());
}

public static boolean isValidPosition(ServerLevel level, BlockPos pos) {
var checkPosBlockState = level.getBlockState(pos);
var aboveCheckPosBlockState = level.getBlockState(pos.above());
var aboveCheckPosFluidState = level.getFluidState(pos.above());

var isSolid = checkPosBlockState.isSolid();
var isAirAbove = aboveCheckPosBlockState.isAir();
var isLavaAbove = aboveCheckPosBlockState.is(Blocks.LAVA) || aboveCheckPosFluidState.is(Fluids.LAVA);

return isSolid && isAirAbove && !isLavaAbove;
}



}
50 changes: 50 additions & 0 deletions src/main/java/red/jackf/lenientdeath/ItemResilience.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import red.jackf.lenientdeath.mixinutil.LDDeathContextHolder;
import red.jackf.lenientdeath.mixinutil.LDGroundedPosHolder;


public class ItemResilience {
private static final TagKey<DamageType> ITEMS_IMMUNE_TO = TagKey.create(
Registries.DAMAGE_TYPE,
Expand All @@ -37,18 +38,57 @@ public static boolean areItemsImmuneTo(DamageSource source) {
var groundedPosHolder = (LDGroundedPosHolder) serverPlayer;
var ctx = deathContextHolder.lenientdeath$getDeathContext();
var groundedPos = groundedPosHolder.lenientdeath$getLastGroundedPosition();

//If the groundedPos is not valid (e.g Air or Lava) then find a safe block.
if(!ClosestSafeBlock.isValidPosition(serverPlayer.serverLevel(),groundedPos.pos()))
{
//If there is no LastGroundedPosition or if the groundedPos is too far away from the death location, use the actual player location
if(groundedPos == null || groundedPos.pos().distSqr(serverPlayer.getOnPos()) >= 100)
groundedPos = GlobalPos.of(serverPlayer.serverLevel().dimension(), serverPlayer.getOnPos());

groundedPos = ClosestSafeBlock.find(serverPlayer.serverLevel(),groundedPos);
}

if (ctx != null && groundedPos != null && ctx.source().is(DamageTypes.FELL_OUT_OF_WORLD)) {
return ifTrue.apply(ctx, groundedPos, serverPlayer);
}
}
return null;
}

public static <T> @Nullable T ifHandledLavaDeath(
Object player,
TriFunction<DeathContext, GlobalPos, ServerPlayer, T> ifTrue) {
if (LenientDeath.CONFIG.instance().itemResilience.lavaRecovery.mode == LenientDeathConfig.ItemResilience.LavaRecovery.Mode.closest_safe_location
&& player instanceof ServerPlayer serverPlayer) {
var deathContextHolder = (LDDeathContextHolder) serverPlayer;
var groundedPosHolder = (LDGroundedPosHolder) serverPlayer;
var ctx = deathContextHolder.lenientdeath$getDeathContext();
var groundedPos = groundedPosHolder.lenientdeath$getLastGroundedPosition();

//If there is no LastGroundedPosition or if the groundedPos is too far away from the death location, use the actual player location
if(groundedPos == null || groundedPos.pos().distSqr(serverPlayer.getOnPos()) >= 30)
groundedPos = GlobalPos.of(serverPlayer.serverLevel().dimension(), serverPlayer.getOnPos());

//Determine the closet safe block
var safePos = ClosestSafeBlock.find(serverPlayer.serverLevel(), groundedPos);

if (ctx != null && safePos != null && ctx.source().is(DamageTypes.LAVA)) {
return ifTrue.apply(ctx, safePos, serverPlayer);
}
}
return null;
}

public static boolean shouldForceKeep(ServerPlayer player) {
if (LenientDeath.CONFIG.instance().itemResilience.voidRecovery.mode == LenientDeathConfig.ItemResilience.VoidRecovery.Mode.preserve) {
var deathContext = ((LDDeathContextHolder) player).lenientdeath$getDeathContext();
return deathContext != null && deathContext.source().is(DamageTypes.FELL_OUT_OF_WORLD);
}
if (LenientDeath.CONFIG.instance().itemResilience.lavaRecovery.mode == LenientDeathConfig.ItemResilience.LavaRecovery.Mode.preserve) {
var deathContext = ((LDDeathContextHolder) player).lenientdeath$getDeathContext();
return deathContext != null && deathContext.source().is(DamageTypes.LAVA);
}
return false;
}

Expand All @@ -60,7 +100,17 @@ public static void onPlayerDeath(ServerPlayer serverPlayer) {
Formatting.variable(groundedPos.pos().above().toShortString()),
Formatting.variable(groundedPos.dimension().location().toString()))
));
return null;
});
}

if (LenientDeath.CONFIG.instance().itemResilience.lavaRecovery.announce) {
ifHandledLavaDeath(serverPlayer, (ctx, groundedPos, serverPlayer1) -> {
serverPlayer1.sendSystemMessage(Formatting.infoLine(
Component.translatable("lenientdeath.itemResilience.announce",
Formatting.variable(groundedPos.pos().above().toShortString()),
Formatting.variable(groundedPos.dimension().location().toString()))
));
return null;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ private static LiteralArgumentBuilder<CommandSourceStack> createItemResilience()
));

root.then(makeVoidRecoveryNode());
root.then(makeLavaRecoveryNode());

return root;
}
Expand Down Expand Up @@ -460,6 +461,30 @@ private static LiteralArgumentBuilder<CommandSourceStack> makeVoidRecoveryNode()
return root;
}

private static LiteralArgumentBuilder<CommandSourceStack> makeLavaRecoveryNode() {
var root = Commands.literal("lavaRecovery");

root.then(makeEnum(
"mode",
"itemResilience.lavaRecovery.mode",
WikiPage.ITEM_RESILIENCE,
LenientDeathConfig.ItemResilience.LavaRecovery.Mode.class,
config -> config.itemResilience.lavaRecovery.mode,
(config, newValue) -> config.itemResilience.lavaRecovery.mode = newValue
));

root.then(makeBoolean(
"announce",
"itemResilience.lavaRecovery.announce",
WikiPage.ITEM_RESILIENCE,
config -> config.itemResilience.lavaRecovery.announce,
(config, newValue) -> config.itemResilience.lavaRecovery.announce = newValue
));

return root;
}


private static LiteralArgumentBuilder<CommandSourceStack> createPresetsNode() {
var root = Commands.literal("presets");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ public static class ItemResilience {
Default: false""")
public boolean allDeathItemsAreExplosionProof = false;

@Comment("""
Dertermines the search range for getting the closest safe block when using LavaRecovery or when the last position of the VoidRecovery is invalid.
Options: Integer
Default: 256""")
public int closestSafeBlockSearchRange = 256;

@Comment("""
Features related to handling item drops when a player dies to the void; for example if they fall off the
end island, or are playing SkyBlock.""")
Expand Down Expand Up @@ -239,6 +245,37 @@ public enum Mode {
preserve
}
}

@Comment("""
Features related to handling item drops when a player dies to swimming in lava; for example if they fall into a lava lake in the nether.""")
public LavaRecovery lavaRecovery = new LavaRecovery();

public static class LavaRecovery {

@Comment("""
How death drop items when the player is killed by lava should be handled?
Options:
- disabled (keeps items position)
- closest_safe_location (teleport to closest safe position (not in air or lava)
- preserve (keep items in the inventory even if they wouldn't normally; applies to everyone)
Default: closest_safe_location""")
public Mode mode = Mode.closest_safe_location;

@Comment("""
When a player dies to being in lava, should Lenient Death notify them where their items were moved to? Only
applies if mode = closest_safe_location.
This option exists because players who may not be aware of this feature probably would not look for
their items otherwise.
Options: true, false
Default: true""")
public boolean announce = true;

public enum Mode {
disabled,
closest_safe_location,
preserve
}
}
}

@Comment("""
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/red/jackf/lenientdeath/config/Presets.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ private static LenientDeathConfig makeDisabled() {
config.droppedItemGlow.enabled = false;

config.itemResilience.voidRecovery.mode = LenientDeathConfig.ItemResilience.VoidRecovery.Mode.disabled;
config.itemResilience.lavaRecovery.mode = LenientDeathConfig.ItemResilience.LavaRecovery.Mode.disabled;

config.deathCoordinates.sendToServerLog = false;
config.deathCoordinates.sendToDeadPlayer = false;
Expand Down Expand Up @@ -59,6 +60,7 @@ private static LenientDeathConfig makeOnlyVisuals() {
config.preserveItemsOnDeath.byItemType.enabled = false;

config.itemResilience.voidRecovery.mode = LenientDeathConfig.ItemResilience.VoidRecovery.Mode.disabled;
config.itemResilience.lavaRecovery.mode = LenientDeathConfig.ItemResilience.LavaRecovery.Mode.disabled;

return config;
}
Expand All @@ -71,6 +73,7 @@ private static LenientDeathConfig makeOnlyRandom() {
config.preserveItemsOnDeath.randomizer.enabled = true;

config.itemResilience.voidRecovery.mode = LenientDeathConfig.ItemResilience.VoidRecovery.Mode.disabled;
config.itemResilience.lavaRecovery.mode = LenientDeathConfig.ItemResilience.LavaRecovery.Mode.disabled;

return config;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.Tag;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.Entity;
Expand Down Expand Up @@ -98,35 +99,81 @@ private void saveGroundedPos(CompoundTag tag, CallbackInfo ci) {
@ModifyReceiver(method = "drop(Lnet/minecraft/world/item/ItemStack;ZZ)Lnet/minecraft/world/entity/item/ItemEntity;",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;addFreshEntity(Lnet/minecraft/world/entity/Entity;)Z"))
private Level lenientdeath$moveIfVoidedAndEnabled(Level original, Entity entity) {
var targetLevel = ItemResilience.ifHandledVoidDeath(this, (deathContext, lastGroundedPos, player) -> {
ServerLevel targetLevel = null;

// Handling void death scenario
var voidTargetLevel = ItemResilience.ifHandledVoidDeath(this, (deathContext, lastGroundedPos, player) -> {
if (!lastGroundedPos.dimension().equals(original.dimension())) {
return this.server.getLevel(lastGroundedPos.dimension());
} else {
return null;
}
});

// Handling lava death scenario
var lavaTargetLevel = ItemResilience.ifHandledLavaDeath(this, (deathContext, lastGroundedPos, player) -> {
if (!lastGroundedPos.dimension().equals(original.dimension())) {
return this.server.getLevel(lastGroundedPos.dimension());
} else {
return null;
}
});

// Assigning the target level if any of the scenarios is met
if (voidTargetLevel != null) {
targetLevel = voidTargetLevel;
} else if (lavaTargetLevel != null) {
targetLevel = lavaTargetLevel;
}

if (targetLevel != null) return targetLevel;
return original;
}

@WrapOperation(method = "createItemStackToDrop", at = @At(value = "NEW", target = "(Lnet/minecraft/world/level/Level;DDDLnet/minecraft/world/item/ItemStack;)Lnet/minecraft/world/entity/item/ItemEntity;"))
private ItemEntity spawnDeathItemAtDifferentPosition(Level level, double posX, double posY, double posZ, ItemStack itemStack, Operation<ItemEntity> original) {
ItemEntity item = ItemResilience.ifHandledVoidDeath(this, (deathContext, groundPos, player) -> {

ItemEntity item = null;

// Handling void death scenario
var voidDeathItem = ItemResilience.ifHandledVoidDeath(this, (deathContext, groundPos, player) -> {
Vec3 pos = groundPos.pos().getCenter();
return original.call(this.server.getLevel(groundPos.dimension()), pos.x(), pos.y() + 1, pos.z(), itemStack);
});

// Handling lava death scenario
var lavaDeathItem = ItemResilience.ifHandledLavaDeath(this, (deathContext, groundPos, player) -> {
Vec3 pos = groundPos.pos().getCenter();
return original.call(this.server.getLevel(groundPos.dimension()), pos.x(), pos.y() + 1, pos.z(), itemStack);
});

// Assigning the item if any of the scenarios is met
if (voidDeathItem != null) {
item = voidDeathItem;
} else if (lavaDeathItem != null) {
item = lavaDeathItem;
}

if (item == null) item = original.call(level, posX, posY, posZ, itemStack);

return item;
}

@ModifyReturnValue(method = "createItemStackToDrop", at = @At("RETURN"))
private ItemEntity killDeathItemVelocity(ItemEntity original) {

// Handling void death scenario
ItemResilience.ifHandledVoidDeath(this, (deathContext, groundPos, player) -> {
if (original != null) original.setDeltaMovement(Vec3.ZERO);
return null;
});

// Handling lava death scenario
ItemResilience.ifHandledLavaDeath(this, (deathContext, groundPos, player) -> {
if (original != null) original.setDeltaMovement(Vec3.ZERO);
return null;
});

return original;
}
}
Loading