Minecraft(我的世界)中文论坛
标题: Fabric开发文档翻译
作者: 森林蝙蝠 时间: 2019-8-19 11:19
标题: Fabric开发文档翻译
本帖最后由 森林蝙蝠 于 2019-9-28 19:00 编辑
译者序
fabric是asie(foamfix作者)和modmuss50(techreborn作者)共同搞出的一个mod加载器(和forge相同定位),具有轻量事少的特性,以下是它相对于forge的优势:
1.forge还是一体化的,而fabric则是模块化的,默认可以不选择fabric-api而只有fabric-loader而开发,这样就只使用原版轮子,fabric-api也有诸多分包,可以选择性的去进行开发而不用下载一整个api。
2.forge一直都不支持gradle 5+,到了1.13更是限定只能4.9,fabric则无此限制。
3.fabric对高版本的Java和Eclipse OpenJ9支持更好。
4.采用了更加简单的mixin而不是原生asm,更安全,而且减少了开发者的心智负担。
5.yarn的版权是开放的,不需要担心MCP受searge和forge开发组控制。
6.对optifine,bukkit这些别的软件支持较好,这一点比forge的态度好得多。
以下是所需的拓展资源,能帮读者更好地理解1.13以上的MC和MC开发工具机制:
安装开发环境
预准备
步骤- 从 fabric-example-mod (如果你想用kotlin的话,从kotlin模板下载) ,许可证和readme不必下载,因为它跟你的mod没什么关系。
- 编辑gradle.properties:
- 确保archives_base_name和maven_group已经设置为你喜欢的值。
- 确保已经更新了MC,反混淆和fabric loader(译注:fabric loader和FML是一个定位) - 你可以从这个网站根据你的版本,查询到所有你需要的配置。
- 在build.gradle文件里添加所有你需要的依赖项。
- 在IDE中打开build.gradle(译注:idea是“认识”build.gradle文件的,你打开的时候,它会问你是否打开gradle project,点是它就会自动给你导入,但是不会生成源码),以导入工程。
- 在IDE中执行genSources(见右侧gradle-tasks)任务。如果你的IDE没有gradle集成,在命令行中执行./gradlew genSources命令(这时源码才会生成)。
- 如果希望有IDE内运行配置,需要执行如下命令:
- 对IntelliJ IDEA: ./gradlew idea.
- 对Eclipse: ./gradlew eclipse.
- 对VSCode:详见这里。
- 好好开始吧!
准备开始
建议- 虽然Fabric API(译注:相当于除了FML以外的forge本体)并不一定要用,但其初衷是提供统一兼容性,和原版没有的钩子,因此也强烈推荐带上它!
- 因为fabric还在开发早期,偶尔fabricloom会突然出问题,需要手动清除一下gradle缓存(位于.gradle/caches/fabric-loom),问题确定后我们会公布出来。
- 不要害怕提问!风里雨里,我们等你,助你美梦成真。
一些问题
我的世界没有声音?有时候,gradle工程导入到IDE的时候,资源文件可能下载有误,这种情况下,在IDE内部运行downloadAssets任务,或者命令行执行./gradlew downloadAssets就可以了。
Notch的客户端和服务器(还有启动器)会生成额外的日志信息,正常都会显示出来,其中一些对调试大有帮助,可以靠自定义log4j配置做到。 原生启动器提供了一个“log配置”部分可以直接办到,其他情况需要给游戏加一个-Dlog4j.configurationFile=fullpathtoconfigfile.xml参数:
使用原生启动器时,使用log配置的xml变种,并检查“自定义log配置输出的xml文件”,这能让启动器生成更清楚的信息,如果是Java启动器或者是运行在服务器上时,xml变种就不用了——你不会喜欢控制台出现xml,而且xml也不会出现在服务器的gui上。
警告:如果用-Dlog4j.configurationFile指定的配置项无法找到,那么就会悄悄地用默认的代替。
可以从你想处理的jar中提取出log4j2.xml文件获取默认的游戏配置。
选项
log4j的配置项中只有几个选项有有用的行为,主要的一个是<Root>中的level(日志级别),正常情况下是info等级,但是也接受trace,debug,info,warn,error,all和off这几个等级(不难看出,越往后消息越严重),在MC最低的等级其实是debug,所以debug应该会占用最多的信息。
另一个主要选项是<filter>的清单,默认情况下,游戏使用<filter>过滤掉有关包(Packet)的信息,拦阻了所有标记为“NETWORK_PACKETS"的日志信息,不过你可以反着来,让过滤器(filter)只接受NETWORK_PACKETS,并拒止其他一切消息,或者可以完全移除<filter>,这样所有的信息都学会显示,除了NETWORK_PACKETS之外,还有几个别的日志标签:SOUNDS, NETWORK, PACKET_SENT, 和PACKET_RECEIVED。NETWORK和NETWORK_PACKETS作用相同;PACKET_SENT和PACKET_RECEIVED可用来查看仅一组包;SOUND可以在声道开始和结束时发出信息(大多数情况下只在讨论声音为什么听上去响两次一样的bug时有效)。
还有一件要注意的是,默认情况下,log4j会在一天出现超过7次日志时,覆盖掉老日志(看MC-100524)。也可以往<RollingRandomAccessFile>里面加一个<DefaultRolloverStrategy max="1000"/>,使一天的最大日志文件数量是1000(很难达到这个数量了,1000用于更大的东西上,因为巨大的数字会导致性能损失)。
示例配置
fabric限定的改动
(注意:最近的Loom 0.2.5有改动)
用于fabric的重要改动,已经用绿色高亮了。 如果你用fabricloom 0.2.5,remapJar.out应该换成remapJar。
按顺序介绍:
- afterEvaluate { ... } - 审核(Evaluate)之后,fabricloom的remapJar就会启动,这样remapJar.output就只能读取。
- mainArtifact(remapJar) (或者loom0.2.4之前的 mainArtifact(remapJar.output)) - 提交到curseforge的mod实例应为remapJar任务的输出, 也就是重新混淆的(可用于玩家安装的)mod jar文件。
- uploadTask.dependsOn(remapJar) - 确保curseforge只在remapJar重新混淆任务完成后,才进行上传任务。
- forgeGradleIntegration = false - 因为你用的不是forgegradle,所以这个联动被设定为false。
在开始用fabric写mod之前,有必要了解一下以后的章节需要用到的术语和常见短语,知道modid命名和包结构的基本术语也有好处,了解这些会帮你更好地理解教程,需要的时候提出更好的问题。
Mod ID(mod标识符)
纵览全篇,经常会引用到一个modid,modid代表着“Mod Identifier(mod标识符)”,是一个唯一标识你的mod的字符串,modid一般和同名的标志性命名空间(包名)关联,遵循同样的限制。modid只能含有小写字符a-z,数字0-9,以及下划线和短横线。例如Minecraft(作为一个mod而存在时)就是用了minecraft包名,另外,一个modid必须包括至少两个字符(“a”这种是不合法的)。
modid经常是mod名的缩写,但是需要有标志性,避免命名冲突,惯例上说,一个叫“My Project”的工程,可以叫做myproject, my_project,有时候my-project也可以,但是短线在处理的时候会有点麻烦[来源请求],mod将使用modid作为注册命名空间,来注册物品和方块。
一些初学者教程,会使用占位符modid,然后在占位符命名空间下注册物品和方块,可以看作是个初学者模板——虽然不改占位符不危害调试,但是发布的时候记得改过来。
Tags还在草案中。
Maven组名和包名
按照Oracle的Java文档,全小写以避免和类/接口名的冲突(译注:因为在Java里面,类名是大写驼峰的),你的域名倒过来(user.com->com.user)就可以用作包名,可以在这篇文档里了解更多。
side(端)
MC用的是C/S(客户端/服务器)模型,玩家安装客户端,连接到服务端玩游戏,fabric允许用户面向客户端或服务端其中之一或两者皆有进行编程。
以前服务器和客户端有一个简单的分离,但是随着玩家切换到内置的服务端,这种简单的模型就不合适了,因此我们区分客户端和服务端时,有两个角度,那就是物理端和逻辑端。
客户机和服务器两头都有一个“服务器”和一个“客户端”,不过逻辑客户端不等同于物理客户端,同样逻辑服务端也不等同于物理服务端,逻辑客户端寄宿于物理客户端,服务端同理。
逻辑端才是Minecraft的客户端和服务端的架构核心,所以,逻辑端的理解对任何fabric mod的开发都至关重要。
物理端物理端指代的是MC的两个jar,client.jar(原版启动器运行的那个)和server.jar(可以从minecraft.net下载),也即当前环境中可用的代码。客户端和服务端环境则是同一个程序的最小分发副本,只包括了代码中需要的部分。
在fabric中,经常会看到@Environment(EnvType.CLIENT)这样的注解(译注:类似于forge的@SideOnly),表示这些代码只在一个环境中运行,比方说客户端。
在fabric的fabric.mod.json和mixin配置文件中,client/server指代的都是环境,每个物理端都有入点(entry point)类net.minecraft.data.Main,这个入点被其他类和数据生成器(data generator)使用。
逻辑端逻辑端和游戏逻辑有关,逻辑客户端负责渲染,将玩家输入发送给服务器,处理资源包,部分模拟游戏世界,逻辑服务端责处理游戏核心逻辑,数据包,并维护游戏世界的真实状态。
客户端维护服务器世界的一部分副本,还带有一些对象的副本,比方说:
- net.minecraft.world.World
- net.minecraft.entity.Entity
- net.minecraft.block.entity.BlockEntity</font>
复制代码
这些复制出来的对象允许客户端和服务端执行一些共同的游戏逻辑,客户端可以和这些对象交互,服务端则负责保持它们同步,一般来说,要区分逻辑客户端和逻辑服务器上的两个相同对象,需要访问对象所在的世界,然后检查其“isClient”字段,之后可以用于在服务端上进行授权的操作,比如生成实体,模拟客户端上的动作。这一技术对避免两个逻辑端上的不同步很重要。
我们需要再深入些!
明白了有什么端,以及如何区分它们之后,可以深入细致地了解一下了。
物理客户端刚才说过,物理客户端就是启动器下载的那个minecraft.jar,它包含了一个逻辑客户端和逻辑服务端(内置),入点是net.minecraft.client.main.Main。虽然物理客户端能在几个分立的逻辑服务器上加载几个世界,但是一次只能运行一个,相对于物理服务端(minecraft server.jar)的逻辑服务端,物理客户端的逻辑服务端(内置服务端)会被物理客户端的逻辑客户端控制(比如,F3+T会重载数据包,关闭客户端和内置服务端),也会在物理客户端重载绑定到逻辑客户端世界的资源包。所有的逻辑客户端内容都是仅限于物理客户端的,所以你会在渲染,声音和其他逻辑客户端代码看到@Environment注解。一些mod只面向物理客户端,比如Liteloader, Optifine,和Minecraft PvP客户端(Badlion, Hyperium之类)。
物理服务端物理服务端就是专门的minecraft-server.jar,相对于物理客户端,它只有一个逻辑服务端(dedicated server),其入点是net.minecraft.server.MinecraftServer。物理服务端在运行时只能有一个世界,如果需要切换世界,就得重启。
其逻辑服务端和物理客户端的逻辑服务端没有太大差别,因为物理服务端运行时,只有一个逻辑服务端实例。此外,物理服务端的逻辑服务端会被Rcon远程控制,有一个叫server.properties的配置文件,还可以发送服务器资源包。
尽管有这些个不同,大多数mod仍然可以用于物理客户端/服务端的逻辑服务端,只要它们不引用逻辑客户端的内容(然后@Environment的重要性凸显出来了)。
物理服务端单世界和资源包发送的特性使原版mod(数据包和资源包的混合体)的安装比在客户端上简单得多,因为原版物理客户端自动连接到服务端时才会被设置。
一些mod只适用于物理服务端,比方说bukkit和其衍生物(Spigot, Paper, Cauldron, Xxx-Bukkit hybrids)。
逻辑客户端逻辑客户端是玩家的接口,渲染(LWJGL负责),资源包,玩家输入处理,声音,都发生在逻辑客户端上,这东西不会出现在物理服务端中。
逻辑服务端逻辑服务端是大多数游戏逻辑执行的地方,数据包,世界更新,BlockEntity(也就是箱子和熔炉之类)和实体的tick计时,生物AI,游戏/世界保存,世界生成,都发生在逻辑服务端上。
物理客户端的逻辑服务端叫做“内置服务端(Integrated Server)”,物理服务端的逻辑服务端则叫做“专用服务端(Dedicated Server)”(也即物理服务端自己的名字)。
逻辑服务端运行在主线程上,还有几个工作线程。逻辑服务端的生命周期取决于其寄宿的物理服务端,在物理服务端上,只要它的进程还在运行,逻辑服务端就一直在运行。但是在物理客户端上,可能有几个逻辑服务端(对应着几个世界),不过一段时间只会有一个逻辑服务端运行,一个逻辑服务端伴随着玩家的加载和关闭世界,而新建和关闭。
大多数的mod都是面向逻辑服务器的,所以它们可以在单人或者多人情况下同时运行。
通信客户端和服务端唯一交换数据的方式就是发包。包(Packet,这里有讲)在逻辑端之间发送,而不是物理端,mod可以添加包,传输自定义的信息。如果是物理客户端,那么包会在内存中,在逻辑客户端和内置服务端之间交互,否则就会通过网络协议交换。 逻辑客户端发送C2S(Client to Server,2是to的谐音,你还会在很多地方看到用2代替to的)包到逻辑服务端,反之逻辑服务器则会发送S2C(Server-To-Client) 包到客户端,包会在网络线程,通过一个写入(write)方法发送,同一个网络线程通过一个读取(read)方法接收,查看更多网络处理细节,请看[链接被删除哈哈哈]。
逻辑服务端的杂项大多数情况下,面向物理服务端的mod,也可以工作在物理客户端的逻辑服务端上,不过,面向物理服务端的开发者一般会假设不用于内置服务端的情况,包括但不限于:
- 游戏运行时只有一个逻辑服务端存在
- 世界和实体总在计算游戏逻辑(换句话说,world对象的isClient字段被写死成false)
- 远程控制,资源包发送,提供图标
这些假设需要进行修正,使mod可以运行在逻辑服务端上。
结论
| 逻辑客户端 | 逻辑服务端 |
物理客户端 | 单例 始终存在 | 存档加载时存在 |
逻辑服务端 | 没有 | 始终存在 |
这是一些第三方类库,提供了有用的工具。
重启MC要花很多时间,不过还好,有工具让你可以在游戏运行时作出修改。
重载修改过的类
在eclipse或者idea中,用调试模式启动MC,应用代码中的修改只需要执行idea里的run → reload changes classes或者eclipse里的save。
注意:这只允许重载某个方法体,如果改了别的地方(字段,类等),就得重启了。不过可以用
DCEVM(适用于OpenJDK 7-11,OracleJDK 7/8)进行大多数改动的重载,包括方法和类的移除。
重载材质
如果想重载材质(.png),可以先重载修改过的类,然后按F3+T就可以重置材质。
重载合成和战利品表
可以先重载修改过的类,然后用MC的/reload指令重载任何你在data路径下的修改。
你的物品现在有个奇怪的名字,比如item.tutorial.my_item对吧?那是因为你的物品名还没有本地化。
新建语言文件
就像搞过汉化的人所做的那样,可以添加语言文件来为游戏内可翻译的字符串进行翻译,首先要起一个正确的lang文件名字(如中文是zh_cn),详见
wiki,英文则是en_us。然后在resources/assets/modid/lang/下面新建一个json文件(而不是txt文件),一个完整的英语文件应该是resources/assets/tutorial/lang/en_us.json.
添加翻译
建立了lang文件之后,可以按照这个基本的模板添加翻译:
- { "item.tutorial.my_item": "My Item", "item.tutorial.my_awesome.item": "My Awesome Item", [...]}
复制代码
前一个字符串(比如上面的"item.tutorial.my_item")可以是任何可以翻译的东西(不管是物品名还是TranslatableText),如果你在按照这个教程做,记得把modid重命名为“tutorial”,或者任何你喜欢的modid。
使用自定义可翻译文本
如果一个函数需要接受文本参数,你可能会传给它一个LiteralText对象,这样MC会按原样使用构造函数里的参数,不过这样并不可取,因为很难翻译成其他语言,所以你应该传给函数一个带有翻译条目(translation key)的TranslatableText(可翻译文本)对象,然后在lang文件中翻译那个条目(所对应的内容),比如说,添加一个物品信息(tooltip),可以这样:
- @Override
- public void appendTooltip(ItemStack itemStack, World world, List<Text> tooltip, TooltipContext tooltipContext) {
- tooltip.add(new TranslatableText("item.tutorial.fabric_item.tooltip"));
- }
复制代码然后语言文件可以这么写:
- {
- "item.tutorial.fabric_item.tooltip": "My Tooltip"
- }
复制代码
这样物品信息就会显示成你翻译的 “My Tooltip” !
为TranslatableText添加动态值
有时候你希望文本根据某个变量(比如当前日期)进行变化,对动态数字而言,可以往希望出现数字的地方,放入一个%d符号,例如:
- {
- "item.tutorial.fabric_item.tooltip": "My Tooltip in day %d, and month %d"
- }
复制代码
然后就可以根据符号的顺序,传入想要的值,注意日在月的前头;
- int currentDay = 4;
- int currentMonth = 7;
- tooltip.add(new TranslatableText("item.tutorial.fabric_item.tooltip", currentDay, currentMonth));
复制代码
然后物品信息就会显示成 “My Tooltip in day 4, and month 7”. 如果要传入字符串, 就要用 %s 而不是 %d做占位符,如果想显示%, 就用 %%. 更多信息详查
Java String.format类。
换行
对Mojang来讲,换行符'\n'能工作太难了,所以为了让你的文本能换行,要把翻译条目分离成多个:
- {
- "item.tutorial.fabric_item.tooltip_1": "Line 1 of my tooltip"
- "item.tutorial.fabric_item.tooltip_2": "Line 2 of my tooltip"
- }
复制代码
然后各自添加TranslatableText的每一部分:
- tooltip.add(new TranslatableText("item.tutorial.fabric_item.tooltip_1"));
- tooltip.add(new TranslatableText("item.tutorial.fabric_item.tooltip_2"));
复制代码
然后物品信息就会显示成:
- Line 1 of my tooltip
- Line 2 of my tooltip
复制代码
翻译条目格式注册的翻译条目格式是<object-type>.<modid>.<registry-id>这样:
游戏元素类型 | 格式 | 实例 |
方块 | block.<modid>.<registry-id> | "block.tutorial.example_block": "Example Block" |
物品 | item.<modid>.<registry-id> | "item.tutorial.my_item": "My Item" |
创造标签栏 | itemGroup.<modid>.<registry-id> | "itemGroup.tutorial.my_group": "My Group" |
流体 | fluid.<modid>.<registry-id> |
声音事件 | sound_event.<modid>.<registry-id> |
效果 | mob_effect.<modid>.<registry-id> |
附魔 | enchantment.<modid>.<registry-id> |
实体类型 | entity_type.<modid>.<registry-id> |
药水 | potion.<modid>.<registry-id> |
群系 | biome.<modid>.<registry-id> |
不在这张表的类型,可以查看net.minecraft.util.registry.Registry类(译注:这是原版使用的注册方式,但是大家似乎只知道forge的GameRegistry和RegistryEvent)。
添加基础的合成(其实跟原版合成一样)
在干这个之前,先确定你已经添加了相应的物品,到现在为止,我们添加的物品只能从命令和创造栏中获取,要让生存玩家也能获取,需要给物品添加一个合成表。
在resources/data/tutorial/recipes下新建一个fabric_item.json文件(如果是自己的mod,需要把tutorial换成你想要的modid),下面是fabric_item的一个示例:
- {
- "type": "minecraft:crafting_shaped",
- "pattern": [
- "WWW",
- "WR ",
- "WWW"
- ],
- "key": {
- "W": {
- "tag": "minecraft:logs"
- },
- "R": {
- "item": "minecraft:redstone"
- }
- },
- "result": {
- "item": "tutorial:fabric_item",
- "count": 4
- }
- }
复制代码
这个合成json的关键点:
- type: 代表一个有序工作台合成。
- result: 这个合成的产物是4个tutorial:fabric_item物品,count字段则是可选的,如果没有指定count,默认则是1.
- pattern: 一个“模板”代表了合成表的形式,每个字母代表了一个物品,空白区域代表那个格子没有物品,至于字母代表的物品是什么,在key中定义。
- key:pattern中的字母代表的物品,在这里W代表了任何具有minecraft:logs标签的物品(所有的木头),R则代表了红石这个物品,如果想了解更多标签的事情,请看这里。
总之,合成表看上去就是这样:
4个fabric_item的合成 |
任何原木 | 任何原木 | 任何原木 |
任何原木 | 红石 | 空 |
任何原木 | 任何原木 | 任何原木 |
添加自定义合成 type的值可以换成一个自定义的合成类型(区别于原版的合成),还没写。
反混淆表(Mappings)定义了类,字段和方法的名称,一般的fabric构建使用yarn反混淆(相当于mcp name)和intermediary(中间名)反混淆(相当于forge的srg name),由社区提供命名。反混淆的需求来自MC本身的混淆,不得不说有很多挑战。重混淆(Remapping)是把编译后的类或者源码重新混淆成MC的形式,不同(包括不同版本)的反混淆表会导致名字的不一样。
yarn是fabricloom的默认反混淆,靠接受社区贡献推陈出新。fabricloom的反混淆表由build.gradle的mappings依赖配置项指定,也可以通过更新依赖项而更新。MC本身和带有mod-argument的配置项(例如modCompile,相当于gradle自带的compile项加了个mod)的其他依赖,在运行时会被重混淆。还没有在yarn中反混淆的类,方法和字段名会被中间名(像class_1234, method_1234 ,field_1234)这种替代。
- dependencies { [...] mappings "net.fabricmc:yarn:${project.yarn_mappings}"}
复制代码
给定版本的反混淆表,其类名,方法名和字段名以及mod带来的修改都是可预期的,你的代码可能要按照修改后的名字更新。不过这个过程是半自动的,你可以运行genSources命令,使用更新后的反混淆表访问MC源码。
fabric的remapJar任务可以生成mod成品,是一个使用了intermediary名的构建后jar(里面是class文件),还有,如果有sourcesJar任务,那么remapSourcesJar任务,会生成一个使用了中间名的源码jar(里面是java文件),这些jar文件既可以用作mod,也可以放在开发环境当中,使用modCompile依赖配置项进行依赖。
- jar任务输出的“dev”jar文件没有被重混淆,所以没用,不能用做mod,只能以相符的反混淆表在开发环境工作,实际场合应该使用remapJar来构建mod本身,以及使用modCompile之类来引入依赖。
- yarn反混淆名只用于开发环境。在开发环境之外,就只有中间名存在,也就是说你(反编译mod)看到的和你写的代码并不一样,很显然loom会帮你进行yarn和中间名的转换,但是如果用到反射,就要谨慎。
重混淆
重混淆是反混淆的逆过程,Java源代码和编译后的Java码都可以重混淆,这涉及到根据反混淆表更改引用的名称,谨慎重命名方法以保留对父类方法的覆写,这不会更改代码的功能,不过会影响使用反射的名字。
TinyRemapper是个能重混淆Java代码的工具,具有命令行接口(CLI)和编程接口。fabricloom用它完成一些任务,fabricloader则使用它将MC代码重混淆成intermediary。fabricloom也可以重混淆Java源码。
混淆与反混淆
MC Java版都是混淆过的,也即它们是编译后的二进制文件,抹去了所有有用的命名信息,只剩下运行逻辑,混淆的理由是避免逆向工程,减少文件尺寸。像MC这样的Java程序,反编译是比较简单的,但是混淆抹去了很多mod开发用到的信息,你很可能想搞明白如何给MC做开发。
像yarn这样的反混淆表,给MC提供了有意义的命名,这样你才能知道MC源码,为其编写mod。反混淆表可以为类,字段,方法,参数,和局部变量提供名称,很显然反混淆表是不完美的,反混淆整个MC很多靠猜,可能不完整,有时候会变成更准确的名字。
Intermediary(中间名)
MC的混淆有个特点,在版本之间混淆结果不一定一样,一个类在MC的某个版本可能叫abc,换了个版本就叫abd了,对字段和方法名也是一样,这种差异导致了MC不同版本之间的不兼容。
Java代码可能编译成一个版本的类库后,下一个版本还能用,也就是说这个类库的版本是兼容的。简单来说,类库至少暴露出相同命名的类,方法和字段,才能保证兼容性,而MC混淆后造成的这种版本间分歧,使其很难作为mod类库。
中间名定义了MC版本间的稳定名称,目的是代码可以一直引用到相同的类,字段和方法,不同于yarn名,中间名是不可读的,有个模板,类似于class_1234,method_1234,field_1234这种。
作为稳定的反混淆表,中间名可以使MC二进制文件跨版本(指快照版)兼容。不过版本间,只有那些没改过的部分才能确保兼容性。在游戏启动之前(开发环境之外),fabric loader通过重混淆MC(以及领域服客户端),提供了一个中间名环境。可以从安装了fabric loader的游戏崩溃报告中看到这一点,其中有中间名。用中间名编译的Mod,因为应用于fabricloom,自然与此环境兼容。
更新yarn反混淆表
需要- Fabric-Loom 0.2.2或更高版本
- Java代码(kotlin和scala还没有)
引导- 写出你想用的yarn版本,比如“net.fabricmc:yarn:1.14.1 Pre-Release 2+build.2”。
- 确定这个版本的yarn表已经在本地创建。目前唯一的办法是在build.gradle文件里将”minecraft“和”mappings“字段改成新版本,然后执行任何gradle命令(反正都会崩溃,在此之前yarn表会下载下来),然后再把字段改回去。(译注:这段话把我搞得云里雾里,因为在idea中,启用了gradle的auto import后,idea就会自动为你下载yarn表,或者自动执行你的其他改动)
- 运行这段魔法一样的命令: gradle migrateMappings -PtargetMappingsArtifact="net.fabricmc:yarn:1.14.1 Pre-Release 2+build.2" -PinputDir=src/main/java -PoutputDir=remappedSrc,在这里:
- “targetMappingsArtifact” 指代目标反混淆表的版本,重要的是,当运行此命令时,build.gradle必须是你现在的(而不是你要换的)yarn版本!
- “inputDir” 是输入路径,包含Java源代码。
- “outputDir” 是输出路径,如果没有会重建。
- 如果一切正常,将修改完毕的代码复制到输入路径。
注意:如果弹出“file not found(文件未找到)”问题,尝试使用完整路径。
译注:这一段的目的不是单纯地修改build.gradle下载一个新的yarn而已,而是把你的所有代码都自动更新到新版yarn名,让开发者不必手动变更。
添加基础的物品是写mod的初级工作之一,你需要创建一个Item(物品)对象,注册它并给它一个材质。如果要向它添加额外的用处/功能/特性,你需要自定义item类。在本教程和接下来的教程中,用tutorials命名空间占位,或者把它换成你想要的modid。
注册物品
首先,创建一个物品,放在类的顶部初始化。构造函数接受Item.Settings对象,该对象用于设置项目属性,例如类型,耐久,堆叠数(比如泥土能叠64个)。
- public class ExampleMod implements ModInitializer
- {
- // 创建新物品的实例
- public static final Item FABRIC_ITEM = new Item(new Item.Settings().group(ItemGroup.MISC));
- [...]
- }
复制代码
这里使用原版注册方式来注册,基本语法是Registry.register(Registry Type, Identifier, Content),Type参数指Registry类型中预定义的静态字段,Identifier指资源文件位置(等同于forge的ResourceLocation)。Content则是你定义的实例(FABRIC_ITEM),可以在任何地方调用它。
- public class ExampleMod implements ModInitializer
- {
- // 创建新item的实例
- public static final Item FABRIC_ITEM = new Item(new Item.Settings().group(ItemGroup.MISC));
-
- @Override
- public void onInitialize()
- {
- Registry.register(Registry.ITEM, new Identifier("tutorial", "fabric_item"), FABRIC_ITEM);
- }
- }
复制代码
现在新物品已添加完毕,运行“runClient” gradle任务以查看它的运行情况。
添加物品材质
为物品注册材质需要json模型文件和材质图片。你需要把这些添加到资源目录中。如:
物品模型:... / resources / assets / tutorial / models / item / fabric_item.json
物品材质:... / resources / assets / tutorial / textures / item / fabric_item.png
如果物品被正确注册,那么在缺少材质的时候,会这么提醒你:
[Server-Worker-1/WARN]: Unable to load model: 'tutorial:fabric_item#inventory' referenced from: tutorial:fabric_item#inventory: java.io.FileNotFoundException: tutorial:models/item/fabric_item.json
这可以方便的告诉你,游戏找材质的位置。(如有疑问,请看日志)。
物品json基础模板:
- {
- “parent” : “item / generated” ,
- “textures” : {
- “layer0” : “tutorial:item / fabric_item”
- }
- }
复制代码
parent会更改物品在手里的渲染方式和在背包里的渲染方式,item/handheld用于那些需要从材质左下角(工具的柄-如剑)握住的工具的材质,textures / layer0是图像文件的位置。
物品添加完材质的最终结果:
创建item类
要为物品添加功能/用处/特性,需要创建一个item类,其默认构造函数需要Item.Settings对象。
- public class FabricItem extends Item
- {
- public FabricItem(Settings settings)
- {
- super(settings);
- }
- }
复制代码
实例-当你点击时播放声音:
- public class FabricItem extends Item
- {
- public FabricItem(Settings settings)
- {
- super(settings);
- }
-
- @Override
- public TypedActionResult<ItemStack> use(World world, PlayerEntity playerEntity, Hand hand)
- {
- playerEntity.playSound(SoundEvents.BLOCK_WOOL_BREAK, 1.0F, 1.0F);
- return new TypedActionResult<>(ActionResult.SUCCESS, playerEntity.getStackInHand(hand));
- }
- }
复制代码
实例-将原来的item对象变成新Item类的对象:
- public class ExampleMod implements ModInitializer
- {
- // 创建新item的实例
- public static final FabricItem FABRIC_ITEM = new FabricItem(new Item.Settings().group(ItemGroup.MISC));
- [...]
- }
复制代码
如果以上操作均正确,那么使用该物品就能发出声音。
那么要是想更改物品堆叠数量怎么办?使用maxCount(int size)来设置最大堆叠数。请注意,如果你的物品是有耐久的(及耐久归零后会被破坏),那么此物品无法设置最大堆叠数,否则游戏将抛出RuntimeException。
- public class ExampleMod implements ModInitializer
- {
- // 创建新item的实例,其中最大堆叠数是16
- public static final FabricItem FABRIC_ITEM = new FabricItem(new Item.Settings().
- group(ItemGroup.MISC).maxCount(16));
- [...]
- }
复制代码
创建一个简单的创造标签栏
使用FabricItemGroupBuilder来创建创造标签栏,来让它们正常显示在创造栏中:
- public class ExampleMod implements ModInitializer
- {
- // ...
- public static final ItemGroup ITEM_GROUP = FabricItemGroupBuilder.build(
- new Identifier("tutorial", "general"),
- () -> new ItemStack(Blocks.COBBLESTONE));
-
- public static final ItemGroup OTHER_GROUP = FabricItemGroupBuilder.create(
- new Identifier("tutorial", "other"))
- .icon(() -> new ItemStack(Items.BOWL))
- .build();
- // ...
- }
复制代码
一旦FabricItemGroupBuilder.build()被调用,该标签栏就将添加在创造栏的列表中,确保传入Identifier构造函数里的参数已经替换成你的modid,和这个创造标签栏指定的本地化翻译键。
注:请记住,传递给Identifier构造函数的参数只允许小写和下划线来命名。namespace&path这两个参数可以包含小写字母,数字,下划线,句点或短划线[a-z0-9_.-]。第二个参数the path也可以包含斜杠[a-z0-9/._-]。请避免使用其他符号,否则将会抛出InvalidIdentifierException!
将物品添加到相应标签栏
创建自定义的物品时,需要调用Item.Settings.itemGroup(),将物品设置为显示在该标签栏中:
- public static final Item YOUR_ITEM = new Item(new Item.Settings().itemGroup(ExampleMod.ITEM_GROUP));
复制代码
使标签栏按特定的顺序显示物品
调用FabricItemGroupBuilder.appendItems()并传入Consumer<List<Itemstack>>,然后按顺序把物品加到物品组里。可以用ItemStack.EMPTY来给标签栏添加空格。
- public class ExampleMod implements ModInitializer
- {
- // ...
- public static final ItemGroup ITEM_GROUP = FabricItemGroupBuilder.build(
- new Identifier("tutorial", "general"),
- () -> new ItemStack(Blocks.COBBLESTONE));
-
- public static final ItemGroup OTHER_GROUP = FabricItemGroupBuilder.create(
- new Identifier("tutorial", "other"))
- .icon(() -> new ItemStack(Items.BOWL))
- .appendItems(stacks ->
- {
- stacks.add(new ItemStack(Blocks.BONE_BLOCK));
- stacks.add(new ItemStack(Items.APPLE));
- stacks.add(PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.WATER));
- stacks.add(ItemStack.EMPTY);
- stacks.add(new ItemStack(Items.IRON_SHOVEL));
- })
- .build();
- // ...
- }
复制代码
添加物品信息
在你的item类中,像这样把appendTooltip重写(请参阅lang来了解如何翻译物品信息):
- @Override
- public void appendTooltip(ItemStack itemStack, World world, List<Text> tooltip, TooltipContext tooltipContext) {
- tooltip.add(new TranslatableText("item.tutorial.fabric_item.tooltip"));
- }
复制代码
对于方块,执行相同的操作,但是要在block类中重写的是buildTooltip方法。
要添加方块,需要注册Block类的实例,如果要更灵活地设置方块,则可以自定义Block类,还要考虑添加方块模型。
创建方块
首先,在mod主类中创建一个Block实例。Block的构造函数使用FabricBlockSettings来设置方块的基本属性,如硬度和抗爆性:
- public class ExampleMod implements ModInitializer
- {
- // an instance of our new block
- public static final Block EXAMPLE_BLOCK = new Block(FabricBlockSettings.of(Material.METAL).build());
- [...]
- }
复制代码
注册方块
注册方块与注册物品相同,调用Registry.register并传入适当的参数。
- public class ExampleMod implements ModInitializer
- {
- // block creation
- […]
-
- @Override
- public void onInitialize()
- {
- Registry.register(Registry.BLOCK, new Identifier("tutorial", "example_block"), EXAMPLE_BLOCK);
- }
- }
复制代码
没注册ItemBlock的话,方块不能从创造栏取出,但是可以通过输入指令/setblock ~ ~ ~ tutorial:example_block在游戏中看到。
注册一个方块物品(ItemBlock)
绝大多数情况下,都是把方块所对应的物品放到世界中来放置方块的,这样的话,就需要在物品注册中,注册一个相关的方块物品,可以用Registry.Item来注册。这个“物品”的注册名应该和方块本身的注册名相同。
- public class ExampleMod implements ModInitializer
- {
- // block creation
- […]
-
- @Override
- public void onInitialize()
- {
- // block registration
- [...]
-
- Registry.register(Registry.ITEM, new Identifier("tutorial", "example_block"), new BlockItem(EXAMPLE_BLOCK, new Item.Settings().group(ItemGroup.MISC)));
- }
- }
复制代码
给方块一个模型
可能你已经注意到了,没有模型的方块在游戏中是个紫黑棋盘格子形式。给方块加模型比给物品加模型稍难点,需要三个文件:Blockstate(方块状态)文件,block model(方块模型)文件,如果这个方块有方块物品,还要有个item model(物品模型)文件,如果你不用原版材质的话,还需要Texture(译注:材质图片,其实正经的应该叫纹理)文件,文件应该放在这些位置:
- Blockstate: src/main/resources/assets/tutorial/blockstates/example_block.json
- Block Model: src/main/resources/assets/tutorial/models/block/example_block.json
- Item Model: src/main/resources/assets/tutorial/models/item/example_block.json
- Block Texture: src/main/resources/assets/tutorial/textures/block/example_block.png
复制代码
方块状态文件决定了方块在不同状态下(blockstate)的模型,因为我们的方块只有一个状态,文件就会像这样简单:
- {
- "variants": {
- "": { "model": "tutorial:block/example_block" }
- }
- }
复制代码
方块模型文件定义了形状和材质,在这里使用cube_all,让方块的所有面简单地使用同一个材质。
- {
- "parent": "block/cube_all",
- "textures": {
- "all": "tutorial:block/example_block"
- }
- }
复制代码
大多数情况下,方块和手里拿着的方块看上去一样,这样从方块模型文件派生出一个物品模型文件就可以了:
- {
- "parent": "tutorial:block/example_block"
- }
复制代码
(为你的方块画一个texture.png)现在打开MC,方块已经有材质了!
添加战利品表破坏方块的时候,方块必须有个战利品表来指定掉落物,假设你为方块注册了一个物品,而且和方块有一样的注册名,那么这个文件(src/main/resources/data/wikitut/loot_tables/blocks/example_block.json)会指定产出掉落物。
- "type": "minecraft:block",
- "pools": [
- {
- "rolls": 1,
- "entries": [
- {
- "type": "minecraft:item",
- "name": "tutorial:example_block"
- }
- ],
- "conditions": [
- {
- "condition": "minecraft:survives_explosion"
- }
- ]
- }
- ]
- }
复制代码
在生存模式下破坏这个方块,方块就会掉个物品。
创建方块类干得不错,创造了一个简单的方块,但是如果你想要个有独特机制的特殊方块,就需要建立一个Block的子类,其构造函数引入了一个BlockSettings参数。
- public class ExampleBlock extends Block
- {
- public ExampleBlock(Settings settings)
- {
- super(settings);
- }
- }
复制代码
就像在物品教程里做的那样,方块类的方法也可以重写以自定义功能,比方说你想让方块变得透明:
- @Environment(EnvType.CLIENT)
- public BlockRenderLayer getRenderLayer() {
- return BlockRenderLayer.TRANSLUCENT;
- }
复制代码
注册的时候,把new Block换成new ExampleBlock,来将方块添加到游戏中。
- public class ExampleMod implements ModInitializer
- {
- // 新物品的实例
- public static final ExampleBlock EXAMPLE_BLOCK = new ExampleBlock(Block.Settings.of(Material.STONE));
- [...]
- }
复制代码
添加方块状态
MC的每种方块,其实都是相应的Block类的单例,所以简单地改写方块实例的状态,来指定世界中某个方块的状态是不可能的,因为这一类型的其他方块也会受到影响!不过如果你想给某个方块一个单一的状态,使其会在某些条件下发生变化呢?这就是BlockState类所做的事情了,比方说我们想要个正常硬度为0.5的方块,但是如果在破坏之前右击它,它就会变得更硬,硬度是2.
首先定义一个方块的布尔属性(BooleanProperty),也就是这个方块是不是硬的:
- public class MyBlock extends Block {
- public static final BooleanProperty MyBlockIsHard = BooleanProperty.of("is_hard");
- }
复制代码
重写appendProperties方法注册这个属性:
- public class MyBlock extends Block {
- [...]
- @Override
- protected void appendProperties(StateFactory.Builder<Block, BlockState> stateFactory) {
- stateFactory.add(MyBlockIsHard);
- }
- }
复制代码
然后在方块类的构造函数中指定属性的默认状态:
- public class MyBlock extends Block {
- [...]
- public MyBlock(Settings settings) {
- super(settings);
- setDefaultState(getStateFactory().getDefaultState().with(MyBlockIsHard, false));
- }
- }
复制代码
现在为了指定属性,需要调用
- public class MyBlock extends Block {
- [...]
- @Override
- public boolean activate(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult blockHitResult) {
- world.setBlockState(pos, MyBlocks.MY_BLOCK_INSTANCE.getDefaultState().with(MyBlockIsHard, true));
- return true;
- }
- }
复制代码
(记得把MyBlocks.MY_BLOCK_INSTANCE换成你的方块实例)
用上这个属性则需要调用blockState.get(你的属性名,例如MyBlockIsHard):
- public class MyBlock extends Block {
- [...]
- @Override
- public float getHardness(BlockState blockState, BlockView blockView, BlockPos pos) {
- boolean isHard = blockState.get(MyBlockIsHard);
- if(isHard) return 2.0f;
- else return 0.5f;
- }
- }
复制代码
也可以根据方块状态的改变,来改变方块的材质(还没写完)。
关于性能启动游戏时,一个方块所有可能的状态都会被注册,这就意味着如果你有14个布尔属性,这个方块就有2^14=16384个状态被注册,因为这个原因,方块不应该有太多的方块状态属性。准确来说,方块状态是用来看的,BlockEntity(方块实体)才是用于更高级状态的。
BlockEntity(方块实体)主要用于向方块存储数据(等同于forge的TileEntity),因此在创建方块实体之前,得先有个方块,这一章节讲BlockEntity的创建和注册。
译注:箱子就是最典型的方块实体,也就是这一章节名称的由来。
新建方块实体最简单的方块实体类继承BlockEntity,使用无参构造器,这样当然有效,但不会让方块有什么功能。
- public class DemoBlockEntity extends BlockEntity {
- public DemoBlockEntity() {
- super(ExampleMod.DEMO_BLOCK_ENTITY);
- }
- }
复制代码
下面会告诉你如何新建DEMO_BLOCK_ENTITY字段。
可以向这个类添加变量,或者让其实现Inventory或者Tickable这样的接口来实现更多功能,Tickable接口提供了一个tick()方法,世界中每个被加载的这种方块,每tick都会调用一次这个方法。Inventory则会让方块实体具有库存,和漏斗这样的自动化工具交互——后面可能会有一个关于Inventory的专门教程。
注册方块实体
创建BlockEntity类后,就需要注册它使之生效,第一步是新建一个BlockEntityType,这东西将Block和BlockEntity连接到一起。假设你的Block实例已经被创建并被保存到一个局部变量DEMO_BLOCK中,接下来就要新建匹配的BlockEntityType,modid:demo应该换成你自己的modid和你接下来想注册的BlockEntity名称。
应该在onInitialize方法中注册BlockEntityType,确保其在正确的时间注册。
- public static BlockEntityType<DemoBlockEntity> DEMO_BLOCK_ENTITY;
-
- @Override
- public void onInitialize() {
- DEMO_BLOCK_ENTITY = Registry.register(Registry.BLOCK_ENTITY, "modid:demo", BlockEntityType.Builder.create(DemoBlockEntity::new, DEMO_BLOCK).build(null));
- }
复制代码
BlockEntityType创建和注册之后,就可以在你的Block类中实现BlockEntityProvider了:
- @Override
- public BlockEntity createBlockEntity(BlockView blockView) {
- return new DemoBlockEntity();
- }
复制代码
序列化数据
如果想存储方块实体中的任何数据,就必须保存和加载它,否则其数据只能在方块实体加载的时候才能保存,如果不加载就会重置,不过还好,保存数据很简单——只需要重写toTag()和fromTag()。
toTag()返回一个CompoundTag(NBT)的实例,应该保存有方块实体的所有数据,这个数据会被保存到硬盘,如果要和客户端同步方块实体数据,也是通过包发送的。了解toTag()的默认实现很重要,这个方法将方块实体的”标识数据“(即位置和id)保存到NBT,没有这个标识,任何尝试保存的数据都会丢失,因为和位置以及BlockEntityType都没有关系了。了解了这个,下面的示例展示了如何把一个整数从你的方块实体保存到NBT,在这里,这个整数有个键”number“,number可以换成任何你想要的字串,但是标签里面,每个键只有一个入口,需要记住,键是为了检索后面的数据的。
- public class DemoBlockEntity extends BlockEntity {
-
- // Store the current value of the number
- private int number = 7;
-
- public DemoBlockEntity() {
- super(ExampleMod.DEMO_BLOCK_ENTITY);
- }
-
- // Serialize the BlockEntity
- public CompoundTag toTag(CompoundTag tag) {
- super.toTag(tag);
- // Save the current value of the number to the tag
- tag.putInt("number", number);
- return tag;
- }
- }
复制代码
为了获取数据,需要重写fromTag(),这个方法是toTag()的逆过程,从已经保存的NBT中获取你需要的数据而不是保存过程本身。和toTag()一样,你必须调取super.fromTag(),使用和先前一样的键(number),来获取你保存的数据,见下:
- // Deserialize the BlockEntity
- public void fromTag(CompoundTag tag) {
- super.fromTag(tag);
- number = tag.getInt("number");
- }
复制代码
实现了toTag()和fromTag()方法之后,只需要正确的时间调用它们就行了,当你的方块实体数据被改变,需要保存时,都调用markDirty(),这会把你的方块所在的区块标记成”dirty“,在下一次保存世界时强制调用toTag()方法保存数据。一般说来,你的BlockEntity改变了任何自定义变量时,都直接调用markDirty()就行了。
如果想把BlockEntity的一些数据同步到客户端,比如渲染需要,应该实现fabric api提供的BlockEntityClientSerializable,这个类包括了fromClientTag和toClientTag方法,和上面的fromTag和toTag大体相同,除了它们是专门用来向客户端发送和接收数据之外。
将物品存储在”箱子“中
在读到这里之前确定你已经写了个方块实体了。把物品存在方块实体(说白了就是新建个箱子)的标准方式就是使其实现Inventory,允许漏斗(或者其他mod的管道)从你的”箱子“中插入或提取物品,而不需要做什么别的工作。
实现Inventory接口
Inventory只是个接口,ItemStack状态实际上存储在你的BlockEntity中。DefaultedList<ItemStack>(译注:相当于forge的NonNullList<T>)可以用来存储这些ItemStack,默认可设置成ItemStack.Empty,声明在格子里没有物品。实现Inventory很简单,但是又枯燥又容易出错,所以使用一个默认实现,只需要一个DefaultList<ItemStack>:
- /**
- * A simple {@code Inventory} implementation with only default methods + an item list getter.
- *
- * Originally by Juuz
- */
- public interface ImplementedInventory extends Inventory {
- /**
- * 获取“箱子”里的所有物品。
- * 每次调用都应该返回相同实例。
- */
- DefaultedList<ItemStack> getItems();
- // Creation
- /**
- * 从物品清单新建一个Invnetory
- */
- static ImplementedInventory of(DefaultedList<ItemStack> items) {
- return () -> items;
- }
- /**
- * 新建一个指定大小的Inventory
- */
- static ImplementedInventory ofSize(int size) {
- return of(DefaultedList.ofSize(size, ItemStack.EMPTY));
- }
- // Inventory
- /**
- * 返回Inventory大小
- */
- @Override
- default int getInvSize() {
- return getItems().size();
- }
- /**
- * @如果Inventory是空的,返回true,否则返回false
- */
- @Override
- default boolean isInvEmpty() {
- for (int i = 0; i < getInvSize(); i++) {
- ItemStack stack = getInvStack(i);
- if (!stack.isEmpty()) {
- return false;
- }
- }
- return true;
- }
- /**
- * 获取某个格子中的物品。
- */
- @Override
- default ItemStack getInvStack(int slot) {
- return getItems().get(slot);
- }
- /**
- * 从格子里取出“一堆”物品(ItemStack)
- * <p>(默认实现) 如果格子里的物品数量比你要取出的物品数量少,就取出所有物品
- */
- @Override
- default ItemStack takeInvStack(int slot, int count) {
- ItemStack result = Inventories.splitStack(getItems(), slot, count);
- if (!result.isEmpty()) {
- markDirty();
- }
- return result;
- }
- /**
- * 移除并返回当前格子里的ItemStack
- */
- @Override
- default ItemStack removeInvStack(int slot) {
- return Inventories.removeStack(getItems(), slot);
- }
- /**
- * 用你提供的ItemStack替换格子里的
- * <p>如果ItemStack堆叠数太大 ({@link Inventory#getInvMaxStackAmount()}),
- * 就会被限制到Inventory允许的堆叠数
- */
- @Override
- default void setInvStack(int slot, ItemStack stack) {
- getItems().set(slot, stack);
- if (stack.getCount() > getInvMaxStackAmount()) {
- stack.setCount(getInvMaxStackAmount());
- }
- }
- /**
- * 清除物品 {@linkplain #getItems() the item list}}.
- */
- @Override
- default void clear() {
- getItems().clear();
- }
- @Override
- default void markDirty() {
- // 如果你想在保存时做些什么就重写这个
- }
- @Override
- default boolean canPlayerUseInv(PlayerEntity player) {
- return true;
- }
- }
复制代码
现在你的BlockEntity实现了ImplementedInventory接口(由于全是default实现,跟直接继承了抽象类一样),提供给它一个DefaultedList<ItemStack>的实例来存储物品,然后向里存储最多两种物品:
- public class DemoBlockEntity extends BlockEntity implements ImplementedInventory {
- private final DefaultedList<ItemStack> items = DefaultedList.ofSize(2, ItemStack.EMPTY);
-
- @Override
- public DefaultedList<ItemStack> getItems() {
- return items;
- }
- [...]
- }
复制代码
还需要把这些内容物保存到nbt,然后从此加载,Inventories类(译注:相当于forge的ItemStackHelper)有相应的方法可以简单做到:
- public class DemoBlockEntity extends BlockEntity implements ImplementedInventory {
- [...]
- @Override
- public void fromTag(CompoundTag tag) {
- super.fromTag(tag);
- Inventories.fromTag(tag,items);
- }
-
- @Override
- public CompoundTag toTag(CompoundTag tag) {
- Inventories.toTag(tag,items);
- return super.toTag(tag);
- }
- }
复制代码
从Inventory插入和提取物品
在方块类中,重写activate方法,来从”箱子“插入或者提取物品,注意这适用于任何Inventory实例(包括箱子方块),首先处理放入”箱子“,玩家会把他手持的物品放入其中,如果”箱子“是空的,物品会进入第一个格子,如果第一个不是就进入第二个,如果第二个也不是,就打印信息,注意放入”箱子“的时候调用了copy()方法,所以它和玩家原来的物品都不会被删除。
- public class ExampleBlock extends Block implements BlockEntityProvider {
- [...]
- @Override
- public boolean activate(BlockState blockState, World world, BlockPos blockPos, PlayerEntity player, Hand hand, BlockHitResult blockHitResult) {
- if (world.isClient) return true;
- Inventory blockEntity = (Inventory) world.getBlockEntity(blockPos);
- if (!player.getStackInHand(hand).isEmpty()) {
- // Check what is the first open slot and put an item from the player's hand there
- if (blockEntity.getInvStack(0).isEmpty()) {
- // Put the stack the player is holding into the inventory
- blockEntity.setInvStack(0, player.getStackInHand(hand).copy());
- // Remove the stack from the player's hand
- player.getStackInHand(hand).setCount(0);
- } else if (blockEntity.getInvStack(1).isEmpty()) {
- blockEntity.setInvStack(1, player.getStackInHand(hand).copy());
- player.getStackInHand(hand).setCount(0);
- } else {
- // If the inventory is full we'll print it's contents
- System.out.println("The first slot holds "
- + blockEntity.getInvStack(0) + " and the second slot holds " + blockEntity.getInvStack(1));
- }
- }
- return true;
- }
- }
复制代码
再写一个反向动作,如果玩家(右击时)手上没有物品,从第二个格子里取出物品,如果第二个格子空了就从第一个取,如果第一个也是空的,那就什么都不做。
- public class ExampleBlock extends Block implements BlockEntityProvider {
- [...]
- @Override
- public boolean activate(BlockState blockState, World world, BlockPos blockPos, PlayerEntity player, Hand hand, BlockHitResult blockHitResult) {
- ...
- if (!player.getStackInHand(hand).isEmpty()) {
- ...
- } else {
- // If the player is not holding anything we'll get give him the items in the block entity one by one
-
- // Find the first slot that has an item and give it to the player
- if (!blockEntity.getInvStack(1).isEmpty()) {
- // Give the player the stack in the inventory
- player.inventory.offerOrDrop(world, blockEntity.getInvStack(1));
- // Remove the stack from the inventory
- blockEntity.removeInvStack(1);
- } else if (!blockEntity.getInvStack(0).isEmpty()) {
- player.inventory.offerOrDrop(world, blockEntity.getInvStack(0));
- blockEntity.removeInvStack(0);
- }
- }
- return true;
- }
- }
复制代码
实现SidedInventory
如果你想基于(方块的)每一个面,有不同的交互逻辑(比如漏斗或者其他mod),需要实现SidedInventory接口,比方说你不想从方块上方插入物品,就可以这样做:
- public class DemoBlockEntity extends BlockEntity implements ImplementedInventory, SidedInventory {
- [...]
- @Override
- public int[] getInvAvailableSlots(Direction var1) {
- // Just return an array of all slots
- int[] result = new int[getItems().size()];
- for (int i = 0; i < result.length; i++) {
- result[i] = i;
- }
-
- return result;
- }
-
- @Override
- public boolean canInsertInvStack(int slot, ItemStack stack, Direction direction) {
- return direction != Direction.UP;
- }
-
- @Override
- public boolean canExtractInvStack(int slot, ItemStack stack, Direction direction) {
- return true;
- }
- }
复制代码
根据群系改变方块颜色
在这个教程里,会让方块根据生物群系改变颜色。开始之前当然要先有个方块,该方块的模型具有染色索引(tint index)。看看叶子和草方块模型文件做例子。
记住将可视化相关的逻辑放在客户端(onInitializeClient),否则在服务端上就会崩溃。注册一个自定义的方块颜色需要使用ColorProviderRegistry.BLOCK.register(),对于物品则使用ColorProviderRegistry.ITEM.register(),在这里使用草方块的群系颜色,传入你的方块来代替最后一个参数(YOUR_BLOCK_INSTANCE)。
- public class ExampleModClient implements ClientModInitializer {
- @Override
- public void onInitializeClient() {
- ColorProviderRegistry.BLOCK.register((block, pos, world, layer) -> {
- BlockColorProvider provider = ColorProviderRegistry.BLOCK.get(Blocks.GRASS);
- return provider == null ? -1 : provider.getColor(block, pos, world, layer);
- }, YOUR_BLOCK_INSTANCE);
- }
- }
复制代码
现在发生了什么?register方法希望返回一个颜色,在以上情况中颜色从草方块来。为物品染色很类似,可以返回任何颜色,记得把最后一个参数替换成你的Item。
- public class ExampleModClient implements ClientModInitializer {
- @Override
- public void onInitializeClient() {
- ColorProviderRegistry.ITEM.register((item, layer) -> {
- // These values are represented as temperature and humidity, and used as coordinates for the color map
- double temperature = 0.5D; // a double value between 0 and 1
- double humidity = 1.0D; // a double value between 0 and 1
- return GrassColorHandler.getColor(temperature, humidity);
- }, YOUR_ITEM_INSTANCE);
- }
- }
复制代码
让方块变透明
你可能注意到,即使你的方块材质是透明的,方块看上去还是不透明,解决这个问题需要重写getRenderLayer()方法,返回0BlockRenderLayer.TRANSLUCENT:
- class MyBlock extends Block {
- @Override
- public BlockRenderLayer getRenderLayer() {
- return BlockRenderLayer.TRANSLUCENT;
- }
- [...]
- }
复制代码
你可能还想让你的方块变成透明方块(译注:一般指不能放火把的方块),需要使用Material构造器,将blocksLight设置成false。
- class MyBlock extends Block {
- private static Material myMaterial = new Material(
- MaterialColor.AIR, //materialColor,
- false, //isLiquid,
- false, // isSolid,
- true, // blocksMovement,
- false,// blocksLight, <----- 这部分很重要,其他随意
- true,// !requiresTool,
- false, // burnable,
- false,// replaceable,
- PistonBehavior.NORMAL// 活塞行为
- );
-
- public MyBlock() {
- super(Settings.of(myMaterial);
- }
-
- [...]
- }
复制代码
使方块隐形
首先要让方块看上去”隐形“,要实现这个,需要重写Block的getRenderType()方法,返回BlockRenderType.INVISIBLE:
- @Override
- public BlockRenderType getRenderType(BlockState blockState) {
- return BlockRenderType.INVISIBLE;
- }
复制代码
然后需要让方块的”外框线“不存在,来让其无法被滚轮点击选中,因此重写getOutlineShape()方法,返回一个空的VoxelShape:
- @Override
- public VoxelShape getOutlineShape(BlockState blockState, BlockView blockView, BlockPos blockPos, EntityContext entityContext) {
- return VoxelShapes.cuboid(0,0,0,0,0,0);
- }
复制代码
使用BlockEntityRenders动态渲染方块和物品
单纯的方块是无趣的,在被破坏之前一直是固定的位置,和固定的尺寸,可以用BlockEntityRenderer(相当于forge的TileEntitySpecialRender),根据一个方块实体更加动态地渲染物品和方块,在不同的位置,以不同的尺寸渲染多个物品,等等。
示例在这篇教程中,会给我们写的方块实体添加一个BlockEntityRenderer,这个渲染器会显示一个悬浮在方块上的唱片机,上下旋转。
第一件事当然是写一个BlockEntityRenderer类:
- public class MyBlockEntityRenderer extends BlockEntityRenderer<DemoBlockEntity> {
- // A jukebox itemstack
- private static ItemStack stack = new ItemStack(Items.JUKEBOX, 1);
-
- @Override
- public void render(DemoBlockEntity blockEntity, double x, double y, double z, float partialTicks, int destroyStage) {
- }
- }
复制代码
需要注册我们的BlockEntityRenderer,但是只针对客户端。在单人游戏里这不影响什么,因为服务端和客户端运行在一个进程上,但是在多人游戏里,客户端和服务端运行在不同的进程上,服务端可没有BlockEntityRenderer,那么就不会接受注册。要运行只针对客户端的初始化代码,需要设置一个client入点。
在主类旁边新建一个实现了ClientModInitializer的新类:
- public class ExampleModClient implements ClientModInitializer {
- @Override
- public void onInitializeClient() {
- // Here we will put client-only registration code
- }
- }
复制代码
在fabric.mod.json文件中,把这个类设置成client入点(按需调整路径):
- "entrypoints": {
- [...]
- "client": [
- {
- "value": "tutorial.path.to.ExampleModClient"
- }
- ]
- }
复制代码
在ClientModInitializer中注册BlockEntityRenderer:
- @Override
- public void onInitializeClient() {
- BlockEntityRendererRegistry.INSTANCE.register(DemoBlockEntity.class, new MyBlockEntityRenderer());
- }
复制代码
需要重写render()方法,这个方法每帧都会被调用!在其中进行渲染,对于初学者来说,先调用GLStateManager.pushMatrix();在进行GL调用时这一步必不可少(译注:这是为了保证待渲染对象不会被长时间的变换“走形”),也就是下面要做的:
- public void render(DemoBlockEntity blockEntity, double x, double y, double z, float partialTicks, int destroyStage) {
- GlStateManager.pushMatrix();
- }
复制代码
然后开始执行音乐盒的平行移动(GlStateManager.translatef,译注:translate意为变换,在OpenGL中指平行移动,f指float)和旋转操作(GlStateManager.rotatef),变换(平行移动)分两部分:将其移动到方块中心上方(x + 0.5, y + 1.25, and z + 0.5)的位置,第二部分是y值的偏移(offset),在这里偏移值是任何一帧下,物品的高度,每次都会重新计算,因为我们希望其动态地弹跳,如下计算:
- 获取当前的世界时间,这个“实时”变化。
- 添加不完整刻(partial ticks),不完整刻是一个小数值,代表上一个完整刻(full tick)到现在过去的时间,如果不用它,动画会变得不平稳,因为一秒的tick数(正常是20)少于一秒的帧数(一般是60)。
- 除以8来减慢移动速度;
- 取其正弦值来生成一个-1~1之间的值,类似一个正弦波。
- 除以4来垂直压缩正弦波,这样物品的跳动幅度不会太大。
译注:GLStateManager.method可以用GL11,GL12等类中的等效方法代替(事实上前者调用了后者而已),例如GLStateManager.rotatef()可以用GL11.glRotatef()代替,不过好处只是少打了两个字。
- public void render(DemoBlockEntity blockEntity, double x, double y, double z, float partialTicks, int destroyStage) {
- [...]
- // 计算y值的偏移值
- double offset = Math.sin((blockEntity.getWorld().getTime() + partialTicks) / 8.0) / 4.0;
- // 平移物品
- GlStateManager.translated(x + 0.5, y + 1.25 + offset, z + 0.5);
- // 旋转物品,这里指以一个四元float数做转轴去旋转
- GlStateManager.rotatef((blockEntity.getWorld().getTime() + partialTicks) * 4, 0, 1, 0);
- }
复制代码
最后,我们获取到MC的ItemRenderer,然后用其renderItem方法渲染这个唱片机,还需要传入ModelTransformation.Type.GROUND到renderItem方法中,因为我们想要一个类似于物品放在地上的效果,试一下这个枚举值看看会发生什么。在GL方法调用后,需要调用GlStateManager.popMatrix()方法:
- public void render(DemoBlockEntity blockEntity, double x, double y, double z, float partialTicks, int destroyStage) {
- [...]
- MinecraftClient.getInstance().getItemRenderer().renderItem(stack, ModelTransformation.Type.GROUND);
-
- // 必不可少
- GlStateManager.popMatrix();
- }
复制代码
现在,可以试试新建的BlockEntityRenderer了,但是,如果你没把方块弄成透明的,就会发现一个问题:漂浮的唱片机是黑色的!这是因为默认情况下,无论你在BlockEntity里渲染了什么玩意,都会接收光照,就好像它和BlockEntity在一个位置上一样,所以漂浮的方块从不透明的方块中接受光照,自然就是黑的(因为根本就没有光)。修复这个问题需要告诉MC,让物体从BlockEntity的上一格接受光照。
得到光照需要在方块实体之上的位置(译注:可能是作者写forge写多了,在这里写了个tileentity)调用World.getLightmapIndex(),调用GLX.glMultiTexCoord2f()来利用这个光照值:
- @Override
- public void render(DemoBlockEntity blockEntity, double x, double y, double z, float partialTicks, int destroyStage) {
- [...]
-
- // 把这部分放在 "MinecraftClient.getInstance().getItemRenderer().renderItem(stack, ModelTransformation.Type.GROUND);"之上
- int light = blockEntity.getWorld().getLightmapIndex(blockEntity.getPos().up(), 0);
- GLX.glMultiTexCoord2f(GLX.GL_TEXTURE1, (float) (light & 0xFFFF), (float) ((light >> 16) & 0xFFFF));
-
- [...]
- }
复制代码
现在,唱片机应该有正常的光照了。
添加实体是在添加完物品和方块后的下一个步骤。
要添加实体,需要3个类。
- Entity类,它用来提供AI(逻辑/思维方式)
- Renderer类,让实体与模型绑定(给实体提供模型)
- Model类,玩家所看到的模型
我们将创建一个曲奇苦力怕,它在爆炸时会把曲奇射的到处都是。
注册实体
与方块和物品不同,一般实体都会有一个专门的类。由于我们又做了个苦力怕,所以让实体类型继承CreeperEntity:(译注:fabric的很多反混淆名其实是forge的倒过来)
public class CookieCreeperEntity extends CreeperEntity {
[...]
}
IDE将会提示你,建立一个和父类相匹配的构造函数。
注册实体需要使用Registry.ENTITY_TYPE,可以使用EntityType.Builder或 FabricEntityTypeBuilder来获取所需的注册表实例(这里推荐使用后者)。
- public static final EntityType<CookieCreeperEntity> COOKIE_CREEPER =
- Registry.register(
- Registry.ENTITY_TYPE,
- new Identifier("wiki-entity", "cookie-creeper"),
- FabricEntityTypeBuilder.create(EntityCategory.AMBIENT, CookieCreeperEntity::new).size(EntityDimensions.fixed(1, 2)).build()
- );
复制代码
使用size()方法来设置实体的碰撞箱。苦力怕宽为一格,高为二格,所以这里填(1,2)。
完成后进入游戏,你可以用/summon来查看你创建的实体。如果一切正常,那么就可以看到一个普通的苦力怕。
创建渲染器
我们创建的苦力怕自带一个模型,因为它继承了Creeper类。现在我们要把它的绿色迷彩色皮肤换成曲奇皮肤。
首先,创建一个有两个泛型参数<Entity,Model>的MobEntityRenderer类,因为我们现在用的是爬行者模型,所以我们需要给苦力怕模型一个泛型参数来告诉它,这不是苦力怕实体(译注:比如说,模型是一件衣服,但它不知道自己的主人是谁,给予了类型后就能让它明白,XXX不是它的主人,它不需要穿上这个模型)
- public class CookieCreeperRenderer extends MobEntityRenderer<CookieCreeperEntity, CreeperEntityModel<CookieCreeperEntity>> {
- [...]
- }
复制代码
需要重写getTexture方法并添加构造函数。默认情况下,构造函数有3个参数(EntityRenderDispatcher,EntityModel,float),但我们可以删除后2个并自己创建它们:
- public CookieCreeperRenderer(EntityRenderDispatcher entityRenderDispatcher_1)
- {
- super(entityRenderDispatcher_1, new CreeperEntityModel<>(), 1);
- }
复制代码
对于getTexture方法,需要返回模型的材质。如果为null,那么实体将不可见。这里有一个人人可用的曲奇苦力怕皮肤,点击此处下载。
默认的实体材质位置为:textures/entity/entity_name/entity.png 。示例如下:
- @Override
- protected Identifier getTexture(CookieCreeperEntity cookieCreeperEntity)
- {
- return new Identifier("wiki-entity:textures/entity/cookie_creeper/creeper.png");
- }
复制代码
文件存储在resources/assets/wiki-entity/textures/entity/cookie_creeper/creeper.png中。
最后。需要把实体与渲染器连接。由于渲染只发生在客户端,因此你需要在ClientModInitializer中执行此类工作:
EntityRendererRegistry.INSTANCE.register(CookieCreeperEntity.class, (entityRenderDispatcher, context) -> new CookieCreeperRenderer(entityRenderDispatcher));
- public void render(DemoBlockEntity blockEntity, double x, double y, double z, float partialTicks, int destroyStage) {
- [...]
- MinecraftClient.getInstance().getItemRenderer().renderItem(stack, ModelTransformation.Type.GROUND);
-
- // Mandatory call after GL calls
- GlStateManager.popMatrix();
- }
复制代码
这将把实体与渲染器连接。现在进入游戏,就能看见创建的实体了:
如果你想用自己的模型,可以创建一个继承EntityModel的新类,并在渲染器中提供模型。这非常复杂,将单独给一个教程。
向世界中添加矿石
很多mod都有矿石,需要一种方式将它们放在已有的群系当中供玩家寻找,这里讲的是如何让矿石生成在原版或者mod添加的生物群系中,需要两步走:
- 挨个迭代注册的生物群系,将矿石添加到其中;
- 使用RegistryEntryAddedCallback(译注:XXXCallback即为fabric提供的事件,fabric有一些事件)确保你的矿石添加到任何mod添加的群系中。
姑且假设你已经新建了自己的矿石方块,在这里用石英矿做演示,目标是让它生成在主世界的群系,自己写的时候把石英矿替换成你想要的矿石方块。
将矿石添加到群系
首先要写个方法处理群系,检查群系有效性,然后再添加矿石。
- private void handleBiome(Biome biome) {
- if(biome.getCategory() != Biome.Category.NETHER && biome.getCategory() != Biome.Category.THEEND) {
- biome.addFeature(
- GenerationStep.Feature.UNDERGROUND_ORES,
- Biome.configureFeature(
- Feature.ORE,
- new OreFeatureConfig(
- OreFeatureConfig.Target.NATURAL_STONE,
- Blocks.NETHER_QUARTZ_ORE.getDefaultState(),
- 8 //矿脉的大小(有几个矿)
- ),
- Decorator.COUNT_RANGE,
- new RangeDecoratorConfig(
- 8, //Number of ms per chunk
- 0, //偏移量
- 0, //最低y轴高度
- 64 //最高y轴高度
- )));
- }
- }
复制代码
这个方法就会带着生成的相关设置,将矿物添加到主世界,按照你的需求,随意修改这个方法吧。
迭代注册的生物群系
下一步要做的是处理所有注册的生物群系和未来可能会添加的生物群系(别的mod加的),首先迭代当前的注册,然后注册一个为以后准备的监听器。
- @Override
- public void onInitialize() {
- //循环迭代所有已经注册的群系
- Registry.BIOME.forEach(this::handleBiome);
-
- //准备监听其他群系
- RegistryEntryAddedCallback.event(Registry.BIOME).register((i, identifier, biome) -> handleBiome(biome));
- }
复制代码
然后就可以看到生成在主世界的石英矿:
这部分所有的示例代码都在这里: fabric-structure-example-repo
这里我们关注结构注册和如何生成在世界中。IglooGenerator和IglooFeature是很好的1.14原版结构生成范例(Igloo:冰屋)。
最基础的结构需要一个Feature(特性)和Generator(生成器),特性处理结构的注册过程,并在世界生成的时候加载它——特性回答了这样的问题:“我应该生成在这里吗?”和“我是什么?”。生成器则处理方块放置,或者在你选择的时候加载一个结构文件。
建立一个特性(feature)
我们推荐新建一个AbstractTempleFeature<DefaultFeatureConfig>的子类来创建一个基础的特性,原版的各种结构,比如沉船,冰屋,神殿,都使用AbstractTempleFeature做父类,需要重写这些方法:
- shouldStartAt: 测试目的,返回true。
- getName: 结构的名称
- getRadius: 结构的半径,用于放置结构
- getSeedModifier:获取种子调整器
可以向构造函数传入DefaultFeatureConfig::deserialize以供测试。
为了getStructureStartFactory()方法,大多数原版结构都在其特性类中写了一个继承于StructureStart的类:
- public static class MyStructureStart extends StructureStart {
- public MyStructureStart (StructureFeature<?> structureFeature, int x, int y, Biome biome, MutableIntBoundingBox mutableIntBoundingBox, int z, long w) {
- super(structureFeature, x , y, biome, mutableIntBoundingBox, z, w);
- }
- @Override
- public void initialize(ChunkGenerator<?> chunkGenerator, StructureManager structureManager, int chunkX, int chunkZ, Biome biome) {
- DefaultFeatureConfig defaultFeatureConfig = chunkGenerator.getStructureConfig(biome, MyMainclass.myFeature);
- int x = chunkX * 16;
- int z = chunkZ * 16;
- BlockPos startingPos = new BlockPos(x, 0, z);
- Rotation rotation = Rotation.values()[this.random.nextInt(Rotation.values().length)];
- MyGenerator.addParts(structureManager, startingPos, rotation, this.children, this.random, defaultFeatureConfig);
- this.setBoundingBoxFromChildren();
- }
- }
复制代码
世界尝试生成新结构时会调用这个类,是特性和生成器之间的分歧。对主类中变量的对象引用还不存在,但是结尾会声明一个。你也可以将其配置项设置成一个DefaultFeatureConfig,然后在getStructureStartFactory()方法中返回MyStructureStart::new来返回这个类的实例。以上是结构文件和直接从generate方法生成的部分,有两个方法处理这个:
- 如果你想,可以简单地重写generate()方法,然后使用setBlockState()把方块直接放在世界中,这样做是有效的,在1.13之前很流行。
- 使用结构文件和生成器,它们功能强大,非常推荐。
声明一个生成器
现在你看到了,需要一个生成器,姑且叫MyGenerator,在StructureStart.initialize()方法中引用,不需要重写什么,但是需要按照下面的做:
- 一个指向你的结构文件的Identifier,比方说“igloo/top”。
- 一些安放结构的方法——addPart是个好名字。
- public static void addParts(StructureManager structureManager, BlockPos blockPos, Rotation rotation,
- List<StructurePiece> list, Random random, DefaultFeatureConfig featureConfig)
- }
复制代码
在addParts方法中,可以选择向你的生成过程添加结构分块(Piece),示例如下:
- list.add(new MyGenerator.Piece(structureManager, identifier, blockPos, rotation));
复制代码
identifier是刚刚新建的路径。
现在新建一个分块的实例,在你的生成器类中,声明一个SimpleStructurePiece的子类Piece。
重写需要的方法,添加一个使用StructureManager, Identifier, BlockPos, Rotation做参数的构造函数。不需要toNbt方法,但是需要的时候也能用。由于还要用不同的参数实现自己的setStructureData方法,所以这不是重写。我们有两个构造函数,一个针对结构分块,一个用于注册,模板如下:
- public static class Piece extends SimpleStructurePiece {
- private Rotation rotation;
- private Identifier template;
-
- public Piece(StructureManager structureManager, Identifier identifier, BlockPos blockPos, Rotation rotation) {
- super(MyModClass.myStructurePieceType, 0);
- this.pos = blockPos;
- this.rotation = rotation;
- this.template = identifier;
- this.setStructureData(structureManager);
- }
-
- public Piece(StructureManager structureManager, CompoundTag compoundTag) {
- super(MyModClass.myStructurePieceType, compoundTag);
- this.identifier = new Identifier(compoundTag.getString("Template"));
- this.rotation = Rotation.valueOf(compoundTag.getString("Rot"));
- this.setStructureData(structureManager);
- }
-
- @Override
- protected void toNbt(CompoundTag compoundTag) {
- super.toNbt(compoundTag);
- compoundTag.putString("Template", this.template.toString());
- compoundTag.putString("Rot", this.rotation.name());
- }
-
- public void setStructureData(StructureManager structureManager) {
- Structure structure = structureManager.getStructureOrBlank(this.identifier);
- StructurePlacementData structurePlacementData = (new StructurePlacementData()).setRotation(this.rotation).setMirrored(Mirror.NONE).setPosition(pos).addProcessor(BlockIgnoreStructureProcessor.IGNORE_STRUCTURE_BLOCKS);
- this.setStructureData(structure, this.pos, structurePlacementData);
- }
-
- @Override
- protected void handleMetadata(String string, BlockPos blockPos, IWorld iWorld, Random random, MutableIntBoundingBox mutableIntBoundingBox) {
-
- }
-
- @Override
- public boolean generate(IWorld iWorld, Random random, MutableIntBoundingBox mutableIntBoundingBox, ChunkPos chunkPos) {
- }
- }
复制代码
在handleMetadata()中,查看结构中的数据方块,并基于查找的结果做些什么,在原版结构中,数据方块放在箱子上,所以战利品能以这种方式装入箱子中。
设置一个StructurePieceType类型的MyModClass.myStructurePiece变量,这个变量存储你注册的结构分块,generate()结束之后就会开始处理,设置结构的位置并将其生成:
- @Override
- public boolean generate(IWorld iWorld, Random random, MutableIntBoundingBox mutableIntBoundingBox, ChunkPos chunkPos) {
- int yHeight = iWorld.getTop(Heightmap.Type.WORLD_SURFACE_WG, this.pos.getX() + 8, this.pos.getZ() + 8);
- this.pos = this.pos.add(0, yHeight - 1, 0);
- return super.generate(iWorld, random, mutableIntBoundingBox, chunkPos);
- }
复制代码
这样,就可以获得区块中间最高的方块的y坐标,在该点下生成结构了。
注册特性
最后一步是注册特性,需要注册:
- StructurePieceType
- StructureFeature<DefaultFeatureConfig>
- StructureFeature<?>
还需要将结构添加到STRUCTURES链表,将其作为feature(生物群系的feature)和generation step(生成步骤)添加到每个生物群系。
- public static final StructurePieceType myStructurePieceType = Registry.register(Registry.STRUCTURE_PIECE, "my_piece", MyGenerator.Piece::new);
复制代码- public static final StructureFeature<DefaultFeatureConfig> myFeature = Registry.register(Registry.FEATURE, "my_feature", new MyFeature());
复制代码
- public static final StructureFeature<?> myStructure = Registry.register(Registry.STRUCTURE_FEATURE, "my_structure", myFeature);
复制代码- Feature.STRUCTURES.put("My Awesome Feature", myFeature);
复制代码
出于测试,建议把你的特性注册到所有群系,生成率100%,这样就可以确认它会生成起效了。你可能不希望你的结构漂在水上,所以需要把水面排除,迭代所有生物群系,添加一个特性和生成步骤:
- for(Biome biome : Registry.BIOME) {
- if(biome.getCategory() != Biome.Category.OCEAN && biome.getCategory() != Biome.Category.RIVER) {
- biome.addStructureFeature(myFeature, new DefaultFeatureConfig());
- biome.addFeature(GenerationStep.Feature.SURFACE_STRUCTURES, Biome.configureFeature(myFeature, new DefaultFeatureConfig(), Decorator.CHANCE_PASSTHROUGH, new ChanceDecoratorConfig(0)));
- }
- }
复制代码
ChanceDecoratorConfig的参数大体上代表生成之前会跳过多少个区块,0是每个区块(什么都不跳过),1是每两个(隔一个),100就是每101个(隔100个)。需要将结构添加成群系特性(feature),这样的群系才能知道结构的存在,将其添加为生成步骤(generation step),结构才能真正生成。
进入世界,如果工作正常,你会看到一大片冰屋。
这一部分着眼于向世界中注册和添加群系。
添加群系之前要先新建和注册,然后用fabric api的辅助方法添加到世界中,教程步骤如下:
- 新建群系
- 注册群系
- 将群系添加到世界的气候区域
- 允许玩家生成在群系中
我们还会简要地谈谈fabric api中其他有用的辅助性群系添加方法。
新建群系 新建一个群系很显然要写个Biome的子类,Biome类作为一个父类,存储着群系的信息,所有的原版群系都派生于此,这个类定义了:
- 群系的基本属性
- 群系会有什么特性(树,植物和结构)
- 群系会生成什么实体
在写群系的时候,需要向父类构造器传递一个带有群系所有基础属性的Biome.Settings实例,缺失一个属性都可能导致游戏崩溃,推荐看看MountainsBiome和ForestBiome这种原版群系。
一些重要的设置包括depth(高度),scale(山的高度),和precipitation(天气)。(译注:能反混淆出降水这个词,也是佩服得紧)
- public class MyBiome extends Biome
- {
- public MyBiome()
- {
- super(new Biome.Settings().configureSurfaceBuilder(SurfaceBuilder.DEFAULT, SurfaceBuilder.GRASS_CONFIG).precipitation(Biome.Precipitation.RAIN).category(Biome.Category.PLAINS).depth(0.24F).scale(0.2F).temperature(0.6F).downfall(0.7F).waterColor(4159204).waterFogColor(329011).parent((String)null));
- }
- }
复制代码
然后指定生成在群系中的特性和实体,除了某些特殊结构(比如末地城),树和石头,植物和自定义的实体,每个群系生成的大体相同,原版群系特性通过DefaultBiomeFeatures的方法进行配置。
- public class MyBiome extends Biome
- {
- public MyBiome()
- {
- super(new Biome.Settings().configureSurfaceBuilder(SurfaceBuilder.DEFAULT, SurfaceBuilder.GRASS_CONFIG).precipitation(Biome.Precipitation.RAIN).category(Biome.Category.PLAINS).depth(0.24F).scale(0.2F).temperature(0.6F).downfall(0.7F).waterColor(4159204).waterFogColor(329011).parent((String)null));
-
- this.addStructureFeature(Feature.MINESHAFT, new MineshaftFeatureConfig(0.004D, MineshaftFeature.Type.NORMAL));
- this.addStructureFeature(Feature.STRONGHOLD, FeatureConfig.DEFAULT);
- this.addStructureFeature(Feature.VILLAGE, new VillageFeatureConfig("village/plains/town_centers", 6));
- DefaultBiomeFeatures.addLandCarvers(this);
- DefaultBiomeFeatures.addDefaultStructures(this);
- DefaultBiomeFeatures.addDefaultLakes(this);
- DefaultBiomeFeatures.addDungeons(this);
- DefaultBiomeFeatures.addExtraMountainTrees(this);
- DefaultBiomeFeatures.addDefaultFlowers(this);
- DefaultBiomeFeatures.addDefaultGrass(this);
- DefaultBiomeFeatures.addMineables(this);
- DefaultBiomeFeatures.addDefaultOres(this);
- DefaultBiomeFeatures.addDefaultDisks(this);
- DefaultBiomeFeatures.addDefaultVegetation(this);
- DefaultBiomeFeatures.addSprings(this);
- DefaultBiomeFeatures.addFrozenTopLayer(this);
- this.addSpawn(EntityCategory.CREATURE, new Biome.SpawnEntry(EntityType.SHEEP, 12, 4, 4));
- this.addSpawn(EntityCategory.CREATURE, new Biome.SpawnEntry(EntityType.PIG, 10, 4, 4));
- this.addSpawn(EntityCategory.CREATURE, new Biome.SpawnEntry(EntityType.CHICKEN, 10, 4, 4));
- this.addSpawn(EntityCategory.CREATURE, new Biome.SpawnEntry(EntityType.COW, 8, 4, 4));
- this.addSpawn(EntityCategory.AMBIENT, new Biome.SpawnEntry(EntityType.BAT, 10, 8, 8));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.SPIDER, 100, 4, 4));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.ZOMBIE, 95, 4, 4));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.ZOMBIE_VILLAGER, 5, 1, 1));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.SKELETON, 100, 4, 4));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.CREEPER, 100, 4, 4));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.SLIME, 100, 4, 4));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.ENDERMAN, 10, 1, 4));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.WITCH, 5, 1, 1));
- }
- }
复制代码
注册群系注册群系之前,先写一个群系的实例字段,将其添加到Registry.BIOME中,推荐使用单独的一个类持有你的Biome对象。
- public class TutorialBiomes
- {
- public static final Biome MY_BIOME = Registry.register(Registry.BIOME, new Identifier("tutorial", "my_biome"), new MyBiome());
- }
复制代码
还需要在en_us.json文件中指定一个翻译条目:
- {
- "biome.tutorial.my_biome": "My Biome"
- }
复制代码
将群系添加到世界生成器中要让你的群系生成在世界中,可以使用fabric-biomes api模块提供的辅助方法,这样的代码应该完美地在mod初始化期间运行。
需要指定这个群系的气候和权重(一个double值),权重是群系生成几率的度量,高权重代表高几率生成,正比于其他群系的权重。每种气候的Javadoc注释(译注:形同/** */的注释,编译时不会抹除),会在每个气候中给予原版群系相应的权重,出于测试要给你的群系一个高权重,这样更好找。
在这里,我们给自定义群系加上TEMPERATE(温和)和COOL(凉爽)气候看看:
- public class ExampleMod implements ModInitializer
- {
- @Override
- public void onInitialize()
- {
- OverworldBiomes.addContinentalBiome(OverworldClimate.TEMPERATE, TutorialBiomes.MY_BIOME, 2D);
- OverworldBiomes.addContinentalBiome(OverworldClimate.COOL, TutorialBiomes.MY_BIOME, 2D);
- }
- }
复制代码
允许玩家生成在这个群系,要用到fabric biomes api的另一个方法:
- FabricBiomes.addSpawnBiome(TutorialBiomes.MY_BIOME);
复制代码
恭喜!你的群系现在应该生成在世界中了!
其他的实用Biome方法在fabric biomes api中还有别的实用方法,添加了更多功能:
比方说让你的群系不生成河流:
- OverworldBiomes.setRiverBiome(TutorialBiomes.MY_BIOME, null);
复制代码第三个参数是用特定变体替换群系的几率,比如把你的群系设置成平原(plain)的变体:
- OverworldBiomes.addBiomeVariant(Biomes.PLAINS, TutorialBiomes.MY_BIOME, 0.33);
复制代码
接下来的方法都会采用一个权重参数,指定了群系相对于其他其他变体的生成概率。- OverworldBiomes.addHillsBiome(TutorialBiomes.MY_BIOME, Biomes.MOUNTAINS, 1);
复制代码
- OverworldBiomes.addEdgeBiome(TutorialBiomes.MY_BIOME, Biomes.FOREST, 1);
复制代码
- OverworldBiomes.addShoreBiome(TutorialBiomes.MY_BIOME, Biomes.STONE_BEACH, 1);
复制代码
创建维度是高级内容,需要掌握很多东西,简单的维度做起来很容易,但随着深入了解,你会被无数新类轰炸,短时间内难以掌握,这部分教程是维度内容的一个大纲,段落标题即为类名。
Dimension(维度)
Dimension类是新维度的核心,持有你的维度的重要逻辑,玩家能在这个维度睡觉吗?有世界边境吗?玩家能看见天空吗?还可以建立你自己的区块生成器(ChunkGenerator)生成陆地。
DimensionType(维度类型)DimensionType是维度的注册包装,具有一个ID,允许你注册维度,这个类还负责维度的保存和读取。
ChunkGenerator(区块生成器)负责使用噪波函数放置方块,不负责(或者说不应该负责)实际的装饰和选择单独的方块——大多数情况下,区块生成器只会放置石头或者别的基础方块,如果没有装饰或者别的步骤,主世界理论上只是个用石头堆成的地块,什么都没有。
ChunkGeneratorType(区块生成器类型)如上,也是区块生成器的注册包装,但是目前不能注册,需要点修改手段——急需一个修复。
Biome(生物群系)Biome是维度的一部分,决定了这片区域的外貌。负责生物生成,植物,湖泊河流,洞穴,草方块颜色,等等。在实际生成中,它还负责将已经生成的石头用合适的方块替换,比如草方块或者泥土,矿石。
BiomeSource(生物群系源)创建维度时,可以选择全维度只有一个群系,或者使用BiomeSource类,这个类用来从几个不同的群系中随机挑选。
SurfaceBuilder(表面构建器)
表面构建器负责将石头替换成其他方块,每个群系都有一个附带的表面构建器,比如,平原和森林都带有默认的表面构建器,因为这些群系的顶部方块都是草方块和泥土。它们(指两个群系的表面构造)彼此不同,因为群系也可以放置树或者不同的高度缩放——换句话说,表面构建器是根据现实情况,半重用的。
创建一个基础的维度
还没写。
虽然护甲比一般的物品和方块麻烦一些,不过只要你弄懂了,就很简单,要添加护甲首先要有一个自定义材料(material)类,然后再注册物品,这里看看如何给它们加材质。
新建一个护甲材料类因为新护甲需要有新名字,护甲点数和耐久之类的,所以需要为其新建一个类。这个类会实现ArmorMaterial接口,是个枚举。其构造器需要很多参数,主要是名字,耐久,等等……这是一个列表:
- 名称(name),稍后会用做“护甲标签”。
- 耐久因子(durabilityMultiplier),基础数值乘以耐久因子即为最终耐久。
- 护甲值(armorValues),或者原版代码中的“保护点数(Protection Amounts)” ,这是个整型数组。
- 附魔能力(Enchantability),代表了护甲在附魔时得到高级附魔或者多个附魔的概率。
- 声音事件(equipSound),用在原版护甲的声音事件是 SoundEvents.ITEM.EQUIP.ARMOR.X, X是护甲的类型。
- 护甲韧性(toughness). 这是第二个保护值,遭受高伤害时护甲会更加坚韧,掉耐久少(译注:只有钻石护甲有这个参数)。
- 修复材料(repairIngredient),这是一个 Supplier<Ingredient>实例而不是物品(Item), 一会会讲。
有了这些参数之后的示例如下:
- public enum CustomArmorMaterial implements ArmorMaterial {
- private final String name;
- private final int durabilityMultiplier;
- private final int[] armorValues;
- private final int enchantability;
- private final SoundEvent equipSound;
- private final float toughness;
- private final Lazy<Ingredient> repairIngredient;
-
- CustomArmorMaterial(String name, int durabilityMultiplier, int[] armorValueArr, int enchantability, SoundEvent soundEvent, float toughness, Supplier<Ingredient> repairIngredient) {
- this.name = name;
- this.durabilityMultiplier = durabilityMultiplier;
- this.armorValues = armorValueArr;
- this.enchantability = enchantability;
- this.equipSound = soundEvent;
- this.toughness = toughness;
- this.repairIngredient = new Lazy(repairIngredient); // 需要一个Lazy类型的变量,为以后做准备
- }
- }
复制代码
ArmorMaterial 需要一些别的方法,这里就会加上。还要有四件套中每一件基础的耐久值,这里采用原版的[13,15,16,11]。
- public enum CustomArmorMaterial implements ArmorMaterial {
- private static final int[] baseDurability = {13, 15, 16, 11};
- private final String name;
- private final int durabilityMultiplier;
- private final int[] armorValues;
- private final int enchantability;
- private final SoundEvent equipSound;
- private final float toughness;
- private final Lazy<Ingredient> repairIngredient;
-
- CustomArmorMaterial(String name, int durabilityMultiplier, int[] armorValueArr, int enchantability, SoundEvent soundEvent, float toughness, Supplier<Ingredient> repairIngredient) {
- this.name = name;
- this.durabilityMultiplier = durabilityMultiplier;
- this.armorValues = armorValueArr;
- this.enchantability = enchantability;
- this.equipSound = soundEvent;
- this.toughness = toughness;
- this.repairIngredient = new Lazy(repairIngredient);
- }
-
- public int getDurability(EquipmentSlot equipmentSlot_1) {
- return BASE_DURABILITY[equipmentSlot_1.getEntitySlotId()] * this.durabilityMultiplier;
- }
-
- public int getProtectionAmount(EquipmentSlot equipmentSlot_1) {
- return this.protectionAmounts[equipmentSlot_1.getEntitySlotId()];
- }
-
- public int getEnchantability() {
- return this.enchantability;
- }
-
- public SoundEvent getEquipSound() {
- return this.equipSound;
- }
-
- public Ingredient getRepairIngredient() {
- // We needed to make it a Lazy type so we can actually get the Ingredient from the Supplier.
- return this.repairIngredientSupplier.get();
- }
-
- @Environment(EnvType.CLIENT)
- public String getName() {
- return this.name;
- }
-
- public float getToughness() {
- return this.toughness;
- }
- }
复制代码
现在有了基本的护甲材料类,就可以用自己的材料制作护甲了,在上面像这样指定一个枚举值:
- public enum CustomArmorMaterial implements ArmorMaterial {
- WOOL("wool", 5, new int[]{1,3,2,1}, 15, SoundEvents.BLOCK_WOOL_PLACE, 0.0F, () -> {
- return Ingredient.ofItems(Items.WHITE_WOOL);
- });
- [...]
- }
复制代码
创建护甲回到这类中,这样新建护甲:
- public class ExampleMod implements ModInitializer {
- public static final Item WOOL_HELMET = new ArmorItem(CustomArmorMaterial.WOOL, EquipmentSlot.HEAD, (new Item.Settings().group(ItemGroup.COMBAT)));
- public static final Item WOOL_CHESTPLATE = new ArmorItem(CustomArmorMaterial.WOOL, EquipmentSlot.CHEST, (new Item.Settings().group(ItemGroup.COMBAT)));
- public static final Item WOOL_LEGGINGS = new ArmorItem(CustomArmorMaterial.WOOL, EquipmentSlot.LEGS, (new Item.Settings().group(ItemGroup.COMBAT)));
- public static final Item WOOL_BOOTS = new ArmorItem(CustomArmorMaterial.WOOL, EquipmentSlot.FEET, (new Item.Settings().group(ItemGroup.COMBAT)));
- }
复制代码
然后注册(和一般物品一样):
- [...]
- public void onInitialize() {
- Registry.register(Registry.ITEM,new Identifier("tutorial","wool_helmet"), WOOL_HELMET);
- Registry.register(Registry.ITEM,new Identifier("tutorial","wool_chestplate"), WOOL_CHESTPLATE);
- Registry.register(Registry.ITEM,new Identifier("tutorial","wool_leggings"), WOOL_LEGGINGS);
- Registry.register(Registry.ITEM,new Identifier("tutorial","wool_boots"), WOOL_BOOTS);
- }
复制代码
添加材质
考虑到以前的章节讲过模型和材质,就不再重复了(护甲跟一般物品一样)。护甲材质看上去有点不一样,因为MC认为这是个原版护甲,因此需要一个pack.mcmeta文件,使我们的资源文件变成一个资源包。
- {
- "pack":{
- "pack_format":4, //4是1.13以后的资源包号,1.12是3
- "description":"Tutorial Mod"
- }
- }
复制代码
现在可以把材质放在src/main/resources/assets/minecraft/textures/models/armor/下面,注意分成两张图。(参考原版护甲材质)
按照以上步骤,应该就能看到完整的护甲了!
要添加附魔,需要:
- 新建一个Enchantment或者已经存在的附魔类(例如DamageEnchantment)的子类
- 注册你的附魔(注:注册附魔时,对应的附魔书也会自动注册,附魔的翻译条目enchantment.modid.enchantname会作为书名而存在)
- 自定义附魔机制和功能
- 为附魔添加翻译条目
附魔可以有自定义的功能(比如挖矿时熔炼),或者使用已有的机制(比如DamageEnchantment),想用什么就用什么。Enchantment基类也有几个带功能的方法,比如“打击敌人时做了什么”(译注:这里指onTargetDamaged()方法)
创建附魔类我们要新建一个叫做霜冻(Frost)的附魔,用来减慢生物移动速度,缓慢效果的时长和效力随附魔等级而递增。
- public class FrostEnchantment extends Enchantment
- {
- public WrathEnchantment(Weight weight, EnchantmentTarget target, EquipmentSlot[] slots)
- {
- super(weight, target, slots);
- }
- }
复制代码
必须重写几个基本的方法来实现基础的功能:
getMininumPower()是附魔台中得到这个附魔的最低等级,设置成1就可以任何等级得到:
- @Override
- public int getMinimumPower(int int_1)
- {
- return 1;
- }
复制代码
getMaximumLevel代表这个附魔一共有多少级:
- @Override
- public int getMaximumLevel()
- {
- return 3;
- }
复制代码
最后,在onTargetDamage()方法中实现缓慢效果,在用带有此附魔的工具打击敌人时,这个方法0会被调用:
- @Override
- public void onTargetDamaged(LivingEntity user, Entity target, int level)
- {
- if(target instanceof LivingEntity)
- {
- ((LivingEntity) target).addPotionEffect(new StatusEffectInstance(StatusEffects.SLOWNESS, 20 * 2 * level, level - 1));
- }
- super.onTargetDamaged(user, target, level);
- }
复制代码
逻辑就这么简单:如果被打的实体可以有药水效果(译注:StatusEffect相当于forge的PotionEffect)。时长=等级*2秒,效果正比于等级。
注册附魔注册附魔也是一样的过程:
- private static Enchantment FROST;
-
- @Override
- public void onInitialize()
- {
- FROST = Registry.register(
- Registry.ENCHANTMENT,
- new Identifier("tutorial", "frost"),
- new FrostEnchantment(
- Enchantment.Weight.VERY_RARE,
- EnchantmentTarget.WEAPON,
- new EquipmentSlot[] {
- EquipmentSlot.MAINHAND
- }
- )
- );
- }
复制代码
这样我们的附魔就会以tutorial:frost命名空间注册,并将其设定为一个“very rare(非常稀有)的附魔”,只在主手手持工具上启用它。
添加翻译和测试还需要给附魔添加一个翻译条目,转到语言文件,添加新条目:
- {
- "enchantment.tutorial.frost": "Frost"
- }
复制代码
现在进入游戏,应该就可以看到附魔的主手持武器。
MC使用快捷键机制处理玩家外设(键盘,鼠标等)的输入,按下W,玩家实体就会前进,按E打开物品栏,每个快捷键都可以用设置菜单去调整,如果你想,可以让玩家用箭头键移动而不是WASD。
添加个快捷键还是很简单的,需要:
- 新建一个FabricKeyBinding对象
- 注册你的按键
- 对你的按键起反应
创建快捷键 fabric api有个FabricKeyBinding类,可以简化快捷键的注册,在你想要的地方声明一个变量:
- private static FabricKeyBinding keyBinding;
复制代码
FabricKeyBinding类有一个初始化用的构造器(译注:Builder,指构造器模式),采用了一个Identifier(标识符),InputUtil.Type,key code,和快捷键类别(binding category):
- keyBinding = FabricKeyBinding.Builder.create(
- new Identifier("tutorial", "spook"),
- InputUtil.Type.KEYSYM, //KEYSYM指键盘,MOUSE指鼠标
- GLFW.GLFW_KEY_R, //R键
- "Wiki Keybinds"
- ).build();
复制代码
GLFW.GLFW_KEY_R可以换成任何你想要的键位,快捷键类别关系到你的快捷键在设置页面中的分类。
译注(以下是原版定义的快捷键类别,指定的时候输入最后一节比如misc就行):
- private static final Map<String, Integer> categoryOrderMap = (Map)SystemUtil.consume(Maps.newHashMap(), (hashMap_1) -> {
- hashMap_1.put("key.categories.movement", 1);
- hashMap_1.put("key.categories.gameplay", 2);
- hashMap_1.put("key.categories.inventory", 3);
- hashMap_1.put("key.categories.creative", 4);
- hashMap_1.put("key.categories.multiplayer", 5);
- hashMap_1.put("key.categories.ui", 6);
- hashMap_1.put("key.categories.misc", 7);
- });
复制代码
注册快捷键使用KeybindingRegistry注册快捷键:
- KeyBindingRegistry.INSTANCE.register(keyBinding);
复制代码登录进游戏,就可以在设置页面看到你的快捷键了。
响应快捷键让人不爽的是,没有一个直截了当的方法响应快捷键,大多数人认为最好的方式是给ClientTick事件挂钩子:
- ClientTickCallback.EVENT.register(e ->
- {
- if(keyBinding.isPressed()) System.out.println("was pressed!");
- });
复制代码注意,这是完全客户端的,如果要让服务端响应快捷键,需要发送一个自定义的包让服务端单独处理。
fabric api提供了一个事件(Event)系统,允许mod响应游戏中的各种事件。添加事件的目的在于添加日常所需的钩子,以及给在同一处代码挂钩的mod之间,提供更好的兼容性和性能。对事件的使用经常代替mixin的使用。fabric api提供了一些情况的事件,但不是所有,所以你必须使用其他方法比如mixin,来添加一个钩子,然后将这个钩子实现成一个事件。
在这你将学会新建自己的事件,剪羊毛时触发。新建事件的流程如下:
新建回调接口回调接口定义了事件监听者(译注:指调用你的事件的开发者或者类)必须实现什么,还描述了事件如何从mixin被调用,出于方便,在回调接口里声明一个Event对象字段,标志事件本身。
根据我们的事件实现,选用数组支持的事件,这个数组包括了所有该事件的监听者。该实现会依次呼叫所有的监听者,直到某一个没有返回ActionResult.PASS(译注:相当于forge的EnumActionResult.PASS)——意思是一个监听者可以使用它的返回值说出:“取消这个事件(使之不执行)”,“同意它”,“不管它,交给下一个监听者”。使用ActionResult做返回值便于将事件处理者以这种形式结合起来。
现在新建一个接口,带有Event实例和响应的实现方法,一个基本的剪羊毛回调接口如下:
- public interface SheepShearCallback</font>
- {
- Event<SheepShearCallback> EVENT = EventFactory.createArrayBacked(SheepShearCallback.class,
- (listeners) -> (player, sheep) -> {
- for (SheepShearCallback event : listeners) {
- ActionResult result = event.interact(player, sheep);
- if(result != ActionResult.PASS) {
- return result;
- }
- }
- return ActionResult.PASS;
- });
-
- ActionResult interact(PlayerEntity player, SheepEntity sheep);
复制代码
再深入地看看这段代码,当一个调用者被调用时,会迭代所有的监听者:
- (listeners) -> (player, sheep) -> {for (SheepShearCallback event : listeners) {
复制代码
然后给监听者调用我们自己的方法(在这里是interact()方法),得到其返回值:
- ActionResult result = event.interact(player, sheep);
复制代码
如果监听者说“我要取消”(即ActionResult.FAIL)或者“完全完成”(ActionResult.SUCCESS),回调方法就会返回这个结果,结束循环,“过”(ActionResult.PASS)则会转到下一个调用者,一般来讲,如果再也没有监听者被注册,那么就会返回SUCCESS。
- // ....
- if(result != ActionResult.PASS) {
- return result;
- }
- }
-
- return ActionResult.PASS;
复制代码
在fabric api中,我们给回调类加了Javadoc注解,说明了每个ActionResult效果是什么,在这里效果就是:
- /**
- * 剪羊毛的回调。
- * 在羊被剪毛,物品掉落和破坏之前调用。
- * 返回:
- * - SUCCESS 会取消事件传递给下一个监听者,继续正常的剪毛行为。
- * - PASS 传递到下一个监听者,如果没有监听者则返回SUCCESS
- * - FAIL取消事件传递,不剪毛。
- /**
复制代码
从Mixin触发事件有了基本的事件单例,但是还要触发啊,因为我们希望玩家“尝试”剪羊毛时调用这个事件,所以当调用dropItems()时,调用SheepEntity.interactMob()中的调用者(换句话说此时玩家拿着剪刀,羊可以被剪毛):
- @Mixin(SheepEntity.class)
- public class SheepShearMixin
- {
- @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/passive/SheepEntity;dropItems()V"),
- method = "interactMob", cancellable = true)
- private void onShear(final PlayerEntity player, final Hand hand, final
- CallbackInfoReturnable<Boolean> info) {
- ActionResult result = SheepShearCallback.EVENT.invoker().interact(player, (SheepEntity) (Object) this);
- if(result == ActionResult.FAIL) {
- info.cancel();
- }
- }
- }
复制代码
这个简单的mixin调用了事件调用者 (SheepShearCallback.EVENT.invoker().[…]),然后调用所有的监听者确认应该做什么,基于此返回一个ActionResult,如果返回的是FAIL,就不剪毛,或者破坏玩家的物品(info.cancel();)。确保你的mixin已经注册到mixins.json文件中!
用监听器测试事件现在可以注册一个事件(不管在什么地方)然后加入自定义的逻辑以测试事件,下面演示了让羊掉钻石而不是掉羊毛:
- SheepShearCallback.EVENT.register((player, sheep) ->
- {
- sheep.setSheared(true);
-
- // 在羊的位置新建一个钻石掉落物
- ItemStack stack = new ItemStack(Items.DIAMOND);
- ItemEntity itemEntity = new ItemEntity(player.world, sheep.x, sheep.y, sheep.z, stack);
- player.world.spawnEntity(itemEntity);
- return ActionResult.FAIL;
- });
复制代码
注意这个事件会把羊设置成手动剪毛,因为返回FAIL时事件就被取消了,如果不需要取消事件,确保你要返回PASS,这样其他监听者也能操作,不遵守这个“潜规则”可能会导致一群愤怒的mod作者来真人快打。
(译注:演示效果是视频,就不放了)
有时你想往已有的战利品表(比如原版方块和实体的掉落物)添加物品,最简单的方法当然是替换战利品表文件,然后其他mod就炸了——如果它们也想改战利品呢?这章要讲的是如何向战利品表添加物品而不需要重写,以向煤矿的战利品表添加鸡蛋为例。
监听战利品表
fabric api有一个战利品表加载时触发的事件,即LootTableLoadingCallback,可以在主类中为其注册一个事件监听者,在此之前先检查一下minecraft:blocks/coal_ore这个战利品表。
- private static final Identifier COAL_ORE_LOOT_TABLE_ID = new Identifier("minecraft", "blocks/coal_ore");
-
- // Actual code
-
- LootTableLoadingCallback.EVENT.register((resourceManager, lootManager, id, supplier, setter) -> {
- if (COAL_ORE_LOOT_TABLE_ID.equals(id)) {
- // Our code will go here
- }
- });
复制代码
将物品添加到战利品表中
战利品表里,物品存储在“战利品条目(loot entries)”中,战利品条目又存储在“战利品池(loot pool)”中,所以要添加战利品,就要把一个带有战利品条目的池加入到表中。
用FabricLootPoolBuilder新建一个战利品池,将其添加到表中:
- LootTableLoadingCallback.EVENT.register((resourceManager, lootManager, id, supplier, setter) -> {
- if (COAL_ORE_LOOT_TABLE_ID.equals(id)) {
- FabricLootPoolBuilder poolBuilder = FabricLootPoolBuilder.builder()
- .withRolls(ConstantLootTableRange.create(1)); // 等同于战利品表json里的 "rolls": 1
-
- supplier.withPool(poolBuilder);
- }
- });
复制代码
池里还没有物品,所以需要添加一个条目,如下:
- LootTableLoadingCallback.EVENT.register((resourceManager, lootManager, id, supplier, setter) -> {
- if (COAL_ORE_LOOT_TABLE_ID.equals(id)) {
- FabricLootPoolBuilder poolBuilder = FabricLootPoolBuilder.builder()
- .withRolls(ConstantLootTableRange.create(1))
- .withEntry(ItemEntry.builder(Items.EGG));
-
- supplier.withPool(poolBuilder);
- }
- });
复制代码
mod开发者可以添加命令,让玩家用命令执行功能,这部分教程教你如何注册命令,Brigadier(译注:mojang在1.13之后用的新命令工具),以及一些高级命令结构。
注册命令
如果你只想看注册命令,看这儿就对了,由CommandRegistry.register()方法注册命令。
register()方法指定了两个参数,服务端标记(dedicated flag)和一个代表CommandDispatcher类的Consumer(译注:Consumer指参数为类型T,返回为void的lambda表达式),服务端标记设为true会告知fabric只将命令注册到专用服务端上,如果为false,则会同时注册在内部服务端和专用服务端上,下面是几个注册命令的例子。
- CommandRegistry.INSTANCE.register(false, dispatcher -> TutorialCommands.register(dispatcher)); // All commands are registered in a single class that references every command.
-
- CommandRegistry.INSTANCE.register(false, dispatcher -> { // You can also just reference every single class also. There is also the alternative of just using CommandRegistry
- TutorialCommand.register(dispatcher);
- TutorialHelpCommand.register(dispatcher);
- });
-
- CommandRegistry.INSTANCE.register(true, dispatcher -> { // Or directly registering the command to the dispatcher.
- dispatcher.register(LiteralArgumentBuilder.literal("tutorial").executes(ctx -> execute(ctx)));
- });
复制代码
一个基础的命令
等等,这不是跟Brigadier教程里的指令一样?当然是,不过这里会解释命令的结构。
- // 命令的基础,必须是字面量参数
- dispatcher.register(CommandManager.literal("foo")
- // 添加一个叫做bar的整型参数
- .then(CommandManager.argument("bar", integer())
- //当输入命令foo和参数bar并回车之后,命令就会执行
复制代码
主要流程是注册了“foo”(根节点)和“bar”可选参数(子节点),因为根节点必须是字面量,所以命令发送者必须输入完全一样的字母去执行命令,“Foo”,“fOo”,还是“fooo”都不行。
Brigadier分析
基于CommandDispatcher的Brigadier更应该看成是树而不是链表,CommandDispatcher就是这棵树的主干,register(LiteralArgumentBuilder)指定了分支的头,接下来的方法指定了分支的长度和形状,最后的执行方法体(execute blocks)可以从树的叶子看到,也就是系统的输出结果。
执行方法体决定了最后执行的指令,因为Brigadier的命令是个函数式接口,所以可以用lambda表达式指定命令。
CommandContexts(命令上下文)
命令执行时,Brigadier为其提供了一个上下文,该上下文包括了所有的参数,包括其他对象,比如输入的字符串和CommandSource对象(MC实现中的ServerCommandSource)
参数
Brigadier中的参数(或者说是参数类型)对输入的参数即进行解析又进行错误检查。MC为自己做了几个特殊参数,例如EntityArgumentType代表游戏内置的实体选择器@a, @r, @p, @e[type=!player, limit=1, distance=..2],或者NBTTagArgumentType解析NBT,验证输入语法是否正确。
CommandManager.literal(“foo”) 是有效的,但未免有点长,可以静态导入参数,将其简化成literal(“foo”)。这也适用于获取参数,将StringArgumentType.getString(ctx, “string”) 缩短成getString(ctx, “string”),还适用于MC的参数。
你应该像这样导入:
- import static com.mojang.brigadier.arguments.StringArgumentType.getString; // getString(ctx, "string")
- import static com.mojang.brigadier.arguments.StringArgumentType.word; // word(), string(), greedyString()
- import static net.minecraft.server.command.CommandManager.literal; // literal("foo")
- import static net.minecraft.server.command.CommandManager.argument; // argument("bar", word())
- import static net.minecraft.server.command.CommandManager.*; // Import everything
复制代码
Brigadier的默认参数存储在 com.mojang.brigadier.arguments包中,MC的参数存储在net.minecraft.command.arguments包中,CommandManager位于net.minecraft.server.command中。
Suggestion(建议)
可以给客户端提“建议”(译注:即自动提示),推荐给命令输入什么,在游戏中用于计分板和战利品表,游戏将其存储在SuggestionProviders类中,示例如下:
- SUMMONABLE_ENTITIES
- AVAILIBLE_SOUNDS
- ALL_RECIPES
- ASK_SERVER
复制代码
比如战利品表会在LootCommand(译注:即为/loot命令)类中指定其SuggestionProvider,而下面的例子会动态修改SuggestionProvider,为StringArgumentType列出一些词语:
- public static SuggestionProvider<ServerCommandSource> suggestedStrings() {
- return (ctx, builder) -> getSuggestionsBuilder(builder, /*Access to a list here*/);
- }
-
- private static CompletableFuture<Suggestions> getSuggestionsBuilder(SuggestionsBuilder builder, List<String> list) {
- String remaining = builder.getRemaining().toLowerCase(Locale.ROOT);
-
- if(list.isEmpty()) { // If the list is empty then return no suggestions
- return Suggestions.empty(); // No suggestions
- }
-
- for (String str : list) { // Iterate through the supplied list
- if (str.toLowerCase(Locale.ROOT).startsWith(remaining)) {
- builder.suggest(str); // Add every single entry to suggestions list.
- }
- }
- return builder.buildFuture(); // Create the CompletableFuture containing all the suggestions
- }
复制代码
SuggestionProvider也是个函数式接口,返回CompletableFuture(译注:即为Java的异步机制),包含一些建议。这些建议会随着命令的输入提供给客户端,也会在服务端运行期间被改变。SuggestionProvider提供了一个CommandContext和一个SuggestionBuilder定义所有的建议,CommandSource也可以在建议建立过程中引入,因为这个类可以通过CommandContext使用。
尽管记住了这些是建议,但是输入的命令可能不包括你所建议的参数,所以仍然需要解析命令,查看参数是不是合法的,如果输入了错误的语法,解析器可能还会抛出异常。
要使用建议,应在要给予建议的参数输入之后立即附加该建议,可以是任何参数,正常的客户端异常仍会弹出,注意这不能用作字面量。
- argument(argumentName, word())
- .suggests(CompletionProviders.suggestedStrings())
- .then(/*Rest of the command*/));
复制代码
Require(需求)
比方说,你有个命令,希望只能由OP来执行,这是require()方法就派上用场了。方法需要传入一个Predicate<ServerCommandSource>(谓词,返回布尔值的lambda表达式)参数,也即提供一个ServerCommandSource参数测试,决定CommandSource会不会执行这个命令。例子如下:
- dispatcher.register(literal("foo")
- .requires(source -> source.hasPermissionLevel(4))
- .executes(ctx -> {
- ctx.getSource().sendFeedback(new LiteralText("You are an operator", false));
- return 1;
- });
复制代码
这个命令只会在命令发起者是一个4级以上的OP时才会执行,如果这个谓词返回false,命令就不会执行。如果不是4级以上的OP,还不会在tab自动完成中显示此命令。
异常Brigadier支持命令异常,用于在参数没有正确解析或者命令执行失败时终结命令。所有的Brigadier异常都基于CommandSyntaxException类。Brigadier提供的两个主要异常类型是Dynamic(动态)和Simple(简单),且你必须调用create()来创建异常才能抛出。如果调用createWithContext(ImmutableStringReader)方法,异常还会允许你在其中指定上下文。虽然这只能用在自定义的解析器上,但是可以在命令执行期间的特定场景中定义和抛出,下面是一个抛硬币指令异常的示例:
- dispatcher.register(CommandManager.literal("coinflip")
- .executes(ctx -> {
- Random random = new Random();
-
- if(random.nextBoolean()) { // If heads succeed.
- ctx.getSource().sendMessage(new TranslateableText("coin.flip.heads"))
- return Command.SINGLE_SUCCESS;
- }
- throw new SimpleCommandExceptionType(new TranslateableText("coin.flip.tails")).create(); // Oh no tails, you lose.
- }));
复制代码
如果不满足简单类型的异常,还有动态类型呢:
- DynamicCommandExceptionType used_name = new DynamicCommandExceptionType(name -> {
- return new LiteralText("The name: " + (String) name + " has been used");
- });
复制代码
还有更多动态异常类型,参数的数量不同(比如Dynamic2CommandExceptionType, Dynamic3CommandExceptionType, Dynamic4CommandExceptionType, DynamicNCommandExceptionType),应该记住动态类型异常采用Object参数,所以可能要转型。
别名重定向
Redirects(重定向)是Brigadier形式的别名机制,以下是MC如何将别名/tell和/w处理成/msg命令的:
- public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
- LiteralCommandNode node = registerMain(dispatcher); // 注册主命令
- dispatcher.register(literal("tell")
- .redirect(node)); // 别名1, 重定向到主命令
- dispatcher.register(literal("w")
- .redirect(node)); // 别名2,重定向到主命令
- }
-
- public static LiteralCommandNode registerMain(CommandDispatcher<ServerCommandSource> dispatcher) {
- return dispatcher.register(literal("msg")
- .then(argument("targets", EntityArgumentType.players())
- .then(argument("message", MessageArgumentType.message())
- .executes(ctx -> {
- return execute(ctx.getSource(), getPlayers(ctx, "targets"), getMessage(ctx, "message"));
- }))));
- }
复制代码
redirect()方法在命令树中注册了一个分支,执行一个重定向指令时告知CommandDispatcher,转向不同的分支查找更多参数,并执行命令体,redirect()使用的字面量参数也将重命名新命令中目标分支上的第一个字面量。
redirects()无法用于被缩短的别名,比方说/mod thing <argument>有个/things <argument>就不行,因为Brigadier不允许转向有子节点的节点,你只能用别的替代方法减少冗余代码。
ServerCommandSource
如果你想要一个发起者(CommandSource)必须是实体(不是玩家)的命令呢?ServerCommandSource类提供了几个方法。
- ServerCommandSource source = ctx.getSource();
- // 获取命令发起者,一直有效。
-
- Entity sender = source.getEntity();
- // 未检查,如果发起者是控制台的话则可能是null
-
- Entity sender2 = source.getEntityOrThrow();
- // 如果发起者不是实体则终止指令
- // 获取结果可能是玩家,向玩家发送反馈,告知发起者必须是实体
- // 该方法需要你的方法抛出一个CommandSyntaxException.
- // ServerCommandSource中可以返回一个CommandBlock(命令方块)实体,任何活的实体和玩家
复制代码
ServerCommandSource还提供了命令发起者的其他信息:
- source.getPosition();
- //以Vec3(三维向量)的形式获取发起者的位置,可以定位实体和命令方块,如果是控制台则是这个世界的出生点
复制代码
一些实例
广播消息:
- public static void register(CommandDispatcher<ServerCommandSource> dispatcher){
- dispatcher.register(literal("broadcast")
- .requires(source -> source.hasPermissionLevel(2)) // Must be a game master to use the command. Command will not show up in tab completion or execute to non op's or any op that is permission level 1.
- .then(argument("color", ColorArgumentType.color())
- .then(argument("message", greedyString())
- .executes(ctx -> broadcast(ctx.getSource(), getColor(ctx, "color"), getString(ctx, "message")))))); // You can deal with the arguments out here and pipe them into the command.
- }
-
- public static int broadcast(ServerCommandSource source, Formatting formatting, String message) {
- Text text = new LiteralText(message).formatting(formatting);
-
- source.getMinecraftServer().getPlayerManager().broadcastChatMessage(text, false);
- return Command.SINGLE_SUCCESS; // Success
- }
复制代码
/giveMeDiamond首先是注册“giveMeDiamond”指令的基础代码,然后执行体告知CommandDispatcher运行哪个方法。
- public static LiteralCommandNode register(CommandDispatcher<ServerCommandSource> dispatcher) { // You can also return a LiteralCommandNode for use with possible redirects
- return dispatcher.register(literal("giveMeDiamond")
- .executes(ctx -> giveDiamond(ctx)));
- }
复制代码
因为钻石只能给玩家,所以要检查CommandSource是不是玩家,可以用getPlayer()方法,同时检测,如果发起者不是玩家,就抛出错误。
- public static int giveDiamond(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
- ServerCommandSource source = ctx.getSource();
-
- PlayerEntity self = source.getPlayer(); // 如果不是玩家命令则结束
复制代码
然后我们将其添加到玩家的库存(背包)中,检查一下这个Inventory对象是否为空:
- if(!player.inventory.insertStack(new ItemStack(Items.DIAMOND))){
- throw new SimpleCommandExceptionType(new TranslateableText("inventory.isfull")).create();
- }
- return 1;
- }
复制代码
安条克手雷
(译注:上图取自rimworld的中世纪mod翻译,安提拉和安条克是antioke音译)
来开个玩笑,这个指令会把一个点燃的TNT放到给定位置,或者命令发起者的准星位置。
首先向CommandDispatcher注册一个条目,采取一个字面量“安条克手雷”,以及一个可选参数,为召唤实体的位置。
- public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
- dispatcher.register(literal("antioch")
- .then(required("location", BlockPosArgumentType.blockPos()
- .executes(ctx -> antioch(ctx.getSource(), BlockPosArgument.getBlockPos(ctx, "location")))))
- .executes(ctx -> antioch(ctx.getSource(), null)));
- }
复制代码
然后创建笑话,发送消息:
- public static int antioch(ServerCommandSource source, BlockPos blockPos) throws CommandSyntaxException {
-
- if(blockPos==null) {
- blockPos = LocationUtil.calculateCursorOrThrow(source, source.getRotation());
复制代码
用命令找群系
这个实例展示了重定向,异常和建议,和一点文本,注意这个命令可以工作,但是需要一点时间才能和/locate效果类似:
- public class CommandLocateBiome {
- // 首先注册
- public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
- LiteralCommandNode<ServerCommandSource> basenode = dispatcher.register(literal("findBiome")
- .then(argument("biome_identifier", identifier()).suggests(BiomeCompletionProvider.BIOMES) // We use Biome suggestions for identifier argument
- .then(argument("distance", integer(0, 20000))
- .executes(ctx -> execute(ctx.getSource(), getIdentifier(ctx, "biome_identifier"), getInteger(ctx, "distance"))))
- .executes(ctx -> execute(ctx.getSource(), getIdentifier(ctx, "biome_identifier"), 1000))));
- // 注册重定向
- dispatcher.register(literal("biome")
- .redirect(basenode));
- }
- // 方法开始
- private static int execute(ServerCommandSource source, Identifier biomeId, int range) throws CommandSyntaxException {
- Biome biome = Registry.BIOME.get(biomeId);
-
- if(biome == null) {
复制代码
问答
我能给CommandSource发送什么样的反馈信息?可以用Brigadier的默认LiteralMessage,或者任何MC自己的文本类(LiteralText, TranslatableText)。
为什么我的IDE会报告命令的execute方法“needs to catch or throw a CommandSyntaxException”?解决办法是将你那抛出CommandSyntaxException的方法放到调用链底端,因为执行体处理异常。
可以在运行时注册命令吗?可以是可以但是不推荐,需要获取CommandManager的实例,并添加任何你想添加到CommandDispatcher的东西到其中。此后需要用CommandManager.sendCommandTree(PlayerEntity)将你的命令树发送到每个玩家。
可以在运行时取消注册命令吗?也可以这么做,但是很不稳定,会导致不可预料的副作用,反正就是涉及到一堆反射。
而且你还要CommandManager.sendCommandTree(PlayerEntity)将你的命令树发送到每个玩家。
可以注册客户端的命令吗?如果你只是想让命令只在本地服务端可见,像/publish那样,可以这样调整你的代码:
- dispatcher.register(literal("publish")
复制代码
我想要在命令运行时访问我的mod可以用getInstance()方法,静态访问你的mod,下面是一个很简单的系统,可以放在你的mod中:
- private static Type instance;
-
- static { // 类初始化的静态选项,方便别的api调取
- instance = new Type();
- }
-
- public void onInitalize() { //如果在你的主类中
- instance = this;
- }
-
- public static Type getInstance() {
- return instance;
- }
复制代码
想在玩游戏的时候把僵尸的声音替换成纸片人老婆的声音吗?看这里看这里。
播放已经存在的声音播放预置的声音很简单,确定在逻辑服务端上,然后如此调用world.playSound():
- if (!world.isClient) {
- world.playSound(
- null, // 玩家,不知道所以置null,如果知道的话写上
- blockPos, // 声音的来源位置
- SoundEvents.BLOCK_ANVIL_LAND, // 在这里是铁砧掉落的声音
- SoundCategory.BLOCKS, // 指定哪个音量条控制声音
- 1f, // 音量因子, 1是正常, 0.5是半音量, 等等
- 1f // 音高因子, 1为正常, 0.5是半音高, 等等
- );
- }
复制代码
通过重写Block.activate()方法,指定方块右击时的行为来播放声音:
- public class ExampleBlock extends Block {
- [...]
-
- @Override
- public boolean activate(BlockState blockState, World world, BlockPos blockPos, PlayerEntity placedBy, Hand hand, BlockHitResult blockHitResult) {
- if (!world.isClient) {
- world.playSound(null, blockPos, SoundEvents.BLOCK_ANVIL_LAND, SoundCategory.BLOCKS, 1f, 1f);
- }
- return false;
- }
- }
复制代码
添加自定义声音
添加MC没有的声音需要点别的步骤,在这里添加一个“
punch”音效(CC0许可证)。
步骤1:添加.ogg声音文件播放一段乐曲,首先需要声音文件,MC使用.ogg文件,如果你的乐曲格式不一样(比方说示例的这个是wav),需要一个在线转换器转成.ogg,现在将.ogg文件放在resources/assets/modid/sounds文件夹下,这里就是resources/assets/tutorial/sounds/my_sound.ogg文件。
步骤2: 添加sounds.json文件,或者添加到已有的文件在resources/assets/modid路径下创建一个新文件sounds.json,如果你还没有的话。然后以你的声音文件名添加一个新条目,在“sounds”部分填入你的Identifier,像这样:
- {
- "my_sound": {
- "sounds": [
- "tutorial:my_sound"
- ]
- }
- }
复制代码
还可以给你的声音文件添加一个类别(category)和子标题(subtitle):
- {
- "my_sound": {
- "category": "my_sounds",
- "subtitle": "*punch*",
- "sounds": [
- "tutorial:my_sound"
- ]
- }
- }
复制代码
步骤3:新建声音事件用modid:sound_name这个Identifier新建一个SoundEvent实例,如下:
- public class ExampleMod {
- [...]
- public static final Identifier MY_SOUND_ID = new Identifier("tutorial:my_sound")
- public static SoundEvent MY_SOUND_EVENT = new SoundEvent(MY_SOUND_ID);
- }
复制代码
步骤4:注册事件以SOUND_EVENT形式注册你的SoundEvent:
- @Override
- public void onInitialize(){
- [...]
- Registry.register(Registry.SOUND_EVENT, ExampleMod.MY_SOUND_ID, MY_SOUND_EVENT);
- }
复制代码
步骤5:使用事件像刚才那样使用SoundEvent(Block.activate()只是个示例,任何你能访问World实例的地方都能写这个):
- public class ExampleBlock extends Block {
- @Override
- public boolean activate(BlockState blockState, World world, BlockPos blockPos, PlayerEntity placedBy, Hand hand, BlockHitResult blockHitResult) {
- if (!world.isClient) {
- world.playSound(
- null,blockPos, ExampleMod.<b>MY_SOUND_EVENT</b>,SoundCategory.BLOCKS, 1f,1f);
- }
- return false;
- }
- }
复制代码
现在右击方块时就能听到“punch”声音了!
问题解决
这里是由fabric api用户提出的fabric编程建议集锦。
基础
- 由于fabric api是基于注入的,所以不建议给类打补丁(patch),让修改赤裸裸暴露在用户面前,同时如果你遇到了难以解决的问题,fabric的原版类扩展可能会有帮助,比如:
- Block.Settings → FabricBlockSettings
- EntityType.Builder → FabricEntityTypeBuilder
- 官方配置系统尚未完成,目前可以用Java的.properties或者json文件代替。
- 对于内置的资源包和数据包,请确保各自的路径是“assets/[mod id]”和“data/[mod id]”,idea用户可能会无意间建立一个“assets.[modid]”路径,这是无效的。
Mixins- 要把类转换成它没有实现的接口,或者转换一个final类,或者将mixin转成目标类,可以用“(TargetClass) (Object) sourceClassObject”的把戏。
- 要调整构造函数,使用“<init>”(静态构造器则是“<cinit>”)作为mixin的方法名,注意构造器上的@Inject注解只适用于@At("RETURN"),官方不支持其他的注入方式!
- @Redirect和@ModifyConstant注解目前还不能嵌套(应用于同时修改同一个地方的多个mod),后续开发中可能改进——但是就现在,尽可能避免使用它们和@Overwrite注解(我们在讨论将钩子带到fabric api中,或者再细一点——将其放入一个jar in jar(指大jar里套的小jar))。
- 如果添加自定义的字段和方法,尤其是当它们和接口没有关联时——可以考虑给它们加“modid_”前缀,或者别的什么特别的字符串,比如“mymod_secretValue”代替“secretValue”,用来避免不同的mod添加了同名字段或方法之间的冲突!
网络- 包总是在网络线程(network thread)被执行,但是MC大多数东西的访问都是线程不安全的。一般来说如果你不确定你在做什么,就需要在网络线程上解析所有包(读取出所有值),然后在服务端/客户端主线程上使用TaskQueue<T>这样的任务队列执行附加操作。
坑- 避免使用任何java.awt包,这个(古老的)包在所有的系统上都有问题,几个用户说它往往会挂起MC。
以下教程告诉你如何将你的mod更新到fabric 0.4或者更高版本(译注:如果你是看到这里才开始做mod那么就不需要看这篇文章)
模式改变模式(shema)有一些改变,0.4.x版本用的是schemaVersion 0,但是0.5.x用的就不是了。
有用的资源:
嵌套的jar嵌套jar的使用有很多混淆不清的地方。
嵌套jar是:
- fabric mod形式的依赖提供方案,允许fabric loader根据整合包的依赖设置匹配最好的依赖版本。
- 允许将类库转为fabric mod的解决方案,避免内嵌不干净(译注:指玩家装了一个前置,mod内部又带一个不同版本的前置)造成的冲突,还允许fabric mod开发者成为权威的版本来源。
- 在联立的大jar中,清晰地分开子项目/子模块,使之可以分离使用。
嵌套jar不是:
- (直接)用在非mod的Java类库上。
- 如果类库可以安全嵌入不同的包中,那么嵌套jar就不是最好的解决方案。注意fabric的modid只能存在一个,意思是潜在的版本冲突会阻止加载一个包——内嵌的类库没有这个问题。
如有疑问,参见下图:
不兼容
- 如果你的mod使用了plugin-loader,请抛弃它然后使用新的入点(entrypoint)机制。
fabric.mod.json
fabric.mod.json文件是fabric loader用来加载mod的一个元数据文件,换句话说,一个mod要将此文件放在mod jar文件的根目录下,不能改名才能被加载。
必要字段
可选字段- environment:定义了mod运行在哪里,只在客户端 (客户端mod), 只在服务端 (插件), 两边都有 (一般mod).。包含了三种标识符:
- *:两处都运行,默认
- client:只运行在客户端
- server:只运行在服务端
- entrypoints: 定义了mod的主类在哪里。
- mod有三种entrypoint(入点):
- main:首先运行,针对实现了ModInitializer的类。
- client:第二个运行,只在客户端,针对实现了ClientModInitializer的类。
- server: 第二个运行,只在服务端,针对实现了DedicatedServerModInitializer的类。
- 每个入点都可以加载任意数量的类。类(或者是方法,静态字段)可以用如下两个方法指定:
- 如果用的是Java,直接填入全引用名即可,例如:
- "main": [
- "net.fabricmc.example.ExampleMod",
- "net.fabricmc.example.ExampleMod::handle"
- ]
复制代码 - 如果用的是别的和Java兼容的语言,还有fabric适配器的话,应该使用下面的语法:
- "main": [
- {
- "adapter": "kotlin",
- "value": "package.ClassName"
- }
- ]
复制代码
- jars: 一批放在你的jar文件里准备加载的小jar,在使用jar字段前,请先阅读“高级:从fabric loader 0.3迁移到0.4”部分。每个条目都是一个包含有“file”键的对象,是你的mod jar到嵌套的小jar之间的通路,比如:
- "jars": [
- {
- "file": "nested/vendor/dependency.jar"
- }
- ]
复制代码- languageAdapters:使用适配器字典,将你所用的语言适配到Java,如下:
- "languageAdapters": {
- "kotlin": "net.fabricmc.language.kotlin.KotlinAdapter"
- }
复制代码
mixins: 一些mixin配置文件。每个条目都是mixin配置文件的位置,或者是持有以下字段的对象:
- config: 指向你的mod jar内部的mixin文件位置。
- environment:和上面说的environment字段作用一样,如下:
- "mixins": [
- "modid.mixins.json",
- {
- "config": "modid.client-mixins.json",
- "environment": "client"
- }
- ]
复制代码
依赖关系
下面的每个条目都是modid,而条目所对应的值是字符串或者字串数组,指定了支持的版本范围,如果是数组,那么数组元素间关系就是“OR”,换句话说,要指定很多版本,只要一个范围就行了。
在所有版本中,*是个特殊符号,声明可以匹配任何版本,顺便,无论版本类型如何,都必须能够进行精确的字符串匹配。
- depends 代表必要前置,没有这个mod,游戏就会崩溃。
- recommends 代表不必要的依赖,没有这个mod游戏会有警告。
- suggests 代表联动,作为一个元数据而存在。
- breaks 代表不兼容,它们和你的mod共存,则会导致崩溃。
- conflicts 代表冲突,这些mod和你的mod共存会引发一些bug,这时游戏会有警告。
- "depends": {
- "fabricloader": ">=0.4.0",
- "fabric": "*"
- },
- "suggests": {
- "flamingo": "*"
- }
- }
复制代码
元数据
- name: 定义了对玩家友好的名称,如果没有提供,默认会和modid相同。
- description: 定义了mod的描述,如果没提供就是空的。
- contact: 定义了开发者的联系方式,具有如下字段:
- email:开发者的邮箱,需要是个有效邮箱地址。
- irc: 开发者的IRC频道,必须要是个有效链接,比如irc://irc.esper.net:6667/charset。端口可选,若不指定默认是6667.
- homepage:开发者的主页,必须是有效的HTTP/HTTPS地址。
- issues: 反馈页面,需要是有效地址。
- sources: 源代码库位置。可以根据使用的VCS写成特殊的URL。
- mod还会提供其他的字段,比如微博,推特,QQ之类的,都要是有效URL。(QQ不一定是)
- authors 作者名单。
- contributors 贡献者名单。
- license 许可证信息。
- icon 定义了mod的图标,一个方的png图片(MC资源包用的是128×128,不过不硬性要求,可以是更高清晰度) ,有两种形式提供:
自定义字段
可以在custom里面定义自己的字段,fabric loader会忽略它们,不过如果你的字段进入了标准规范,那么高度推荐命名空间化你的字段,防止冲突。
重要:这段文档描述的是还在开发中的特性,目前还不可用,可能会修改。发布的目的是帮助开发者社区评估和改进所提到的特性。
fabric渲染API
渲染API为Renderer(渲染器)类的实现指定了接口和钩子,一个实现应该有如下特性:
- 增强的方块模型渲染:同一个模型下应有直射光,漫射光和封闭环境光的控制,多个混合模式(固体,镂空,透明)。
- 动态方块模型:一些或者所有的方块模型都可以基于世界状态,在区块重建的过程中生成或者调整,无论有没有方块实体。
- 增强的物品模型渲染:物品模型应和方块模型有相似的选项,输出的模型也可以根据物品状态动态变化。
这个API是灵活的,这样渲染器的实现就可以引入新光照,特殊效果和性能优化,和依赖于此API的模型的良好兼容性,一些渲染器可能着眼于艳丽的效果,一些可能着眼于性能,还有一些尝试取得平衡。
自由度取决于两个关键的设计:首先,fabric在渲染器自身实现中代理了大多数功能——所以fabric几乎没有补丁(作为API的一部分)。
其二,API的规范隐藏了模型的顶点格式,顶点数据结构和其他实现细节,不直接获取和操纵模型的顶点数据,相反,API定义了轻量接口获取材质(material)和构建/输出模型内容,使用这些接口的mod开发者可以确保他们的内容能够以各种实现方法得到良好的渲染。
不同的人应该知道……
mod开发者很多mod作者会使用三方库创建和加载模型,但是使用库的时候,很可能需要API的许多功能,甚至是必需的,如果这符合你的情况,这部分wiki还是有用的,但是不必阅读所有部分。
模型加载器开发者
fabric渲染api做来就是为了做任何模型加载器的后端,但是不指定也不实现任何模型格式,制作一个模型加载器,对fabric开发是很大的贡献!如果你想制作模型加载器,应该看完一整个API和下面的部分。
模型类库开发者
渲染API只给创建模型提供了最基本的原型,创建一个模型生成和变换程序对fabric的开发贡献也很大,如果你想制作模型类库,应该看完一整个API和下面的部分。
注意: 如果你的类库是在运行时(即MC加载后)生成模型,应该好好想想,这可能会拖慢性能,然后再熟悉熟悉技术,不要这样做。
渲染器开发者渲染API的质量只和相应的实现一样,建立和维护一个新渲染器很可能非常麻烦,在开始之前,问自己几个问题:
- 你要做什么?你的渲染器非常快吗?有尝试节约内存吗?可以带来新特效吗?
- 你的渲染器应该支持所有fabric api所支持的特性,你明白整个api怎么用了吗?
- 你想引入或者支持什么样的api扩展?
- 你知道怎么编写和调试mixin吗?
- 你的渲染器如何调整MC的渲染流水线?需要在哪打补丁?
- 如何避免过度内存分配?
- 如何保证线程安全?
还想做渲染器吗? 那么下面的教程可以帮你开始,顺便在discord里面说一声,我们就把你的实现放在这里面!
准备开始!
要在开发环境中准备fabric api,渲染api也是其中的一部分。查看“配置开发环境”部分了解更多。
fabric带有一个默认的渲染器(IndigoRenderer),提供了API中的绝大多数特性(不处理直射光物品渲染),默认渲染器的兼容性很好,视觉效果等同原版,大多数情况下都有良好性能。
如果fabric api提供的特性足够,你其实需要的就是默认渲染器。玩家和整合包作者可能会用别的实现来渲染你的物品,得到更好的性能或者不同的视觉效果(比如一个光影包)。
游戏会话中只能有一个渲染器实现,如果有非默认的渲染器实现,默认的就会自动关闭,如果有两个非默认渲染器,MC就会在第二个实现尝试注册自己时崩溃。
一些渲染器实现可能会提供更大的特性集,如果你做的mod重度依赖这些特性,请跟用你mod的人说清楚,你的客户会被限定成使用渲染器来支持这些额外特性的玩家和整合包作者。
获取渲染器实例
要使用API的特性,需要一个渲染器实例的引用,获得实例很简单:
- RendererAccess.INSTANCE.getRenderer()
复制代码
保留对渲染器实例的引用是安全的,这个实例可以保证非空,除非你这里的fabric不带渲染器或者默认的渲染器被禁用了(这并不正常),如果这时你的mod依赖了api中的特性,那么没有渲染器,你的mod就该崩了。
材质(Material,而不是Texture)
每个通过渲染api发送的四边形都有一个相关的RenderMaterial,材质(Material)可以让你控制纹理(Texture)的混合和发光,在未来的API扩展中,材质将成为其他行为的连接点,很多你想达成的效果都可以用材质选择完成。
获取材质使用Renderer.materialFinder()获取一个MaterialFinder引用,保留这个引用是安全的,使用MaterialFinder指定你想要的材质属性,然后使用MaterialFinder.find()获取RenderMaterial实例,然后就可以把这些东西发送给渲染器。
注意:调用find()方法不会改变MaterialFinder的任何属性,如果想要几个有相似属性的材质,这么做很有用。如果相反,你想要大为不同的材质,可以调用MaterialFinder.clear()重置MaterialFinder实例,然后再指定你的下一个材质。
材质实例一经取得就是不可变和不透明的,有相同属性的材质可能不会通过“==”和equals()的测试,它们存在的意义只是把材质属性传达给渲染器——并不意味着会被模型分析。这样给予了渲染器实现最大的材质处理灵活性。
命名材质
可以用Renderer.registerMaterial(),传入一个命名空间化标识符(identifier)来注册材质,用Renderer.materialById()来取回材质,如果你发布了一个允许三方扩展的mod,还希望给这些扩展一个使用恒定材质的方法,这么做就很有用——注册之后的材质才会被扩展“看到”。
被命名的材质还可以用于渲染器实现本身,暴露出那些有特殊效果的材质,这种特殊效果,标准材料一辈子也没有。提供自定义材质的渲染器负责声明和暴露获取材质的标识符。
没有注册材质的渲染器/mod,就没有这些材质,如果你的mod依赖于被命名的材质,应该让mod显式依赖于它们的实现,运行时检查这些材质是否存在,如果材质不可用则换上备用材质。
材质属性
可以从按照如下属性挑选材质:
混合模式(BlendMode)- 允许多个渲染层的效果叠加在一个方块/模型上。
- 通过MaterialFinder.blendMode()设置
- 接受BlockRenderLayer枚举值
- 材质不一定在给定的通路(pass)中渲染——只是看上去如此(一些渲染器可能会合并通路,或者做其他的优化,不要给BlockRenderLayer通路写mixin)。
- 如果模式是空(即默认),地形渲染器会使用Block.getRenderLayer()的值,其他的渲染上下文会使用BlockRenderLayer.TRANSLUCENT作为默认。
漫射光(Diffuse)开启和关闭- 控制漫射光的颜色调整。
- 默认开启。
- 用MaterialFinder.disableDiffuse()禁用漫射光。
- 在原版MC,这会导致方块的边缘和底部变得更暗,差别可见。
- 渲染器实现可能使用不同的漫射光模型,但是仍然遵守这个设置。
密闭环境光(Ambient Occlusion Shading)开启/关闭
- 控制密闭环境光的颜色调整。
- 默认打开。
- MaterialFinder.disableAo()以禁用。
- 在原版这会导致内部的角落更暗,创造视差。
- 渲染器实现可能会换密闭环境光照模型,但仍然遵守这个设置。
直射光渲染(Emissive Rendering)开启/关闭
- 启用之后,四边形会全亮度发光。
- 默认禁用。
- 通过MaterialFinder.emissive()启用。
- 不需要提供逐顶点光照图,也不建议你去提供。
- 除非被禁用,否则漫射光和密闭环境光也会起作用。
精灵(Sprite)深度- 默认是1,渲染api目前也只支持1.
- 大于1的值以后会有,为扩展准备。
- 扩展可以用这个在四边形上放若干个精灵,制作表面特效
颜色索引(ColorIndex)开启/关闭- 控制方块颜色索引应用于顶点颜色。
- 如果方块的colorindex != -1则会引用默认值(MutableQuadView.colorIndex()来设置)。
- 使用MaterialFinder.disableColorIndex()阻止颜色索引的应用。
- 一般只有在支持精灵深度>1的扩展上才有用,这种情况下允许方块颜色只应用于特定精灵图层。
其他的Mesh(网格),RenderContext(渲染上下文),Dynamic Rendering(动态渲染)……全都没写,因为只是草案。
fabric loader是fabric的轻量级mod加载器,提供了使MC可修改(modifiable)的必要工具,而不依赖特定的MC版本。游戏和版本限定的钩子则在fabric api中,换言之fabric loader可以适配给很多Java应用。
fabric loader有允许mod在初始化期间执行一些代码的服务,在多个不同的环境中修改类,声明和提供mod依赖。
mod(模组)
mod就是个带有fabric.mod.json元数据文件的jar文件,元数据文件指定了这个modze怎么加载,主要是声明了一个modid和版本,还有mod入点(主类)和mixin配置。modid唯一标识了mod,任何带有相同id的mod都会被认为是相同的mod,一次只会有一个mod版本会被加载,一个mod可能会声明其所依赖或者跟它冲突的mod,fabric loader会尝试满足依赖,加载合适版本的mod,否则就启动失败。 类路径(classpath)和mods文件夹都可以用来加载mod,它们应该匹配当前环境的反混淆表(译注:如果版本不匹配就可能有NoSuchMethodError之类的报错),也就是说fabric loader不会再重混淆任何mod了。
fabric loader使所有的mod平等地修改游戏,举例,任何fabric api做的事情都可以由别的mod完成。
嵌套Jar
一个mod(比如fabric api本身)可能是一堆mod的集合体,查看“从fabric loader 0.3更新到0.4”来更好地使用嵌套mod jar。嵌套jar文件必须要在其(指外面的大jar)根目录下声明其路径,被嵌套的小jar必须也是个mod(不是的可以按照教程改),下面还可以继续嵌套,fabric loader在尝试满足依赖时,会加载嵌套的jar文件。
嵌套的jar文件允许mod提供自己的依赖,这样fabric loade就可以选择最好的依赖版本,而不是分别安装,还使得子模块打包更清晰,每个模块都可以分开使用,非mod的类库也可以被重新包装成mod,用于嵌套jar。
当被加载时,嵌套jar会使用Jimfs加载到一个内存内文件系统,然后再添加到类路径。
入点(EntryPoint)
入点是mod暴露自身代码元素,比如类,字段和方法供fabric loader和其他mod使用的途径,fabric loader使用入点初始化mod,其他mod可以指定自己的入点类型。入点由语言适配器(language adapter)加载,后者会根据代码元素的名称,尝试提供给定类型的Java对象。
只有相应类型的入点要被加载时,入点才会被加载,这使得入点成为了一个可选mod集合的绝佳工具。一个mod可通过通告其他mod,应该提供特定名称,使用该mod在其api中提供的类或者接口的入点,来支持一个特定的入点类型。其他mod请求的时候入点才会被加载,避免了mod没安装的时候,可能没有提供类的任何问题。
fabric loader有三个内建的入点,与物理端有关,这些入点在游戏初始化期间尽可能早地执行,也就是说不是所有东西都会被初始化,或者准备进行调整的。这些入点一般通过注册Registry对象,事件监听器和其他以后处理的回调来启动mod。
- main:首先运行,针对实现了ModInitializer的类。
- client:第二个运行,只在客户端,针对实现了ClientModInitializer的类。
- server: 第二个运行,只在服务端,针对实现了DedicatedServerModInitializer的类。
默认的语言适配器是给Java用的,支持类,方法和字段这些代码元素:
- 类: net.fabricmc.example.ExampleMod 用类名来引用那个主类,这个类必须有一个公共无参构造器,必须实现所期望的类型(默认指ModInitializer),返回值是类的实例。
- 方法: net.fabricmc.example.ExampleMod::method 用方法名来引用方法,如果方法不是静态的,那么就会构造一个类的实例,来调用这个方法,所以说方法归属的那个类还是要有无参构造器。方法(入点)只能用于接口类型,且必须和接口中的抽象方法有一样的参数和返回值,返回值是个接口的代理实现,通过代理来实现抽象方法。
- 字段: net.fabricmc.example.ExampleMod::field 用字段名去引用字段,字段必须是公开和静态的。字段类型必须是期望的类型,返回值是字段的值。
对类型成员的引用必须是无歧义的,也就是说类型必须包含唯一一个这个命名的成员,否则入点就加载失败了。
Mixin
mod可以用mixin修改MC的类,和其他mod的类,也是fabric loader唯一允许修改类的工具,一个mod可声明其mixin配置(使之生效),fabric使用的是一个轻量魔改版的mixin,但是原始文档仍然有效,对mixin的魔改主要是为了让其不依靠LegacyLauncher/LaunchWrapper而运行。
混淆与反混淆
译注:前面已经讲过,重要的就一句:重混淆的jar缓存在游戏目录(gameDir)/.fabric/remapedJars/minecraft版本 文件夹下,下次启动重用。
类加载和修改
fabric loader靠自定义的类加载器来在运行时修改一些类,属于mod或者MC的类会在其加载之前就被一个应用了修改的类加载器“抢占”,其他的类,比方说类库,不会被修改。Knot负责将这些类代理到fabric的默认类加载器来保证隔离性和性能。
fabric loader会根据类所运行的物理端对原版和mod的类执行剥离,涉及到完全移除带有@Environment或者@EnvironmentInterface注解且环境对不上的类成员,在MC的类中,这一功能用于模拟可用于目标运行时环境的类和类成员,这个注解还可以用于mod类,避免类加载问题。
基于当前的反混淆环境,可能要对MC包进行访问级别的改动(public/private/protected)。按照官方混淆和中间名,大多数类都是放在同一个包里的,但是,yarn反混淆把类放在不同的包里,有时会导致访问无效(IllegalAccessException),就是因为访问级别的不同。因此在开发环境下,MC类被修改,所以private级别的包和protected的类成员都改成public的了,不过在游戏环境中,包结构就变成一个包,所以不再需要访问级别修改。注意这一修改是用在运行时的,源码里不可见。
启动器(当然,不是HMCL那种)
“启动器”是Java进程中使用fabric loader的途径,一个启动器必须提供一些支持fabric loader功能的特性,比如类修改和动态类加载,Knot和LegacyLauncher/LaunchWrapper是当前支持的启动器。
Knot是fabric默认使用的启动器,专为fabric loader而设计,支持现代版本(指9-11)的Java,具有主类net.fabricmc.loader.launch.knot.KnotClient和KnotServer,分别用于客户端和服务端。
在生产环境中运行一个带Knot的服务端时,net.fabricmc.loader.launch.server.FabricServerLauncher 会开始执行,这是一个包装了KnotServer运行的主类,可以用fabric-server-launcher.properties来配置。这个文件有个属性叫serverJar,默认值是“server.jar”,用于配置minecraft-server.jar的路径。
也可以用net.fabricmc.loader.launch.FabricClientTweaker和FabricServerTweaker来为客户端和服务端,用LegacyLauncher/LaunchWrapper启动fabric loader。
fabric loom,或者简称loom,是一个用于fabric mod开发的gradle插件。loom提供了在开发环境中安装MC和mod的工具,这样你才能根据Minecraft混淆和分发版本之间的差异来链接它们(指MC和mod)。loom还提供了使用fabric loader的运行配置,mixin编译进程,和fabric loader的jar-in-jar系统所用的工具。
依赖配置项
- minecraft: 决定了在开发环境中使用的MC版本。
- mappings: 决定了开发环境中使用的反混淆表版本。
- modCompile, modImplementation, modApi and modRuntime: compile, implementation, api和runtime 这几个gradle配置项的mod版变种,会根据当前的反混淆表版本重混淆mod,嵌套的jar可以选择提取和重混淆。
- include: 声明一个在remapJar的输出中,应该以jar-in-jar形式打包的依赖,此依赖配置项不可传递。对于非mod的依赖,loom会为其生成一个带有fabric.mod.json的mod jar文件,使用其名称作为modid,版本也是相同的。
默认的gradle任务
- cleanLoomBinaries: 针对当前配置版本的MC和反混淆表,从用户缓存删除合并后的Minecraft.jar,中间名反混淆的minecraft.jar和已经反混淆的minecraft.jar。
- cleanLoomMappings: 针对当前配置版本的MC和反混淆表,从用户缓存删除反混淆表, 中间名反混淆的minecraft.jar和已经反混淆的minecraft.jar,还有项目配置缓存。
- migrateMappings: 将当前的源码迁移到指定版本的反混淆表,参考“使用反混淆表和更新yarn反混淆”一章。
- remapJar: 提供重混淆的jar文件(真正的mod),也可以放在jar-in-jar文件中嵌套。
- genSources: 两个任务的集合体。
- genSourcesDecompile 用FernFlower反编译已经反混淆的MC.jar来生成一个源码(source)jar文件,还生成一个行数表。
- genSourcesRemapLineNumbers 使用生成的行数表生成一个“行对齐反混淆”的jar文件,重新对齐了二进制文件和源码jar文件的行数,行对齐的jar文件会取代一开始的反混淆jar。
- downloadAssets: 根据当前MC版本,下载资源索引和资源文件本身,存储到用户缓存中。
- genIdeaWorkspace: 需要idea和先运行genSources指令,在当前idea项目中安装MC的运行配置项,并创建run文件夹(如果不存在的话)。
- genEclipseRuns: 需要eclipse和先运行genSources指令,安装eclipse的运行配置项,并创建run文件夹(如果不存在的话)。
- vscode: 需要先运行genSources指令,用.vscode目录下的运行配置生成/重写vscode的launch.json文件,并创建run文件夹(如果不存在的话)。
- remapSourcesJar:只有在一个AbstractArchiveTask类型的sourcesJar任务存在时才存在。重新混淆sourcesJar输出的文件。
- runClient: 一个JavaExec任务,将fabric loader作为MC客户端执行。
- runServer:一个JavaExec任务,将fabric loader作为MC专用服务端(远程)执行。
默认配置(译注:真的不是build.gradle和settings.gradle里的东西又讲了一遍?)
- 应用如下插件:java, eclipse和idea.
- 添加如下maven库: Fabric自己:https://maven.fabricmc.net/, Mojang:https://libraries.minecraft.net/, Maven中心库和JCenter(译注:后面两个也可以用腾讯/阿里/华为等的国内maven源代替)
- 配置idea插件排除掉.gradle,.build,.idea和out文件夹,下载javadocs源码,产生输出目录。
- 用genIdeaWorkspace任务,完成idea的配置。
- 用genEclipseWorkspace任务,完成eclipse的配置。
- 如果.idea文件夹已经存在于项目(root project)下,下载资源(dowanloadAssets),在.idea/runConfigurations中安装运行配置。
- 使用annotationProcessor配置项,添加net.fabricmc:fabric-mixin-compile-extensions和其依赖。
- 使用Mixin annotation processor(mixin注解处理器),配置所有的免测试JavaCompile(Java编译)任务。
- 配置remapJar任务,输出一个jar文件(与jar任务输出的jar文件同名),然后给jar任务加上一个“dev”分类标签。
- 配置remapSourcesJar任务,处理sourcesJar任务输出的jar文件,如果后者存在的话。
- 添加remapJar任务和remapSourcesJar任务作为build任务的依赖。
- 配置remapJar任务和remapSourcesJar任务,将其输出作为存档的实例。
- 对每个MavenPublication (来自maven-publish插件):
- 以mod特化版的依赖配置,手动将依赖添加到POM文件中,让依赖配置项有一个maven作用域。
运行配置的主类往往由fabric-installer.json文件定义,这个文件在作为mod依赖被包含时,位于fabric loader的jar文件下,但其实这个文件可以被任何mod依赖所定义,如果没有这个文件,主类就会使用默认的net.fabricmc.loader.launch.knot.KnotClient和KnotServer。
客户端运行配置由–assetsIndex和–assetsDir程序参数(program arguments)控制,分别指向loom缓存文件夹带的资源文件路径,和当前MC版本所带的index(索引)文件,如果在OSX系统上运行,需要加上“-XstartOnFirstThread” JVM参数。
配置项
MC扩展的属性:
- runDir (String): 默认的“运行” 。由运行配置和runServer和runClient任务定义的run文件夹。
- refmapName (String):默认的 “${projectName}-refmap.json” 定义了mixin引用表。
- loaderLaunchMethod (String): 默认String参数是空的,在运行配置里面定义这个参数,指定启动fabric loader的方法。Knot使用的launch()方法是默认的,如果设置成另一个值,loom会尝试读取fabric-installer.${method}.json获取运行配置,如果什么都没找到就退回fabric-installer.json,如果设置成“launchwrapper”且找不到fabric installer的定义,那么运行配置将默认使用launchwrapper来启动fabric loader。
- remapMod (boolean): 默认为true,如果设置成false,就会禁用remapJar, remapSourcesJar和jar任务的配置。
- autoGenIDERuns (boolean): 默认为true,如果是false,就会禁用资源的自动下载和intellij运行配置的生成(如果项目中已经存在.idea文件夹的话)。
- extractJars (boolean): 默认为false,如果是true,loom会递归提取和重混淆mod依赖带来的嵌套jar。
发布
remapJar任务的输出才是我们应该发布的mod实例,而不是jar任务输出的。重要的是,任何使用remapJar的输出的发布任务都依赖于任务本身。不像jar任务,remapJar不是一个AbstractArchiveTask,意味着它需要更多的关心,当和CurseGradle或者maven-publish这样的插件协同工作时,需要正确安装任务依赖,remapSourcesJar在发布源码时,用处与此类似。
使用maven-publish插件时,避免使用components.java,而应该如下声明实例:
- mavenJava(MavenPublication) {
- artifact(jar.archivePath) {
- builtBy remapJar
- }
- // artifact(sourcesJar) {
- // builtBy remapSourcesJar
- // }
- ...
- }
复制代码
使用嵌套jar发布一个项目到maven库留待开发用时,把嵌套的依赖换成传递(transitive)的依赖应该会让你满意。放在pom文件里的传递依赖,被用户用构建系统引入到开发环境时,有更好的集成机会,允许用户将源码附着在传递依赖上而不需要给编译和运行类路径添加额外的配置。
任务类型- net.fabricmc.loom.task.RemapJarTask: 输入一个jar,输出一个重混淆之后的jar,应该被配置成依赖于生成输入jar的任务。remapJar任务不是AbstractArchiveTask,属性如下:
- input (Object): 默认参数是空的,该参数定义了要被重混淆的jar文件,用Project.file解决。
- output (Object): 默认参数是空的,定义了要输出的文件,用Project.file解决。
- addNestedDependencies (boolean):默认为false,如果是true,loom会使用include依赖配置项指定的依赖嵌套进jar文件中的META-INF/jars,还会在fabric.mod.json文件中声明它们。
- net.fabricmc.loom.task.RemapSourcesJarTask: 输入一个Java源代码jar,输出一个重混淆之后的java源码jar,应该被配置成依赖于生成输入jar的任务。该任务不是AbstractArchiveTask,属性如下:
- input (Object): 默认参数是空的,该参数定义了要被重混淆的源码jar文件,用Project.file解决。
- output (Object): 默认参数是空的,定义了要输出的源码jar文件,用Project.file解决。
- targetNamespace (String): 默认是“intermediary”(中间名) ,定义了要重混淆到的命名空间,只要参数值不是“named”,一律重混淆成中间名。
安装开发环境loom做来是为了在用户的IDE中,建个工作空间/项目,开箱即用的,但是在这背后,为了创建一个MC的开发环境,它做了很多事情:
- 根据当前配置的MC版本,从MC官方渠道下载客户端和服务端jar。
- 把客户端和服务端合并起来,产生一个带有@Environment和@EnvironmentInterface的合并后(merged)jar。
- 下载当前配置的反混淆表。
- 将合并jar文件用中间名表反混淆一下,生成一个中间名jar文件。
- 将合并jar文件用yarn表反混淆,会生成一个“反混淆的jar文件”。
- 可选:反编译反混淆之后的jar文件,产出一个源码jar和行对齐jar,然后行对齐jar替换反混淆的jar。
- 添加MC的依赖。
- 下载MC的资源。
- 处理和容纳mod特化的依赖(可选提取和重混淆嵌套的jar)。
缓存- ${GRADLE_HOME}/caches/fabric-loom: 用户缓存文件夹,可以被所有loom项目共享,用来缓存MC的资源,jar,合并的jar,中间名jar和反混淆的jar。(译注:可以自己新建GRADLE_HOME这个用户变量,借此修改.gradle缓存文件夹的位置)
- .gradle/loom-cache: 项目持久缓存,可以被项目本身和子项目使用,用于缓存重混淆的mod,和include生成的嵌套mod jar。
- build/loom-cache: 项目构建缓存。
- **/build/loom-cache: 子项目构建缓存。
构建流水线(草案)
EnigmaEnigma是用来反混淆Java应用的工具,比如MC,fabric用的是enigma的魔改版,修复了很多bug,用在yarn文件上更有效率。
StitchMatcher和TinyRemapper没写。
作者: GreyElaina 时间: 2019-8-19 11:32
不愧是蝙蝠大佬,轻易的完成了我完成不能的事情.
作者: ruhuasiyu 时间: 2019-8-19 11:50
见鬼,你才翻译一条怎么就发出来了
作者: 森林蝙蝠 时间: 2019-8-19 11:51
我可以多翻译几条,看一条也是看,看20几条也是看。
作者: 南派董卓 时间: 2019-8-22 22:46
翻译这个后发现了自己很多问题啊
作者: 双杀步枪 时间: 2019-8-27 05:01
太棒了,正需要这个
作者: 机械自律体 时间: 2019-8-28 07:57
fabric开发文档,有意思
作者: a2939672760 时间: 2019-8-28 09:07
完成任务
作者: a2939672760 时间: 2019-8-28 09:08
完成任务
作者: lvWG 时间: 2019-8-28 22:58
作者可真是个鬼才
作者: tian051011 时间: 2019-8-31 22:29
优势那段直接用了知乎回答...不过还是很贴切的
感谢翻译,滋磁一波
作者: 森林蝙蝠 时间: 2019-8-31 22:44
本帖最后由 森林蝙蝠 于 2019-8-31 22:46 编辑
我还以为你早就看过呢。
作者: tian051011 时间: 2019-8-31 22:59
森林蝙蝠 发表于 2019-8-31 22:44
我还以为你早就看过呢。
英文版的话 是早看过了 当时没发现站内已经有翻译了
希望你的翻译能带更多人入坑 让Fabric更好的在国内发展
作者: Aikini 时间: 2019-9-11 02:51
qwq不愧是蝙蝠大佬
作者: 鸭蛋只吃黄 时间: 2019-9-12 09:06
public class ExampleMod implements ModInitializer
{
@Override
public void onInitialize()
{
OverworldBiomes.addContinentalBiome(OverworldClimate.TEMPERATE, TutorialBiomes.MY_BIOME, 2D);
OverworldBiomes.addContinentalBiome(OverworldClimate.COOL, TutorialBiomes.MY_BIOME, 2D);
}
}
这两个方法调用好想错了, 应该是生物群系在第一个参数的位置...= =
作者: 森林蝙蝠 时间: 2019-9-12 10:04
确实,应该是作者在写的时候写错了。
作者: langyo 时间: 2019-9-13 15:41
我本来想占坑的,没想到你抢先了
万恶的暑假补课啊...
作者: dobi_dada 时间: 2019-9-13 17:52
BBS因为你们变得更美好!加油
作者: 鸭蛋只吃黄 时间: 2019-9-14 12:30
唉, 上官网连个 javadoc 都没找着, 啃源码啃得我想死......
作者: wulinlin313 时间: 2019-9-15 19:55
ok谢谢随机打劫方便你是哪的就发飞机
作者: meishiffx 时间: 2019-9-17 17:20
大佬呀虽然我看不懂但是感觉好牛逼的样子
作者: 糖果QAQ 时间: 2019-9-20 02:26
长
作者: 容岩 时间: 2019-10-11 16:45
ChanceDecoratorConfig的参数大体上代表生成之前会跳过多少个区块,0是每个区块(什么都不跳过),1是每两个(隔一个),100就是每101个(隔100个)
这一句存在错误:ChanceDecoratorConfig指的是生成概率,而不是“跳过”。设置为100意为“有百分之的可能运行结构生成器”,并不是跳过前100个区块。也就是说,即使设置为100万,仍然有可能在第一个区块就生成此结构。附代码(mcp):
- public Stream<BlockPos> getPositions(IWorld worldIn, ChunkGenerator<? extends GenerationSettings> generatorIn, Random random, ChanceConfig configIn, BlockPos pos) {
- if (random.nextFloat() < 1.0F / (float)configIn.chance) {
- int i = random.nextInt(16);
- int j = random.nextInt(16);
- int k = worldIn.getHeight(Heightmap.Type.MOTION_BLOCKING, pos.add(i, 0, j)).getY() * 2;
- if (k <= 0) {
- return Stream.empty();
- } else {
- int l = random.nextInt(k);
- return Stream.of(pos.add(i, l, j));
- }
- } else {
- return Stream.empty();
- }
- }
复制代码
作者: 容岩 时间: 2019-10-11 17:12
追加一下:
- Biome.configureFeature(myFeature, new DefaultFeatureConfig(), Decorator.CHANCE_PASSTHROUGH, new ChanceDecoratorConfig(0)));
复制代码
在这条代码中,第三个参数和第四个参数必须泛型配套。区块生成时会使用第四个参数的配置来执行第三个参数的相关方法,得到一个BlockPos流。流中记录了此区块中可以生成此特性的方块位置. 第四个有很多配置类,有的类的流中包含若干个坐标。但是就此类来说它只会返回一个“推荐坐标”,只是推荐,具体能不能生成,还是要看特性中的相关方法来决定。
至于第二个参数可以设置为null,上面那句也是如此。因为这两项配置均为特性类自己读取,如果确定自己用不着任何的配置需要写出到文件系统,那么即使设置为null也不会报错(但是ide给出的黄线真的很烦人!)
作者: 森林蝙蝠 时间: 2019-10-11 17:12
ChanceDecoratorConfig's argument is basically how many chunks it will skip over before generating. 0 is every chunk, 1 is every other, and 100 is every 100.
原文就是skip,照章翻译而已。
作者: 容岩 时间: 2019-10-11 17:16
本帖最后由 容岩 于 2019-10-11 17:19 编辑
森林蝙蝠 发表于 2019-10-11 17:12
ChanceDecoratorConfig's argument is basically how many chunks it will skip over before generating. ...
抱歉,文章翻译的太好了,我都忘了是翻译作品。不过翻译顺带纠错也不是不可以嘛(复读机警告)
(这原教程怎么写的连自己的API都不会用了)
作者: 森林蝙蝠 时间: 2019-10-11 17:24
能得到这样的评价,是我的荣幸,感谢。
作者: Light_colour 时间: 2019-12-15 17:40
我在6月做过这个翻译6月的wiki版本和现在不一样了
作者: q549365815 时间: 2019-12-28 06:33
你好 我看下了官网和GitHub wiki 我并没有发现有javadoc 请问我能在哪里看到详细的javadoc呢?
作者: Martmists 时间: 2020-1-29 07:25
Hello 森林蝙蝠,
I have seen you did the main translations for Fabric, would you be interested in helping out in the official support area for Chinese users? I found this forum through a mod on Github, and seeing the translation attempt made me want to help out.
作者: chenzeyi 时间: 2020-1-29 20:31
6666666666666666
作者: Jackson233 时间: 2020-2-6 16:46
* What went wrong:
A problem occurred configuring root project 'fabric-1.15.2-Twilight Forest'.
> Failed to notify project evaluation listener.
> Failed to provide com.mojang:minecraft:1.15.2
> java.lang.NullPointerException (no error message)
出现这个问题该怎么解决
作者: 389582589 时间: 2020-2-27 11:29
LZ辛苦了
作者: 2729477791 时间: 2020-2-27 13:16
我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界我的世界
作者: qind 时间: 2020-4-15 20:38
请问怎么给方块添加ui
作者: hallele 时间: 2020-5-30 14:54
666666666666
作者: 德芙DakFu 时间: 2020-8-13 20:09
强啊我天,大佬
作者: Mooshame 时间: 2020-8-13 21:49
高端妈耶,想学
作者: teddyxlandlee 时间: 2020-8-14 11:10
1.16党表示不知所云
作者: kayn- 时间: 2020-8-15 00:12
有目录也太良心了吧
作者: GregTao 时间: 2020-9-12 21:05
战利品表修改的一直报错
作者: 小天使呀 时间: 2020-9-13 17:32
6
作者: GregTao 时间: 2020-9-18 00:08
RecipeRemainder没办法返回自己模组的物品怎么解决啊
作者: SolidBlock 时间: 2020-11-4 16:02
Fabric官方维基上的文章也是你翻译的吗?
作者: 金属光泽 时间: 2020-11-6 14:09
好!很有精神!
作者: 阿曦少爷 时间: 2020-11-6 21:54
下好的fabric-example-mod要放在哪里鸭
作者: 干就完了 时间: 2020-11-21 11:55
看也都是看,不如用点自己的语言通俗的将文档翻译出来。
作者: lhk960309 时间: 2021-1-4 16:46
MCBBS有你更精彩~
作者: 1908418760 时间: 2021-1-5 03:13
MCBBS有你更精彩~
作者: 洞穴夜莺 时间: 2021-1-29 23:09
本帖最后由 洞穴夜莺 于 2021-1-29 23:11 编辑
[数据删除,回错帖]
作者: Preliterate 时间: 2021-2-5 00:42
我在mod里面使用了sponge的configurate,build.gradle的dependencies是这样写的。
- // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs.
- // You may need to force-disable transitiveness on them.
- compile "com.google.code.gson:gson:2.8.6"
- compile "org.spongepowered:configurate-gson:4.0.0"
- compile "org.yaml:snakeyaml:1.26"
- compile "org.spongepowered:configurate-yaml:4.0.0"
复制代码
在IDEA测试运行的时候是可以的,但是通过gradle打包后就访问不了configurate里面的class了。。
有办法解决嘛?
作者: 神农与炎帝 时间: 2021-2-9 16:38
fabric 官网新更新了添加状态效果等内容,快捷键部分 FabricKeyBinding 类在1.16中已经@deprecated了,取而代之的是 KeyBindingHelper 这个玩意。