Minecraft(我的世界)中文论坛

 找回密码
 注册(register)
查看: 2030|回复: 15

[Mod开发教程] Coremod导论—从切比雪夫多项式说起

[复制链接]
森林蝙蝠 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
5670
钻石
性别
保密
注册时间
2016-6-16
查看详细资料
发表于 2018-10-5 00:18:35 | 显示全部楼层 |阅读模式

您尚未登录,立即登录享受更好的浏览体验!

您需要 登录 才可以下载或查看,没有帐号?注册(register)

x
本帖最后由 森林蝙蝠 于 2019-7-27 23:11 编辑

所谓的coremod,是forge提供的代码注入机制,用来方便开发者修改原版代码或者控制mod加载用的,要实现coremod,首先应该实现IFMLLoadingPlugin接口:

public class WarhammerLoader implements IFMLLoadingPlugin {
    public String[] getASMTransformerClass(){
        //代码注入需要用到asm,你的asm操作一般在IClassTransformer中进行,这个类用来获取实现了IClassTransformer的类
        //LaunchClassLoader.registerTransformer()中IClassTransformer的实现会被ClassLoader加载

        return new String[]{
            "com.forestbat.warhammer.asm.ChebishevTransformer","com.forestbat.warhammer.asm.ParentTransformer"
         };
    }
    public String getModContainerClass(){
        //用来获取实现了ModContainer的类
        return "com.forestbat.warhammer.asm.WarhammerModContainer";
    }
    public String getSetupClass(){
        //用来获取实现了IFMLCallHook的类
        return null;
    }
    public void injectData(Map<String,Object> data){
        //注入一些重要的数据,例如MC路径
        data.put("mcLocation", File mcDir);//MC路径
        data.put("coremodList", List loadPlugins);//coremod表单
        data.put("runtimeDeobfuscationEnabled", !deobfuscatedEnvironment);//确认运行时反混淆开启
        data.put("coremodLocation", File location);//coremod路径        
    }
    public String getAccessTransformerClass()
    {
        //AccessTransformer用来改变类的访问权限,例如从public改成private,forge已经自带
        return "net.minecraftforge.fml.common.asm.transformers.AccessTransformer";
    }
}

getASMTransformerClass()
众所周知,Optifine的快速渲染是使用了自己的sin和cos算法,但可惜optifine是不开源的,那我们能不能实现自己的算法,以图对原版的算法实现优化呢?答案是肯定的。
在这里跟大家介绍一下切比雪夫逼近:切比雪夫多项式(百度百科)
比起泰勒公式,切比雪夫的优势在于可以以更低的次数逼近目标函数,降低运算量。出于简洁,使用第一类切比雪夫多项式去逼近sin(x),也就是sin(x)=aT0(x)+bT1(x)+cT2(x)+……这样的形式。
为了保证精度,我们选用六次切比雪夫逼近式,可以达到误差限4e-7,远高于原版算法的精度1e-6级别,最后的公式如下:
  1. <font color="Black">sin = (((((-0.000960664 * value + 0.0102697866) * value - 0.00198601997) * value - 0.1656067221)
  2.          * value - 0.0002715666) * value + 1.000026227) * value;
  3. //value是传入的弧度参数,范围为(0,π/2),其他角度都可以通过诱导公式变过来
  4. </font>
复制代码
什么你问我怎么算出来的?取特殊点(例如sin(pi/6)=1/2)带入你的逼近多项式,得到一个7元一次方程组,然后求解这个方程组对应的矩阵(如何用Excel求逆矩阵和矩阵相乘可百度),即可得出a,b,c等相应系数。

好的,式子得到了,那么是时候将其置入游戏体验一下了。
如果你只是给自己的mod用一下,就不会有接下来的东西,直接声明一个static方法调用即可;如果你像我一样的“大公无私”,想让原版游戏的算法也变成这样大家一起用,下面的内容或许就很重要了。
准备好的了话,请看下面↓↓↓或这里
  1. <font color="Black">public class ChebishevTransformer implements IClassTransformer {
  2. //IClassTransformer:MC自带的launchwrapper工具所提供的类转换接口
  3.     @Override
  4.     public byte[] transform(String name, String transformedName, byte[] basicClass) {
  5.         if (transformedName.equals("net.minecraft.util.math.MathHelper")) {
  6.         FMLLog.log.warn("[Transforming...]");
  7.         //在这里输出一个告警信息,告诉大家你在做什么,可选项
  8.         ClassNode classNode=new ClassNode(ASM5);
  9.         //声明一个ASM5的classNode
  10.         ClassReader classReader=new ClassReader(basicClass);
  11.         //声明一个读取basicClass的classReader
  12.         classReader.accept(classNode,0);
  13.         //让classReader接收到要修改的classNode/classVisitor,以产生事件
  14.         List<MethodNode> methodNodeList=new ArrayList<>(classNode.methods);
  15.         //由于我们的目标是要修改方法,因此要遍历一下MethodNode,将其保存在list中
  16.         for(MethodNode methodNode:methodNodeList)
  17.             if (methodNode.name.equals("func_76126_a(F)F")) {
  18.             //上文的transformedName是转换后的类名,因为你看到的代码是根据notch name反混淆出来的,methodName是它的srg name,              //(F)F指参数为float,返回float
  19.                 methodNode.instructions.clear();
  20.                 //清除methodNode下的结构
  21.                 methodNode.instructions.add(new InsnNode(FLOAD))
  22.                 //添加你自己的实现,FLOAD代表传入float参数,其在ASM中规定的操作码(Opcode)是23
  23.                 methodNode.instructions.add(new MethodInsnNode
  24.                         (ACC_PUBLIC + ACC_STATIC, "Lcom/forestbat/warhammer/asm/ChebishevTransformer", "sin", "(F)D", false));
  25.                 //添加上你自己的实现,ACC_PUBLIC,ACC_STATIC是ASM中public和static的操作码(分别是0x0001和0x0008);
  26.                 //“Lcom/forestbat/warhammer/asm/ChebishevTransformer”是owner参数,表明方法的源头类;
  27.                 //“sin”是name参数,表明我们的方法名;
  28.                 //“(F)D”是“float形参,返回double”的description(描述符);
  29.                 //false表明了owner类(不是)一个接口,若是,则为true
  30.                 methodNode.instructions.add(new InsnNode(DRETURN));
  31.                 //DRETURN代表返回double值,其操作码是175
  32.             }
  33.             ClassWriter classWriter = new ClassWriter(2);
  34.             //声明一个classWriter写入内容,参数2的意义是COMPUTE_FRAMES,自动计算局部变量与操作数栈
  35.             //每当方法被调用一次栈上就会生成一个帧,帧又被分为局部变量和操作数栈,COMPUTE_FRAMES会帮你自动计算,但会拖慢性能
  36.             classNode.accept(classWriter);
  37.             //classNode接收classWriter做参数,向方法内部生成二进制代码
  38.             return classWriter.toByteArray();
  39.         }
  40.         return basicClass;
  41.         //返回被修改的basicClass
  42. }
  43. </font>
复制代码
//我们自己的sin方法
public static double
sin(float value){
    if(value>=0&&value<=PI/2)
        return  (((((-0.000960664 * value + 0.0102697866) * value - 0.00198601997) * value - 0.1656067221)
                * value - 0.0002715666) * value + 1.000026227) * value;

    if(value<0){
        return -sin(-value);
    }
    else{
       return sin((float)(value%PI));
    }
}
首先解释下,为什么basicClass的类型是byte[]——因为basicClass在这里指的是未加载的类字节码,字节码本质上还是一串字节,因此类型是byte[]。
asm修改字节码的步骤是固定的,ClassReader读取basicClass(使之可被修改),接收ClassNode(来自asm的tree api)/ClassVisitor(来自asm的core api),而ClassNode/ClassVisitor又会接受ClassWriter,将开发者的改动通过ClassWriter写入Class中,最后返回修改后的basicClass。

tree api的性能低于core api,我们不妨康康core api的写法:
public class ParentTransformer implements IClassTransformer {//一个修改父类的轮子
    private static String parentName;
    private static String[] baseInterfaces;

    @Override
    public byte[] transform(String name, String transformedName, byte[] basicClass) {
      if(baseInterfaces!=null && parentName!=null){
        ClassReader classReader = new ClassReader(basicClass);
        ClassWriter classWriter = new ClassWriter(2);//2代表自动计算栈帧
        ClassVisitor classVisitor = new ClassVisitor(ASM5, classWriter) {
            @Override
            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                super.visit(52, access, transformedName, null, null, null);
                //52代表Java8,access代表public/private/protected,superName即父类,由于我们要修改父类,因此原类不应有父类,否则会发生错误
            }
        };
        classReader.accept(classVisitor, 2);
        classWriter.visit(52, 0x0001, transformedName, null,
                setParentName(transformedName, parentName), setInterfacesName(transformedName, baseInterfaces));
        classWriter.toByteArray();
        return basicClass;
    }
    else return basicClass;

    }
    public static String setParentName(String transformedName,String parentName){
        ParentTransformer.parentName=parentName;
        return parentName;
    }
    public static String[] setInterfacesName(String transformedName,String[] baseInterfaces) {
        ParentTransformer.baseInterfaces=baseInterfaces;
        for (String baseInterface : baseInterfaces) {
            try {
                ClassReader classReader = new ClassReader(baseInterface);
                    if (classReader.getItemCount() == 0)
                        return baseInterfaces;
                }catch (IOException e){FMLLog.bigWarning("No any interfaces!");}
            }
            return null;
        }
}

getModContainerClass()
每一个被@Mod注解标记从而被认为是mod的mod,都会有自己的ModContainer,如无意外将会在FMLModContainer类中进行处理,读取@Mod注解中的信息,但是开发者也可以自己改写ModContainer。
public class YourModContainer implements ModContainer {
    private ModMetadata modMetadata;
    private boolean enabled=true;
    private VersionRange versionRange;
    private ModContainer modContainer;
    public String getModId(){
        return MOD_ID;
    }
    public String getName(){
        return MOD_NAME;
    }
    public String getVersion(){
        return VERSION;
    }
    public File getSource(){
        Path path= Paths.get(".minecraft","mods",getName());
        return path.toFile();
    }
    public ModMetadata getMetadata(){
        return modMetadata; //ModMetaData就是mod的“标签”,由@Mod注解信息决定,也可以由玩家自己编写
    }

    @Override   
    public void bindMetadata(MetadataCollection mc) {
    try {
        ModMetadata metadata = mc.getMetadataForId("Gregtech", Maps.newConcurrentMap());
        if (metadata != null & metadata.dependencies.contains("ic2"))
            getUpdateUrl().openConnection();
    }catch (IOException e){LOGGER.info("IC2 WebSite Error!");}}


    @Override
    public void setEnabledState(boolean enabled) {
        this.enabled=enabled;
    }
    public Set<ArtifactVersion> getRequirements(){
        return modMetadata.requiredMods;
    }
    public List<ArtifactVersion> getDependencies(){
        return modMetadata.dependencies;
    }
    public List<ArtifactVersion> getDependants(){
        return null;
    }

    @Override
    public boolean registerBus(EventBus bus, LoadController controller) {        
        bus.unregister(ForgeRegistries.BIOMES);//从EventBus移除所有生物群系,当然这么做很扯淡
        controller.getActiveModList().remove("titan");//从活跃加载mod列表中移除泰坦,换句话说,禁用了泰坦
        return true;    }

    @Override
    public boolean shouldLoadInEnvironment() {
        return false;
    }

    @Override
    public String getSortingRules() {
         //forge使用的是拓扑排序(TopologicalSort),当然你可以写一个自己的
         return “com.yourmod.yourSortList";
    }

    @Override
    public boolean matches(Object mod) {
        return mod.equals(YourMod.INSTANCE);
    }

    @Override
    public Object getMod() {
        return YourMod.INSTANCE;
    }

    @Override
    public ArtifactVersion getProcessedVersion() {
        return new DefaultArtifactVersion(YourMod.VERSION);
    }
    public boolean isImmutable(){
        return false;
    }

    @Override
    public String getDisplayVersion() {
        return YourMod.VERSION;
    }

    @Override
    public VersionRange acceptableMinecraftVersionRange() {
        try {
            versionRange=VersionRange.createFromVersionSpec("[1.12,1.12.2]");//限定你的mod版本范围是1.12-1.12.2
        }catch (InvalidVersionSpecificationException e){FMLLog.bigWarning("No 1.12!");}
        return versionRange;
    }

    @Nullable
    @Override
    public Certificate getSigningCertificate() {
        Certificate[] certificates = getClass().getProtectionDomain().getCodeSource().getCertificates();
        return certificates != null ? certificates[0] : null;
    }

    @Override
    public Map<String, String> getCustomModProperties() {
        return null;
        //向description中添加自己的信息,forge本身不一定会作出处理,这一部分由开发者自行处理,与下文descriptor并不一样
    }

    @Override
    public Class<?> getCustomResourcePackClass() {
        return null;
    }

    @Override
    public Map<String, String> getSharedModDescriptor() {Map<String, String> descriptor = Maps.newHashMap();
        descriptor.put("modsystem", "FML");
        descriptor.put("id", getModId());
        descriptor.put("version", getDisplayVersion());
        descriptor.put("name", getName());
        descriptor.put("url", modMetadata.url);
        descriptor.put("description", modMetadata.description);
        return descriptor;
    }

    public Disableable canBeDisabled(){
        return Disableable.NEVER;//设置其不会被禁用
    }

    @Override
    public String getGuiClassName() {
        return null;
    }

    @Override
    public List<String> getOwnedPackages() {
        return null;
    }

    @Override
    public URL getUpdateUrl() {        
        try {
          return new URL("https://minecraft.curseforge.com/projects/botania/files/2524591");//方便所以打了个botania的更新链接
        }catch (MalformedURLException e){}   
    }

    @Override
    public int getClassVersion() {
        return 52;//52代表Java8
    }
    public void setClassVersion(int version){
        
    }
}

getSetupClass()
由于IFMLCallHook这个接口的调用时间非常早,类似于.NET Core的startup接口,所以这里不应该出现MC本身的代码(因为MC代码的反混淆还在IFMLCallHook之后)
    public class WarhammerHook implements IFMLCallHook {
    @Override
    public void injectData(Map<String, Object> data) {         
         liveEnv = (Boolean)data.get("runtimeDeobfuscationEnabled");
         cl = (LaunchClassLoader) data.get("classLoader");
         File mcDir = (File)data.get("mcLocation");
         fmlLocation = (File)data.get("coremodLocation");
         ClassPatchManager.INSTANCE.setup(FMLLaunchHandler.side());
         FMLDeobfuscatingRemapper.INSTANCE.setup(mcDir, cl, (String) data.get("deobfuscationFileName"));    }

    @Override
    public Void call() throws Exception {        
        Class<?> classTitan=Class.forName("titan(or its obfuscate name)",false, (LaunchClassLoader)cl) //LaunchClassLoader是MC自带的类加载器        
        CodeSource codeSource = classTitan.getProtectionDomain().getCodeSource();
        Files.delete(Paths.get(codeSource.getLocation().toURI())); //探测到泰坦所在的类后删除它
}
}


最后
我们的东西已经写完了,应该加载到游戏中使之生效。
打开你的build.gradle文件,向里面写入这样一段:
  1. jar {
  2.     manifest {
  3. //实现了IFMLLoadingPlugin的类(你的coremod类)
  4.         attributes 'FMLCorePlugin': 'com.forestbat.warhammer.asm.WarhammerLoader'
  5.         attributes 'FMLCorePluginContainsFMLMod': 'true'
  6. //access transformer配置文件,一般是modid_at.cfg,放在resources/META_INF文件夹下
  7.         attributes 'FMLAT': 'warhammer_at.cfg'
  8. //https://github.com/Ipsis/Woot/blob/1_12/src/main/resources/META-INF/woot_at.cfg(woot的AT配置文件)
  9.     }
  10. }
复制代码



———————————————————————— 分割线 ——————————————————————————
如有需要继续更新ITweaker相关

来自群组: HAYO Studio

评分

参与人数 7人气 +17 金粒 +168 收起 理由
2048028141 + 2 MCBBS有你更精彩~
skpsk自己家 + 2 Ssssssssssssssssssss
葉子の + 2 + 33 虽然看不懂,但是感觉很厉害的样子.
ZhuZhexi + 2 + 30 MCBBS有你更精彩~
乙烯_中国 + 4 + 50 靠谱,老哥稳
gooding300 + 3 + 25 MCBBS有你更精彩~
Lss233 + 2 + 30 MCBBS有你更精彩~

查看全部评分

NoName德里奇 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
6556
钻石
性别
保密
注册时间
2018-8-2
查看详细资料
发表于 2018-10-5 07:15:31 | 显示全部楼层
这很数学.jpg
蝙蝠出品,必属精品。
回复

使用道具 举报

ruhuasiyu 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
15115
钻石
性别
保密
注册时间
2014-10-16
查看详细资料
发表于 2018-10-5 09:50:35 | 显示全部楼层
完全可以只考虑0~pi/4,然后用ax+bx^3+cx^5逼近吧?
回复

使用道具 举报

langyo 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
5731
钻石
性别
保密
注册时间
2015-4-17
查看详细资料
发表于 2018-10-5 09:52:51 | 显示全部楼层
1.13 要换成用 JavaScript 写 Mod 了?太恐怖了……

是不是还要再内嵌个 V8 和 Webkit……

咳咳……



切比雪夫我听都没听过,只能仰望
回复

使用道具 举报

森林蝙蝠 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
5670
钻石
性别
保密
注册时间
2016-6-16
查看详细资料
 楼主| 发表于 2018-10-5 10:05:48 | 显示全部楼层
ruhuasiyu 发表于 2018-10-5 09:50
完全可以只考虑0~pi/4,然后用ax+bx^3+cx^5逼近吧?

可以,不过这样需要再推一遍cos()的逼近,而且当时也没有考虑的太深。
回复

使用道具 举报

森林蝙蝠 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
5670
钻石
性别
保密
注册时间
2016-6-16
查看详细资料
 楼主| 发表于 2018-10-5 10:23:51 | 显示全部楼层
langyo 发表于 2018-10-5 09:52
1.13 要换成用 JavaScript 写 Mod 了?太恐怖了……

是不是还要再内嵌个 V8 和 Webkit……

是coremod,不是mod。
typescript会编译成JS,所以你也可以用TS去编。
回复

使用道具 举报

liach 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
2755
钻石
性别
保密
注册时间
2014-4-2
查看详细资料
发表于 2019-3-3 18:24:46 | 显示全部楼层
个人表示modcontainer类应该和coremod其他部分分开,与普通mod的代码放一起。不然和sponge有兼容问题。

同时修改字节码的内容最好放独立的sourceset,和mod主体避免出现coremod导致classloading的现象。

还有最好别在coremod写法中唐突地插入一段fast math,混淆了主题。
回复

使用道具 举报

森林蝙蝠 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
5670
钻石
性别
保密
注册时间
2016-6-16
查看详细资料
 楼主| 发表于 2019-3-3 18:33:59 | 显示全部楼层
liach 发表于 2019-3-3 18:24
个人表示modcontainer类应该和coremod其他部分分开,与普通mod的代码放一起。不然和sponge有兼容问题。

同 ...

前两者会注意,但第三条有必要说下,其实整篇文章是从fastmath那部分扩写出来的,所以会有切比雪夫多项式的论述。

评分

参与人数 1金粒 +10 收起 理由
liach + 10 个人觉得这样……有点太跑题了。还行.

查看全部评分

回复

使用道具 举报

a1016746326 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
2523
钻石
性别
保密
注册时间
2014-6-26
查看详细资料
发表于 2019-3-8 08:51:31 | 显示全部楼层
很有用,收藏了
回复

使用道具 举报

roj234 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
855
钻石
性别
保密
注册时间
2017-9-24
查看详细资料
发表于 2019-3-12 23:43:35 | 显示全部楼层
为什么我测试了却StackOverFlow
sin输入的是啥?
回复

使用道具 举报

森林蝙蝠 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
5670
钻石
性别
保密
注册时间
2016-6-16
查看详细资料
 楼主| 发表于 2019-3-13 00:05:56 | 显示全部楼层
roj234 发表于 2019-3-12 23:43
为什么我测试了却StackOverFlow
sin输入的是啥?

你是不是输入了一个特别大的数?sin一般的参数是介于0-2pi的,如果太大就会无限递归,从而引发爆栈。
回复

使用道具 举报

roj234 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
855
钻石
性别
保密
注册时间
2017-9-24
查看详细资料
发表于 2019-3-13 12:46:18 | 显示全部楼层
森林蝙蝠 发表于 2019-3-13 00:05
你是不是输入了一个特别大的数?sin一般的参数是介于0-2pi的,如果太大就会无限递归,从而引发爆栈。 ...

        for(float i=0;i<3.1;i+=0.0001f){
        double a = sin(i);
回复

使用道具 举报

森林蝙蝠 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
5670
钻石
性别
保密
注册时间
2016-6-16
查看详细资料
 楼主| 发表于 2019-3-13 22:14:53 | 显示全部楼层
本帖最后由 森林蝙蝠 于 2019-3-13 22:36 编辑

public static double sin(float value){
            //修复程序,直接return,恢复正常
            //原因在于主公式sin= (((((-0.000960664 * value + 0.0102697866) * value - 0.00198601997) * value - 0.1656067221)
                        * value - 0.0002715666) * value + 1.000026227) * value 处没有return,导致程序找不到出口就这么爆栈了
            if(value>=0&&value<=PI/2)
                return  (((((-0.000960664 * value + 0.0102697866) * value - 0.00198601997) * value - 0.1656067221)
                        * value - 0.0002715666) * value + 1.000026227) * value;
            if(value<0&&value>=-PI/2){
                return sin(-value);
            }
            else{
               return sin((float)(value-PI));
            }
        }

评分

参与人数 1人气 +1 收起 理由
roj234 + 1 MCBBS有你更精彩~

查看全部评分

回复

使用道具 举报

roj234 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
855
钻石
性别
保密
注册时间
2017-9-24
查看详细资料
发表于 2019-3-14 18:48:00 | 显示全部楼层
本帖最后由 roj234 于 2019-3-14 18:56 编辑
森林蝙蝠 发表于 2019-3-13 22:14
public static double sin(float value){
            //修复程序,直接return,恢复正常
            // ...

!Thank u   


                               
登录/注册后可看大图

回复

使用道具 举报

森林蝙蝠 当前离线
帖子
主题
精华
贡献
最后登录
1970-1-1
爱心
积分
5670
钻石
性别
保密
注册时间
2016-6-16
查看详细资料
 楼主| 发表于 2019-3-14 19:17:31 | 显示全部楼层

问一下,你这个origin time是哪个算法的?
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册(register)

本版积分规则

Archiver|小黑屋|Mcbbs.net ( 京ICP备15023768号-1 ) | 京公网安备 11010502037624号 | 手机版

GMT+8, 2019-8-21 08:29 , Processed in 0.059615 second(s), Total 26, Slave 22 queries , Gzip On, MemCached On.

"Minecraft"以及"我的世界"为Mojang Synergies AB的商标 本站与Mojang以及微软公司没有从属关系

© 2010-2019 我的世界中文论坛 版权所有 本站原创图文内容版权属于原创作者,未经许可不得转载

快速回复 返回顶部 返回列表