package electroblob.wizardry.client.animation; import electroblob.wizardry.Wizardry; import electroblob.wizardry.item.SpellActions; import electroblob.wizardry.util.InventoryUtils; import electroblob.wizardry.util.JavaUtils; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.AbstractClientPlayer; import net.minecraft.client.model.ModelBiped; import net.minecraft.client.model.ModelPlayer; import net.minecraft.client.model.ModelRenderer; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.entity.Render; import net.minecraft.client.renderer.entity.RenderLivingBase; import net.minecraft.client.renderer.entity.RenderPlayer; import net.minecraft.client.renderer.entity.layers.LayerBipedArmor; import net.minecraft.client.renderer.entity.layers.LayerRenderer; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.EntityEquipmentSlot; import net.minecraft.item.EnumAction; import net.minecraft.item.ItemStack; import net.minecraft.util.EnumHand; import net.minecraft.util.EnumHandSide; import net.minecraftforge.client.ForgeHooksClient; import net.minecraftforge.client.event.RenderPlayerEvent; import net.minecraftforge.client.event.RenderSpecificHandEvent; import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.ObfuscationReflectionHelper; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.relauncher.Side; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * Handles the setup and rendering events for custom player animations, as well as registering of animations. Addons * can also register their own animations here, see {@link PlayerAnimator#registerAnimation(Animation)}. * @author Electroblob * @since Wizardry 4.3 * @see electroblob.wizardry.item.SpellActions SpellActions */ @Mod.EventBusSubscriber(Side.CLIENT) public class PlayerAnimator { // TODO: Attempt to extend this to all bipeds private static final List animations = new ArrayList<>(); /** Stores all registered layer renderers for lazy-loading of armour models later. */ private static final Map>> playerLayers = new HashMap<>(); /** Stores the main model for each renderer plus all models for registered layer renderers. */ private static final Map> playerLayerModels = new HashMap<>(); /** Reflected into {@link RenderLivingBase}{@code #layerRenderers}. */ private static final Field layerRenderers; /** True if wizardry's player animations have been overridden with another mod (currently only MoBends) */ private static boolean overridden; static { layerRenderers = ObfuscationReflectionHelper.findField(RenderLivingBase.class, "field_177097_h"); } /** * Registers a new player animation. This method should be called from the {@code init()} phase via a client proxy. * @param animation The animation to register */ // N.B. This can (and will) be called before PlayerAnimator#init() is called public static void registerAnimation(Animation animation){ if(animations.contains(animation)){ Wizardry.logger.warn("Animation {} is already registered!", animation.getName()); }else{ animations.add(animation); } } /** Returns true if spellcasting animations are enabled, false otherwise. */ public static boolean areAnimationsEnabled(){ return Wizardry.settings.spellcastingAnimations && !overridden; } public static void init(){ if(Loader.isModLoaded("mobends")) overridden = true; if(!areAnimationsEnabled()) return; for(RenderPlayer renderer : Minecraft.getMinecraft().getRenderManager().getSkinMap().values()){ loadLayersForRenderer(renderer); } } @SuppressWarnings("unchecked") private static void loadLayersForRenderer(RenderPlayer renderer){ List models = new ArrayList<>(); ModelRendererExtended.wrap(renderer.getMainModel()); models.add(renderer.getMainModel()); try { List> layers = (List>)layerRenderers.get(renderer); // Save armour layers for lazy-loading later (ignore other layers so iteration is faster) playerLayers.put(renderer, layers.stream().filter(l -> l instanceof LayerBipedArmor).collect(Collectors.toList())); for(LayerRenderer layer : layers){ for(Field field : JavaUtils.getAllFields(layer.getClass())){ field.setAccessible(true); // If your layer model doesn't extend ModelBiped, you DESERVE to be incompatible! // (Just kidding... there's nothing I can do about it anyway) if(field.get(layer) instanceof ModelBiped){ ModelBiped model = (ModelBiped)field.get(layer); ModelRendererExtended.wrap(model); models.add(model); } } } }catch(IllegalAccessException e){ Wizardry.logger.error("Error during reflective access of render layers: ", e); } playerLayerModels.put(renderer, models); } private static void updateModels(EntityPlayer player, Render renderer, float partialTicks, boolean firstPerson){ // Lazy-load models from custom renderers (some mods assign their custom renderers in postInit, so they might // not be picked up from PlayerAnimator#init) if(renderer instanceof RenderPlayer && !playerLayerModels.containsKey(renderer)){ loadLayersForRenderer((RenderPlayer)renderer); } List models = playerLayerModels.get(renderer); // If no models were registered for the given renderer, don't do anything if(models != null){ List> layers = playerLayers.get(renderer); // If no layers were registered for the given renderer there's no point trying to lazy-load anything! if(layers != null){ // Biped armour is special because of Forge's armour item render hook // This needs to be lazy-loaded because we need access to an actual item for(LayerRenderer layer : layers){ if(layer instanceof LayerBipedArmor){ for(EntityEquipmentSlot slot : InventoryUtils.ARMOUR_SLOTS){ ItemStack armour = player.getItemStackFromSlot(slot); // This method could behave dynamically depending on stuff like NBT so there's not really any way to // optimise it further, we *have* to retrieve the model every time (shouldn't be too bad though) ModelBiped model = ForgeHooksClient.getArmorModel(player, armour, slot, ((LayerBipedArmor)layer).getModelFromSlot(slot)); if(!models.contains(model)){ // Ignore already-wrapped models models.add(model); ModelRendererExtended.wrap(model); } } } } } for(Animation animation : animations){ if(animation.shouldDisplay(player, firstPerson)){ boolean autoRotateSecondLayer = animation.autoRotateSecondLayer(player, firstPerson); for(ModelBiped model : models){ if(!ModelRendererExtended.isWrapped(model)) continue; // Should never happen but somehow it does animation.setRotationAngles(player, model, partialTicks, firstPerson); if(autoRotateSecondLayer && model instanceof ModelPlayer){ alignSecondLayer((ModelPlayer)model); } } } } } } /** Rotates the second layer boxes of the given (wrapped) player model to match the first layer. */ public static void alignSecondLayer(ModelPlayer model){ ((ModelRendererExtended)model.bipedBodyWear) .setRotationTo((ModelRendererExtended)model.bipedBody); ((ModelRendererExtended)model.bipedRightArmwear).setRotationTo((ModelRendererExtended)model.bipedRightArm); ((ModelRendererExtended)model.bipedLeftArmwear) .setRotationTo((ModelRendererExtended)model.bipedLeftArm); ((ModelRendererExtended)model.bipedRightLegwear).setRotationTo((ModelRendererExtended)model.bipedBody); ((ModelRendererExtended)model.bipedLeftLegwear) .setRotationTo((ModelRendererExtended)model.bipedBody); } @SubscribeEvent public static void onRenderHandEvent(RenderSpecificHandEvent event){ if(!areAnimationsEnabled()) return; AbstractClientPlayer player = Minecraft.getMinecraft().player; EnumAction action = event.getItemStack().getItemUseAction(); if(player.isHandActive() && player.getActiveHand() == event.getHand() && SpellActions.getSpellActions().contains(action)){ // Minecraft's item renderer helpfully doesn't have a default case for the usage action so it doesn't do any // transformations at all (which is VERY ANNOYING!) - the following lines are from ItemRenderer#transformSideFirstPerson int i = (player.getPrimaryHand() == EnumHandSide.RIGHT) == (event.getHand() == EnumHand.MAIN_HAND) ? 1 : -1; GlStateManager.translate((float)i * 0.56F, -0.52F + event.getEquipProgress() * -0.6F, -0.72F); } updateModels(player, Minecraft.getMinecraft().getRenderManager().getEntityRenderObject(player), event.getPartialTicks(), true); } @SubscribeEvent public static void onRenderPlayerPreEvent(RenderPlayerEvent.Pre event){ if(!areAnimationsEnabled()) return; // boolean firstPerson = event.getEntityPlayer() == Minecraft.getMinecraft().player // && Minecraft.getMinecraft().gameSettings.thirdPersonView == 0; updateModels(event.getEntityPlayer(), event.getRenderer(), event.getPartialRenderTick(), false); } @SubscribeEvent public static void onRenderPlayerPostEvent(RenderPlayerEvent.Post event){ if(!areAnimationsEnabled()) return; List models = playerLayerModels.get(event.getRenderer()); if(models != null){ for(ModelBiped model : models){ for(ModelRenderer box : model.boxList){ // For ModelPlayer, this will include the second layer // Some models have extra boxes, they (probably) don't need wrapping but we need this check! if(box instanceof ModelRendererExtended){ ((ModelRendererExtended)box).resetRotation(); } } } } } }