From 227477dac60a11bff59018680a7cda5fd68219be Mon Sep 17 00:00:00 2001 From: Stephen Chryn Date: Sat, 30 May 2026 21:11:49 -0400 Subject: [PATCH 1/5] Add debugs to check for health discrepancy --- .../common/damagesystem/DamageablePart.java | 15 +++++++++++++++ .../common/damagesystem/PlayerDamageModel.java | 3 +++ 2 files changed, 18 insertions(+) diff --git a/src/main/java/ichttt/mods/firstaid/common/damagesystem/DamageablePart.java b/src/main/java/ichttt/mods/firstaid/common/damagesystem/DamageablePart.java index ffa8911..427bc1e 100644 --- a/src/main/java/ichttt/mods/firstaid/common/damagesystem/DamageablePart.java +++ b/src/main/java/ichttt/mods/firstaid/common/damagesystem/DamageablePart.java @@ -191,6 +191,21 @@ public void setMaxHealth(int maxHealth) { maxHealth = 128; this.maxHealth = Math.max(2, maxHealth); //set 2 as a minimum this.currentHealth = Math.min(currentHealth, this.maxHealth); + + int requestedMax = maxHealth; + float oldCurrent = currentHealth; + int oldMax = this.maxHealth; + + FirstAid.LOGGER.info( + "[FirstAid part clamp] part={} requestedMax={} oldMax={} newMax={} oldCurrent={} newCurrent={} capMaxHealth={}", + part, + requestedMax, + oldMax, + this.maxHealth, + oldCurrent, + this.currentHealth, + FirstAidConfig.SERVER.capMaxHealth.get() + ); } @Override diff --git a/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java b/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java index ad59dc6..85e9b70 100644 --- a/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java +++ b/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java @@ -383,6 +383,9 @@ public void revivePlayer(Player player) { public void runScaleLogic(Player player) { if (FirstAidConfig.SERVER.scaleMaxHealth.get()) { //Attempt to calculate the max health of the body parts based on the maxHealth attribute player.level().getProfiler().push("healthscaling"); + if (FirstAidConfig.GENERAL.debug.get()) { + FirstAid.LOGGER.info("[FirstAid scale] player={} tick={} vanillaMax={} prevScaleFactor={} limbMaxTotal={}", player.getName().getString(), player.tickCount, player.getMaxHealth(), prevScaleFactor, getCurrentMaxHealth()); + } float globalFactor = player.getMaxHealth() / 20F; if (prevScaleFactor != globalFactor) { if (FirstAidConfig.GENERAL.debug.get()) { From b103c5922c35df9515613f837f9733a7ec152147 Mon Sep 17 00:00:00 2001 From: Stephen Chryn Date: Sat, 30 May 2026 21:12:32 -0400 Subject: [PATCH 2/5] Fix compatability issue with mods that give player extra health --- .../damagesystem/PlayerDamageModel.java | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java b/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java index 85e9b70..f9186c6 100644 --- a/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java +++ b/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java @@ -135,6 +135,8 @@ public void tick(Level world, Player player) { else if (sleepBlockTicks < 0) throw new RuntimeException("Negative sleepBlockTicks " + sleepBlockTicks); + runScaleLogic(player); + float newCurrentHealth = calculateNewCurrentHealth(player); if (Float.isNaN(newCurrentHealth)) { FirstAid.LOGGER.warn("New current health is not a number, setting it to 0!"); @@ -164,8 +166,6 @@ else if (sleepBlockTicks < 0) if (!this.hasTutorial) this.hasTutorial = CapProvider.tutorialDone.contains(player.getName().getString()); - runScaleLogic(player); - MobEffect morphineEffect = RegistryObjects.MORPHINE_EFFECT.get(); //morphine update if (this.needsMorphineUpdate) { @@ -379,6 +379,18 @@ public void revivePlayer(Player player) { FirstAid.NETWORKING.send(PacketDistributor.PLAYER.with(() -> (ServerPlayer) player), new MessageSyncDamageModel(this, true)); //Upload changes to the client } + private static int sanitizeMaxHealth(int maxHealth) { + if (maxHealth > 12 && FirstAidConfig.SERVER.capMaxHealth.get()) { + maxHealth = 12; + } + + if (maxHealth > 128) { + maxHealth = 128; + } + + return Math.max(2, maxHealth); + } + @Override public void runScaleLogic(Player player) { if (FirstAidConfig.SERVER.scaleMaxHealth.get()) { //Attempt to calculate the max health of the body parts based on the maxHealth attribute @@ -396,6 +408,7 @@ public void runScaleLogic(Player player) { int added = 0; float expectedNewMaxHealth = 0F; int newMaxHealth = 0; + Map targetMaxHealth = new IdentityHashMap<>(); for (AbstractDamageablePart part : this) { float floatResult = ((float) part.initialMaxHealth) * globalFactor; expectedNewMaxHealth += floatResult; @@ -416,11 +429,13 @@ public void runScaleLogic(Player player) { reduced++; } } + result = sanitizeMaxHealth(result); + targetMaxHealth.put(part, result); newMaxHealth += result; if (FirstAidConfig.GENERAL.debug.get()) { FirstAid.LOGGER.info("Part {} max health: {} initial; {} old; {} new", part.part.name(), part.initialMaxHealth, part.getMaxHealth(), result); } - part.setMaxHealth(result); + // part.setMaxHealth(result); } player.level().getProfiler().popPush("correcting"); if (Math.abs(expectedNewMaxHealth - newMaxHealth) >= 2F) { @@ -431,24 +446,37 @@ public void runScaleLogic(Player player) { for (AbstractDamageablePart part : this) { prioList.add(part); } - prioList.sort(Comparator.comparingInt(AbstractDamageablePart::getMaxHealth)); + prioList.sort(Comparator.comparingInt(part -> targetMaxHealth.getOrDefault(part, part.getMaxHealth()))); + // prioList.sort(Comparator.comparingInt(AbstractDamageablePart::getMaxHealth)); for (AbstractDamageablePart part : prioList) { - int maxHealth = part.getMaxHealth(); + int oldTarget = targetMaxHealth.getOrDefault(part, part.getMaxHealth()); if (FirstAidConfig.GENERAL.debug.get()) { FirstAid.LOGGER.info("Part {}: Second stage with total diff {}", part.part.name(), Math.abs(expectedNewMaxHealth - newMaxHealth)); } + int newTarget = oldTarget; if (expectedNewMaxHealth > newMaxHealth) { - part.setMaxHealth(maxHealth + 2); - newMaxHealth += (part.getMaxHealth() - maxHealth); + newTarget = sanitizeMaxHealth(oldTarget + 2); } else if (expectedNewMaxHealth < newMaxHealth) { - part.setMaxHealth(maxHealth - 2); - newMaxHealth -= (maxHealth - part.getMaxHealth()); + newTarget = sanitizeMaxHealth(oldTarget - 2); + } + targetMaxHealth.put(part, newTarget); + newMaxHealth += newTarget - oldTarget; + if (FirstAidConfig.GENERAL.debug.get()) { + FirstAid.LOGGER.info("Part {}: corrected target {} -> {}; total now {}; expected {}", part.part.name(), oldTarget, newTarget, newMaxHealth, expectedNewMaxHealth); } if (Math.abs(expectedNewMaxHealth - newMaxHealth) < 2F) { break; } } } + player.level().getProfiler().popPush("apply"); + for (AbstractDamageablePart part : this) { + int target = targetMaxHealth.getOrDefault(part, part.getMaxHealth()); + if (FirstAidConfig.GENERAL.debug.get()) { + FirstAid.LOGGER.info("Part {} applying final max health: {} old; {} final", part.part.name(), part.getMaxHealth(), target); + } + part.setMaxHealth(target); + } player.level().getProfiler().pop(); } prevScaleFactor = globalFactor; From 576b187943e5b24dfba2cf45abc1c13e16fb9304 Mon Sep 17 00:00:00 2001 From: Stephen Chryn Date: Sat, 30 May 2026 22:17:04 -0400 Subject: [PATCH 3/5] Set scale logic to serverside only - Fixes phantom limb bug --- .../damagesystem/PlayerDamageModel.java | 177 ++++++++++-------- 1 file changed, 102 insertions(+), 75 deletions(-) diff --git a/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java b/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java index f9186c6..0a05821 100644 --- a/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java +++ b/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java @@ -135,7 +135,9 @@ public void tick(Level world, Player player) { else if (sleepBlockTicks < 0) throw new RuntimeException("Negative sleepBlockTicks " + sleepBlockTicks); - runScaleLogic(player); + if (!world.isClientSide) { + runScaleLogic(player); + } float newCurrentHealth = calculateNewCurrentHealth(player); if (Float.isNaN(newCurrentHealth)) { @@ -393,95 +395,120 @@ private static int sanitizeMaxHealth(int maxHealth) { @Override public void runScaleLogic(Player player) { - if (FirstAidConfig.SERVER.scaleMaxHealth.get()) { //Attempt to calculate the max health of the body parts based on the maxHealth attribute - player.level().getProfiler().push("healthscaling"); - if (FirstAidConfig.GENERAL.debug.get()) { - FirstAid.LOGGER.info("[FirstAid scale] player={} tick={} vanillaMax={} prevScaleFactor={} limbMaxTotal={}", player.getName().getString(), player.tickCount, player.getMaxHealth(), prevScaleFactor, getCurrentMaxHealth()); + if (!FirstAidConfig.SERVER.scaleMaxHealth.get()) { + return; + } + if (player.level().isClientSide) { + return; + } + player.level().getProfiler().push("healthscaling"); + if (FirstAidConfig.GENERAL.debug.get()) { + FirstAid.LOGGER.info("[FirstAid scale] player={} tick={} vanillaMax={} prevScaleFactor={} limbMaxTotal={}", player.getName().getString(), player.tickCount, player.getMaxHealth(), prevScaleFactor, getCurrentMaxHealth()); + } + float globalFactor = player.getMaxHealth() / 20F; + if (prevScaleFactor != globalFactor) { + if (FirstAidConfig.GENERAL.debug.get()) { + FirstAid.LOGGER.info( "Starting health scaling factor {} -> {} (max health {})", prevScaleFactor, globalFactor, player.getMaxHealth()); } - float globalFactor = player.getMaxHealth() / 20F; - if (prevScaleFactor != globalFactor) { + player.level().getProfiler().push("distribution"); + class Target { + final AbstractDamageablePart part; + final float desired; + final float remainder; + int target; + Target(AbstractDamageablePart part, float desired, int target) { + this.part = part; + this.desired = desired; + this.target = target; + this.remainder = desired - target; + } + } + List targets = new ArrayList<>(); + float expectedNewMaxHealth = 0F; + int newMaxHealth = 0; + for (AbstractDamageablePart part : this) { + float desired = ((float) part.initialMaxHealth) * globalFactor; + expectedNewMaxHealth += desired; + int lowerEven = ((int) Math.floor(desired / 2F)) * 2; + lowerEven = sanitizeMaxHealth(lowerEven); + Target target = new Target(part, desired, lowerEven); + targets.add(target); + newMaxHealth += lowerEven; if (FirstAidConfig.GENERAL.debug.get()) { - FirstAid.LOGGER.info("Starting health scaling factor {} -> {} (max health {})", prevScaleFactor, globalFactor, player.getMaxHealth()); + FirstAid.LOGGER.info( "Part {} max health base target: {} initial; {} old; desired {}; {} base", part.part.name(), part.initialMaxHealth, part.getMaxHealth(), desired, lowerEven); } - player.level().getProfiler().push("distribution"); - int reduced = 0; - int added = 0; - float expectedNewMaxHealth = 0F; - int newMaxHealth = 0; - Map targetMaxHealth = new IdentityHashMap<>(); - for (AbstractDamageablePart part : this) { - float floatResult = ((float) part.initialMaxHealth) * globalFactor; - expectedNewMaxHealth += floatResult; - int result = (int) floatResult; - if (result % 2 == 1) { - int partMaxHealth = part.getMaxHealth(); - if (part.currentHealth < partMaxHealth && reduced < 4) { - result--; - reduced++; - } else if (part.currentHealth > partMaxHealth && added < 4) { - result++; - added++; - } else if (reduced > added) { - result++; - added++; - } else { - result--; - reduced++; + } + player.level().getProfiler().popPush("correcting"); + int wantedTotal = Math.round(expectedNewMaxHealth / 2F) * 2; + int minPossibleTotal = 0; + int maxPossibleTotal = 0; + for (Target target : targets) { + minPossibleTotal += sanitizeMaxHealth(2); + maxPossibleTotal += sanitizeMaxHealth(128); + } + wantedTotal = Math.max(minPossibleTotal, wantedTotal); + wantedTotal = Math.min(maxPossibleTotal, wantedTotal); + int healthToAdd = wantedTotal - newMaxHealth; + if (healthToAdd > 0) { + targets.sort( + Comparator.comparingDouble(target -> target.remainder) + .reversed() + .thenComparingInt(target -> target.part.part.ordinal()) + ); + for (Target target : targets) { + if (healthToAdd < 2) break; + int oldTarget = target.target; + int newTarget = sanitizeMaxHealth(oldTarget + 2); + if (newTarget != oldTarget) { + target.target = newTarget; + newMaxHealth += newTarget - oldTarget; + healthToAdd -= newTarget - oldTarget; + if (FirstAidConfig.GENERAL.debug.get()) { + FirstAid.LOGGER.info("Part {} corrected target {} -> {}; total now {}; wanted {}; expected {}", target.part.part.name(), oldTarget, newTarget, newMaxHealth, wantedTotal, expectedNewMaxHealth); } } - result = sanitizeMaxHealth(result); - targetMaxHealth.put(part, result); - newMaxHealth += result; - if (FirstAidConfig.GENERAL.debug.get()) { - FirstAid.LOGGER.info("Part {} max health: {} initial; {} old; {} new", part.part.name(), part.initialMaxHealth, part.getMaxHealth(), result); - } - // part.setMaxHealth(result); } - player.level().getProfiler().popPush("correcting"); - if (Math.abs(expectedNewMaxHealth - newMaxHealth) >= 2F) { - if (FirstAidConfig.GENERAL.debug.get()) { - FirstAid.LOGGER.info("Entering second stage - diff {}", Math.abs(expectedNewMaxHealth - newMaxHealth)); - } - List prioList = new ArrayList<>(); - for (AbstractDamageablePart part : this) { - prioList.add(part); - } - prioList.sort(Comparator.comparingInt(part -> targetMaxHealth.getOrDefault(part, part.getMaxHealth()))); - // prioList.sort(Comparator.comparingInt(AbstractDamageablePart::getMaxHealth)); - for (AbstractDamageablePart part : prioList) { - int oldTarget = targetMaxHealth.getOrDefault(part, part.getMaxHealth()); - if (FirstAidConfig.GENERAL.debug.get()) { - FirstAid.LOGGER.info("Part {}: Second stage with total diff {}", part.part.name(), Math.abs(expectedNewMaxHealth - newMaxHealth)); - } - int newTarget = oldTarget; - if (expectedNewMaxHealth > newMaxHealth) { - newTarget = sanitizeMaxHealth(oldTarget + 2); - } else if (expectedNewMaxHealth < newMaxHealth) { - newTarget = sanitizeMaxHealth(oldTarget - 2); - } - targetMaxHealth.put(part, newTarget); + } else if (healthToAdd < 0) { + targets.sort( + Comparator.comparingDouble(target -> target.remainder) + .thenComparingInt(target -> target.part.part.ordinal()) + ); + for (Target target : targets) { + if (healthToAdd > -2) break; + int oldTarget = target.target; + int newTarget = sanitizeMaxHealth(oldTarget - 2); + if (newTarget != oldTarget) { + target.target = newTarget; newMaxHealth += newTarget - oldTarget; + healthToAdd -= newTarget - oldTarget; if (FirstAidConfig.GENERAL.debug.get()) { - FirstAid.LOGGER.info("Part {}: corrected target {} -> {}; total now {}; expected {}", part.part.name(), oldTarget, newTarget, newMaxHealth, expectedNewMaxHealth); - } - if (Math.abs(expectedNewMaxHealth - newMaxHealth) < 2F) { - break; + FirstAid.LOGGER.info("Part {} corrected target {} -> {}; total now {}; wanted {}; expected {}", target.part.part.name(), oldTarget, newTarget, newMaxHealth, wantedTotal, expectedNewMaxHealth); } } } - player.level().getProfiler().popPush("apply"); - for (AbstractDamageablePart part : this) { - int target = targetMaxHealth.getOrDefault(part, part.getMaxHealth()); - if (FirstAidConfig.GENERAL.debug.get()) { - FirstAid.LOGGER.info("Part {} applying final max health: {} old; {} final", part.part.name(), part.getMaxHealth(), target); - } - part.setMaxHealth(target); + } + player.level().getProfiler().popPush("apply"); + boolean changed = false; + for (Target target : targets) { + AbstractDamageablePart part = target.part; + if (FirstAidConfig.GENERAL.debug.get()) { + FirstAid.LOGGER.info( "Part {} applying final max health: {} old; {} final", part.part.name(), part.getMaxHealth(), target.target); + } + if (part.getMaxHealth() != target.target) { + changed = true; } - player.level().getProfiler().pop(); + part.setMaxHealth(target.target); + } + if (changed && player instanceof ServerPlayer serverPlayer) { + FirstAid.NETWORKING.send( + PacketDistributor.PLAYER.with(() -> serverPlayer), + new MessageSyncDamageModel(this, true) + ); } - prevScaleFactor = globalFactor; player.level().getProfiler().pop(); } + prevScaleFactor = globalFactor; + player.level().getProfiler().pop(); } @Override From 30c036d0f1396ec83259c42233e5ea38c730430f Mon Sep 17 00:00:00 2001 From: Stephen Chryn Date: Tue, 2 Jun 2026 19:25:24 -0400 Subject: [PATCH 4/5] Fix memory leak where player keeps dying slightly after death --- gradle.properties | 2 +- .../common/damagesystem/PlayerDamageModel.java | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gradle.properties b/gradle.properties index f12d366..9950781 100644 --- a/gradle.properties +++ b/gradle.properties @@ -36,4 +36,4 @@ mapping_version=2023.08.20-1.20.1 # Must match the String constant located in the main mod class annotated with @Mod. mod_id=firstaid -mod_version=1.20.1-1.1 \ No newline at end of file +mod_version=1.20.1-1.3 diff --git a/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java b/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java index 0a05821..35ba32b 100644 --- a/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java +++ b/src/main/java/ichttt/mods/firstaid/common/damagesystem/PlayerDamageModel.java @@ -401,12 +401,15 @@ public void runScaleLogic(Player player) { if (player.level().isClientSide) { return; } + if (player.isRemoved() || !player.isAlive() || player.isDeadOrDying() || player.getHealth() <= 0F || isDead(player)) { + return; + } player.level().getProfiler().push("healthscaling"); if (FirstAidConfig.GENERAL.debug.get()) { FirstAid.LOGGER.info("[FirstAid scale] player={} tick={} vanillaMax={} prevScaleFactor={} limbMaxTotal={}", player.getName().getString(), player.tickCount, player.getMaxHealth(), prevScaleFactor, getCurrentMaxHealth()); } float globalFactor = player.getMaxHealth() / 20F; - if (prevScaleFactor != globalFactor) { + if (Math.abs(prevScaleFactor - globalFactor) > 0.0001F) { if (FirstAidConfig.GENERAL.debug.get()) { FirstAid.LOGGER.info( "Starting health scaling factor {} -> {} (max health {})", prevScaleFactor, globalFactor, player.getMaxHealth()); } @@ -499,11 +502,8 @@ class Target { } part.setMaxHealth(target.target); } - if (changed && player instanceof ServerPlayer serverPlayer) { - FirstAid.NETWORKING.send( - PacketDistributor.PLAYER.with(() -> serverPlayer), - new MessageSyncDamageModel(this, true) - ); + if (changed) { + scheduleResync(); } player.level().getProfiler().pop(); } From be09ccd0dd3c82a1d41811017712ba1cf10c8393 Mon Sep 17 00:00:00 2001 From: Stephen Chryn Date: Tue, 2 Jun 2026 19:27:45 -0400 Subject: [PATCH 5/5] Correct version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9950781..a2ca799 100644 --- a/gradle.properties +++ b/gradle.properties @@ -36,4 +36,4 @@ mapping_version=2023.08.20-1.20.1 # Must match the String constant located in the main mod class annotated with @Mod. mod_id=firstaid -mod_version=1.20.1-1.3 +mod_version=1.20.1-1.2.1