program - CHanzyLazer/gregtech6-CH_Edition GitHub Wiki

程序部分

主要用来梳理我做的改动,也可以帮助想要加入这个项目的人

目录

JSON 配置文件

实现方法是通过第三方库 fastjson 来实现 JSON 格式的数据读取和写入

相关代码都存于 main\java\gregtechCH\config

包含第三方库

需要在项目的 gradle 文件 build.gradledependencies 项中加入:

dependencies {
    shade 'com.alibaba:fastjson:1.2.79'
}

shade 表示将这个第三方库全部复制一份包含到 mod 包里,这样玩家就不需要安装这个前置了

修改后需要重新搭建工作区 ./gradlew setupDevWorkspace setupDecompWorkspace

数据存储

所有负责数据存储的类都以 Data 开头,fastjson 直接操作这些类将其序列化或者反序列化

对于机器属性还有以 Attributes 开头的类,用于存储和(根据材料)计算各种机器的属性,可能存在一些继承关系来减少重复代码

数据类通过包含机器属性类的对象的 List 来实现嵌套的效果:

public class DataMachinesMultiblock_CH {
    public List<AttributesLargeBoilerTank_CH> LargeBoilerTank = new ArrayList<>();
    public List<AttributesLargeSteamTurbine_CH> LargeSteamTurbine = new ArrayList<>();
    public List<AttributesLargeGasTurbine_CH> LargeGasTurbine = new ArrayList<>();
}

对应序列化得到的结果:

{
    "LargeBoilerTank":[
        {
            "ID":17201,
            "material":"StainlessSteel",
            "nbtCapacity":40960000,
            "nbtCapacity_SU":40960000,
            "nbtDesign":18022,
            "nbtEfficiency_CH":8000,
            "nbtHardness":6.0,
            "nbtInput":8192,
            "nbtResistance":6.0,
            "recipeObject":["PPh","PMP","wPP","M","gregtech:gt.multitileentity:18022","P","OreDictItemData:plateDenseStainlessSteel"],
            "stackSize":16
        }
    ],
    "LargeSteamTurbine":[
        {
            "ID":17211,
            "material":"StainlessSteel",
            "nbtCooldownRate":[1024,2048,3072,4096,5120,6144,7168,8192,9216,10240],
            "nbtDesign":18022,
            "nbtEfficiency":[3250,5000,6000,6500,7000,7250,7333,7500,7666,7750],
            "nbtEfficiencyOC":5000,
            "nbtEfficiencyWater":9500,
            "nbtHardness":6.0,
            "nbtLengthMax":10,
            "nbtLengthMid":4,
            "nbtLengthMin":1,
            "nbtOutput":[1365,2560,3431,4096,4618,5041,5389,5681,5930,6144],
            "nbtPreheatCost":[8,16,24,32,40,48,56,64,72,80],
            "nbtPreheatEnergy":[8192000,16384000,24576000,32768000,40960000,49152000,57344000,65536000,73728000,81920000],
            "nbtResistance":6.0,
            "recipeObject":["PPP","PMP","PPP","M","gregtech:gt.multitileentity:18022","P","OreDictItemData:blockPlateMagnalium"],
            "rotorMaterial":"Magnalium",
            "stackSize":16
        }
    ],
    "LargeGasTurbine":[
        {
            "ID":17231,
            "material":"StainlessSteel",
            "nbtCooldownRate":[1024,2048,3072,4096,5120,6144,7168,8192,9216,10240],
            "nbtDesign":18022,
            "nbtEfficiency":[3250,5000,6000,6500,7000,7250,7333,7500,7666,7750],
            "nbtHardness":6.0,
            "nbtLengthMax":12,
            "nbtLengthMid":6,
            "nbtLengthMin":3,
            "nbtOutput":[1365,2560,3431,4096,4618,5041,5389,5681,5930,6144],
            "nbtPreheatCost":[8,16,24,32,40,48,56,64,72,80],
            "nbtPreheatEnergy":[8192000,16384000,24576000,32768000,40960000,49152000,57344000,65536000,73728000,81920000],
            "nbtPreheatRate":[5460,10240,13724,16384,18472,20164,21556,22724,23720,24576],
            "nbtResistance":6.0,
            "recipeObject":["PwP","BMC","PEP","M","gregtech:gt.multitileentity:17211","B","ore:gt:re-battery1","C","IL:Processor_Crystal_Diamond","E","IL:Electric_Motor_ULV","P","OreDictItemData:plateDenseInvar"],
            "rotorMaterial":"Magnalium",
            "stackSize":16
        }
    ]
}

数据类实现了 initDefault() 方法来对这些机器属性初始化,主要逻辑是给出可选的材料,然后为每个材料生成一种机器属性, 而对于每种材料对应的属性则是在机器属性类中处理

为了方便确定材料的属性,在 main\java\gregtechCH\data 中存储了常见默认材料对应数据,以 MA_ 开头(material attribute) 使用时一定要注意特殊情况特殊处理

文件读写

序列化和反序列化

需要对于 GT6 的某些数据重写序列化和反序列化方法,都存于 main\java\gregtechCH\config\serializer

由于许多数据的性质,只能进行反序列化(deserializer)而不能序列化(serializer), 所以会在数据中专门存储一份 String/String[] 来专门用于默认数据的序列化

进行专门重写了序列化和反序列化方法的数据有:

  • ItemData 存储物品的材料,支持多种材料和对应的数量,对应的类型为OreDictItemData,详见 附加:材料
  • Item 存储物品,对应的类型为 ItemStack,调用了 GT6 的 ST.make() 方法来通过 modID,物品名称,物品的 meta 值来创建 ItemStack,实现了和 CraftTweaker 一致的名称格式的我反序列化
  • Material 存储材料种类,对应的类型为 OreDictMaterial,直接调用 GT6 的 OreDictMaterial.get() 方法获取材料类型
  • RecipeObject 存储合成台的有序合成,对应的类型为 Object[],详见 合成表介绍。其中对于已有的常量的反序列化使用了反射的方法实现

其中 ItemData Item RecipeObject(序列化方法已弃用) 都不提供序列化方法,而是实现给定文本的方式序列化。 为了避免重复编写的问题,并且减少错误,在这些反序列化类中都提供了 get() 方法,使用给定的文本来得到所需的类

写入和读取文件

json 配置文件操作都在类 main\java\gregtechCH\config\ConfigJson_CH 中进行, 这里会通过创建对象的方式初始化材料数据类(以 MA_ 开头),以及需要序列化或反序列化的数据类(以 Data 开头)

为了方便控制配置文件加载的阶段,创建了 preInit() Init() postInit() 三个方法,并且分别在 mod gregtech 对应加载阶段进行调用(最先调用)

和 GT6 处理 cfg 配置文件一致,统一在 preInit 阶段声明需要写入或读取的文件夹

统一在 Init 阶段读取或者写入 JSON 配置文件,为了简化调用,采用的是直接调用数据类(以 Data 开头)的 initJsonFile() 方法, 而具体的读写文件在这个方法内部实现

为了减少重复代码,所有的数据类都继承抽象类 DataJson_CH,规范了数据类必须实现方法 initDefault() 来进行默认数据的生成。 目前没有找到合适的方法,只能让数据类实现方法 setMember() 来将反序列化得到的对象设置成自己的数据,这里稍微使用了一些泛型的知识, setMember() 接受任意种类的反序列化对象,而实际数据类中会将其转换成自己的类,对于每个成员变量分别赋值,大致如下(略去了 initDefault() 的实现):

public class DataMachinesMultiblock_CH  extends DataJson_CH {
    public List<AttributesLargeBoilerTank_CH> LargeBoilerTank = new ArrayList<>();
    public List<AttributesLargeSteamTurbine_CH> LargeSteamTurbine = new ArrayList<>();
    public List<AttributesLargeGasTurbine_CH> LargeGasTurbine = new ArrayList<>();

    @Override
    public void initDefault() {/*...*/}

    protected void setMember(DataMachinesMultiblock_CH aData) {
        this.LargeBoilerTank = aData.LargeBoilerTank;
        this.LargeSteamTurbine = aData.LargeSteamTurbine;
        this.LargeGasTurbine = aData.LargeGasTurbine;
    }
    @Override
    protected <Type extends DataJson_CH> void setMember(Type aData) {
        setMember((DataMachinesMultiblock_CH)aData);
    }
}

实战:将机器数据存入配置文件

(WIP)

汉化文件

为了保证原本汉化文件不需要修改,新添加的文本,需要汉化的都放入文件 gregtechCH.lang

为了实现这个效果,添加了类 LanguageHandler_CH(位于 lang)继承自 GT6 的语言管理类 LanguageHandler,用来将语言文本放入我添加的文件 gregtechCH.lang

其中 translate() 方法保留了原本的方法,也就是和原本的语言管理使用同一套字典,仅仅是将我添加的文本放入另一个文件中,这样可以方便一些调用

添加了 LH_CH 类(位于 data),功能和 GT6 的 LH 类一致,只是将其 add() 等方法改为调用 LanguageHandler_CH 类中的方法,实现将文本添加到文件 gregtechCH.lang 中。 一些常用的文本也会作为静态常量添加到 LH_CH 中(和 LH 类一致)

LH_CH 类中还添加了方法 getNumber(),用于使用 String.format() 方法来给文本插入变量,解决了带变量文本翻译的语序问题

实战:汉化放大镜操作的文本

GT6 使用放大镜的文本是硬编码的,可以将其汉化。直接添加到原版的汉化文件会有版本更新的问题(可能原版汉化的进度更快,我没有跟进) ,所以最好的办法就是将其添加到另外的文件中。

汉化方法十分简单,和多方快机器的 tooltip 一样,在需要汉化的机器类中创建静态代码块:

public class MultiTileEntityBoilerTank {
    /*...*/
    static {
        LH_CH.add("gtch.toolclick.magnifyingglass.boilertank.1", "Calcification:");
        LH_CH.add("gtch.toolclick.magnifyingglass.boilertank.2", "No Calcification in this Boiler");
        LH_CH.add("gtch.toolclick.magnifyingglass.boilertank.3", "WARNING: NO WATER!!!");
    }
    /*...*/
}

用来给文件 gregtechCH.lang 设置默认的文本,由于是默认文本所以需要是英文的,即硬编码的原文

而在需要汉化的地方修改为:

public class MultiTileEntityBoilerTank {
    /*...*/
    public long onToolClick2(/*...*/) {
        if (aTool.equals(TOOL_magnifyingglass)) {
            if (aChatReturn != null) {
                if (mEfficiency < 10000) {
                    aChatReturn.add(LH_CH.get("gtch.toolclick.magnifyingglass.boilertank.1") + " " + LH.percent(10000 - mEfficiency) + "%");
                } else {
                    aChatReturn.add(LH_CH.get("gtch.toolclick.magnifyingglass.boilertank.2"));
                }
                aChatReturn.add(mTanks[0].content(LH_CH.get("gtch.toolclick.magnifyingglass.boilertank.3")));
            }
            return 1;
        }
        return 0;
    }
    /*...*/
}

直接调用 LH_CH.get() 方法即可,启动游戏后则会在 gregtechCH.lang 文件中生成上面的三个条目,修改文件中的条目即可实现汉化

需要注意的细节(NOTE)

由于这个文本读取的机制,汉化末端的空格不会保留,所以上面的 "Calcification:" 后面的空格改为了硬编码的方式

由于我使用的是和原本语言管理公用字典的形式,所以获取文本时调用 LH.get() 方法也可以实现汉化,这里这样做主要是为了代码的一致性

文本标签(key)的命名还是需要有一定的规律,像上面给出的一样,从一般到特殊,会比较有利于汉化

流体

通过让所有 GT6 的机器在输出流体的时候加入一个判断,从而实现了流体容器白名单的效果, 目前实现了只允许 GT6 的管道和一些机器使用能量流体(蒸汽和热冷却液)

实现思路

创建了一个接口 IFluidHandler_CH (位于fluid)继承自 IFluidHandler,添加了一个方法 canFillExtra(), 根据输入的流体种类来判断这种流体是否是可以存入的(额外)流体

而思路就是需要加入白名单的容器就将原本的 IFluidHandler 接口改为 IFluidHandler_CH,实现方法 canFillExtra() 来设置能额外存储那些种类的流体。

修改 GT6 统一的流体管理方法 FL.fill_(),添加检测实体是否继承了接口 IFluidHandler_CH,再以此判断能否存入流体

除此之外,还修改了 TileEntityBase01Root 类中的 drain() 方法,让别的模组尝试抽取 GT6 的容器时,也会进行判断

具体细节

首先需要将不能存储的流体种类加入一个“黑名单”,这里放在了 FL 类中(位于 gregapi\data),方法为 canFillDefault(), 输入流体,根据流体种类返回其是否是默认状况下能存储的(现在如果流体是能量流体则返回 false,否则为 ture,并且加入了配置文件控制)

对于继承了接口 IFluidHandler_CH 的容器,会取 canFillExtra() || canFillDefault() 来作为判断流体能否存储 (即 canFillExtra() 是对 canFillDefault() 的一个扩充)

实战:让一种容器能够存放蒸汽

注意这里是只能存放蒸汽而不包括其他热冷却液

将容器的类中继承的接口 IFluidHandler 改为 IFluidHandler_CH,实现方法 canFillExtra() 如下即可:

public class MultiTileEntityXXX extends TileEntityBase09FacingSingle implements IFluidHandler_CH {
    @Override
    public boolean canFillExtra(FluidStack aFluid) {
        return FL.anysteam(aFluid);
    }
}

多方快部件材质

TODO 近期会进行改动,因此即将过时

原版多方快部件没有每 tick 检测状态更改材质动画,所以多方快机器的动画只能在主方块完成,限制很大

这里修改了多方快机器部件的材质代码,并且保持了不去做每 tick 检测,所以原则上和原本一样高效

实现方法是扩展了原本多方快部件的材质种类变量 mDesign,将小于零的部分用于拓展的自定义材质,目前支持的自定义材质有:

  • TRANSPARENT = -1 完全透明的多方快部件,用于大型坩埚这类多方快机器使用
  • FLUID_EMITTER = -2 流体输出口,与原本流体输出口不同的是其只会在主方块的背面方向绘制
  • ENERGY_EMITTER_RU = -4 RU 输出口,与原本 RU 输出口不同的是其只会在主方块的背面方向绘制(后续会加入根据主方块活动状态切换是否旋转)

由于 mDesign 已经占用了 greg 提供的 getVisualData() 的 1 byte 的大小, 所以额外添加了 getVisualData_CH() 再来提供 1 byte 的大小用于传输图像数据(朝向,是否运行等)

由于没有每 tick 检测图像更新,所以需要进行图像更新时,要在主方块处调用部件方块的 refreshVisual() 进行图像数据的更新

实战:让大型电炉的电线部件在工作时发光

(WIP)

光照修复

由于 mc 原版一个方块 id 只有一种不透光度,而 gt 使用方块的 metadata 将许多方块公用了同一个方块 id,并且其中有一些不同的不透光度。不仅如此,即使是同一个 metadata 也有可能会有不同的不透光度(管道和填充了建筑泡沫的管道)。使用重写方块的 getLightOpacity 方法来实现自定义不透光度。

但是前提是此时实体并没有被卸载,在更新光照时由于往往受影响的区域较大,一般客户端上其余区域的实体应被卸载,导致无法获取被修改过的不透光度,只能采用默认的不透光度(一般为完全透光),导致亮度计算错误(和服务端结果不同),并且引发重新计算光照从而造成卡顿。

而在放置方块时实体一定是被加载的,此时不透光度正确,计算得到的亮度是正确的,并且一直存储在服务端的数据中。 在原版如果存在大量的修改了不透光度的 gt 方块,以下两种情况会造成不同程度的卡顿:

  • 如果服务端存储的光照正确,则客户端只会在接近时因为需要重新计算光照发生一次卡顿。此时客户端看上去这些方块是完全透光的,实际上并不是(为正确的不透光度计算结果)
  • 如果服务端存储的光照不正确(因为各种原因),则在客户端靠近时还会触发服务端的重新计算光照由于许多实体已经被卸载,服务端并不能重新计算得到正确的光照,因此会造成长期持续性的卡顿。并且因为继续改动存储的光照值,导致错误结果不断增加,进一步加大卡顿范围。

这可能是原版 gt 在较多密集机器时会发生卡顿的原因,这里最终确定的修改方案如下:

让 gt 方块能根据 metadata 获取不透光度

0.2.5 之后直接通过 ASM 向 MC 的区块数据中注入了 GT 方块不透光度的数据,目前测试下来应该是完全解决了这个问题。由于使用了 ASM 向原版注入了代码,可能会和其他 mod 存在兼容性问题,如果遇到这个问题可以在 config\gregtech\asm.ini 中关闭 transformer:gregtech.asm.transformers.Minecraft_LightOpacityFix_CH 项来禁用这些注入代码,并把错误报告提交到 issues 或者到 qq 群(752292640)中告诉我。

cfg 配置文件 中依旧提供一个选项 disable_GT_block_lightopacity 来完全关闭 GT 方块的的不透明度,由于 GT 原版的不透光度经常失效,因此这样可以得到和原版一样的不透光度效果。 在 config\gregtech\asm.ini 中关闭不透光度相关的代码注入后 cfg 配置文件中的设置会自动失效,统一变为完全关闭不透光度的结果。

在加载 gt 方块时强制更新不透光度

由于当服务端存储的光照不正确时会造成更加严重的卡顿,这里在加载 gt 方块时会强制更新它的不透光度来保证服务端存储的光照值正确。 目前没有较好的方法检测何时可以不需要更新不透光度,因此对于所有的加载都进行更新,并没有发现明显的性能损失。

更新不透光度的函数位于自用的通用工具类 UT_CH.Light 中,基于原版的 setBlock 函数,对更新已有方块的不透光度的情况进行了一定优化,使用了反射来调用原版的私有方法 relightBlock propagateSkylightOcclusion

方块改变不透光度时强制更新

对于可变不透光度的方块,在改变不透光度时需要调用 UT_CH.Light 中的函数来更新此方块的不透光度。 这里在所有 gt 实体方块的基类 TileEntityBase01Root 中提供了一个方法 updateLightOpacity 来方便子类调用,注意客户端和服务端都需要调用这个方法。

方块加载时的延迟计划调用

由于 gt 放置方块时进行了比较复杂的操作,导致实体在读取 NBT 的阶段,其对应的方块的 metadata 并不是正确的结果。 这里对其使用标记的方法,而后在完成放置之后的 updateEntity 阶段再进行不透光度的更新。

照明度的更新

由于照明度更新时不存在需要周围方块照明度的情况,因此不会出现计算错误,只需要在方块照明度发生改变的时候,调用 UT_CH.Light 中的函数来更新此方块的照明度即可。 这里在所有 gt 实体方块的基类 TileEntityBase01Root 中提供了一个方法 updateLightValue 来方便子类调用,注意客户端和服务端都需要调用这个方法。

常数类

位于 data\CS_CH,主要存储一些额外的常量,包含:

  • 零长度的二位数组
  • 改进染色需要的颜色相关常量
  • 对于 gt 矿物词典 prefix 进行的一些分类,方便判断类型
  • 额外的 NBT 标签,以 gtch. 开头
  • 配置文件名枚举类
  • 其他的通用的枚举类

通用工具类

位于 util\UT_CH,主要存储一些通用的函数,包含:

  • UT_CH.STL 存储一些 java 中没有的容器基本操作,也不一定在 C++ STL 有对应操作
  • UT_CH.Code 存储一些通用的或者暂未分类的算法,包含渲染运算中需要的算法,让效率结果规整的算法,颜色混合需要的一些算法
  • UT_CH.NBT 存储一些额外的 NBT 读写操作,包含为了处理物品 NBT 强制抹去零值的妥协操作,考虑减少占用空间的存储任意数组 NBT 的算法
  • UT_CH.Texture 提供了一些额外的获取材质的接口,主要是更加方便的关闭环境光遮蔽的接口
  • UT_CH.Hack 提供使用反射调用的一些私有成员函数
  • UT_CH.Light 提供了一个更新方块亮度的方法和一个更新方块不透光度的方法