From 101bb0984c9e30878138ae73d96202781607ce1e Mon Sep 17 00:00:00 2001 From: OpexHunter Date: Tue, 18 Feb 2025 16:32:02 +0300 Subject: [PATCH] init --- build.gradle | 26 +- gradle.properties | 2 + .../java/com/punkcraft/punkapi/PunkAPI.java | 112 ---- .../java/com/rejahtavi/rfp2/ClientProxy.java | 68 +++ .../com/rejahtavi/rfp2/EntityPlayerDummy.java | 123 +++++ src/main/java/com/rejahtavi/rfp2/IProxy.java | 18 + src/main/java/com/rejahtavi/rfp2/RFP2.java | 193 +++++++ .../java/com/rejahtavi/rfp2/RFP2Config.java | 160 ++++++ .../java/com/rejahtavi/rfp2/RFP2Keybind.java | 54 ++ .../java/com/rejahtavi/rfp2/RFP2State.java | 515 ++++++++++++++++++ .../com/rejahtavi/rfp2/RenderPlayerDummy.java | 295 ++++++++++ .../java/com/rejahtavi/rfp2/ServerProxy.java | 39 ++ .../rejahtavi/rfp2/compat/RFP2CompatApi.java | 50 ++ .../compat/handlers/RFP2CompatHandler.java | 54 ++ .../resources/assets/rfp2/lang/en_us.lang | 6 + src/main/resources/mcmod.info | 14 +- src/main/resources/pack.mcmeta | 4 +- 17 files changed, 1609 insertions(+), 124 deletions(-) delete mode 100644 src/main/java/com/punkcraft/punkapi/PunkAPI.java create mode 100644 src/main/java/com/rejahtavi/rfp2/ClientProxy.java create mode 100644 src/main/java/com/rejahtavi/rfp2/EntityPlayerDummy.java create mode 100644 src/main/java/com/rejahtavi/rfp2/IProxy.java create mode 100644 src/main/java/com/rejahtavi/rfp2/RFP2.java create mode 100644 src/main/java/com/rejahtavi/rfp2/RFP2Config.java create mode 100644 src/main/java/com/rejahtavi/rfp2/RFP2Keybind.java create mode 100644 src/main/java/com/rejahtavi/rfp2/RFP2State.java create mode 100644 src/main/java/com/rejahtavi/rfp2/RenderPlayerDummy.java create mode 100644 src/main/java/com/rejahtavi/rfp2/ServerProxy.java create mode 100644 src/main/java/com/rejahtavi/rfp2/compat/RFP2CompatApi.java create mode 100644 src/main/java/com/rejahtavi/rfp2/compat/handlers/RFP2CompatHandler.java create mode 100644 src/main/resources/assets/rfp2/lang/en_us.lang diff --git a/build.gradle b/build.gradle index 6c041ab..0ac8e65 100644 --- a/build.gradle +++ b/build.gradle @@ -13,9 +13,10 @@ apply plugin: 'net.minecraftforge.gradle' apply plugin: 'eclipse' apply plugin: 'maven-publish' -version = '1.0' -group = 'com.yourname.modid' // http://maven.apache.org/guides/mini/guide-naming-conventions.html -archivesBaseName = 'modid' + +version = "${mc_version}-${mod_version}" +group = 'com.rejahtavi.rfp2' +archivesBaseName = 'RealFirstPerson2' sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.8' // Need this here so eclipse task generates correctly. @@ -45,6 +46,25 @@ dependencies { minecraft 'net.minecraftforge:forge:1.12.2-14.23.5.2860' } +// Обработка ресурсов +processResources { + // Убедитесь, что задача пересоздается при изменении версий + inputs.property "version", project.version + inputs.property "mcversion", mc_version + + // Замена значений в mcmod.info + from(sourceSets.main.resources.srcDirs) { + include 'mcmod.info' + expand 'version': project.version, 'mcversion': mc_version + } + + // Копирование остальных файлов без изменения + from(sourceSets.main.resources.srcDirs) { + exclude 'mcmod.info' + } +} + + // Example for how to get properties into the manifest for reading by the runtime.. jar { manifest { diff --git a/gradle.properties b/gradle.properties index 4cd9793..53fcfba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,3 +3,5 @@ org.gradle.jvmargs=-Xmx3G org.gradle.daemon=false org.gradle.java.home=/usr/lib/jvm/openjdk8 +mc_version=1.12.2 +mod_version=1.3.3 diff --git a/src/main/java/com/punkcraft/punkapi/PunkAPI.java b/src/main/java/com/punkcraft/punkapi/PunkAPI.java deleted file mode 100644 index 9c44e08..0000000 --- a/src/main/java/com/punkcraft/punkapi/PunkAPI.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.punkcraft.punkapi; - -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; -import io.netty.buffer.Unpooled; - -import net.minecraft.client.entity.EntityPlayerSP; -import net.minecraft.entity.ai.attributes.IAttributeInstance; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.init.Items; -import net.minecraft.item.ItemStack; -import net.minecraft.item.ItemSword; -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -import net.minecraft.client.Minecraft; -import net.minecraft.network.NetworkManager; -import net.minecraft.network.PacketBuffer; -import net.minecraft.network.play.client.CPacketCustomPayload; -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.fml.common.event.FMLInitializationEvent; -import net.minecraftforge.fml.common.gameevent.TickEvent; -import net.minecraftforge.fml.common.network.FMLNetworkEvent; -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.fml.relauncher.SideOnly; - -import java.io.File; - -@Mod(modid = PunkAPI.MODID, name = PunkAPI.NAME, version = PunkAPI.VERSION) -public class PunkAPI { - public static final String MODID = "punkapi"; - public static final String NAME = "PunkAPI"; - public static final String VERSION = "1.0"; - private String token; - private ItemStack prevMainHandItem = ItemStack.EMPTY; - - @Mod.EventHandler - public void init(FMLInitializationEvent event) { - token = System.getProperty("token"); - String c = System.getProperty("c"); - - if (c == null || !c.equals("ea42ba5b1a35b89e628e07f881198144")) { - // Minecraft.getMinecraft().shutdown(); - } - - MinecraftForge.EVENT_BUS.register(this); - File minecraftDir = Minecraft.getMinecraft().mcDataDir.getAbsoluteFile().getParentFile(); - String directoryName = minecraftDir.getName(); - - if (!"ZombieExtrieme".equals(directoryName)) { - Minecraft.getMinecraft().shutdown(); - } - } - - @Mod.EventHandler - public void preInit(FMLPreInitializationEvent event) { - } - - @SubscribeEvent - @SideOnly(Side.CLIENT) - public void onClientConnected(FMLNetworkEvent.ClientConnectedToServerEvent event) { - NetworkManager networkManager = event.getManager(); - sendToken(networkManager); - } - - @SubscribeEvent - public void onClientItemChange(TickEvent.ClientTickEvent event) { - EntityPlayerSP player = Minecraft.getMinecraft().player; - if (player == null) { - return; - } - - ItemStack currentMainHandItem = player.getHeldItemMainhand(); - - if (!ItemStack.areItemStacksEqual(prevMainHandItem, currentMainHandItem)) { - onItemChange(player, currentMainHandItem); - prevMainHandItem = currentMainHandItem.copy(); - } - } - - private void onItemChange(EntityPlayer player, ItemStack newItem) { - IAttributeInstance attribute = player.getEntityAttribute(EntityPlayer.REACH_DISTANCE); - - if (newItem.getItem() instanceof ItemSword) { - attribute.setBaseValue(1.17); - } else if (newItem.getItem() == Items.WOODEN_AXE) { - attribute.setBaseValue(0); - } else { - attribute.setBaseValue(4.0); - } - } - - @SubscribeEvent - @SideOnly(Side.CLIENT) - public void onClientTick(TickEvent.ClientTickEvent event) { - Minecraft.getMinecraft().gameSettings.gammaSetting = 0.0f; - } - - private void sendToken(NetworkManager networkManager) { - String playerName = Minecraft.getMinecraft().getSession().getUsername(); - - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - //out.writeUTF(token); - out.writeUTF("ea42ba5b1a35b89e628e07f881198144"); - out.writeUTF(playerName); - out.writeUTF("ea42ba5b1a35b89e628e07f881198144"); - - CPacketCustomPayload packet = new CPacketCustomPayload("custom:token", - new PacketBuffer(Unpooled.wrappedBuffer(out.toByteArray()))); - networkManager.sendPacket(packet); - } -} diff --git a/src/main/java/com/rejahtavi/rfp2/ClientProxy.java b/src/main/java/com/rejahtavi/rfp2/ClientProxy.java new file mode 100644 index 0000000..94b2f40 --- /dev/null +++ b/src/main/java/com/rejahtavi/rfp2/ClientProxy.java @@ -0,0 +1,68 @@ +package com.rejahtavi.rfp2; + +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.client.registry.ClientRegistry; +import net.minecraftforge.fml.client.registry.RenderingRegistry; +import net.minecraftforge.fml.common.ModMetadata; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.event.FMLServerStartingEvent; +import net.minecraftforge.fml.common.registry.EntityRegistry; + +// RFP2.PROXY will be instantiated with this class if we are running as a client. +public class ClientProxy implements IProxy +{ + // Called at the start of mod loading + @Override + public void preInit(FMLPreInitializationEvent event) + { + // Initialize logging + RFP2.logger = event.getModLog(); + + // Register mod metadata + ModMetadata m = event.getModMetadata(); + m.modId = RFP2.MODID; + m.name = RFP2.MODNAME; + m.version = RFP2.MODVER; + m.description = "Implements full body rendering in first person."; + m.authorList.clear(); + m.authorList.add("Rejah Tavi"); + m.authorList.add("don_bruce"); + m.autogenerated = false; + + // Register entity rendering handler for the player dummy + RenderingRegistry.registerEntityRenderingHandler(EntityPlayerDummy.class, RenderPlayerDummy::new); + } + + // Called after all other mod preInit()s have run + @Override + public void init(FMLInitializationEvent event) + { + // Load config + RFP2.config = new RFP2Config(); + + // Register keybinds + ClientRegistry.registerKeyBinding(RFP2.keybindArmsToggle.keyBindingInstance); + ClientRegistry.registerKeyBinding(RFP2.keybindModToggle.keyBindingInstance); + ClientRegistry.registerKeyBinding(RFP2.keybindHeadRotationToggle.keyBindingInstance); + + // Register player dummy entity + EntityRegistry.registerModEntity(new ResourceLocation(RFP2.MODID, "PlayerDummy"), EntityPlayerDummy.class, "PlayerDummy", 0, RFP2.MODID, 5, 100, false); + } + + // Called after all other mod init()s have run + @Override + public void postInit(FMLPostInitializationEvent event) + { + // Begin tracking state + RFP2.state = new RFP2State(); + } + + // Called when starting up a dedicated server + @Override + public void serverStarting(FMLServerStartingEvent event) + { + // This will never get called on client side + } +} diff --git a/src/main/java/com/rejahtavi/rfp2/EntityPlayerDummy.java b/src/main/java/com/rejahtavi/rfp2/EntityPlayerDummy.java new file mode 100644 index 0000000..acc26b0 --- /dev/null +++ b/src/main/java/com/rejahtavi/rfp2/EntityPlayerDummy.java @@ -0,0 +1,123 @@ +package com.rejahtavi.rfp2; + +import net.minecraft.block.BlockLiquid; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +/* + * This class implements the functionality of the PlayerDummy entity. + * + * The PlayerDummy's presence is used as a trigger to draw the fake player body every frame, + * as well as a way to manipulate the precise positioning of the fake player body. + */ +public class EntityPlayerDummy extends Entity +{ + // Stores last time the entity state changed + public long lastTickUpdated; + + // Stores the last known swimming state + private boolean swimming = false; + + // Constructor + public EntityPlayerDummy(World world) + { + // Call parent Entity() constructor with appropriate world reference + super(world); + + // Set up new dummy object + this.ignoreFrustumCheck = true; + this.setSize(0, 2); + this.lastTickUpdated = world.getTotalWorldTime(); + } + + // Called when entity should update itself + public void onUpdate() + { + // Get reference to current local player and null check it + EntityPlayer player = Minecraft.getMinecraft().player; + if (player == null) + { + // Can't find our player, so remove ourself from the world + this.setDead(); + } + else + { + // Record the current tick number to prove we did an update + this.lastTickUpdated = world.getTotalWorldTime(); + + // Match our position and rotation to player, then record the current tick + this.setPositionAndRotation(player.posX, player.posY, player.posZ, player.rotationYaw, player.rotationPitch); + + // Update swimming status; some conditions are necessary to start or stop swimming + // this provides hysteresis and avoids any "flickering" of the swimming state + + // Get references to the block at the player's feet, plus one above and one below + BlockPos atHead = new BlockPos(player.posX, player.posY + player.eyeHeight, player.posZ); + BlockPos atFeet = new BlockPos(player.posX, player.posY, player.posZ); + BlockPos belowFeet = atFeet.down(); + + // Check each block location for liquid + boolean liquidAtHead = player.world.getBlockState(atHead).getBlock() instanceof BlockLiquid; + boolean liquidAtFeet = player.world.getBlockState(atFeet).getBlock() instanceof BlockLiquid; + boolean liquidBelowFeet = player.world.getBlockState(belowFeet).getBlock() instanceof BlockLiquid; + + // Are we currently swimming? + if (swimming) + { + // Currently swimming. Figure out if we should stop. + if (RFP2Config.compatibility.useAggressiveSwimmingCheck) + { + // use aggressive version of swimming checks + // requires player to FULLY clear water to stop swimming + if (!liquidAtHead && !liquidAtFeet && !liquidBelowFeet) swimming = false; + } + else + { + // use normal version of swimming checks + // considers player no longer swimming when standing in 1 block deep water + if (!liquidAtHead && !liquidBelowFeet) swimming = false; + } + } + else + { + // Currently NOT swimming. Figure out if we should start. + if (RFP2Config.compatibility.useAggressiveSwimmingCheck) + { + // use aggressive version of swimming checks + // only requires player to touch water to start swimming + // (below feet not checked, because you can "lean over" the edge of water and this is definitely not swimming) + if (liquidAtHead || liquidAtFeet) swimming = true; + } + else + { + // use normal version of swimming checks + // only considers player swimming once their head goes under + if (liquidAtHead && liquidAtFeet) swimming = true; + } + } + } + } + + // returns whether player is swimming or not + public boolean isSwimming() + { + return swimming; + } + + // Remaining methods are required by the interface but we don't have anything special to do in them. + public void entityInit() + { + } + + public void readEntityFromNBT(NBTTagCompound x) + { + } + + public void writeEntityToNBT(NBTTagCompound x) + { + } +} diff --git a/src/main/java/com/rejahtavi/rfp2/IProxy.java b/src/main/java/com/rejahtavi/rfp2/IProxy.java new file mode 100644 index 0000000..7835bd0 --- /dev/null +++ b/src/main/java/com/rejahtavi/rfp2/IProxy.java @@ -0,0 +1,18 @@ +package com.rejahtavi.rfp2; + +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.event.FMLServerStartingEvent; + +public interface IProxy +{ + // FML life cycle events for mod loading + void preInit(FMLPreInitializationEvent event); + + void init(FMLInitializationEvent event); + + void postInit(FMLPostInitializationEvent event); + + void serverStarting(FMLServerStartingEvent event); +} diff --git a/src/main/java/com/rejahtavi/rfp2/RFP2.java b/src/main/java/com/rejahtavi/rfp2/RFP2.java new file mode 100644 index 0000000..7689d62 --- /dev/null +++ b/src/main/java/com/rejahtavi/rfp2/RFP2.java @@ -0,0 +1,193 @@ +package com.rejahtavi.rfp2; + +import java.util.ArrayList; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; +import org.lwjgl.input.Keyboard; +import com.rejahtavi.rfp2.compat.RFP2CompatApi; +import com.rejahtavi.rfp2.compat.handlers.RFP2CompatHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.TextComponentString; +import net.minecraft.util.text.TextFormatting; +import net.minecraftforge.fml.common.Loader; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.Mod.EventHandler; +import net.minecraftforge.fml.common.SidedProxy; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; + +// Register mod with forge +@Mod(modid = RFP2.MODID, name = RFP2.MODNAME, version = RFP2.MODVER, dependencies = RFP2.MODDEPS, clientSideOnly = true, acceptedMinecraftVersions = "1.12.2", acceptableRemoteVersions = "*") +public class RFP2 { + // Conflicting Mods + public static final String[] CONFLICT_MODIDS = { "obfuscate", "moreplayermodels", "playerformlittlemaid" }; + + // Mod info + public static final String MODID = "rfp2"; + public static final String MODNAME = "Real First Person 2"; + public static final String MODVER = "@VERSION@"; + + // Provide list of mods to load after, so that that compatibility handlers can + // load correctly. + public static final String MODDEPS = ( + ("after:obfuscate;") + + ("after:moreplayermodels;")); + + // Collection of compatibility handler objects + public static ArrayList compatHandlers = new ArrayList(); + + // Constants controlling dummy behavior + public static final int DUMMY_MIN_RESPAWN_INTERVAL = 40; // min ticks between spawn attempts + public static final int DUMMY_UPDATE_TIMEOUT = 20; // max ticks between dummy entity updates + public static final int DUMMY_MAX_SEPARATION = 5; // max blocks separation between dummy and player + + // Constants controlling compatibility + public static final int MAX_SUSPEND_TIMER = 60; // maximum number of ticks another mod may suspend RFP2 for + public static final int MIN_IGNORED_ERROR_LOG_INTERVAL = 60; // interval between logging events when errors are + // ignored. + + // Constants controlling optimization / load limiting + // every 4 ticks is enough for global mod enable/disable checks + public static final int MIN_ACTIVATION_CHECK_INTERVAL = 4; // min ticks between mod enable checks + public static final long MIN_TICKS_BETWEEN_ERROR_LOGS = 1200; // only log errors once per minute (20tps * 60s/m) + + // arm checks need to be faster to keep up with hotbar scrolling, but we still + // want to limit it to once per tick. + public static final int MIN_REAL_ARMS_CHECK_INTERVAL = 1; // min ticks between arms enable checks + + // Main class instance forge will use to reference the mod + @Mod.Instance(MODID) + public static RFP2 INSTANCE; + + // The proxy reference will be set to either ClientProxy or ServerProxy + // depending on execution context. + @SidedProxy(clientSide = "com.rejahtavi." + MODID + ".ClientProxy", serverSide = "com.rejahtavi." + MODID + + ".ServerProxy") + public static IProxy PROXY; + + // Key bindings + public static RFP2Keybind keybindArmsToggle = new RFP2Keybind("key.arms.desc", Keyboard.KEY_SEMICOLON, + "key.rfp2.category"); + public static RFP2Keybind keybindModToggle = new RFP2Keybind("key.mod.desc", Keyboard.KEY_APOSTROPHE, + "key.rfp2.category"); + public static RFP2Keybind keybindHeadRotationToggle = new RFP2Keybind("key.head.desc", Keyboard.KEY_H, + "key.rfp2.category"); + + // State objects + public static RFP2Config config; + public static RFP2State state; + public static Logger logger; + public static long lastLoggedTimestamp = 0; + public static long ignoredErrorCount = 0; + + // Handles for optionally integrating with other mods + public static RFP2CompatApi api = new RFP2CompatApi(); + // public static RFP2CompatHandlerCosarmor compatCosArmor = null; + // public static RFP2CompatHandlerMorph compatMorph = null; + + // Sets the logging level for most messages written by the mod -- higher levels + // are usually highlighted in launchers. + public static final Level LOGGING_LEVEL_DEBUG = Level.DEBUG; + public static final Level LOGGING_LEVEL_LOW = Level.INFO; + public static final Level LOGGING_LEVEL_MED = Level.WARN; + public static final Level LOGGING_LEVEL_HIGH = Level.FATAL; + + // Mod Initialization - call correct proxy events based on the @SidedProxy + // picked above + @EventHandler + public void preInit(FMLPreInitializationEvent event) { + PROXY.preInit(event); + } + + @EventHandler + public void init(FMLInitializationEvent event) { + PROXY.init(event); + } + + @EventHandler + public void postInit(FMLPostInitializationEvent event) { + String logMessage = ""; + + // Initialize compatibility handlers + compatHandlers = new ArrayList(); + + // If any compatibility handlers were loaded, log them. + if (logMessage.length() > 0) { + logMessage = "Compatibility handler(s) loaded for: " + (logMessage.substring(0, logMessage.length() - 2)) + + "."; + RFP2.logger.log(LOGGING_LEVEL_MED, logMessage); + } + + // Inform Forge we're done with our postInit() phase + PROXY.postInit(event); + } + + // Provides facility to write a message to the local player's chat log + public static void logToChat(String message) { + // get a reference to the player + EntityPlayer player = Minecraft.getMinecraft().player; + if (player != null) { + // compose text component from message string and send it to the player + ITextComponent textToSend = new TextComponentString(message); + player.sendMessage(textToSend); + } + } + + // Provides facility to write a message to the local player's chat log + public static void logToChatByPlayer(String message, EntityPlayer player) { + // get a reference to the player + if (player != null) { + // compose text component from message string and send it to the player + ITextComponent textToSend = new TextComponentString(message); + player.sendMessage(textToSend); + } + } + + public static void errorDisableMod(String sourceMethod, Exception e) { + // If anything goes wrong, this method will be called to shut off the mod and + // write an error to the logs. + // The user can still try to re-enable it with a keybind or via the config gui. + // This might just result in another error, but at least it will prevent us from + // slowing down the game or flooding the logs if something is really broken. + + if (RFP2Config.compatibility.disableRenderErrorCatching) { + // Get current epoch + long epoch = System.currentTimeMillis() / 1000L; + + // Check if it has been long enough since our last logging event + if (epoch >= (lastLoggedTimestamp + MIN_IGNORED_ERROR_LOG_INTERVAL)) { + // Write error to log but continue + RFP2.logger.log(LOGGING_LEVEL_MED, ": " + sourceMethod + " **IGNORING** exception:" + e.getMessage()); + // Announce number of errors ignored since last report + if (ignoredErrorCount > 0) { + RFP2.logger.log(LOGGING_LEVEL_MED, ": (" + ignoredErrorCount + " errors ignored in last " + + MIN_IGNORED_ERROR_LOG_INTERVAL + "s.)"); + } + // reset counter and timer + ignoredErrorCount = 0; + lastLoggedTimestamp = epoch; + } else { + // hasn't been long enough, just increment the counter + ignoredErrorCount += 1; + } + } else { + // Temporarily disable the mod + RFP2.state.enableMod = false; + + // Write an error, including a stack trace, to the logs + RFP2.logger.log(LOGGING_LEVEL_HIGH, ": first person rendering deactivated."); + RFP2.logger.log(LOGGING_LEVEL_HIGH, ": " + sourceMethod + " encountered an exception:" + e.getMessage()); + e.printStackTrace(); + + // Announce the issue to the player in-game + RFP2.logToChat(RFP2.MODNAME + " mod " + TextFormatting.RED + " disabled"); + RFP2.logToChat(sourceMethod + " encountered an exception:"); + RFP2.logToChat(TextFormatting.RED + e.getMessage()); + RFP2.logToChat(TextFormatting.DARK_RED + e.getStackTrace().toString()); + RFP2.logToChat(TextFormatting.GOLD + "Please check your minecraft log file for more details."); + } + } +} diff --git a/src/main/java/com/rejahtavi/rfp2/RFP2Config.java b/src/main/java/com/rejahtavi/rfp2/RFP2Config.java new file mode 100644 index 0000000..6a62741 --- /dev/null +++ b/src/main/java/com/rejahtavi/rfp2/RFP2Config.java @@ -0,0 +1,160 @@ +package com.rejahtavi.rfp2; + +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.config.Config; +import net.minecraftforge.common.config.Config.Comment; +import net.minecraftforge.common.config.Config.Name; +import net.minecraftforge.common.config.Config.RangeDouble; +import net.minecraftforge.common.config.Config.Type; +import net.minecraftforge.common.config.ConfigManager; +import net.minecraftforge.fml.client.event.ConfigChangedEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.relauncher.Side; + +/* + * Config Annotation system documentation can be found here: + * https://mcforge.readthedocs.io/en/latest/config/annotations/ + */ + +// Identify this as the config class to forge +@Config( + modid = RFP2.MODID, + type = Type.INSTANCE, + name = RFP2.MODID, + category = "") +@Mod.EventBusSubscriber(Side.CLIENT) +public class RFP2Config +{ + // Constructor + public RFP2Config() + { + // Register the config handler to the bus so that it shows up in the forge config gui + MinecraftForge.EVENT_BUS.register(this); + } + + // Create preferences section + @Comment({ "Personal preferences for " + RFP2.MODNAME }) + @Name("Preferences") + public static final Preferences preferences = new Preferences(); + + // Create compatibility section + @Comment("Item and Mount compatability lists for " + RFP2.MODNAME) + @Name("Compatability") + public static final Compatibility compatibility = new Compatibility(); + + // Define structure and defaults of Preferences section + public static class Preferences + { + @Comment({ "Enables/disables mod at startup.", "Default: true" }) + @Name("Enable Mod") + public boolean enableMod = true; + + @Comment({ "Enables/disables real arms at startup", "Default: true" }) + @Name("Enable Real Arm Rendering") + public boolean enableRealArms = true; + + @Comment({ "Enables/disables head turning at startup", "Default: false" }) + @Name("Enable Head Turning") + public boolean enableHeadTurning = false; + + @Comment({ "Enables/disables status messages when a keybind is pressed.", "Default: false" }) + @Name("Enable Status Messages") + public boolean enableStatusMessages = true; + + @Comment({ "How far behind the camera to put the first person player model", "Default: 0.35" }) + @Name("Player Model Offset") + @RangeDouble( + min = 0.0f, + max = 2.0f) + public double playerModelOffset = 0.35f; + } + + // Define structure and defaults of Compatibility section + public static class Compatibility + { + @Comment({ "Vanilla arms are used when holding one of these items.", + "Needed for compasses and maps, stops big items blocking the view.", + "Note: Not case sensitive, accepts simple item names and regex patterns:", + ".* = wildcard, ^ = match beginning of name, $ = match end of name." }) + @Name("Held Item Conflicts") + public String[] heldItemConflictList = { "minecraft:filled_map", + "minecraft:clock", + "minecraft:shield", + "minecraft:bow", + "slashblade:.*", + ".*compass$", + "tconstruct:.*bow", + "tconstruct:battlesign", + "thermalfoundation:shield_.*" }; + + @Comment({ "Mod temporarily disables when riding one of these mounts.", + "Stops legs clipping through minecarts.", + "Note: Not case sensitive, accepts simple item names and regex patterns.", + ".* = wildcard, ^ = match beginning of name, $ = match end of name." }) + @Name("Mount Conflicts") + public String[] mountConflictList = { ".*minecart.*" }; + + @Comment("Disables the mod when swimming.") + @Name("Disable when swimming") + public boolean disableWhenSwimming = false; + + @Comment("Enforces a more aggressive version of the swimming checks.") + @Name("Use aggressive swimming checks") + public boolean useAggressiveSwimmingCheck = false; + + @Comment("Disables the mod when sneaking.") + @Name("Disable when sneaking") + public boolean disableWhenSneaking = false; + + @Comment("Switches to vanilla arms when *any* item is held, not just conflict items.") + @Name("Use vanilla arms when holding any item") + public boolean disableArmsWhenAnyItemHeld = false; + + @Comment("Disables rendering safety checks. May enable compatibility with mods that cause rendering exceptions, but cannot guarantee that the game will be stable.") + @Name("Ignore rendering errors (not recommended).") + public boolean disableRenderErrorCatching = false; + + @Comment("Suppresses alerts about incompatible mods in chat on startup.") + @Name("Suppress startup compatibility alert (not recommended).") + public boolean disableModCompatibilityAlerts = false; + } + + // Subscribe to configuration change event + // This fires whenever the user saves changes in the config gui. + @SubscribeEvent + public static void onConfigChanged(final ConfigChangedEvent.OnConfigChangedEvent event) + { + // Only respond to events meant for us + if (event.getModID().contentEquals(RFP2.MODID)) + { + // Inject the new values and save to the config file when the config has been changed from the GUI. + RFP2.logger.log(RFP2.LOGGING_LEVEL_LOW, "synchronizing config file."); + + // Make sure all referenced items are lower case (makes matching later computationally cheaper) + RFP2Config.compatibility.heldItemConflictList = lowerCaseArray(RFP2Config.compatibility.heldItemConflictList); + RFP2Config.compatibility.mountConflictList = lowerCaseArray(RFP2Config.compatibility.mountConflictList); + + // Save the config + ConfigManager.sync(RFP2.MODID, Config.Type.INSTANCE); + + // Update current state to match preferences that were just selected in the GUI + RFP2.state.enableMod = preferences.enableMod; + RFP2.state.enableRealArms = preferences.enableRealArms; + RFP2.state.enableHeadTurning = preferences.enableHeadTurning; + RFP2.state.enableStatusMessages = preferences.enableStatusMessages; + } + } + + // Takes in an array of strings and converts all members of it to lower case + private static String[] lowerCaseArray(String[] array) + { + // Iterate over all elements in the array + for (int i = 0; i < array.length; i++) + { + // Rewrite each element in the array into lower case + array[i] = array[i].toLowerCase(); + } + return array; + } +} diff --git a/src/main/java/com/rejahtavi/rfp2/RFP2Keybind.java b/src/main/java/com/rejahtavi/rfp2/RFP2Keybind.java new file mode 100644 index 0000000..b4d78be --- /dev/null +++ b/src/main/java/com/rejahtavi/rfp2/RFP2Keybind.java @@ -0,0 +1,54 @@ +package com.rejahtavi.rfp2; + +import net.minecraft.client.settings.KeyBinding; + +/* + * This helper class implements basic key bindings and state tracking of each key + * Notes: + * Each key binding will spawn its own instance of this class + * Each instance will track the last known state of the binding, and watch for "rising edges" to trigger on + * This prevents annoying "multi-triggers" if the button is held down for more than one frame + */ +public class RFP2Keybind +{ + + // Handle to the current instance + public KeyBinding keyBindingInstance; + + // Tracks state of the key on the previous tick + private boolean wasPressed = false; + + // Constructor + public RFP2Keybind(String description, int keyCode, String category) + { + keyBindingInstance = new KeyBinding(description, keyCode, category); + } + + /* + * This function acts as a monostable filter to isolate "rising edges" of key press signals + * + * In other words, this returns true ONLY on the very first tick key is pressed, and false at all other times. + * The player must release and re-press the key to get a second event to fire. + * + * This stops the mod from spam-toggling options when a key is held. + */ + public boolean checkForNewPress() + { + + // Check current key state + boolean currentlyPressed = this.keyBindingInstance.isKeyDown(); + if (!wasPressed && currentlyPressed) + { + // The key WASN'T pressed before, but now it is; Winner! Return True! + wasPressed = true; + return true; + } + else + { + // Something else happened and we don't particularly care what. + // Save the current state of the button and return false. + wasPressed = currentlyPressed; + return false; + } + } +} diff --git a/src/main/java/com/rejahtavi/rfp2/RFP2State.java b/src/main/java/com/rejahtavi/rfp2/RFP2State.java new file mode 100644 index 0000000..4b7ee54 --- /dev/null +++ b/src/main/java/com/rejahtavi/rfp2/RFP2State.java @@ -0,0 +1,515 @@ +package com.rejahtavi.rfp2; + +import java.util.regex.PatternSyntaxException; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.util.text.TextFormatting; +import net.minecraftforge.client.event.RenderHandEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.Loader; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.eventhandler.EventPriority; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.InputEvent.KeyInputEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.fml.relauncher.Side; + +/* + * This class receives and processes all events related to the player dummy. + * It is also responsible for processing events related to keybinds and configuration. + */ + +// register this class as an event handler with forge +@Mod.EventBusSubscriber(Side.CLIENT) +public class RFP2State +{ + // Local objects to track mod internal state + + // handle to the player dummy entity + EntityPlayerDummy dummy; + + // timers for performance waits + int spawnDelay; + long checkEnableModDelay; + long checkEnableRealArmsDelay; + int suspendApiDelay; + + // state flags + boolean lastActivateCheckResult; + boolean lastRealArmsCheckResult; + boolean enableMod; + boolean enableRealArms; + boolean enableHeadTurning; + boolean enableStatusMessages; + boolean conflictsDetected = false; + boolean conflictCheckDone = false; + + // Constructor + public RFP2State() + { + // No dummy exists at startup + dummy = null; + + // Start a timer so that we wait a bit for things to load before first trying to spawn the dummy + spawnDelay = RFP2.DUMMY_MIN_RESPAWN_INTERVAL; + + // Initialize local variables + checkEnableModDelay = 0; + checkEnableRealArmsDelay = 0; + suspendApiDelay = 0; + lastActivateCheckResult = true; + lastRealArmsCheckResult = true; + + // Import initial state from config file + enableMod = RFP2Config.preferences.enableMod; + enableRealArms = RFP2Config.preferences.enableRealArms; + enableHeadTurning = RFP2Config.preferences.enableHeadTurning; + enableStatusMessages = RFP2Config.preferences.enableStatusMessages; + + // Register ourselves on the bus so we can receive and process events + MinecraftForge.EVENT_BUS.register(this); + } + + // Receive key press events for key binding handling + @SubscribeEvent( + priority = EventPriority.NORMAL, + receiveCanceled = true) + public void onEvent(KeyInputEvent event) + { + // DISABLED for 1.3.1 -- now only warns players + // kill mod completely when a conflict is detected. + // if (this.conflictsDetected) return; + + // Check key binding in turn for new presses + if (RFP2.keybindArmsToggle.checkForNewPress()) + { + enableRealArms = !enableRealArms; + if (enableStatusMessages) + { + // log keybind-triggered state changes to chat if configured to do so + RFP2.logToChat(RFP2.MODNAME + " arms " + (enableRealArms ? TextFormatting.GREEN + "enabled" : TextFormatting.RED + "disabled")); + } + } + // Check key binding in turn for new presses + if (RFP2.keybindModToggle.checkForNewPress()) + { + enableMod = !enableMod; + if (enableStatusMessages) + { + // log keybind-triggered state changes to chat if configured to do so + RFP2.logToChat(RFP2.MODNAME + " mod " + (enableMod ? TextFormatting.GREEN + "enabled" : TextFormatting.RED + "disabled")); + } + } + // Check key binding in turn for new presses + if (RFP2.keybindHeadRotationToggle.checkForNewPress()) + { + enableHeadTurning = !enableHeadTurning; + if (enableStatusMessages) + { + // log keybind-triggered state changes to chat if configured to do so + RFP2.logToChat(RFP2.MODNAME + " head rotation " + (enableHeadTurning ? TextFormatting.GREEN + "enabled" : TextFormatting.RED + "disabled")); + } + } + } + + // returns true when mod conflicts are detected + public void detectModConflicts(EntityPlayer player) + { + // Only let this routine run once per startup + if (!this.conflictCheckDone) + { + // Check for conflicting mods + String modConflictList = ""; + for (String conflictingID : RFP2.CONFLICT_MODIDS) + { + if (Loader.isModLoaded(conflictingID)) + { + if (modConflictList.length() != 0) modConflictList += ", "; + modConflictList += conflictingID; + } + } + + // See if we got any hits + if (modConflictList.length() != 0) + { + // If mod compatibility alerts are disabled, JUST put info in the log file. + if (RFP2Config.compatibility.disableModCompatibilityAlerts) + { + RFP2.logger.log(RFP2.LOGGING_LEVEL_HIGH, this.getClass().getName() + ": WARNING: In-game compatibility alerts have been disabled!"); + // this.enableMod unchanged + // this.disabledForConflict unchanged + } + else + { + // Mod compatibility alerts are enabled -- warn the player via in-game chat that something is amiss + //@formatter:off + RFP2.logToChatByPlayer("" + TextFormatting.BOLD + TextFormatting.GOLD + + "WARNING: RFP2 has known compatibility issues with the mod(s): " + + TextFormatting.RESET + TextFormatting.RED + + modConflictList + ".", player); + + RFP2.logToChatByPlayer("" + TextFormatting.BOLD + TextFormatting.GOLD + + "Be aware that visual glitches may occur.", player); + + RFP2.logToChatByPlayer("Press the hotkey (Default: Apostrophe) to use RFP2 anyway.", player); + + RFP2.logToChatByPlayer("" + TextFormatting.RESET + TextFormatting.GRAY + + "(You can disable this warning in mod options.)", player); + //@formatter:on + this.conflictsDetected = true; + this.enableMod = false; + } + RFP2.logger.log(RFP2.LOGGING_LEVEL_HIGH, this.getClass().getName() + ": WARNING: Detected conflicting mod(s): " + modConflictList); + } + this.conflictCheckDone = true; + } + } + + // Receive event when player hands are about to be drawn + @SubscribeEvent( + priority = EventPriority.HIGHEST) + public void onEvent(RenderHandEvent event) + { + // DISABLED for 1.3.1 -- now only warns players + // kill mod completely when a conflict is detected. + // if (this.conflictsDetected) return; + + // Get local player reference + EntityPlayer player = Minecraft.getMinecraft().player; + // if: 1) player exists AND 2) mod is active AND 3) rendering real arms is active + if (player != null && RFP2.state.isModEnabled(player) && RFP2.state.isRealArmsEnabled(player)) + { + // then skip drawing the vanilla 2D HUD arms by canceling the event + event.setCanceled(true); + } + } + + // Receive the main game tick event + @SubscribeEvent + public void onEvent(TickEvent.ClientTickEvent event) + { + // DISABLED for 1.3.1 -- now only warns players + // kill mod completely when a conflict is detected. + // if (this.conflictsDetected) return; + + // Make this block as fail-safe as possible, since it runs every tick + try + { + // Decrement timers + if (checkEnableModDelay > 0) --checkEnableModDelay; + if (checkEnableRealArmsDelay > 0) --checkEnableRealArmsDelay; + if (suspendApiDelay > 0) --suspendApiDelay; + + // Get player reference and null check it + EntityPlayer player = Minecraft.getMinecraft().player; + if (player != null) + { + // Check if dummy needs to be spawned + if (dummy == null) + { + // It does, are we in a respawn waiting interval? + if (spawnDelay > 0) + { + // Yes, we are still waiting; is the mod enabled? + if (enableMod) + { + // Yes, the mod is enabled and we are waiting: decrement the counter + --spawnDelay; + } + else + { + // No, the mod is not enabled, and we are waiting: + // Hold the timer at full so that the delay works when the mod is turned back on + spawnDelay = RFP2.DUMMY_MIN_RESPAWN_INTERVAL; + } + } + else + { + // No, the spawn timer has expired: Go ahead and try to spawn the dummy. + attemptDummySpawn(player); + } + } + // The dummy already exists, let's check up on it + else + { + // Track whether we need to reset the existing dummy. + // We should only reset it ONCE, even if multiple reasons are true. + // (otherwise we will not be able to log the remaining reasons after it is reset) + // This is done this way to ease future troubleshooting. + boolean needsReset = false; + + // Did the player change dimensions on us? If so, reset the dummy. + if (dummy.world.provider.getDimension() != player.world.provider.getDimension()) + { + needsReset = true; + RFP2.logger.log(RFP2.LOGGING_LEVEL_DEBUG, + this.getClass().getName() + ": Respawning dummy because player changed dimension."); + } + + // Did the player teleport, move too fast, or somehow else get separated? If so, reset the dummy. + if (dummy.getDistanceSq(player) > RFP2.DUMMY_MAX_SEPARATION) + { + needsReset = true; + RFP2.logger.log(RFP2.LOGGING_LEVEL_DEBUG, + this.getClass().getName() + ": Respawning dummy because player and dummy became separated."); + } + + // Has it been excessively long since we last updated the dummy's state? (perhaps due to lag?) + if (dummy.lastTickUpdated < player.world.getTotalWorldTime() - RFP2.DUMMY_UPDATE_TIMEOUT) + { + needsReset = true; + RFP2.logger.log(RFP2.LOGGING_LEVEL_DEBUG, + this.getClass().getName() + ": Respawning dummy because state became stale. (Is the server lagging?)"); + } + + // Did one of the above checks necessitate a reset? + if (needsReset) + { + // Yes, proceed with the reset. + resetDummy(); + } + } + } + } + catch (Exception e) + { + // If anything goes wrong, shut the mod off and write an error to the logs. + RFP2.errorDisableMod(this.getClass().getName() + ".onEvent(TickEvent.ClientTickEvent)", e); + } + } + + // Handles dummy spawning + void attemptDummySpawn(EntityPlayer player) + { + // Only runs once per startup. Running it here at dummy spawn is the easiest way to ensure it only happens after everything is fully loaded. + detectModConflicts(player); + + try + { + // Make sure any existing dummy is dead + if (dummy != null) dummy.setDead(); + + // Attempt to spawn a new one at the player's current position + dummy = new EntityPlayerDummy(player.world); + dummy.setPositionAndRotation(player.posX, player.posY, player.posZ, player.rotationYaw, player.rotationPitch); + player.world.spawnEntity(dummy); + } + catch (Exception e) + { + /* + * Something went wrong trying to spawn the dummy! + * We need to write a log entry and reschedule to try again later. + * + * Note that because this code is protected against running too much by a respawn timer, + * we do not call errorDisableMod() when encountering this error. + * + * Should anything unexpected occur in the spawning, there is a good chance that it will + * work itself out within a respawn delay or two. + */ + RFP2.logger.log(RFP2.LOGGING_LEVEL_MED, this.getClass().getName() + ": failed to spawn PlayerDummy! Will retry. Exception:", e.toString()); + e.printStackTrace(); + resetDummy(); + } + } + + // Handles killing off defunct dummies and scheduling respawns + void resetDummy() + { + // If the existing dummy isn't dead, kill it before freeing the reference + if (dummy != null) dummy.setDead(); + dummy = null; + + // DISABLED for 1.3.1 -- now only warns players + // kill mod completely when a conflict is detected. + // if (this.conflictsDetected) return; + + // Set timer to spawn a new one + spawnDelay = RFP2.DUMMY_MIN_RESPAWN_INTERVAL; + } + + public void setSuspendTimer(int ticks) + { + // DISABLED for 1.3.1 -- now only warns players + // kill mod completely when a conflict is detected. + // if (this.conflictsDetected) return; + + // check if tick value is valid; invalid values will be ignored + if (ticks > 0 && ticks <= RFP2.MAX_SUSPEND_TIMER) + { + // Only allow increasing the timer externally + // * This is so multiple mods can use the API concurrently, and RFP2 being suspended is the preferred state. + // * Once all mods stop requesting suspension times, the timer will expire at the longest, last value requested. + if (ticks > suspendApiDelay) suspendApiDelay = ticks; + } + } + + // Check if mod should be disabled for any reason + public boolean isModEnabled(EntityPlayer player) + { + // DISABLED for 1.3.1 -- now only warns players + // kill mod completely when a conflict is detected. + // if (this.conflictsDetected) return; + + // No need to check anything if we are configured to be disabled + if (!enableMod) return false; + + // Don't do anything if we've been suspended by another mod + if (suspendApiDelay > 0) return false; + + // No need to check anything else if player is dead or otherwise cannot be found + if (player == null) return false; + + // No need to check anything else if dummy is dead or otherwise cannot be found + if (dummy == null) return false; + + /* + * Only check the player's riding status if we haven't recently. + * This saves on performance -- it is not necessary to check this list on every single frame! + * Once every few ticks is more than enough to remain invisible to the player. + * Keep in mind that "every few ticks", or several 20ths of a second, + * could be tens of frames where we skip the check with a good GPU! + */ + if (checkEnableModDelay == 0) + { + // The timer has expired, we need to run the checks + + // reset timer + checkEnableModDelay = RFP2.MIN_ACTIVATION_CHECK_INTERVAL; + + // Implement swimming check functionality + if (RFP2Config.compatibility.disableWhenSwimming && dummy.isSwimming()) + { + // we are swimming and are configured to disable when this is true, so we are disabled + lastActivateCheckResult = false; + } + else + { + // we are not swimming, or that check is disabled. proceed to the mount check + + // get a reference to the player's mount, if it exists + Entity playerMountEntity = player.getRidingEntity(); + if (playerMountEntity == null) + { + // Player isn't riding, so we are enabled. + lastActivateCheckResult = true; + } + else + { + // Player is riding something, find out what it is and if it's on our conflict list + if (stringMatchesRegexList(playerMountEntity.getName().toLowerCase(), RFP2Config.compatibility.mountConflictList)) + { + // player is riding a conflicting entity, so we are disabled. + lastActivateCheckResult = false; + } + else + { + // No conflicts found, so we are enabled. + lastActivateCheckResult = true; + } + } + } + + } + return lastActivateCheckResult; + } + + // Check if we should render real arms or not + public boolean isRealArmsEnabled(EntityPlayer player) + { + // DISABLED for 1.3.1 -- now only warns players + // kill mod completely when a conflict is detected. + // if (this.conflictsDetected) return; + + // No need to check anything if we don't want this enabled + if (!enableRealArms) return false; + + // No need to check anything if player is dead + if (player == null) return false; + + // only run the inventory check if we haven't done it recently + // once per tick is enough -- isRealArmsEnabled might be called many times per tick! + if (checkEnableRealArmsDelay == 0) + { + // need to check the player's inventory after all + // reset the check timer + checkEnableRealArmsDelay = RFP2.MIN_REAL_ARMS_CHECK_INTERVAL; + + // get the names of the player's currently held items + String itemMainHand = player.inventory.getCurrentItem().getItem().getRegistryName().toString().toLowerCase(); + String itemOffHand = player.inventory.offHandInventory.get(0).getItem().getRegistryName().toString().toLowerCase(); + + // Modify the check logic based on whether the "any item" flag is set or not + if (RFP2Config.compatibility.disableArmsWhenAnyItemHeld) + { + // "any item held" behavior is enabled; check if player's hands are empty + if (itemMainHand.equals("minecraft:air") && itemOffHand.equals("minecraft:air")) + { + // player is not holding anything; enable arm rendering + lastRealArmsCheckResult = true; + } + else + { + // player is holding something; disable arm rendering + lastRealArmsCheckResult = false; + } + } + else + { + // The "any item" option is not in use, so we need to check the registry names of any + // held items against the conflict list + if (stringMatchesRegexList(itemMainHand, RFP2Config.compatibility.heldItemConflictList) + || (stringMatchesRegexList(itemOffHand, RFP2Config.compatibility.heldItemConflictList))) + { + // player is holding a conflicting item in main or off hand; disable arm rendering + lastRealArmsCheckResult = false; + } + else + { + // no conflicts found; enable arm rendering + lastRealArmsCheckResult = true; + } + } + } + return lastRealArmsCheckResult; + } + + // Check if head rotation is enabled + public boolean isHeadRotationEnabled(EntityPlayer player) + { + // DISABLED for 1.3.1 -- now only warns players + // kill mod completely when a conflict is detected. + // if (this.conflictsDetected) return; + + return enableHeadTurning; + } + + // Check a string against a list of regexes and return true if any of them match + boolean stringMatchesRegexList(String string, String[] regexes) + { + // Loop through regex array + for (String i : regexes) + { + // Handle errors due to bad regex syntax entered by user + try + { + // Check if the provided string matches the regex + if (string.matches(i)) + { + // Found a hit, return true + return true; + } + } + catch (PatternSyntaxException e) + { + // Something is wrong with the regex, switch off the mod and notify the user + enableMod = false; + RFP2.logToChat(RFP2.MODNAME + " " + TextFormatting.RED + "Warning: [ " + i + " ] is not a valid regex, please edit your configuration."); + RFP2.logToChat(RFP2.MODNAME + " mod " + TextFormatting.RED + " disabled"); + return false; + } + } + // Got through the whole array without a hit; return false + return false; + } +} diff --git a/src/main/java/com/rejahtavi/rfp2/RenderPlayerDummy.java b/src/main/java/com/rejahtavi/rfp2/RenderPlayerDummy.java new file mode 100644 index 0000000..be2404e --- /dev/null +++ b/src/main/java/com/rejahtavi/rfp2/RenderPlayerDummy.java @@ -0,0 +1,295 @@ +package com.rejahtavi.rfp2; + +import com.rejahtavi.rfp2.compat.handlers.RFP2CompatHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.AbstractClientPlayer; +import net.minecraft.client.entity.EntityPlayerSP; +import net.minecraft.client.model.ModelPlayer; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.client.renderer.entity.RenderPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; + +/* + * This class handles calls to draw the PlayerDummy object (except not really.) + * + * When doRender() is called, we don't draw anything for the dummy itself. + * Instead, we prepare some things, then call a vanilla player renderer at a very precise position relative to the camera. + * + * In other words: The original doRender() call is *indirectly* triggering a vanilla player render operation, + * instead of rendering our dummy entity (which is invisible anyway). + * + * Using the vanilla renderer means we will automatically inherit any changes *other* mods have made to the player character, + * but it also means that we have to deal with anything they add to the head that could block the view. + */ +public class RenderPlayerDummy extends Render +{ + // Constructor + public RenderPlayerDummy(RenderManager renderManager) + { + // Call parent constructor + super(renderManager); + } + + // Handles requests for texture of the player dummy + @Override + protected ResourceLocation getEntityTexture(EntityPlayerDummy entity) + { + // The PlayerDummy entity is only for tracking the player's position, so it does not have a texture. + return null; + } + + // Implements linear interpolation for animation smoothing + private float linearInterpolate(float current, float target, float partialTicks) + { + // Explanation for linear interpolation math can be found here: + // https://en.wikipedia.org/wiki/Linear_interpolation + return ((1 - partialTicks) * current) + (partialTicks * target); + } + + // Called when the game wants to draw our PlayerDummy entity + @Override + public void doRender(EntityPlayerDummy renderEntity, double renderPosX, double renderPosY, double renderPosZ, float renderYaw, float partialTicks) + { + /* + * NO-OP Checklist: We want to use as few CPU cycles as possible if we aren't + * going to do anything useful. The following checks abort the render *as soon + * as possible* if any of those conditions are true. + */ + + // Grab a reference to the local player entity, null-check it, and abort if it fails. + EntityPlayerSP player = Minecraft.getMinecraft().player; + if (player == null) return; + + // Grab a backup of any items we might possibly touch, so that we can be guaranteed + // to be able to restore them when it comes time for the finally{} block to run. + ItemStack itemMainHand = player.inventory.getCurrentItem(); + ItemStack itemOffHand = player.inventory.offHandInventory.get(0); + ItemStack itemHelmetSlot = player.inventory.armorInventory.get(3); + + // Make quick per-frame compatibility checks based on current configuration and player state + + // Implement config option for disabling when sneaking + if (RFP2Config.compatibility.disableWhenSneaking && player.isSneaking()) return; + + // Grab a reference to the vanilla player renderer, null check, and abort if it fails + Render render = (RenderPlayer) this.renderManager.getEntityRenderObject(player); + RenderPlayer playerRenderer = (RenderPlayer) render; + if (playerRenderer == null) return; + + // Grab a reference to the local player's model, null check, and abort if it fails + ModelPlayer playerModel = (ModelPlayer) playerRenderer.getMainModel(); + if (playerModel == null) return; + + RFP2.logger.log(RFP2.LOGGING_LEVEL_HIGH, playerModel.getClass().getCanonicalName()); + + // Grab a backup of the various player model layers we might adjust + // This way we aren't making any assumptions about what other mods might be doing with these options + // and we can restore everything when we are finished. + boolean[] modelState = { playerModel.bipedHead.isHidden, + playerModel.bipedHead.showModel, + playerModel.bipedHeadwear.isHidden, + playerModel.bipedHeadwear.showModel, + playerModel.bipedLeftArm.isHidden, + playerModel.bipedLeftArm.showModel, + playerModel.bipedLeftArmwear.isHidden, + playerModel.bipedLeftArmwear.showModel, + playerModel.bipedRightArm.isHidden, + playerModel.bipedRightArm.showModel, + playerModel.bipedRightArmwear.isHidden, + playerModel.bipedRightArmwear.showModel + }; + + /* + * With the routine, unlikely-to-fail stuff out of the way, try to make the remainder + * of this routine as fail-safe as possible. It runs every frame, so we want to be able + * to stop it from running anymore if we encounter a problem, to avoid slowing down the + * game and consuming disk space with useless error logs. + */ + try + { + // Note: thirdPersonView can be: 0 = First Person, 1 = Third Person Rear, 2 = Third Person + // If the player is NOT in first person, do nothing + if (Minecraft.getMinecraft().gameSettings.thirdPersonView != 0) return; + + // If the player is flying with an Elytra, do nothing + if (player.isElytraFlying()) return; + + // (Keep this NO-OP check last, it can be more expensive than the others, due to the mount check.) + // If mod is not enabled this frame, do nothing + if (!RFP2.state.isModEnabled(player)) return; + if (!RFP2.state.isRealArmsEnabled(player)) return; + + // Check if any of the compatibility handlers want us to skip this frame + for (RFP2CompatHandler handler : RFP2.compatHandlers) + { + if (handler.getDisableRFP2(player)) + { + return; + } + } + + /* + * Initialization: Pull in state info and set up local variables, now that we + * know we actually need to do some rendering. + */ + + // Initialize remaining local variables + double playerRenderPosX = 0; + double playerRenderPosZ = 0; + double playerRenderPosY = 0; + float playerRenderAngle = 0; + + // Get local copies of config & state + float playerModelOffset = (float) RFP2Config.preferences.playerModelOffset; + boolean isRealArmsEnabled = RFP2.state.isRealArmsEnabled(player); + boolean isHeadRotationEnabled = RFP2.state.isHeadRotationEnabled(player); + + /* + * Adjust Player Model: + * Strip unwanted items and layers from the player model to avoid obstructing the camera + */ + + // Remove the player's helmet, so that it does not obstruct the camera. + player.inventory.armorInventory.set(3, ItemStack.EMPTY); + + // Hide the player model's head layers, again so they do not obstruct the camera. + playerModel.bipedHead.isHidden = true; + playerModel.bipedHead.showModel = false; + playerModel.bipedHeadwear.isHidden = true; + playerModel.bipedHeadwear.showModel = false; + + // Instruct compatibility handlers hide head models (handlers are responsible for caching state for later restoration) + for (RFP2CompatHandler handler : RFP2.compatHandlers) + { + handler.hideHead(player, true); + } + + // Check if we need to hide the arms + if (!isRealArmsEnabled) + { + // The real arms feature is not enabled, so we should NOT render the arms in 3D. + // That means we need to hide them before we draw the player model. + // Remove the player's currently held main and off hand items, so that they do not obstruct the camera. + player.inventory.removeStackFromSlot(player.inventory.currentItem); + player.inventory.offHandInventory.set(0, ItemStack.EMPTY); + + // Hide the player model's arm layers + playerModel.bipedLeftArm.isHidden = true; + playerModel.bipedLeftArm.showModel = false; + playerModel.bipedRightArm.isHidden = true; + playerModel.bipedRightArm.showModel = false; + playerModel.bipedLeftArmwear.isHidden = true; + playerModel.bipedLeftArmwear.showModel = false; + playerModel.bipedRightArmwear.isHidden = true; + playerModel.bipedRightArmwear.showModel = false; + + // Instruct compatibility handlers hide arm models (handlers are responsible for caching state for later restoration) + for (RFP2CompatHandler handler : RFP2.compatHandlers) + { + handler.hideArms(player, true); + } + } + + /* + * Calculate Rendering Coordinates: + * Determine the precise location and angle the player should be rendered this frame, then render it. + * + * Notes: + * player.rotationYaw = The direction the player's camera is facing. + * player.renderYawOffset = The direction the player's 3D model is facing. + * + * (In vanilla minecraft, the player's body lags behind the head to provide a more natural look to movement. + * This can lead to renderYawOffset being off from the main camera by up to 75 degrees!) + */ + + // Generate default rendering coordinates for player body + playerRenderPosX = player.posX - renderEntity.posX + renderPosX; + playerRenderPosY = player.posY - renderEntity.posY + renderPosY; + playerRenderPosZ = player.posZ - renderEntity.posZ + renderPosZ; + + // If the player IS sleeping, we can skip any extra calculations and proceed directly to rendering. + if (!player.isPlayerSleeping()) + { + // The player is NOT sleeping, so we are going to need to make some adjustments. + // If head rotation IS enabled, then we DO NOT need to counteract it and can skip the next adjustment. + if (!isHeadRotationEnabled) + { + // Head rotation is NOT enabled, so we have to prevent the vanilla rotation behavior! + // We can do this by updating the player's body's rendering values to match the camera's + // position before rendering each frame. It is *critical* that we update the data for both + // this frame and the previous one, or else our linear interpolation will be fed bad + // data and the result will not look smooth! + player.renderYawOffset = player.rotationYaw; + player.prevRenderYawOffset = player.prevRotationYaw; + } + + // Interpolate to get final rendering position + playerRenderAngle = this.linearInterpolate(player.prevRenderYawOffset, player.renderYawOffset, partialTicks); + + // Update position of rendered body to include interpolation and model offset + playerRenderPosX += (playerModelOffset * Math.sin(Math.toRadians(playerRenderAngle))); + playerRenderPosZ -= (playerModelOffset * Math.cos(Math.toRadians(playerRenderAngle))); + } + + /* + * Trigger rendering of the fake player model: this is done by calling the + * vanilla player renderer with our adjusted coordinates. + * + * Note that we do NOT pass the playerModel here -- the vanilla renderer already + * has it! That's why we had to actually remove the player's helmet and possibly + * other inventory items. That also means that it is *critical* that we undo all + * of those changes immediately after this, before anything outside of RFP2 can + * interact with these objects and invalidate our cached state. + * + * (That is also why those reversions are implemented within a finally block.) + */ + playerRenderer.doRender(player, playerRenderPosX, playerRenderPosY, playerRenderPosZ, playerRenderAngle, partialTicks); + } + catch (Exception e) + { + // If anything goes wrong, shut the mod off and write an error to the logs. + RFP2.errorDisableMod(this.getClass().getName() + ".doRender()", e); + } + finally + { + /*false + * Cleanup Phase: + * Revert all temporary changes we made to the model for rendering purposes, so that we don't cause any side effects. + * + * Whether or not something went wrong, we want to make ABSOLUTELY SURE to give + * back any items we took and re-enable all player model rendering layers. + * + * If we fail to do this, we could glitch out the player's rendering, or worse, + * accidentally delete someone's hard won inventory items! + */ + + // restore the player's inventory to the state we found it in + player.inventory.armorInventory.set(3, itemHelmetSlot); + player.inventory.setInventorySlotContents(player.inventory.currentItem, itemMainHand); + player.inventory.offHandInventory.set(0, itemOffHand); + + // restore the player model's rendering layers + playerModel.bipedHead.isHidden = modelState[0]; + playerModel.bipedHead.showModel = modelState[1]; + playerModel.bipedHeadwear.isHidden = modelState[2]; + playerModel.bipedHeadwear.showModel = modelState[3]; + playerModel.bipedLeftArm.isHidden = modelState[4]; + playerModel.bipedLeftArm.showModel = modelState[5]; + playerModel.bipedLeftArmwear.isHidden = modelState[6]; + playerModel.bipedLeftArmwear.showModel = modelState[7]; + playerModel.bipedRightArm.isHidden = modelState[8]; + playerModel.bipedRightArm.showModel = modelState[9]; + playerModel.bipedRightArmwear.isHidden = modelState[10]; + playerModel.bipedRightArmwear.showModel = modelState[11]; + + // Instruct compatibility handlers restore head models + for (RFP2CompatHandler handler : RFP2.compatHandlers) + { + handler.restoreHead(player, true); + handler.restoreArms(player, true); + } + } + } +} diff --git a/src/main/java/com/rejahtavi/rfp2/ServerProxy.java b/src/main/java/com/rejahtavi/rfp2/ServerProxy.java new file mode 100644 index 0000000..8389b1c --- /dev/null +++ b/src/main/java/com/rejahtavi/rfp2/ServerProxy.java @@ -0,0 +1,39 @@ +package com.rejahtavi.rfp2; + +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.event.FMLServerStartingEvent; + +// RFP2.PROXY will be instantiated with this class if we are running on the server side. +// When this is the case, RFP2 should do nothing, so there is nothing here. +public class ServerProxy implements IProxy +{ + // Called at the start of mod loading + @Override + public void preInit(FMLPreInitializationEvent event) + { + // unused in this mod + } + + // Called after all other mod preInit()s have run + @Override + public void init(FMLInitializationEvent event) + { + // unused in this mod + } + + // Called after all other mod init()s have run + @Override + public void postInit(FMLPostInitializationEvent event) + { + // unused in this mod + } + + // Called when starting up a dedicated server + @Override + public void serverStarting(FMLServerStartingEvent event) + { + // unused in this mod + } +} diff --git a/src/main/java/com/rejahtavi/rfp2/compat/RFP2CompatApi.java b/src/main/java/com/rejahtavi/rfp2/compat/RFP2CompatApi.java new file mode 100644 index 0000000..17a00ed --- /dev/null +++ b/src/main/java/com/rejahtavi/rfp2/compat/RFP2CompatApi.java @@ -0,0 +1,50 @@ +package com.rejahtavi.rfp2.compat; + +import com.rejahtavi.rfp2.RFP2; +import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.EntityPlayerSP; + +/* + * Compatibility API other mods can use to get the current state of RFP2. + * + * If you have a mod that modifies the player model or player rendering in any way, + * you can call the functions below to determine whether you should hide certain things. + */ +public class RFP2CompatApi +{ + + // During frames that this returns TRUE: + // * RFP2 has hidden the player's head and helmet so that they don't block the first person view camera. + // * Make sure to adjust your mod's behavior to avoid rendering anything that could block the forward field of view. + public boolean rfp2IsHeadHidden() + { + EntityPlayerSP player = Minecraft.getMinecraft().player; + if (player == null) return false; + return RFP2.state.isModEnabled(player); + } + + // During frames that this returns TRUE: + // * RFP2 has hidden the third person model's arms and is currently allowing "RenderHandEvent" to run normally. + // * This means that the player is in first person view, but is using the vanilla first person hands instead of the RFP2 third person hands. + // * Make sure you adjust your mod's rendering behavior accordingly as needed. + // During frames that this returns FALSE: + // * RFP2 has NOT hidden the third person model's arms. + // * In most cases you should be able to render arms normally in this state. + public boolean rfp2AreThirdPersonArmsHidden() + { + EntityPlayerSP player = Minecraft.getMinecraft().player; + if (player == null) return false; + return !RFP2.state.isRealArmsEnabled(player); + } + + // If you are making a mod that for some reason needs to temporarily suspend RFP2, you can use the following call to do so. + // You cannot exceed RFP2.MAX_SUSPEND_TIMER ticks when calling this, if you need to keep RFP2 suspended for longer, + // you will have to call this function at least once every RFP2.MAX_SUSPEND_TIMER ticks to keep it suspended. + // You cannot decrease the timer through this method, so it is a good idea to set it as short as will work for your use case. + // The idea here is that multiple mods can all be calling this function to suspend RFP2, and only once all mods have + // stopped making requests will RFP2 allow itself to start back up again. + public void rfp2AddSuspendTime(int ticks) + { + RFP2.state.setSuspendTimer(ticks); + } +} diff --git a/src/main/java/com/rejahtavi/rfp2/compat/handlers/RFP2CompatHandler.java b/src/main/java/com/rejahtavi/rfp2/compat/handlers/RFP2CompatHandler.java new file mode 100644 index 0000000..3491bc0 --- /dev/null +++ b/src/main/java/com/rejahtavi/rfp2/compat/handlers/RFP2CompatHandler.java @@ -0,0 +1,54 @@ +package com.rejahtavi.rfp2.compat.handlers; + +import net.minecraft.entity.player.EntityPlayer; + +//compatibility module base class / template +public class RFP2CompatHandler +{ + // Mod Info Template + // public static final String modId = ""; + + // Constructor + public RFP2CompatHandler() + { + return; + } + + // Behavior Getter Templates + public boolean getDisableRFP2(EntityPlayer player) + { + // By default, do nothing unless overridden. + // @Overrides should return TRUE if they want RFP2 to completely skip on the current frame, letting vanilla rendering take over. + return false; + } + + // Behavior Setter Templates + public void hideHead(EntityPlayer player, boolean hideHelmet) + { + // By default, do nothing unless overridden. + // @Overrides should call isHeadHidden(), store the result into prevHeadHiddenState, then hide all head objects. + return; + } + + public void hideArms(EntityPlayer player, boolean hideHelmet) + { + // By default, do nothing unless overridden. + // @Overrides should call areArmsHidden(), store the result into prevArmsHiddenState, then hide all arm objects. + return; + } + + public void restoreHead(EntityPlayer player, boolean hideHelmet) + { + // By default, do nothing unless overridden. + // @Overrides should read prevHeadHiddenState, and if it is FALSE, restore all head object visibility. + return; + } + + public void restoreArms(EntityPlayer player, boolean hideHelmet) + { + // By default, do nothing unless overridden. + // @Overrides should read prevArmsHiddenState, and if it is FALSE, restore all arm object visibility. + return; + } + +} diff --git a/src/main/resources/assets/rfp2/lang/en_us.lang b/src/main/resources/assets/rfp2/lang/en_us.lang new file mode 100644 index 0000000..6f41f0c --- /dev/null +++ b/src/main/resources/assets/rfp2/lang/en_us.lang @@ -0,0 +1,6 @@ +key.rfp2.category=Real First Person 2 +key.arms.desc=Toggle arm mode +key.mod.desc=Toggle mod +key.head.desc=Toggle head turning +rfp2.compatability.helditemconflictlist=Held item conflict list +rfp2.compatability.mountconflictlist=Mount conflict list diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info index 73a9e09..f240f5e 100644 --- a/src/main/resources/mcmod.info +++ b/src/main/resources/mcmod.info @@ -1,14 +1,14 @@ [ { - "modid": "pkapi", - "name": "PKAPI", - "description": "", + "modid": "rfp2", + "name": "Real First Person 2", + "description": "Provides full body rendering in first person view.", "version": "${version}", - "mcversion": "${mcversion}", - "url": "", + "mcversion": "1.12.2", + "url": "https://github.com/rejahtavi/rfp2", "updateUrl": "", - "authorList": ["PIVODEVAT"], - "credits": "", + "authorList": ["Rejah Tavi", "don_bruce"], + "credits": "don_bruce, for the original RFPR mod", "logoFile": "", "screenshots": [], "dependencies": [] diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index 4018267..0633c58 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,7 +1,7 @@ { "pack": { - "description": "examplemod resources", + "description": "rfp2", "pack_format": 3, - "_comment": "A pack_format of 3 should be used starting with Minecraft 1.11. All resources, including language files, should be lowercase (eg: en_us.lang). A pack_format of 2 will load your mod resources with LegacyV2Adapter, which requires language files to have uppercase letters (eg: en_US.lang)." + "_comment": "Provides full body rendering in first person view." } }