package electroblob.wizardry.integration; import com.google.common.collect.ImmutableSet; import electroblob.wizardry.Wizardry; import electroblob.wizardry.util.WizardryUtilities; import net.minecraft.entity.Entity; import net.minecraft.util.DamageSource; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; import java.util.Set; /** * This class implements an 'if-all-else-fails' fix for cross-mod infinite looping caused by re-applying damage in * attack events (see GitHub issue #72 for details). * The methods in this class should only be used when intercepting one of the attack/damage events and * dealing damage from within it. */ @Mod.EventBusSubscriber public final class DamageSafetyChecker { /** We don't want to tell users to add any of these to the blacklist, as that would be problematic. */ // NOTE: Make sure this is updated for each new version of Minecraft private static final Set VANILLA_DAMAGE_NAMES = ImmutableSet.of("inFire", "lightningBolt", "onFire", "lava", "hotFloor", "inWall", "cramming", "drown", "starve", "cactus", "fall", "flyIntoWall", "outOfWorld", "generic", "magic", "wither", "anvil", "fallingBlock", "dragonBreath", "fireworks", "mob", "player", "arrow", "thrown", "indirectMagic", "thorns", "explosion", "explosion.player"); /** * Global counter which is incremented once for each call to * {@link DamageSafetyChecker#attackEntitySafely(Entity, DamageSource, float, String, DamageSource, boolean)} * This allows for detection and avoidance of imminent StackOverflowErrors caused by looping between mods. */ private static int attacksThisTick = 0; /** The number of calls per loaded entity after which damage will be reassigned. */ // It's a fair bet that if an entity is being damaged 15 times in a single tick, then something is wrong! // Based on the crash report in issue #72, there were approximately 38 calls before the error was thrown. // The number of calls will vary depending on the stack size and possibly what else is happening at the time. private static final int EXCESSIVE_CALL_THRESHOLD = 15; /** The number of calls per loaded entity after which damage will be cancelled entirely. */ private static final int EXCESSIVE_CALL_LIMIT = 25; /** * Attacks the specified target with specified damage source and damage amount, checking for the blacklist and * excessive looping in the process. Under normal circumstances, this method simply calls * {@code target.attackEntityFrom(...)}. If excessive looping is detected, the damage source is substituted for * the given fallback instead, and a warning is printed to the console. *

* This method should only be used within the attack events (LivingAttackEvent, LivingHurtEvent, LivingDamageEvent * and possibly LivingKnockBackEvent, depending on the circumstances). * @param target The target to apply the damage to. * @param source The source of the damage. * @param damage The amount of damage to be applied. * @param originalSourceName The string identifier for the original damage source (i.e. the one being replaced). * This allows wizardry to request that users add it to the blacklist. * @param fallback The fallback damage source for when excessive looping is detected. This must not be the * same as the re-applied source, or this method is pointless! Usually it will be more general. * @param knockback True to apply knockback as normal, false to use the knockback-free methods in WizardryUtilities * (see {@link WizardryUtilities#attackEntityWithoutKnockback(Entity, DamageSource, float)}). */ public static boolean attackEntitySafely(Entity target, DamageSource source, float damage, String originalSourceName, DamageSource fallback, boolean knockback){ for(String sourceName : Wizardry.settings.damageSourceBlacklist){ if(originalSourceName.equals(sourceName)){ // Blacklist behaviour // Same as fallback behaviour, but without the log message // No harm in still incrementing the counter attacksThisTick++; return knockback ? target.attackEntityFrom(fallback, damage) : WizardryUtilities.attackEntityWithoutKnockback(target, fallback, damage); } } if(attacksThisTick > EXCESSIVE_CALL_LIMIT * target.world.loadedEntityList.size()){ // This should never ever happen unless another mod is intercepting non-entity-based damage and damaging // the same target. logInterception(originalSourceName, true); return false; } if(attacksThisTick > EXCESSIVE_CALL_THRESHOLD * target.world.loadedEntityList.size()){ // Sometimes this is unavoidable, it's neither mod's fault but without some kind of forge standard or // universal cooperation there's no easy way to prevent it. logInterception(originalSourceName, false); // Fallback behaviour attacksThisTick++; return knockback ? target.attackEntityFrom(fallback, damage) : WizardryUtilities.attackEntityWithoutKnockback(target, fallback, damage); }else{ // Normal behaviour attacksThisTick++; return knockback ? target.attackEntityFrom(source, damage) : WizardryUtilities.attackEntityWithoutKnockback(target, source, damage); } } /** * See {@link DamageSafetyChecker#attackEntitySafely(Entity, DamageSource, float, String, DamageSource, boolean)}. * This version is for when the source being replaced is used as a fallback, i.e. when a damage source is being * replaced for technical reasons (e.g. summoned creatures) rather than as part of a game mechanic (e.g. shadow ward). * This means that if excessive looping is detected, the code will work as if the event was never intercepted. */ public static boolean attackEntitySafely(Entity target, DamageSource source, float damage, DamageSource originalSource, boolean knockback){ return attackEntitySafely(target, source, damage, originalSource.getDamageType(), originalSource, knockback); } /** * See {@link DamageSafetyChecker#attackEntitySafely(Entity, DamageSource, float, String, DamageSource, boolean)}. * Fallback defaults to {@link DamageSource#MAGIC} and knockback defaults to true. */ public static boolean attackEntitySafely(Entity target, DamageSource source, float damage, String originalSourceName){ return attackEntitySafely(target, source, damage, originalSourceName, DamageSource.MAGIC, true); } /** Prints the appropriate message about the damage interception to the console. */ private static void logInterception(String originalSourceName, boolean aborted){ if(!Wizardry.settings.compatibilityWarnings) return; // No warnings if they're disabled! boolean vanillaName = VANILLA_DAMAGE_NAMES.contains(originalSourceName); if(aborted){ Wizardry.logger.warn("Entity attack excessive call limit reached, aborting entity damage entirely!"); }else{ Wizardry.logger.warn("Entity attack excessive call threshold reached, substituting for non-entity-based " + "damage to avert a crash."); } if(vanillaName){ Wizardry.logger.info("The damage source in question had a vanilla identifier. If you know which mod may " + "have caused this, consider asking the author to add a custom identifier so it may be blacklisted. " + "You can turn this warning off using the compatibilityWarnings config option. Please do not report " + "it to wizardry's author."); }else{ Wizardry.logger.info("To prevent this message and improve efficiency, add \"" + originalSourceName + "\" " + "(without quotes) to the damage source blacklist in the config. Please do not report " + "this warning unless you have added the damage source to the blacklist already."); } } @SubscribeEvent public static void tick(TickEvent event){ // We actually want this to fire on both sides, because attacks are common code. if(event.phase == TickEvent.Phase.START && event.type == TickEvent.Type.WORLD){ attacksThisTick = 0; // Reset the attack call counter } } }