From cf9b9f26f238d8b5ac836f6977cd6483d1a83718 Mon Sep 17 00:00:00 2001
From: Reece Mackie <20544390+Rover656@users.noreply.github.com>
Date: Wed, 11 Dec 2024 01:31:11 +0000
Subject: [PATCH] fix: Tweak xp obelisk calculations to avoid reaching a
 lockup.

Also adds explicit check for this happening again, so we get useful log messages instead of nothing.
Fixes: GH-913
---
 .../blockentity/XPObeliskBlockEntity.java     | 62 ++++++++++++++-----
 1 file changed, 48 insertions(+), 14 deletions(-)

diff --git a/enderio-machines/src/main/java/com/enderio/machines/common/blockentity/XPObeliskBlockEntity.java b/enderio-machines/src/main/java/com/enderio/machines/common/blockentity/XPObeliskBlockEntity.java
index 41c0ad4e7d..d2bc3f8ef1 100644
--- a/enderio-machines/src/main/java/com/enderio/machines/common/blockentity/XPObeliskBlockEntity.java
+++ b/enderio-machines/src/main/java/com/enderio/machines/common/blockentity/XPObeliskBlockEntity.java
@@ -13,10 +13,12 @@
 import com.enderio.machines.common.io.fluid.MachineTankLayout;
 import com.enderio.machines.common.io.fluid.TankAccess;
 import com.enderio.machines.common.menu.XPObeliskMenu;
+import com.mojang.logging.LogUtils;
 import net.minecraft.core.BlockPos;
 import net.minecraft.core.HolderLookup;
 import net.minecraft.core.component.DataComponentMap;
 import net.minecraft.nbt.CompoundTag;
+import net.minecraft.util.Mth;
 import net.minecraft.world.entity.player.Inventory;
 import net.minecraft.world.entity.player.Player;
 import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -26,6 +28,7 @@
 import net.neoforged.neoforge.fluids.SimpleFluidContent;
 import net.neoforged.neoforge.fluids.capability.IFluidHandler;
 import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
 
 public class XPObeliskBlockEntity extends MachineBlockEntity implements FluidTankUser {
 
@@ -33,12 +36,14 @@ public class XPObeliskBlockEntity extends MachineBlockEntity implements FluidTan
     private final MachineFluidHandler fluidHandler;
     private static final TankAccess TANK = new TankAccess();
 
+    private static final Logger LOGGER = LogUtils.getLogger();
+
     public XPObeliskBlockEntity(BlockPos worldPosition, BlockState blockState) {
         super(MachineBlockEntities.XP_OBELISK.get(), worldPosition, blockState);
         fluidHandler = createFluidHandler();
 
         this.xpTankDataSlot = NetworkDataSlot.INT.create(() -> TANK.getFluidAmount(this),
-            amount -> TANK.setFluid(this, new FluidStack(EIOFluids.XP_JUICE.getSource(), amount)));
+                amount -> TANK.setFluid(this, new FluidStack(EIOFluids.XP_JUICE.getSource(), amount)));
         addDataSlot(xpTankDataSlot);
     }
 
@@ -50,7 +55,9 @@ public AbstractContainerMenu createMenu(int containerId, Inventory playerInvento
 
     @Override
     public MachineTankLayout getTankLayout() {
-        return new MachineTankLayout.Builder().tank(TANK, Integer.MAX_VALUE, fluidStack -> fluidStack.is(EIOTags.Fluids.EXPERIENCE)).build();
+        return new MachineTankLayout.Builder()
+                .tank(TANK, Integer.MAX_VALUE, fluidStack -> fluidStack.is(EIOTags.Fluids.EXPERIENCE))
+                .build();
     }
 
     @Override
@@ -64,7 +71,8 @@ protected void onContentsChanged(int slot) {
 
             @Override
             public int fill(FluidStack resource, FluidAction action) {
-                // TODO: Avoid filling beyond 2,147,483,640 (INT_MAX - 7) to avoid having 7mb that cannot be extracted?
+                // TODO: Avoid filling beyond 2,147,483,640 (INT_MAX - 7) to avoid having 7mb
+                // that cannot be extracted?
 
                 // Convert into XP Juice
                 if (TANK.isFluidValid(this, resource)) {
@@ -99,7 +107,8 @@ public void addLevelsToPlayer(Player player, int levelsToAdd) {
 
     public void removeLevelsFromPlayer(Player player, int levelsToRemove) {
         long playerExperience = ExperienceUtil.getPlayerTotalXp(player);
-        long targetExperience = ExperienceUtil.getTotalXpFromLevel(Math.max(0, player.experienceLevel - levelsToRemove));
+        long targetExperience = ExperienceUtil
+                .getTotalXpFromLevel(Math.max(0, player.experienceLevel - levelsToRemove));
         removePlayerXp(player, playerExperience - targetExperience);
     }
 
@@ -130,10 +139,25 @@ private void addPlayerXp(Player player, long experience) {
 
         // Add the XP to the player
         // Workaround some floating point problems when adding all the exp at once.
-        // If we add it all at once, the experienceProgress gets messed up and then the next extract is wonky.
+        // If we add it all at once, the experienceProgress gets messed up and then the
+        // next extract is wonky.
         int xpToAdd = drained.getAmount() / ExperienceUtil.EXP_TO_FLUID;
         while (xpToAdd > 0) {
-            int xp = Math.min(xpToAdd, (int)Math.floor((1 - player.experienceProgress) * ExperienceUtil.getXpNeededForNextLevel(player.experienceLevel)));
+            int xp = Mth.clamp((int) Math.floor(
+                    (1 - player.experienceProgress) * ExperienceUtil.getXpNeededForNextLevel(player.experienceLevel)),
+                    0, xpToAdd);
+
+            // If we can't add the rest of this level's progress, move on.
+            if (xp <= 0) {
+                xp = Mth.clamp(ExperienceUtil.getXpNeededForNextLevel(player.experienceLevel + 1), 0, xpToAdd);
+            }
+
+            if (xp <= 0) {
+                LOGGER.error("xp <= 0 in addPlayerXp. experienceLevel: {}, experienceProgress: {}, xpToAdd: {}, xp: {}",
+                        player.experienceLevel, player.experienceProgress, xpToAdd, xp);
+                throw new IllegalStateException("xp <= 0 in addPlayerXp.");
+            }
+
             player.giveExperiencePoints(xp);
             xpToAdd -= xp;
         }
@@ -152,20 +176,30 @@ private void removePlayerXp(Player player, long experience) {
         cappedVolume = cappedVolume - cappedVolume % ExperienceUtil.EXP_TO_FLUID;
 
         // Add the fluid
-        int filled = TANK.fill(this, new FluidStack(EIOFluids.XP_JUICE.getSource(), cappedVolume), IFluidHandler.FluidAction.EXECUTE);
+        int filled = TANK.fill(this, new FluidStack(EIOFluids.XP_JUICE.getSource(), cappedVolume),
+                IFluidHandler.FluidAction.EXECUTE);
 
         // Remove the XP from the player
         // Workaround some floating point problems when adding all the exp at once.
-        // If we add it all at once, the experienceProgress gets messed up and then the next extract is wonky.
+        // If we add it all at once, the experienceProgress gets messed up and then the
+        // next extract is wonky.
         int xpToRemove = filled / ExperienceUtil.EXP_TO_FLUID;
         while (xpToRemove > 0) {
-            int xp;
+            int xp = Mth.clamp(
+                    (int) Math.floor(
+                            player.experienceProgress * ExperienceUtil.getXpNeededForNextLevel(player.experienceLevel)),
+                    0, xpToRemove);
+
+            // If we can't remove the rest of this level's progress, move on.
+            if (xp <= 0) {
+                xp = Mth.clamp(ExperienceUtil.getXpNeededForNextLevel(player.experienceLevel - 1), 0, xpToRemove);
+            }
 
-            // Remove current level progress, then strip back level-by-level.
-            if (player.experienceProgress > 0) {
-                xp = Math.min(xpToRemove, (int)Math.floor(player.experienceProgress * ExperienceUtil.getXpNeededForNextLevel(player.experienceLevel)));
-            } else {
-                xp = Math.min(xpToRemove, ExperienceUtil.getXpNeededForNextLevel(player.experienceLevel - 1));
+            if (xp <= 0) {
+                LOGGER.error(
+                        "xp <= 0 in removePlayerXp. experienceLevel: {}, experienceProgress: {}, xpToRemove: {}, xp: {}",
+                        player.experienceLevel, player.experienceProgress, xpToRemove, xp);
+                throw new IllegalStateException("xp <= 0 in removePlayerXp.");
             }
 
             player.giveExperiencePoints(-xp);