Skip to content

Commit

Permalink
Add ExhaustionEvent.ExhaustingAction event
Browse files Browse the repository at this point in the history
Allows control over when/how Minecraft modifies a player's exhaustion based on what the player does (jumping, sprinting, breaking blocks, etc)

Closes #132
  • Loading branch information
squeek502 committed Feb 18, 2019
1 parent 20d7613 commit fc2f3ac
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 10 deletions.
44 changes: 44 additions & 0 deletions java/squeek/applecore/api/hunger/ExhaustionEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public AllowExhaustion(EntityPlayer player)
* Fired each time exhaustion is added to a {@link #player} to allow control over
* its value.
*
* Note: This is a catch-all event for *any* time exhaustion is modified. For more fine-grained
* control, see {@link ExhaustingAction}
*
* This event is fired in {@link FoodStats#addExhaustion}.<br>
* <br>
* This event is not {@link Cancelable}.<br>
Expand All @@ -64,6 +67,47 @@ public ExhaustionAddition(EntityPlayer player, float deltaExhaustion)
}
}

/**
* See {@link ExhaustingAction}
*/
public enum ExhaustingActions
{
HARVEST_BLOCK,
NORMAL_JUMP,
SPRINTING_JUMP,
ATTACK_ENTITY,
DAMAGE_TAKEN,
HUNGER_POTION,
MOVEMENT_DIVE,
MOVEMENT_SWIM,
MOVEMENT_SPRINT,
MOVEMENT_CROUCH,
MOVEMENT_WALK
}

/**
* Fired each time a {@link #player} does something that changes exhaustion in vanilla Minecraft
* (i.e. jumping, sprinting, etc; see {@link ExhaustingActions} for the full list of possible sources)
*
* This event is fired whenever {@link EntityPlayer#addExhaustion} is called from within Minecraft code.<br>
* <br>
* This event is not {@link Cancelable}.<br>
* <br>
* This event does not have a result. {@link HasResult}<br>
*/
public static class ExhaustingAction extends ExhaustionEvent
{
public final ExhaustingActions source;
public float deltaExhaustion;

public ExhaustingAction(EntityPlayer player, ExhaustingActions source, float deltaExhaustion)
{
super(player);
this.source = source;
this.deltaExhaustion = deltaExhaustion;
}
}

/**
* Fired every time max exhaustion level is retrieved to allow control over its value.
*
Expand Down
9 changes: 8 additions & 1 deletion java/squeek/applecore/asm/ASMConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class ASMConstants
public static final class ExhaustionEvent
{
public static final String EXHAUSTED = "squeek.applecore.api.hunger.ExhaustionEvent$Exhausted";
public static final String EXHAUSTING_ACTIONS = "squeek.applecore.api.hunger.ExhaustionEvent$ExhaustingActions";
}
public static final class HealthRegenEvent
{
Expand All @@ -34,7 +35,11 @@ public static final class StarvationEvent

//Minecraft
public static final String BLOCK = "net.minecraft.block.Block";
public static final String BLOCK_CONTAINER = "net.minecraft.block.BlockContainer";
public static final String BLOCK_ICE = "net.minecraft.block.BlockIce";
public static final String BLOCK_POS = "net.minecraft.util.math.BlockPos";
public static final String DAMAGE_SOURCE = "net.minecraft.util.DamageSource";
public static final String ENTITY = "net.minecraft.entity.Entity";
public static final String ENTITY_LIVING = "net.minecraft.entity.EntityLivingBase";
public static final String FOOD_STATS = "net.minecraft.util.FoodStats";
public static final String HAND = "net.minecraft.util.EnumHand";
Expand All @@ -46,7 +51,9 @@ public static final class StarvationEvent
public static final String MINECRAFT = "net.minecraft.client.Minecraft";
public static final String PLAYER = "net.minecraft.entity.player.EntityPlayer";
public static final String PLAYER_SP = "net.minecraft.client.entity.EntityPlayerSP";
public static final String STACK = "net.minecraft.item.ItemStack";
public static final String POTION = "net.minecraft.potion.Potion";
public static final String ITEM_STACK = "net.minecraft.item.ItemStack";
public static final String TILE_ENTITY = "net.minecraft.tileentity.TileEntity";
public static final String STAT_BASE = "net.minecraft.stats.StatBase";
public static final String STAT_LIST = "net.minecraft.stats.StatList";
public static final String WORLD = "net.minecraft.world.World";
Expand Down
7 changes: 7 additions & 0 deletions java/squeek/applecore/asm/Hooks.java
Original file line number Diff line number Diff line change
Expand Up @@ -313,4 +313,11 @@ public static float onExhaustionAdded(FoodStats foodStats, float deltaExhaustion
MinecraftForge.EVENT_BUS.post(event);
return event.deltaExhaustion;
}

public static float fireExhaustingActionEvent(EntityPlayer player, ExhaustionEvent.ExhaustingActions source, float deltaExhaustion)
{
ExhaustionEvent.ExhaustingAction event = new ExhaustionEvent.ExhaustingAction(player, source, deltaExhaustion);
MinecraftForge.EVENT_BUS.post(event);
return event.deltaExhaustion;
}
}
1 change: 1 addition & 0 deletions java/squeek/applecore/asm/TransformerModuleHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class TransformerModuleHandler implements IClassTransformer
registerTransformerModule(new ModuleBlockFood());
registerTransformerModule(new ModulePeacefulRegen());
registerTransformerModule(new ModuleHungerHUD());
registerTransformerModule(new ModuleExhaustingActions());
}

public static void registerTransformerModule(IClassTransformerModule transformerModule)
Expand Down
191 changes: 191 additions & 0 deletions java/squeek/applecore/asm/module/ModuleExhaustingActions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package squeek.applecore.asm.module;

import org.objectweb.asm.tree.*;
import squeek.applecore.asm.ASMConstants;
import squeek.applecore.asm.IClassTransformerModule;
import squeek.asmhelper.applecore.ASMHelper;
import squeek.asmhelper.applecore.InsnComparator;
import squeek.asmhelper.applecore.ObfHelper;

import static org.objectweb.asm.Opcodes.*;

public class ModuleExhaustingActions implements IClassTransformerModule
{
@Override
public String[] getClassesToTransform()
{
return new String[]{ASMConstants.PLAYER, ASMConstants.BLOCK, ASMConstants.BLOCK_CONTAINER, ASMConstants.BLOCK_ICE, ASMConstants.POTION};
}

@Override
public byte[] transform(String name, String transformedName, byte[] basicClass)
{
if (transformedName.equals(ASMConstants.PLAYER))
{
ClassNode classNode = ASMHelper.readClassFromBytes(basicClass);

MethodNode jumpMethod = ASMHelper.findMethodNodeOfClass(classNode, ObfHelper.isObfuscated() ? "func_70664_aZ" : "jump", "()V");
if (jumpMethod == null)
throw new RuntimeException("EntityPlayer.jump method not found");
patchSimpleAddExhaustionCall(classNode, jumpMethod, 1, "NORMAL_JUMP");
patchSimpleAddExhaustionCall(classNode, jumpMethod, 0, "SPRINTING_JUMP");

MethodNode damageEntityMethod = ASMHelper.findMethodNodeOfClass(classNode, ObfHelper.isObfuscated() ? "func_70665_d" : "damageEntity", ASMHelper.toMethodDescriptor("V", ASMConstants.DAMAGE_SOURCE, "F"));
if (damageEntityMethod == null)
throw new RuntimeException("EntityPlayer.damageEntity method not found");
patchDamageEntity(damageEntityMethod);

MethodNode attackEntityMethod = ASMHelper.findMethodNodeOfClass(classNode, ObfHelper.isObfuscated() ? "func_71059_n" : "attackTargetEntityWithCurrentItem", ASMHelper.toMethodDescriptor("V", ASMConstants.ENTITY));
if (attackEntityMethod == null)
throw new RuntimeException("EntityPlayer.attackTargetEntityWithCurrentItem method not found");
patchSimpleAddExhaustionCall(classNode, attackEntityMethod, 0, "ATTACK_ENTITY");

MethodNode addMovementStatMethod = ASMHelper.findMethodNodeOfClass(classNode, ObfHelper.isObfuscated() ? "func_71000_j" : "addMovementStat", ASMHelper.toMethodDescriptor("V", "D", "D", "D"));
if (addMovementStatMethod == null)
throw new RuntimeException("EntityPlayer.addMovementStat method not found");
patchMovementStat(addMovementStatMethod, 0, "MOVEMENT_DIVE");
patchMovementStat(addMovementStatMethod, 1, "MOVEMENT_SWIM");
patchMovementStat(addMovementStatMethod, 2, "MOVEMENT_SPRINT");
patchMovementStat(addMovementStatMethod, 3, "MOVEMENT_CROUCH");
patchMovementStat(addMovementStatMethod, 4, "MOVEMENT_WALK");

return ASMHelper.writeClassToBytes(classNode);
}
else if (transformedName.equals(ASMConstants.BLOCK) || transformedName.equals(ASMConstants.BLOCK_CONTAINER) || transformedName.equals(ASMConstants.BLOCK_ICE))
{
ClassNode classNode = ASMHelper.readClassFromBytes(basicClass);
String methodDesc = ASMHelper.toMethodDescriptor(
"V",
ASMConstants.WORLD,
ASMConstants.PLAYER,
ASMConstants.BLOCK_POS,
ASMConstants.IBLOCKSTATE,
ASMConstants.TILE_ENTITY,
ASMConstants.ITEM_STACK
);
MethodNode harvestBlock = ASMHelper.findMethodNodeOfClass(classNode, ObfHelper.isObfuscated() ? "func_180657_a" : "harvestBlock", methodDesc);
if (harvestBlock == null)
throw new RuntimeException("Block.harvestBlock method not found in class " + classNode.name + " with desc " + methodDesc);
patchSimpleAddExhaustionCall(classNode, harvestBlock, 0, "HARVEST_BLOCK");
return ASMHelper.writeClassToBytes(classNode);
}
else if (transformedName.equals(ASMConstants.POTION))
{
ClassNode classNode = ASMHelper.readClassFromBytes(basicClass);
MethodNode performEffect = ASMHelper.findMethodNodeOfClass(classNode, ObfHelper.isObfuscated() ? "func_76394_a" : "performEffect", ASMHelper.toMethodDescriptor("V", ASMConstants.ENTITY_LIVING, "I"));
if (performEffect == null)
throw new RuntimeException("Potion.performEffect method not found");

AbstractInsnNode call = getAddExhaustionCall(performEffect);
AbstractInsnNode load = ASMHelper.findPreviousInstructionWithOpcode(call, CHECKCAST);
if (load == null)
throw new RuntimeException("Unexpected instruction pattern found in Potion.performEffect:\n" + ASMHelper.getInsnListAsString(performEffect.instructions));

patchAddExhaustionCall(performEffect.instructions, load, call, 1, "HUNGER_POTION");
// inserted ALOAD needs a CHECKCAST
performEffect.instructions.insert(load.getNext(), new TypeInsnNode(CHECKCAST, ObfHelper.getInternalClassName(ASMConstants.PLAYER)));
return ASMHelper.writeClassToBytes(classNode);
}
return basicClass;
}

// transform
// search for pattern:
/*
ALOAD 0
LDC 0.2
INVOKEVIRTUAL net/minecraft/entity/player/EntityPlayer.addExhaustion (F)V
*/
// resulting in:
/*
ALOAD 0
ALOAD 0
GETSTATIC squeek/applecore/api/hunger/ExhaustionEvent$ExhaustingActions.SPRINTING_JUMP : Lsqueek/applecore/api/hunger/ExhaustionEvent$ExhaustingActions;
LDC 0.2
INVOKESTATIC squeek/applecore/asm/Hooks.fireExhaustingActionEvent (Lnet/minecraft/entity/player/EntityPlayer;Lsqueek/applecore/api/hunger/ExhaustionEvent$ExhaustingActions;F)F
INVOKEVIRTUAL net/minecraft/entity/player/EntityPlayer.addExhaustion (F)V
*/

/**
* Usage note: This will modify the instructions so that the search pattern will not be found in the next call
* so doing something like patchAddExhaustionCall(..., 0, ...); patchAddExhaustionCall(..., 1, ...); will fail if there
* are only two instances of that pattern in the function. Should either reverse it (so the second gets modified first) or call
* it twice with index 0 both times.
*/
private void patchSimpleAddExhaustionCall(ClassNode classNode, MethodNode method, int patternIndex, String exhaustingActionEnum)
{
AbstractInsnNode start = null;
AbstractInsnNode haystackStart = method.instructions.getFirst();

for (int i = 0; i <= patternIndex; i++)
{
InsnList needle = new InsnList();
needle.add(new VarInsnNode(ALOAD, InsnComparator.INT_WILDCARD));
needle.add(new LdcInsnNode(InsnComparator.WILDCARD));
needle.add(new MethodInsnNode(INVOKEVIRTUAL, ObfHelper.getInternalClassName(ASMConstants.PLAYER), ObfHelper.isObfuscated() ? "func_71020_j" : "addExhaustion", ASMHelper.toMethodDescriptor("V", "F"), false));

start = ASMHelper.find(haystackStart, needle);
if (start == null || start.getNext() == null)
throw new RuntimeException("EntityPlayer.addExhaustion call pattern (index=" + patternIndex + ") not found in " + classNode.name + "." + method.name);

haystackStart = start.getNext();
}

patchAddExhaustionCall(method.instructions, start, start.getNext().getNext(), ((VarInsnNode) start).var, exhaustingActionEnum);
}

private void patchAddExhaustionCall(InsnList instructions, AbstractInsnNode loadPoint, AbstractInsnNode callPoint, int playerLoadIndex, String exhaustingAction)
{
// load the player
AbstractInsnNode loadPlayer = new VarInsnNode(ALOAD, playerLoadIndex);
instructions.insert(loadPoint, loadPlayer);
// add a GETSTATIC for the enum
AbstractInsnNode getEnum = new FieldInsnNode(GETSTATIC, ObfHelper.getInternalClassName(ASMConstants.ExhaustionEvent.EXHAUSTING_ACTIONS), exhaustingAction, ASMHelper.toDescriptor(ASMConstants.ExhaustionEvent.EXHAUSTING_ACTIONS));
instructions.insert(loadPlayer, getEnum);
// add an INVOKE for the fire event hook before the call
AbstractInsnNode fireEvent = new MethodInsnNode(INVOKESTATIC, ASMConstants.HOOKS_INTERNAL_CLASS, "fireExhaustingActionEvent", ASMHelper.toMethodDescriptor("F", ASMConstants.PLAYER, ASMConstants.ExhaustionEvent.EXHAUSTING_ACTIONS, "F"), false);
instructions.insertBefore(callPoint, fireEvent);
}

// special case for the call in EntityPlayer.damageEntity
private void patchDamageEntity(MethodNode method)
{
AbstractInsnNode addExhaustionCall = getAddExhaustionCall(method);
AbstractInsnNode loadPoint = addExhaustionCall.getPrevious().getPrevious().getPrevious();
patchAddExhaustionCall(method.instructions, loadPoint, addExhaustionCall, 0, "DAMAGE_TAKEN");
}

// special case for the calls in EntityPlayer.addMovementStat
private void patchMovementStat(MethodNode method, int callIndex, String exhaustingAction)
{
AbstractInsnNode addExhaustionCall = getAddExhaustionCall(method, callIndex);

AbstractInsnNode loadPoint = ASMHelper.findPreviousInstructionWithOpcode(addExhaustionCall, ALOAD);
if (loadPoint == null)
throw new RuntimeException("No ALOAD found before addExhaustion call (index=" + callIndex + ") in EntityPlayer.addMovementStat");

patchAddExhaustionCall(method.instructions, loadPoint, addExhaustionCall, 0, exhaustingAction);
}

private AbstractInsnNode getAddExhaustionCall(MethodNode method)
{
return getAddExhaustionCall(method, 0);
}

private AbstractInsnNode getAddExhaustionCall(MethodNode method, int callIndex)
{
AbstractInsnNode addExhaustionCall = null;
AbstractInsnNode haystackStart = method.instructions.getFirst();
AbstractInsnNode needle = new MethodInsnNode(INVOKEVIRTUAL, ObfHelper.getInternalClassName(ASMConstants.PLAYER), ObfHelper.isObfuscated() ? "func_71020_j" : "addExhaustion", ASMHelper.toMethodDescriptor("V", "F"), false);

for (int i = 0; i <= callIndex && haystackStart != null; i++)
{
addExhaustionCall = ASMHelper.find(haystackStart, needle);
if (addExhaustionCall == null)
throw new RuntimeException("No addExhaustion call (index=" + callIndex + ") found in " + method.name + ":\n" + ASMHelper.getInsnListAsString(method.instructions));
haystackStart = addExhaustionCall.getNext();
}

return addExhaustionCall;
}
}
8 changes: 4 additions & 4 deletions java/squeek/applecore/asm/module/ModuleFoodEatingSpeed.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public byte[] transform(String name, String transformedName, byte[] basicClass)
}
else if (transformedName.equals(ASMConstants.ITEM_RENDERER))
{
MethodNode methodNode = ASMHelper.findMethodNodeOfClass(classNode, "func_187454_a", "transformEatFirstPerson", ASMHelper.toMethodDescriptor("V", "F", ASMConstants.HAND_SIDE, ASMConstants.STACK));
MethodNode methodNode = ASMHelper.findMethodNodeOfClass(classNode, "func_187454_a", "transformEatFirstPerson", ASMHelper.toMethodDescriptor("V", "F", ASMConstants.HAND_SIDE, ASMConstants.ITEM_STACK));
if (methodNode != null)
{
patchRenderItemInFirstPerson(methodNode);
Expand All @@ -62,7 +62,7 @@ private void patchRenderItemInFirstPerson(MethodNode method)
{
InsnList needle = new InsnList();
needle.add(new VarInsnNode(ALOAD, 3));
needle.add(new MethodInsnNode(INVOKEVIRTUAL, ASMHelper.toInternalClassName(ASMConstants.STACK), ObfHelper.isObfuscated() ? "func_77988_m" : "getMaxItemUseDuration", ASMHelper.toMethodDescriptor("I"), false));
needle.add(new MethodInsnNode(INVOKEVIRTUAL, ASMHelper.toInternalClassName(ASMConstants.ITEM_STACK), ObfHelper.isObfuscated() ? "func_77988_m" : "getMaxItemUseDuration", ASMHelper.toMethodDescriptor("I"), false));

InsnList replacement = new InsnList();
replacement.add(new VarInsnNode(ALOAD, 0));
Expand All @@ -79,8 +79,8 @@ private void patchGetItemInUseMaxCount(ClassNode classNode, MethodNode method)
{
InsnList needle = new InsnList();
needle.add(new VarInsnNode(ALOAD, 0));
needle.add(new FieldInsnNode(GETFIELD, ObfHelper.getInternalClassName(ASMConstants.ENTITY_LIVING), ObfHelper.isObfuscated() ? "field_184627_bm" : "activeItemStack", ASMHelper.toDescriptor(ASMConstants.STACK)));
needle.add(new MethodInsnNode(INVOKEVIRTUAL, ObfHelper.getInternalClassName(ASMConstants.STACK), ObfHelper.isObfuscated() ? "func_77988_m" : "getMaxItemUseDuration", ASMHelper.toMethodDescriptor("I"), false));
needle.add(new FieldInsnNode(GETFIELD, ObfHelper.getInternalClassName(ASMConstants.ENTITY_LIVING), ObfHelper.isObfuscated() ? "field_184627_bm" : "activeItemStack", ASMHelper.toDescriptor(ASMConstants.ITEM_STACK)));
needle.add(new MethodInsnNode(INVOKEVIRTUAL, ObfHelper.getInternalClassName(ASMConstants.ITEM_STACK), ObfHelper.isObfuscated() ? "func_77988_m" : "getMaxItemUseDuration", ASMHelper.toMethodDescriptor("I"), false));

InsnList replacement = new InsnList();
replacement.add(new VarInsnNode(ALOAD, 0));
Expand Down
Loading

0 comments on commit fc2f3ac

Please sign in to comment.