Skip to content
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ repositories {
}

dependencies {
include(implementation(annotationProcessor("io.github.llamalad7:mixinextras-fabric:0.5.0")))
// To change the versions see the gradle.properties file
minecraft "com.mojang:minecraft:${project.minecraft_version}"
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
Expand Down
304 changes: 210 additions & 94 deletions src/main/java/com/minenash/action_hunger/mixin/HungerManagerMixin.java
Original file line number Diff line number Diff line change
@@ -1,137 +1,253 @@
package com.minenash.action_hunger.mixin;

import com.llamalad7.mixinextras.expression.Definition;
import com.llamalad7.mixinextras.expression.Expression;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.minenash.action_hunger.ActionHunger;
import com.minenash.action_hunger.config.Config;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.entity.damage.DamageSources;
import net.minecraft.entity.player.HungerManager;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Items;
import net.minecraft.registry.DynamicRegistryManager;
import net.minecraft.registry.Registries;
import net.minecraft.world.Difficulty;
import net.minecraft.world.GameRules;
import org.spongepowered.asm.mixin.Debug;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;

import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
import org.spongepowered.asm.mixin.injection.Slice;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

@Debug(export = true)
@Mixin(HungerManager.class)
public abstract class HungerManagerMixin {

@Shadow private int foodLevel;
@Shadow private float exhaustion;
@Shadow private float saturationLevel;
@Shadow private int foodTickTimer;
@Shadow public abstract void addExhaustion(float exhaustion);
@Shadow private void addInternal(int food, float exhaustion) {}

@Shadow private int prevFoodLevel;
@Unique private int constantRegenTimer = 0;
@Unique private int constantHungerTimer = 0;
@Unique private int shieldExhaustionTimer = 0;


@Redirect(method = "eat", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/HungerManager;addInternal(IF)V"))
private void actionHunger$modifyFoodComponent(HungerManager manager, int food, float saturation) {
addInternal(Math.round(food * Config.hungerFromFoodMultiplier), saturation * Config.saturationFromFoodMultiplier);
}

/**
* @author Minenash
* @reason Too complicated to with normal mixins
*/
@Overwrite
public void update(PlayerEntity player) {
Difficulty difficulty = player.getWorld().getDifficulty();
if (exhaustion > 4.0F) {
exhaustion -= 4.0F;
if (saturationLevel > 0.0F)
saturationLevel = Math.max(saturationLevel - 1.0F, 0.0F);
else if (difficulty != Difficulty.PEACEFUL)
foodLevel = Math.max(foodLevel - 1, 0);
}

if (player.getAbilities().invulnerable)
return;
@ModifyExpressionValue(
method = "eat",
at = @At(value = "INVOKE", target = "Lnet/minecraft/component/type/FoodComponent;nutrition()I")
)
private int actionHunger$modifyFoodNutrition(int nutrition) {
return (int) (nutrition * Config.hungerFromFoodMultiplier);
}

double dynamicRegenRateModifier = ActionHunger.getCurveModifier(player.getHealth(), Config.dynamicRegenRateCurve, Config.dynamicRegenRateMultiplier);
@ModifyExpressionValue(
method = "eat",
at = @At(value = "INVOKE", target = "Lnet/minecraft/component/type/FoodComponent;saturation()F")
)
private float actionHunger$modifyFoodSaturation(float saturation) {
return saturation * Config.saturationFromFoodMultiplier;
}

boolean regened = false;
@WrapOperation( // no idea why this was in the original mixin
method = "update",
at = @At(value = "FIELD", target = "Lnet/minecraft/entity/player/HungerManager;foodLevel:I", ordinal = 0)
)
private int actionHunger$dontSetPrevFoodLevel(HungerManager instance, Operation<Integer> original) {
return this.prevFoodLevel;
}

boolean isPlayerUsingShield = player.getActiveItem().getItem() == Items.SHIELD;
@Inject(
method = "update",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/GameRules;getBoolean(Lnet/minecraft/world/GameRules$Key;)Z"),
cancellable = true,
order = 999 // go before shield exhaustion
)
private void actionHunger$earlyExitIfInvuln(PlayerEntity player, CallbackInfo ci) {
if (player.getAbilities().invulnerable)
ci.cancel();
}
@Inject(
method = "update",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/GameRules;getBoolean(Lnet/minecraft/world/GameRules$Key;)Z")
)
private void actionHunger$updateShieldExhaustion(PlayerEntity player, CallbackInfo ci) {

if (isIsPlayerUsingShield(player)) {
++this.shieldExhaustionTimer;
if (this.shieldExhaustionTimer >= Config.shieldExhaustionRate) {
if (Config.debug)
System.out.println("Exhaustion from " + "Shield" + ": " + Config.shieldExhaustionAmount);
this.addExhaustion(Config.shieldExhaustionAmount);
}
} else
this.shieldExhaustionTimer = 0;
}

if (isPlayerUsingShield) {
++shieldExhaustionTimer;
if (shieldExhaustionTimer >= Config.shieldExhaustionRate)
exhaustion("Shield", Config.shieldExhaustionAmount);
@Definition(id = "bl", local = @Local(type = boolean.class))
@Expression("bl")
@ModifyExpressionValue(
method = "update",
at = @At("MIXINEXTRAS:EXPRESSION"),
slice = @Slice(to = @At(value = "FIELD", target = "Lnet/minecraft/entity/player/HungerManager;foodTickTimer:I"))
)
private boolean actionHunger$constantRegen(boolean bl, PlayerEntity player) {
if (bl && !shouldBlockRegenFromShield(player)) {
this.constantRegenTimer++;
if (this.constantRegenTimer >= Config.constantRegenRate * (Config.dynamicRegenOnConstantRegen ? getCurveModifier(player) : 1.0D)) {
if (Config.debug)
System.out.println("Heal from " + "Const" + ": " + Config.constantRegenAmount);
player.heal(Config.constantRegenAmount);
this.constantRegenTimer = 0;
}
return true;
}
else
shieldExhaustionTimer = 0;

boolean blockRegenFromShield = Config.disableRegenWhenUsingShield && isPlayerUsingShield;
if (player.getWorld().getGameRules().getBoolean(GameRules.NATURAL_REGENERATION) && !blockRegenFromShield)
regened = regen(player, dynamicRegenRateModifier);
return false;
}

constantHungerTimer++;
if (constantHungerTimer >= Config.constantExhaustionRate * (Config.dynamicRegenOnConstantExhaustion ? dynamicRegenRateModifier : 1.0D)) {
exhaustion("Const", Config.constantExhaustionAmount);
constantHungerTimer = 0;
}
@ModifyConstant( // explicitly conflict with anyone else trying to do this.
method = "update",
constant = @Constant(intValue = 20)
)
private int actionHunger$replaceMinHunger(int original) {
return Config.hyperFoodRegenMinimumHunger;
}

if (foodLevel <= 0) {
++foodTickTimer;
if (foodTickTimer >= Config.starvationDamageRate) {
if (player.getHealth() > 10.0F || difficulty == Difficulty.HARD || player.getHealth() > 1.0F && difficulty == Difficulty.NORMAL)
player.damage(player.getDamageSources().starve(), Config.starvationDamageAmount);
foodTickTimer = 0;
}
} else if (!regened){
foodTickTimer = 0;
}

@ModifyConstant( // explicitly conflict with anyone else trying to do this.
method = "update",
constant = @Constant(intValue = 10)
)
private int actionHunger$replaceFoodTickRate(int original, PlayerEntity player) {
return (int) (Config.hyperFoodRegenRate * (Config.dynamicRegenOnHyperFoodRegen ? getCurveModifier(player) : 1));
}

private boolean regen(PlayerEntity player, double dynamicRegenRateModifier) {
constantRegenTimer++;
if (constantRegenTimer >= Config.constantRegenRate * (Config.dynamicRegenOnConstantRegen ? dynamicRegenRateModifier : 1.0D)) {
heal(player, "Const", Config.constantRegenAmount);
constantRegenTimer = 0;
}
@ModifyArg(
method = "update",
at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/PlayerEntity;heal(F)V", ordinal = 0),
require = 1, expect = 1, allow = 1 // extra assertions to make sure this only matches once
)
private float actionHunger$multiplyHyperFoodRegen(float health) {
health *= Config.hyperFoodRegenHealthMultiplier;
if (Config.debug) System.out.println("Heal from Hyper: " + health);
return health;
}

if (saturationLevel > 0.0F && player.canFoodHeal() && foodLevel >= Config.hyperFoodRegenMinimumHunger) {
++foodTickTimer;
if (foodTickTimer >= Config.hyperFoodRegenRate * (Config.dynamicRegenOnHyperFoodRegen ? dynamicRegenRateModifier : 1.0D)) {
float f = Math.min(saturationLevel, 6.0F);
heal(player, "Hyper", f / 6.0F * Config.hyperFoodRegenHealthMultiplier);
exhaustion("Hyper", f * Config.hyperFoodRegenExhaustionMultiplier);
foodTickTimer = 0;
}
} else if (foodLevel >= Config.foodRegenMinimumHunger && player.canFoodHeal()) {
++foodTickTimer;
if (foodTickTimer >= Config.foodRegenRate * (Config.dynamicRegenOnFoodRegen ? dynamicRegenRateModifier : 1.0D)) {
heal(player, "Food", Config.foodRegenHealthAmount);
exhaustion("Food", Config.foodRegenExhaustionAmount);
foodTickTimer = 0;
}
}
else
return false;
return true;
@ModifyArg(
method = "update",
at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/HungerManager;addExhaustion(F)V", ordinal = 0),
require = 1, expect = 1, allow = 1 // extra assertions to make sure this only matches once
)
private float actionHunger$multiplyHyperExhaustion(float exhaustion) {
exhaustion *= Config.hyperFoodRegenExhaustionMultiplier;
if (Config.debug) System.out.println("Exhaustion from " + "Hyper" + ": " + exhaustion);
return exhaustion;
}

@Definition(id = "bl", local = @Local(type = boolean.class))
@Expression("bl")
@ModifyExpressionValue( // idk why the original mixin had this
method = "update",
at = @At("MIXINEXTRAS:EXPRESSION"),
slice = @Slice(from = @At(value = "FIELD", target = "Lnet/minecraft/entity/player/HungerManager;foodTickTimer:I"))
)
private boolean actionHunger$removeCondition(boolean bl) {
return true; // it is used in an AND so true is the noop value
}

@ModifyConstant( // explicitly conflict with anyone else trying to do this.
method = "update",
constant = @Constant(intValue = 18)
)
private int actionHunger$replaceMinSlowHealHunger(int original) {
return Config.foodRegenMinimumHunger;
}

private void heal(PlayerEntity player, String source, float amount) {
@ModifyConstant(
method = "update",
constant = @Constant(intValue = 80, ordinal = 0),
require = 1, expect = 1, allow = 1 // extra assertions to make sure this only matches once.
)
private int actionHunger$replaceFoodRegenTicks(int original, PlayerEntity player) {
return (int) (Config.foodRegenRate * (Config.dynamicRegenOnFoodRegen ? getCurveModifier(player) : 1.0D));
}

@ModifyArg( // do not conflict with anyone trying to do this, instead ignore them.
method = "update",
at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/PlayerEntity;heal(F)V", ordinal = 1),
require = 1, expect = 1, allow = 1 // extra assertions to make sure this only matches once
)
private float actionHunger$replaceSlowFoodHealAmount(float amount) {
amount = Config.foodRegenHealthAmount;
if (Config.debug)
System.out.println("Heal from " + source + ": " + amount);
player.heal(amount);
System.out.println("Heal from " + "Food" + ": " + amount);
return amount;
}

private void exhaustion(String source, float amount) {
@ModifyArg(
method = "update",
at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/HungerManager;addExhaustion(F)V", ordinal = 1),
require = 1, expect = 1, allow = 1 // extra assertions to make sure this only matches once.
)
private float actionHunger$replaceSlowFoodExhaustionAmount(float amount) {
amount = Config.foodRegenExhaustionAmount;

if (Config.debug)
System.out.println("Exhaustion from " + source + ": " + amount);
addExhaustion(amount);
System.out.println("Exhaustion from " + "Food" + ": " + Config.foodRegenExhaustionAmount);
return amount;
}


@Inject( // in the original mixin this comes before the starving damage check, but because that does not use exhaustion we can safely put this at the end of the mixin as it is impossible to put it before the starving check
method = "update",
at = @At(value = "TAIL")
)
private void actionHunger$updateConstantExhaustion(PlayerEntity player, CallbackInfo ci) {
this.constantHungerTimer++;
if (this.constantHungerTimer >= Config.constantExhaustionRate * (Config.dynamicRegenOnConstantExhaustion ? getCurveModifier(player) : 1.0D)) {
if (Config.debug)
System.out.println("Exhaustion from " + "Const" + ": " + Config.constantExhaustionAmount);
this.addExhaustion(Config.constantExhaustionAmount);
this.constantHungerTimer = 0;
}
}

@ModifyConstant(
method = "update",
constant = @Constant(intValue = 80, ordinal = 1),
require = 1, expect = 1, allow = 1 // extra assertions to make sure this only matches once.
)
private int actionHunger$replaceStarvationDamageRate(int ticks) {
return Config.starvationDamageRate;
}

@ModifyArg( // do not conflict with anyone trying to do this, instead ignore them.
method = "update",
at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/PlayerEntity;damage(Lnet/minecraft/entity/damage/DamageSource;F)Z")
)
private float actionHunger$replaceStarvationDamageAmount(float amount) {
// we could instead return amount * starvationDamageAmount, which would be the same in vanilla but if other mods modifyied this we would stack
return Config.starvationDamageAmount;
}

@Unique
private static double getCurveModifier(PlayerEntity player) {
return ActionHunger.getCurveModifier(player.getHealth(), Config.dynamicRegenRateCurve, Config.dynamicRegenRateMultiplier);
}


@Unique
private static boolean isIsPlayerUsingShield(PlayerEntity player) {
return player.getActiveItem().getItem() == Items.SHIELD;
}

@Unique
private static boolean shouldBlockRegenFromShield(PlayerEntity player) {
return Config.disableRegenWhenUsingShield && isIsPlayerUsingShield(player);
}


}
6 changes: 6 additions & 0 deletions src/main/resources/action_hunger.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,11 @@
],
"injectors": {
"defaultRequire": 1
},
"mixinextras": {
"minVersion": "0.5.0"
},
"overwrites": {
"requireAnnotations": true
}
}