13

Minecraft modding for competent Java programmers · GitHub

 2 years ago
source link: https://gist.github.com/HybridEidolon/fe467dab9eb885981ddd
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Minecraft modding for competent Java programmers

Minecraft Forge New Mod Guide

Make sure you have Gradle installed and in your path, so you can run it from command line. Otherwise, you should copy a bootstrapper and the gradle jar into your project.

Use this template build.gradle (for the MC 1.8 unstable branch) for your gradle script:

buildscript {
    repositories {
        mavenCentral()
        maven {
            name = "forge"
            url = "http://files.minecraftforge.net/maven"
        }
        maven {
            name = "sonatype"
            url = "https://oss.sonatype.org/content/repositories/snapshots/"
        }
    }
    dependencies {
        classpath 'net.minecraftforge.gradle:ForgeGradle:1.2-SNAPSHOT'
    }
}

apply plugin: 'forge'

version = "0.1.0"
group = "com.mydomain.mymod"
archivesBaseName = "mymod"

minecraft {
    *version = "1.8-11.14.0.1274-1.8"*
    runDir = "eclipse"
    *mappings = "snapshot_nodoc_20141130"*
}

/*
processResources {
    inputs.property "version", project.version
    inputs.property "mcversion", project.minecraft.version

    // substitute values in mcmod.info
    from(sourceSets.main.resources.srcDirs) {
        include 'mcmod.info'

        expand 'version':project.version, 'mcversion':project.minecraft.version
    }

    from(sourceSets.main.resources.srcDirs) {
        exclude 'mcmod.info'
    }
}
*/

Save this in a new directory and run gradle setupDecompWorkspace and gradle build to set everything up on your local cache. Gradle will be smart and not have multiple copies of this cache if you are working on multiple mods with the same target MC version.

Replace version and mappings in the minecraft block to change the MCF version your mod targets, as well as the obfuscation mappings from MCP to use. You do not need mappings if you are using a stable build.

You can uncomment the processResources block and extend it as necessary to provide build-time substitution to your mcmod.info file (which should go in src/main/resources). A template for mcmod.info is as follows:

[
{
  "modid": "mymod",
  "name": "My Mod",
  "description": "A mod that does things.",
  "version": "${version}",
  "mcversion": "${mcversion}",
  "url": "http://mydomain.com",
  "updateUrl": "",
  "authorList": ["Me", "Another person"],
  "credits": "Forge, FML, and MCP, for being incredible",
  "logoFile": "",
  "screenshots": [],
  "dependencies": []
}
]

And a hello world template MyModMain.java for your mod class:

package com.mydomain.mymod;

import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.Mod.EventHandler;

@Mod(modid="mymod", name="My Mod", version="0.1.0")
public class MyModMain {

    @EventHandler
    public void onInit(FMLInitializationEvent event) {
        System.out.println("Hello, world!");
    }

}

You can generate IDE project files with gradlew idea for IntelliJ and gradlew eclipse for Eclipse.

Useful gradle targets:

  • runClient - runs the client using the "eclipse" folder as CWD
  • runServer - similar to the above but for the server
  • build - total build, drops jar in build/libs/
  • clean - cleans the build directory (doesn't remove IDE project files!)

Coding Details

This is a coding tutorial for experienced Java developers. It is not a fully fledged Forge tutorial. You are expected to understand the following concepts:

  • Classes and Interfaces
  • Class Inheritance
  • Annotations
  • Reflection
  • Event-handling as a design concept
  • Reading javadocs and source code tirelessly scrounging up information

Declaring your mod

To declare your mod to ForgeModLoader, create a class (doesn't have to be anything specific, as long as it is instantiable i.e. non abstract) and give it the @Mod class annotation.

@Mod(modid="mymodid", name="My Mod")
public class MyMod {
    
}

Hooking events

In Forge, the preferred way of adding functionality to Minecraft is to hook events and register data classes. Let's start with a simple hook. For ForgeModLoader events (FML), they can simply be dropped into the same class that is marked @Mod. We use the method annotation @EventHandler for this.

@Mod(modid="mymodid", name="My Mod")
public class MyMod {
    @EventHandler
    public void onModInitialized(FMLInitializationEvent event) {
        System.out.println("Hello, world!");
    }
}

On startup of minecraft or minecraft-server, the log will print this message now.

The only thing that matters in an event registration is the argument. FML will infer the handler on its own using the argument list. In this case, the method onModInitialized will only be called when FMLInitializationEvent is fired.

See the documentation for net.minecraftforge.fml.common.Mod.EventHandler for details on the different kinds of FML events.

The other kind of events are global to Forge, and pertain directly to Minecraft functionality. To use them, we use a different annotation.

@Mod(modid="mymodid", name="My Mod")
public class MyMod {
    @EventHandler
    public void onModInitialized(FMLInitializationEvent event) {
        MinecraftForge.EVENT_BUS.register(this);
    }

    @SubscribeEvent
    public void onWorldLoad(WorldEvent.Load event) {
        System.out.println("A world was loaded. The world difficulty is: " +
                I18n.format(event.world.getDifficulty().getDifficultyResourceKey()););
    }
}

When a world loads, the world difficulty will be printed to standard out.

See the documentation for the net.minecraftforge.event package for a list of all the events in Forge.

Proxy classes: separating Client and Server Code

In the dedicated server, client code is not available, and attempting to reference client classes will crash the server. To avoid this, you can use the @SidedProxy annotation on a static field in any class.

Here's an example of how to set up a proxy:

// -- CommonProxy.java
package com.myname.mymodid;
public class CommonProxy {
    public void doThings() {
        // ... this function behaves differently if it's on minecraft-server
        System.out.println("This is common code for all environments.");
        return;
    }    
}

// -- CombinedClientProxy.java
package com.myname.mymodid;

public class CombinedClientProxy extends CommonProxy {
    public void doThings() {
        super.doThings(); // common code print
        System.out.println("This is the user client! Maybe force load a post-process shader here?");
    }
}

// -- MyMod.java
package com.myname.mymodid;
@Mod(modid="mymodid", name="My Mod")
public class MyMod {
    
    @SidedProxy(clientSide="com.myname.mymodid.CombinedClientProxy",serverSide="com.myname.mymodid.CommonProxy")
    public static CommonProxy proxy;

    @EventHandler
    public void onModInitialized(FMLInitializationEvent event) {
        MinecraftForge.EVENT_BUS.register(this);

        proxy.doThings();
    }

    @SubscribeEvent
    public void onWorldLoad(WorldEvent.Load event) {
        System.out.println("A world was loaded. The world difficulty is: " +
                I18n.format(event.world.getDifficulty().getDifficultyResourceKey()););
    }
}

On a client (minecraft.jar) this will call both CommonProxy and CombinedClientProxy's versions of the function.

Do not use @SidedProxy for network related behavior, however, there is a different way to do that.

Doing things on the Minecraft main thread.

AKA Here's how you do reflection as well so that it works even after reobfuscation, fyi. THIS IS REALLY CRITICAL FOR DISTRIBUTING YOUR MOD

TODO: DO THIS RIGHT! THE ABOVE IS WRONG

Some functions need to be called on the Minecraft thread (in particular, anything related to rendering). Thankfully Forge lets us queue functions to be called on the main thread. This is useful when we want to call those functions inside event handlers. Here's an example:

package com.myname.mymodid;
@Mod(modid="mymodid", name="My Mod")
public class MyMod {

    @SubscribeEvent
    public void onWorldLoad(WorldEvent.Load event) {
        // Force FXAA
        // We have to queue this to run on the minecraft thread because it needs
        // to refer to the GL context
        final Minecraft mc = Minecraft.getMinecraft();

        I18n.format(event.world.getDifficulty().getDifficultyResourceKey());

        mc.addScheduledTask(new Runnable() {
            public void run() {
                if (mc.entityRenderer.getShaderGroup() != null)
                    mc.entityRenderer.getShaderGroup().deleteShaderGroup();

                Method method;
                try {
                    method = EntityRenderer.class.getDeclaredMethod("loadShader", ResourceLocation.class); // loadShader
                    method.setAccessible(true);
                    method.invoke(mc.entityRenderer, new ResourceLocation("shaders/post/fxaa.json"));
                } catch (NoSuchMethodException ex) {
                    ex.printStackTrace();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });
    }
}

Providing a Runnable or Callable will queue that code up for execution or immediately run it if we are already on the Minecraft thread.

Registering new items/achievements/blocks

Handling networking

Sending arbitrary packets

Class for custom payloads is Packet250CustomPayload. Send them with:

PacketDispatcher.sendPackettoServer()
PacketDispatcher.sendPacketToServer()
PacketDispatcher.sendPacketToPlayer()
PacketDispatcher.sendPacketToAllAround()
PacketDispatcher.sendPacketToAllInDimension()
PacketDispatcher.sendPacketToAllPlayers()

Minecraft Shader Documentation

Shaders in Minecraft are handled through materials described in json objects. At this time, shaders can only be applied with post-process passes.

post-process descriptors go in:

shaders/post/*.json

material descriptors in:

shaders/program/*.{fsh,vsh}
shaders/program/*.json

Post-process descriptors

These describe how each pass is applied to the main framebuffer. A simple, no-frills post-process (i.e. no post-processing at all) might look like this:

{
    "targets": [ "swap" ],
    "passes": [
        {
            "name": "blit",
            "intarget": "minecraft:main",
            "outtarget": "minecraft:main"
        },
    ],
}

This post-process description uses the material program blit to draw to outtarget minecraft:main, which is in the minecraft jar but as far as I can tell, you would have to copy this into your mod from the official assets to use it because the descriptors don't support resource locators with mod contexts. blit simply takes the intarget and blits it to the outtarget, nothing more or less. (You will see how this works in the example for material descriptors below.)

Materials

Materials are a set of properties for drawing a surface. They include GL blend function settings, the program code to use, and declarations for samplers and uniforms. The calling code that uses the material needs to set these or the render might fail.

An example one from minecraft:shaders/program/blit.json:

{
    "blend": {
        "func": "add",
        "srcrgb": "srcalpha",
        "dstrgb": "1-srcalpha"
    },
    "vertex": "blit",
    "fragment": "blit",
    "attributes": [ "Position" ],
    "samplers": [
        { "name": "DiffuseSampler" }
    ],
    "uniforms": [
        { "name": "ProjMat",       "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
        { "name": "OutSize",       "type": "float",     "count": 2,  "values": [ 1.0, 1.0 ] },
        { "name": "ColorModulate", "type": "float",     "count": 4,  "values": [ 1.0, 1.0, 1.0, 1.0 ] }
    ]
}

As far as I can tell, DiffuseSampler is set to the intarget framebuffer in the post-process handling code.

Post-process in Practice

The "Super Secret Settings" simply cycles through a list of predefined post-process descriptors. Looking at this code, the process for changing the current post-process shader is as so:

// We have to queue this to run on the minecraft thread because it needs
// to refer to the GL context
final Minecraft mc = Minecraft.getMinecraft();

mc.addScheduledTask(new Runnable() {
    public void run() {
        if (mc.entityRenderer.getShaderGroup() != null)
            mc.entityRenderer.getShaderGroup().deleteShaderGroup();

        Method method;
        try {
            method = EntityRenderer.class.getDeclaredMethod("loadShader", ResourceLocation.class); // loadShader
            method.setAccessible(true);
            method.invoke(mc.entityRenderer, new ResourceLocation("shaders/post/fxaa.json"));
        } catch (NoSuchMethodException ex) {
            ex.printStackTrace();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
});

HOWEVER, loadShader(ResourceLocation) is currently private access which means you have to either reflect it using Java Reflection or use the Access Transformers API in Forge to force it to public access.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK