From 434f73a50966661eb4ce9ec1ca12fffd5f2673a5 Mon Sep 17 00:00:00 2001 From: asanetargoss Date: Sun, 3 Jan 2021 18:51:35 -0800 Subject: [PATCH 1/4] Add NoClassDefFoundError checking to InvokeUtil --- .../metamorph/api/morphs/EntityMorph.java | 39 +++-- .../mchorse/metamorph/util/InvokeUtil.java | 136 +++++++++++++----- 2 files changed, 130 insertions(+), 45 deletions(-) diff --git a/src/main/java/mchorse/metamorph/api/morphs/EntityMorph.java b/src/main/java/mchorse/metamorph/api/morphs/EntityMorph.java index d8d009f9..59b283b2 100644 --- a/src/main/java/mchorse/metamorph/api/morphs/EntityMorph.java +++ b/src/main/java/mchorse/metamorph/api/morphs/EntityMorph.java @@ -1,5 +1,17 @@ package mchorse.metamorph.api.morphs; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.apache.commons.lang3.reflect.FieldUtils; + import mchorse.mclib.client.gui.utils.GuiUtils; import mchorse.metamorph.Metamorph; import mchorse.metamorph.api.EntityUtils; @@ -50,17 +62,6 @@ import net.minecraft.world.World; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; -import org.apache.commons.lang3.reflect.FieldUtils; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; /** * Entity morph class @@ -1023,7 +1024,12 @@ public SoundEvent getHurtSound(EntityLivingBase target, DamageSource damageSourc try { Method methodHurtSound = InvokeUtil.getPrivateMethod(entity.getClass(), EntityLivingBase.class, SoundHandler.GET_HURT_SOUND.getName()); - SoundEvent hurtSound = (SoundEvent) methodHurtSound.invoke(entity); + if (methodHurtSound == null) + { + return null; + } + + SoundEvent hurtSound = (SoundEvent)methodHurtSound.invoke(entity); if (hurtSound == null) { hurtSound = SoundHandler.NO_SOUND; @@ -1045,6 +1051,11 @@ public SoundEvent getDeathSound(EntityLivingBase target) try { Method methodDeathSound = InvokeUtil.getPrivateMethod(entity.getClass(), EntityLivingBase.class, SoundHandler.GET_DEATH_SOUND.getName()); + if (methodDeathSound == null) + { + return null; + } + SoundEvent deathSound = (SoundEvent) methodDeathSound.invoke(entity); if (deathSound == null) { @@ -1073,6 +1084,10 @@ public void playStepSound(EntityLivingBase target) try { Method methodPlayStep = InvokeUtil.getPrivateMethod(entity.getClass(), Entity.class, SoundHandler.PLAY_STEP_SOUND.getName(), BlockPos.class, Block.class); + if (methodPlayStep == null) + { + return; + } int x = MathHelper.floor(entity.posX); int y = MathHelper.floor(entity.posY - 0.20000000298023224D); diff --git a/src/main/java/mchorse/metamorph/util/InvokeUtil.java b/src/main/java/mchorse/metamorph/util/InvokeUtil.java index a9505107..f9783896 100644 --- a/src/main/java/mchorse/metamorph/util/InvokeUtil.java +++ b/src/main/java/mchorse/metamorph/util/InvokeUtil.java @@ -1,60 +1,130 @@ package mchorse.metamorph.util; import java.lang.reflect.Method; +import java.util.WeakHashMap; + +import javax.annotation.Nullable; + +import mchorse.metamorph.Metamorph; public class InvokeUtil { + protected static final int MAX_ERRORS_PER_CLASS = 5; + protected static ThreadLocal, Integer>> classErrorCounts = new ThreadLocal<>(); + + protected static WeakHashMap, Integer> getClassBlacklist() + { + WeakHashMap, Integer> blacklist = classErrorCounts.get(); + if (blacklist == null) { + blacklist = new WeakHashMap<>(); + classErrorCounts.set(blacklist); + } + return blacklist; + } + + protected static boolean isClassBlacklisted(Class clazz) + { + WeakHashMap, Integer> blacklist = getClassBlacklist(); + Integer count = blacklist.get(clazz); + if (count == null) + { + count = 0; + } + return count >= MAX_ERRORS_PER_CLASS; + } + + protected static void incrementClassErrors(Class clazz) + { + WeakHashMap, Integer> blacklist = getClassBlacklist(); + Integer count = blacklist.get(clazz); + if (count == null) + { + count = 1; + } + else + { + ++count; + } + blacklist.put(clazz, count); + + if (count == MAX_ERRORS_PER_CLASS) + { + Metamorph.LOGGER.error("Too many errors for class " + clazz.getName() + ". " + + "Class will be blacklisted from reflection on this thread."); + } + } /** * Ascends up a class chain until it finds the specified method, regardless * of access modifier. Assumes finalClazz is the original declarer of the specified method. + * + * If a class emits too many NoClassDefFoundErrors, then give up and return null. */ - public static Method getPrivateMethod(Class clazz, Class finalClazz, String methodName, Class... paramVarArgs) + public static @Nullable Method getPrivateMethod(Class clazz, Class finalClazz, String methodName, Class... paramVarArgs) throws NoSuchMethodException, SecurityException { - Method privateMethod = null; - - for (Class testClazz = clazz; - testClazz != finalClazz && privateMethod == null; - testClazz = testClazz.getSuperclass()) + if (isClassBlacklisted(clazz)) { - for (Method method : testClazz.getDeclaredMethods()) + return null; + } + + try + { + Method privateMethod = null; + + for (Class testClazz = clazz; + testClazz != finalClazz && privateMethod == null; + testClazz = testClazz.getSuperclass()) { - if (!method.getName().equals(methodName)) - { - continue; - } - - Class[] parameters = method.getParameterTypes(); - if (!(parameters.length == paramVarArgs.length)) - { - continue; - } - boolean matchingMethod = true; - for (int i = 0; i < parameters.length; i++) + for (Method method : testClazz.getDeclaredMethods()) { - if (!(parameters[i] == paramVarArgs[i])) + if (!method.getName().equals(methodName)) + { + continue; + } + + Class[] parameters = method.getParameterTypes(); + if (!(parameters.length == paramVarArgs.length)) + { + continue; + } + boolean matchingMethod = true; + for (int i = 0; i < parameters.length; i++) { - matchingMethod = false; + if (!(parameters[i] == paramVarArgs[i])) + { + matchingMethod = false; + break; + } + } + + if (matchingMethod) + { + privateMethod = method; break; } } - - if (matchingMethod) - { - privateMethod = method; - break; - } } + + if (privateMethod == null) + { + privateMethod = finalClazz.getDeclaredMethod(methodName, paramVarArgs); + } + + privateMethod.setAccessible(true); + return privateMethod; } - - if (privateMethod == null) + catch (NoClassDefFoundError e) { - privateMethod = finalClazz.getDeclaredMethod(methodName, paramVarArgs); + Metamorph.LOGGER.error("Failed to do dynamic reflection on class " + clazz.getName() + ". " + + "This is most likely caused by a classloading issue in the class. " + + "For example, it may be referencing a class from a mod that isn't loaded, " + + "or referencing client-only code on a dedicated server."); + e.printStackTrace(); + incrementClassErrors(clazz); + + return null; } - - privateMethod.setAccessible(true); - return privateMethod; } } From 3bb6555cab07d5184d1d5c6ec7c481debafebdd9 Mon Sep 17 00:00:00 2001 From: asanetargoss Date: Sun, 10 Jan 2021 11:55:03 -0800 Subject: [PATCH 2/4] Fix survival morphing keybinds not working until creative morphs GUI opened --- .../creative/categories/AcquiredCategory.java | 2 +- .../api/creative/sections/UserSection.java | 1 + .../metamorph/client/KeyboardHandler.java | 1 + .../gui/survival/GuiSurvivalMorphs.java | 19 ++++----------- .../gui/survival/GuiSurvivalScreen.java | 24 ++++++++++++++----- 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/main/java/mchorse/metamorph/api/creative/categories/AcquiredCategory.java b/src/main/java/mchorse/metamorph/api/creative/categories/AcquiredCategory.java index 5d94890d..e95d5347 100644 --- a/src/main/java/mchorse/metamorph/api/creative/categories/AcquiredCategory.java +++ b/src/main/java/mchorse/metamorph/api/creative/categories/AcquiredCategory.java @@ -21,7 +21,7 @@ public AcquiredCategory(MorphSection parent, String title) public void setMorph(List morphs) { - this.morphs = morphs; + this.morphs = morphs; } @Override diff --git a/src/main/java/mchorse/metamorph/api/creative/sections/UserSection.java b/src/main/java/mchorse/metamorph/api/creative/sections/UserSection.java index d65d2cf5..ad402321 100644 --- a/src/main/java/mchorse/metamorph/api/creative/sections/UserSection.java +++ b/src/main/java/mchorse/metamorph/api/creative/sections/UserSection.java @@ -51,6 +51,7 @@ public UserSection(String title) this.acquired = new AcquiredCategory(this, "acquired"); this.recent = new RecentCategory(this, "recent"); + this.add(this.acquired); } @Override diff --git a/src/main/java/mchorse/metamorph/client/KeyboardHandler.java b/src/main/java/mchorse/metamorph/client/KeyboardHandler.java index b6a5fd67..181e68dc 100644 --- a/src/main/java/mchorse/metamorph/client/KeyboardHandler.java +++ b/src/main/java/mchorse/metamorph/client/KeyboardHandler.java @@ -128,6 +128,7 @@ public void onKey(InputEvent.KeyInputEvent event) { int key = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() + 256 : Keyboard.getEventKey(); + ClientProxy.getSurvivalScreen().onKeyPre(); MorphManager.INSTANCE.list.keyTyped(player, key); } } diff --git a/src/main/java/mchorse/metamorph/client/gui/survival/GuiSurvivalMorphs.java b/src/main/java/mchorse/metamorph/client/gui/survival/GuiSurvivalMorphs.java index 0eaa789a..e4670cd3 100644 --- a/src/main/java/mchorse/metamorph/client/gui/survival/GuiSurvivalMorphs.java +++ b/src/main/java/mchorse/metamorph/client/gui/survival/GuiSurvivalMorphs.java @@ -31,25 +31,14 @@ public void setupSections(boolean creative, Consumer callback) MorphList list = MorphManager.INSTANCE.list; IMorphing cap = Morphing.get(mc.player); - MorphSection section; - AcquiredCategory category; + UserSection section = (UserSection)list.sections.get(0); + AcquiredCategory category = section.acquired; - if (creative || Metamorph.allowMorphingIntoCategoryMorphs.get()) + if (!(creative || Metamorph.allowMorphingIntoCategoryMorphs.get())) { - UserSection user = (UserSection) list.sections.get(0); - - section = user; - section.update(mc.world); - category = user.acquired; - } - else - { - section = new MorphSection("user"); - category = new AcquiredCategory(section, "acquired"); - category.setMorph(cap == null ? Collections.emptyList() : cap.getAcquiredMorphs()); - section.add(category); } + section.update(mc.world); GuiMorphSection element = section.getGUI(mc, null, callback); diff --git a/src/main/java/mchorse/metamorph/client/gui/survival/GuiSurvivalScreen.java b/src/main/java/mchorse/metamorph/client/gui/survival/GuiSurvivalScreen.java index 4a16c680..cdf7ce8b 100644 --- a/src/main/java/mchorse/metamorph/client/gui/survival/GuiSurvivalScreen.java +++ b/src/main/java/mchorse/metamorph/client/gui/survival/GuiSurvivalScreen.java @@ -96,14 +96,10 @@ public boolean doesGuiPauseGame() { return Metamorph.pauseGUIInSP.get(); } - - /** - * Open the survival morph menu and update the morphs element - */ - public GuiSurvivalScreen open() + + public void setupSections() { EntityPlayer player = Minecraft.getMinecraft().player; - IMorphing cap = Morphing.get(player); boolean creative = player.isCreative(); boolean allowed = Metamorph.allowMorphingIntoCategoryMorphs.get(); @@ -113,11 +109,27 @@ public GuiSurvivalScreen open() this.allowed = allowed; this.morphs.setupSections(creative, (section) -> this.fill(section.morph)); } + } + + /** + * Open the survival morph menu and update the morphs element + */ + public GuiSurvivalScreen open() + { + EntityPlayer player = Minecraft.getMinecraft().player; + IMorphing cap = Morphing.get(player); + + this.setupSections(); this.setSelected(cap.getCurrentMorph()); return this; } + + public void onKeyPre() + { + setupSections(); + } /** * Set given morph selected From 2c6c28c292bd4947719500f0a9ecaa0305c5e8ce Mon Sep 17 00:00:00 2001 From: asanetargoss Date: Sun, 10 Jan 2021 21:52:47 -0800 Subject: [PATCH 3/4] Fix double tapping arrow keys causing morphing at end of the survival morphing list --- .../gui/survival/GuiSurvivalScreen.java | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/main/java/mchorse/metamorph/client/gui/survival/GuiSurvivalScreen.java b/src/main/java/mchorse/metamorph/client/gui/survival/GuiSurvivalScreen.java index cdf7ce8b..a7108002 100644 --- a/src/main/java/mchorse/metamorph/client/gui/survival/GuiSurvivalScreen.java +++ b/src/main/java/mchorse/metamorph/client/gui/survival/GuiSurvivalScreen.java @@ -27,6 +27,8 @@ import net.minecraft.client.resources.I18n; import net.minecraft.entity.player.EntityPlayer; +import java.io.IOException; + import org.lwjgl.input.Keyboard; /** @@ -53,7 +55,8 @@ public class GuiSurvivalScreen extends GuiBase AbstractMorph lastMorphSelected = null; private static final int DOUBLE_CLICK_TIME_MS = 500; - private long lastClickTime = -DOUBLE_CLICK_TIME_MS - 1; + private long lastMorphSelectTime = -DOUBLE_CLICK_TIME_MS - 1; + private boolean isClicking = false; public GuiSurvivalScreen() { @@ -161,22 +164,47 @@ public void checkCurrentMorph() checkCurrentMorph(currentMorph, this.morphs.getSelected()); } - private void checkDoubleClick(AbstractMorph morph) + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException + { + isClicking = true; + super.mouseClicked(mouseX, mouseY, mouseButton); + } + + @Override + protected void mouseReleased(int mouseX, int mouseY, int state) + { + isClicking = false; + super.mouseReleased(mouseX, mouseY, state); + } + + @Override + protected void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) + { + isClicking = false; + super.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick); + } + + private void checkDoubleClickMorph(AbstractMorph morph) { if (morph == null) { return; } + if (!isClicking) + { + return; + } - long clickTime = Minecraft.getSystemTime(); - long dt = clickTime - lastClickTime; - lastClickTime = clickTime; + long morphSelectTime = Minecraft.getSystemTime(); + long dt = morphSelectTime - lastMorphSelectTime; + lastMorphSelectTime = morphSelectTime; if (dt > 0 && dt < DOUBLE_CLICK_TIME_MS && lastMorphSelected.equals(morph)) { MorphAPI.selectMorph(morph); GuiUtils.playClick(); // Prevent re-fires - lastClickTime = 0; + lastMorphSelectTime = 0; this.closeScreen(); } lastMorphSelected = morph; @@ -203,7 +231,7 @@ public void fill(AbstractMorph morph) this.keybind.setKeybind(morph.keybind); } - checkDoubleClick(morph); + checkDoubleClickMorph(morph); } /** From d07f8a38cca4ac6d30c1421e42d6f2f8e65104bf Mon Sep 17 00:00:00 2001 From: asanetargoss Date: Mon, 11 Jan 2021 20:32:04 -0800 Subject: [PATCH 4/4] Make reflection class blacklist work across threads --- .../mchorse/metamorph/util/InvokeUtil.java | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/main/java/mchorse/metamorph/util/InvokeUtil.java b/src/main/java/mchorse/metamorph/util/InvokeUtil.java index f9783896..9f603ce7 100644 --- a/src/main/java/mchorse/metamorph/util/InvokeUtil.java +++ b/src/main/java/mchorse/metamorph/util/InvokeUtil.java @@ -9,23 +9,17 @@ public class InvokeUtil { - protected static final int MAX_ERRORS_PER_CLASS = 5; - protected static ThreadLocal, Integer>> classErrorCounts = new ThreadLocal<>(); - - protected static WeakHashMap, Integer> getClassBlacklist() - { - WeakHashMap, Integer> blacklist = classErrorCounts.get(); - if (blacklist == null) { - blacklist = new WeakHashMap<>(); - classErrorCounts.set(blacklist); - } - return blacklist; - } + private static final int MAX_ERRORS_PER_CLASS = 5; + private static Object classErrorCountLock = new Object(); + private static WeakHashMap, Integer> classErrorCounts = new WeakHashMap<>(); protected static boolean isClassBlacklisted(Class clazz) { - WeakHashMap, Integer> blacklist = getClassBlacklist(); - Integer count = blacklist.get(clazz); + Integer count; + synchronized(classErrorCountLock) + { + count = classErrorCounts.get(clazz); + } if (count == null) { count = 0; @@ -35,22 +29,25 @@ protected static boolean isClassBlacklisted(Class clazz) protected static void incrementClassErrors(Class clazz) { - WeakHashMap, Integer> blacklist = getClassBlacklist(); - Integer count = blacklist.get(clazz); - if (count == null) + Integer count; + synchronized(classErrorCountLock) { - count = 1; - } - else - { - ++count; + count = classErrorCounts.get(clazz); + if (count == null) + { + count = 1; + } + else + { + ++count; + } + classErrorCounts.put(clazz, count); } - blacklist.put(clazz, count); if (count == MAX_ERRORS_PER_CLASS) { Metamorph.LOGGER.error("Too many errors for class " + clazz.getName() + ". " + - "Class will be blacklisted from reflection on this thread."); + "Class will be blacklisted from reflection."); } }