init
This commit is contained in:
parent
7e9ac0866a
commit
101bb0984c
26
build.gradle
26
build.gradle
@ -13,9 +13,10 @@ apply plugin: 'net.minecraftforge.gradle'
|
|||||||
apply plugin: 'eclipse'
|
apply plugin: 'eclipse'
|
||||||
apply plugin: 'maven-publish'
|
apply plugin: 'maven-publish'
|
||||||
|
|
||||||
version = '1.0'
|
|
||||||
group = 'com.yourname.modid' // http://maven.apache.org/guides/mini/guide-naming-conventions.html
|
version = "${mc_version}-${mod_version}"
|
||||||
archivesBaseName = 'modid'
|
group = 'com.rejahtavi.rfp2'
|
||||||
|
archivesBaseName = 'RealFirstPerson2'
|
||||||
|
|
||||||
sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.8' // Need this here so eclipse task generates correctly.
|
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'
|
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..
|
// Example for how to get properties into the manifest for reading by the runtime..
|
||||||
jar {
|
jar {
|
||||||
manifest {
|
manifest {
|
||||||
|
@ -3,3 +3,5 @@
|
|||||||
org.gradle.jvmargs=-Xmx3G
|
org.gradle.jvmargs=-Xmx3G
|
||||||
org.gradle.daemon=false
|
org.gradle.daemon=false
|
||||||
org.gradle.java.home=/usr/lib/jvm/openjdk8
|
org.gradle.java.home=/usr/lib/jvm/openjdk8
|
||||||
|
mc_version=1.12.2
|
||||||
|
mod_version=1.3.3
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
68
src/main/java/com/rejahtavi/rfp2/ClientProxy.java
Normal file
68
src/main/java/com/rejahtavi/rfp2/ClientProxy.java
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
123
src/main/java/com/rejahtavi/rfp2/EntityPlayerDummy.java
Normal file
123
src/main/java/com/rejahtavi/rfp2/EntityPlayerDummy.java
Normal file
@ -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 <Entity> 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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
18
src/main/java/com/rejahtavi/rfp2/IProxy.java
Normal file
18
src/main/java/com/rejahtavi/rfp2/IProxy.java
Normal file
@ -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);
|
||||||
|
}
|
193
src/main/java/com/rejahtavi/rfp2/RFP2.java
Normal file
193
src/main/java/com/rejahtavi/rfp2/RFP2.java
Normal file
@ -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<RFP2CompatHandler> compatHandlers = new ArrayList<RFP2CompatHandler>();
|
||||||
|
|
||||||
|
// 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<RFP2CompatHandler>();
|
||||||
|
|
||||||
|
// 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
160
src/main/java/com/rejahtavi/rfp2/RFP2Config.java
Normal file
160
src/main/java/com/rejahtavi/rfp2/RFP2Config.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
54
src/main/java/com/rejahtavi/rfp2/RFP2Keybind.java
Normal file
54
src/main/java/com/rejahtavi/rfp2/RFP2Keybind.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
515
src/main/java/com/rejahtavi/rfp2/RFP2State.java
Normal file
515
src/main/java/com/rejahtavi/rfp2/RFP2State.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
295
src/main/java/com/rejahtavi/rfp2/RenderPlayerDummy.java
Normal file
295
src/main/java/com/rejahtavi/rfp2/RenderPlayerDummy.java
Normal file
@ -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<EntityPlayerDummy>
|
||||||
|
{
|
||||||
|
// 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<AbstractClientPlayer> render = (RenderPlayer) this.renderManager.<AbstractClientPlayer>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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
src/main/java/com/rejahtavi/rfp2/ServerProxy.java
Normal file
39
src/main/java/com/rejahtavi/rfp2/ServerProxy.java
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
50
src/main/java/com/rejahtavi/rfp2/compat/RFP2CompatApi.java
Normal file
50
src/main/java/com/rejahtavi/rfp2/compat/RFP2CompatApi.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
6
src/main/resources/assets/rfp2/lang/en_us.lang
Normal file
6
src/main/resources/assets/rfp2/lang/en_us.lang
Normal file
@ -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
|
@ -1,14 +1,14 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"modid": "pkapi",
|
"modid": "rfp2",
|
||||||
"name": "PKAPI",
|
"name": "Real First Person 2",
|
||||||
"description": "",
|
"description": "Provides full body rendering in first person view.",
|
||||||
"version": "${version}",
|
"version": "${version}",
|
||||||
"mcversion": "${mcversion}",
|
"mcversion": "1.12.2",
|
||||||
"url": "",
|
"url": "https://github.com/rejahtavi/rfp2",
|
||||||
"updateUrl": "",
|
"updateUrl": "",
|
||||||
"authorList": ["PIVODEVAT"],
|
"authorList": ["Rejah Tavi", "don_bruce"],
|
||||||
"credits": "",
|
"credits": "don_bruce, for the original RFPR mod",
|
||||||
"logoFile": "",
|
"logoFile": "",
|
||||||
"screenshots": [],
|
"screenshots": [],
|
||||||
"dependencies": []
|
"dependencies": []
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"pack": {
|
"pack": {
|
||||||
"description": "examplemod resources",
|
"description": "rfp2",
|
||||||
"pack_format": 3,
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user