TeaCon 2023 言传身教奖获奖文章·一·真正的和平模式(一)

原文载于知乎专栏《【真正的和平模式】一、模组简介与开发环境》,作者为 Viola-Siemens,
原地址:https://zhuanlan.zhihu.com/p/644074598


【真正的和平模式】一、模组简介与开发环境

你可曾想过,为何在Minecraft这片大陆上的怪物总会无差别或有条件地攻击玩家们?生活在这里,难免会感觉到孤独和恐惧——难道只有不停地战斗才是生存下去的唯一途径?不!或许战斗会让你血脉喷张,或许战斗会给你不一样的体验,但这远远不是热闹,而是另一种孤独。
如果,你能够去完成怪物们的心愿,与他们和谐共处,一起愉快地跳舞,一起在阳光下自由地奔跑——这才是真正的和平,这才是真正的热闹。
《真正的和平模式》这个模组添加了十余种自然结构,近十种生物实体,数十个方块和物品,以及对原版的怪物们分别添加了剧情任务。玩家要运用自己的脑力与技术,去完成怪物们的委托,或击败强敌,或探寻珍宝,或生产劳作,或寻觅真理……当玩家完成一种怪物的全部委托后,这种怪物将不再与玩家对立。
去吧,玩家,让这片冷酷而孤独的大陆变得热闹起来吧!

没错,这段内容是我开发的这个新模组的简介内容。由于近日比赛和项目较多,没什么时间来知乎创作了。但TeaCon 2023的这场比赛增设了【言传身教】奖,要求开发者通过编写技术博客为萌新开发者们快速入门、答疑解惑,那我何不写(shuǐ)几篇文章呢?

项目以MIT协议开源,github仓库地址:

https://github.com/Viola-Siemens/Real-Peaceful-Mode


背景

首先我们来看TeaCon 2023的题目:

「热闹」仿佛是中国人的向往的生活目标。春节时节,人们放烟花、贴春联,这是「热闹」的新年;夜市里,人头攒动、摊贩吆喝声不绝于耳,这是「热闹」的集市;闲暇时间,三五好友相约一起,去郊游、去唱歌,这是「热闹」的聚会;灯红酒绿的闹市里,悄然步入远离喧嚣人群的小巷,这是反差的「热闹」
那么你对「热闹」的体会和感觉是什么?什么才是让你感觉身心愉悦的「热闹」?请以此为出发点,写一个模组,自圆其说即可。
示例模组:末影龙舟

审题

题目首先提供了四种热闹:

  1. 放烟花、贴春联,以节日氛围为主,通过喜庆的元素烘托出【热闹】。由此立意大概率会做出别具特色的装饰mod。
  2. 闹市中的叫卖声、丰富而吸引人的商品,通过物质的属性吸引人们一起【热闹】。由此立意,结合前几天我老家淄博烧烤的爆火,说不定可以写出夜市、烧烤等风格的食物mod,比如可以取名为淄博乐事(划掉)。
  3. 三五好友相约郊游唱歌,通过玩家间的交互来体现【热闹】。这是本题中最为扩大开发者立意范围的一条,只要丰富了玩家间的交流,都可以算作热闹,甚至还保留了冒险mod等题材的选择可能。示例模组末影龙舟则是通过玩家间一起齐心协力划龙舟、互相组队赛龙舟体现出了【热闹】的氛围。
  4. 远离喧嚣,背弃世俗,选择孤身奋战——也许这是孤独,也许这十分冷清,但这又何尝不是反差的【热闹】?这是本题最难立意的一条,稍有不慎则会跑题,不少参赛者选择放弃从它立意。既然如此,我选择反其道而行之,从这一条上立意,毕竟我头铁,我怕谁!

另外题目的表述似乎在给予开发者们一个暗示,热闹体现在玩家与玩家之间,体现在物品的使用与交互中,事实上并非如此。我也是考虑到了这一点,萌生了一个大胆的想法——玩家怪物之间,是否也算热闹

基于这个想法,我便在考虑,如果在Minecraft的世界,玩家和怪物能够和平共处,互相帮助,从此玩家不再孤身一人,而是有着无数的旅途伙伴,而无需多人联机,那这样的世界该有多热闹!

这便是反向立意法,在立意时考虑【热闹】的反义词是什么,我认为是【冷清】、【孤独】,而解决掉冷清与孤独之后,便是符合题意的热闹!

进一步的,这片大陆中不止有一种怪物,玩家实现了一种怪物的心愿后,悄然离开去帮助其它的生物,正是符合了第四条的立意。

说干就干!模组策划好了,内容便是,玩家需要通过不断探索世界,帮助怪物们完成他们的心愿,从此对应的怪物将不再攻击玩家,实现真真正正的和平模式

环境配置

Minecraft与Forge版本的选择

根据要求,模组的目标平台为Minecraft 1.20.1,Forge 47.0.1以上——事实上TeaCon开幕不久后,Forge 47.1.0稳定版问世,于是我选择使用它来编译和运行模组。

编写build.gradle

1.20+与先前版本不同,需要使用ForgeGradle 6.0,而gradle的版本也变成了8.1.1,我用之前写1.16.5~1.19.4的build.gradle的写法来配置项目时直接失败了,因此需要注意这一点。

由于需要使用spongepowered mixin,因而需要添加一些dependencies:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
buildscript {
repositories {
maven { url = 'https://maven.minecraftforge.net' }
jcenter()
mavenCentral()
maven { name="sponge"; url 'https://repo.spongepowered.org/repository/maven-public/' }
}
dependencies {
classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '[6.0,6.2)', changing: true
classpath 'org.spongepowered:mixingradle:0.7.32'
}
}

plugins {
id 'eclipse'
id 'idea'
id 'maven-publish'
}
apply plugin: 'net.minecraftforge.gradle'

apply plugin: 'org.spongepowered.mixin'

为了方便调试和生成内容,需添加一些runs的配置,这里我写了四个runs,分别是运行客户端、运行服务端、运行服务端测试和生成内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
java.toolchain.languageVersion = JavaLanguageVersion.of(17)

println "Java: ${System.getProperty 'java.version'}, JVM: ${System.getProperty 'java.vm.version'} (${System.getProperty 'java.vendor'}), Arch: ${System.getProperty 'os.arch'}"
minecraft {
mappings channel: 'official', version: '1.20.1'

accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg')

enableIdeaPrepareRuns = true
copyIdeResources = true
generateRunFolders = true

runs {
client {
workingDirectory project.file('run')

property 'forge.logging.markers', 'REGISTRIES'
property 'mixin.env.remapRefMap', 'true'
property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg"

property 'forge.logging.console.level', 'debug'

property 'forge.enabledGameTestNamespaces', 'real_peaceful_mode'
arg "-mixin.config=real_peaceful_mode.mixins.json"

mods {
real_peaceful_mode {
source sourceSets.main
}
}
}

server {
workingDirectory project.file('run')

property 'forge.logging.markers', 'REGISTRIES'
property 'mixin.env.remapRefMap', 'true'
property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg"

property 'forge.logging.console.level', 'debug'
arg "-mixin.config=real_peaceful_mode.mixins.json"

property 'forge.enabledGameTestNamespaces', 'real_peaceful_mode'

mods {
real_peaceful_mode {
source sourceSets.main
}
}
}

gameTestServer {
workingDirectory project.file('run')

property 'forge.logging.markers', 'REGISTRIES'
property 'mixin.env.remapRefMap', 'true'
property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg"

property 'forge.logging.console.level', 'debug'

property 'forge.enabledGameTestNamespaces', 'real_peaceful_mode'
arg "-mixin.config=real_peaceful_mode.mixins.json"

mods {
real_peaceful_mode {
source sourceSets.main
}
}
}

data {
workingDirectory project.file('run')

property 'forge.logging.markers', 'REGISTRIES'
property 'mixin.env.remapRefMap', 'true'
property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg"

property 'forge.logging.console.level', 'debug'

args '--mod', 'real_peaceful_mode', '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/')
arg "-mixin.config=real_peaceful_mode.mixins.json"

mods {
real_peaceful_mode {
source sourceSets.main
}
}
}
}
}

sourceSets.main.resources { srcDir 'src/generated/resources' }

最后则是forge版本、mixin的依赖,打jar包和和mixin的配置,还有最重要的标题、作者、版本与发布设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
dependencies {
minecraft 'net.minecraftforge:forge:1.20.1-47.1.0'

annotationProcessor 'org.spongepowered:mixin:0.8.5:processor'
}

jar {
manifest {
attributes([
"Specification-Title" : "Real Peaceful Mode",
"Specification-Vendor" : "Liu Dongyu, foliet, Mon3yr1, D-Sketon",
"Specification-Version" : "1", // We are version 1 of ourselves
"Implementation-Title" : project.name,
"Implementation-Version" : project.jar.archiveVersion,
"Implementation-Vendor" : "Liu Dongyu, foliet, Mon3yr1, D-Sketon",
"Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
'FMLCorePluginContainsFMLMod': 'true'
])
}
}

jar.finalizedBy('reobfJar')

publishing {
publications {
mavenJava(MavenPublication) {
artifact jar
}
}
repositories {
maven {
url "file://${project.projectDir}/mcmodsrepo"
}
}
}

mixin {
add sourceSets.main, 'real_peaceful_mode.refmap.json'
config 'real_peaceful_mode.mixins.json'

debug.verbose = true
debug.export = true
}

tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}

模组核心架构

常言道,万事开头难,那么编写一个模组该从何处开头呢?

首先,Forge模组通过一个注解了“@Mod(value = “modid”)”的类作为模组入口,通过实例化这样一个类来对Minecraft的游戏内容进行修改。这个类中,你可以监听Forge事件、注册项目,也可以调用和加载其它的类来进行各种修改等。例如,我可以这样编写构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static final String MODID = "real_peaceful_mode";

public RealPeacefulMode() {
RPMLogger.logger = LogManager.getLogger(MODID);
IEventBus bus = FMLJavaModLoadingContext.get().getModEventBus();
MinecraftForge.EVENT_BUS.addListener(this::tagsUpdated);
MinecraftForge.EVENT_BUS.addListener(this::serverStarted);
DeferredWorkQueue queue = DeferredWorkQueue.lookup(Optional.of(ModLoadingStage.CONSTRUCT)).orElseThrow();
Consumer<Runnable> runLater = job -> queue.enqueueWork(
ModLoadingContext.get().getActiveContainer(), job
);
RPMContent.modConstruction(bus, runLater);
DistExecutor.safeRunWhenOn(Dist.CLIENT, bootstrapErrorToXCPInDev(() -> ClientProxy::modConstruction));

ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, RPMCommonConfig.getConfig());

bus.addListener(this::setup);
MinecraftForge.EVENT_BUS.register(new ForgeEventHandler());
MinecraftForge.EVENT_BUS.register(this);
}

注意,这里我调用了Forge的两个不同的bus——ForgeBus和ModBus。ModBus一般执行模组生命周期中发生的注册、加载等事件,而ForgeBus则一般执行游戏过程中Forge的各种Hooks捕获到的事件,如TagsUpdatedEvent(数据包被重新加载)、ServerStartedEvent(服务端,启动!)、VillagerTradesEvent(村民职业对应交易表的添加)等。前者往往是静态的,而后者往往是动态的。这些Event的监听则构成了Forge的API,你可以通过查阅Forge文档,了解Forge的Hooks和注册机制,以及参阅minecraftforge的jar包的源代码内容来了解它们。总之能不mixin尽量不要mixin,除非有一些类中没有Forge的Hooks,或者你需要魔改的部分找不到API(如弓的附魔、新的音符盒乐器等等),而accesstransformer又无法单独完成修改,才考虑去使用mixin。

个人喜欢采用的实现一个功能的逻辑如下:

  1. 确定需求
  2. 从minecraft源代码中找到实现这个需求可行的注入点
  3. 确定有无Forge的Event可以通过监听来实现需求
  4. 若无,考虑可否使用AT(Access Transformer)来修改成员、函数或类的权限(也可以用mixin写Accessor)
  5. 若否,再考虑mixin,尽量使用Inject而避免用Redirect,绝对避免Overwrite和捕获局域变量的方法!

回到构造函数,这里调用了RPMContent.modConstruction,这是各种项目注册的入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void modConstruction(IEventBus bus, Consumer<Runnable> runLater) {
ModsCompatManager.compatModLoaded();

initTags();

RPMFluids.init(bus);
RPMBlocks.init(bus);
RPMItems.init(bus);
Villages.Registers.init(bus);
RPMBlockEntities.init(bus);
RPMCreativeTabs.init(bus);
RPMMenuTypes.init(bus);
RPMStructureTypes.init(bus);
RPMEnchantments.init(bus);
}

private static void initTags() {
RPMBlockTags.init();
RPMBiomeTags.init();
RPMStructureTags.init();
RPMStructureKeys.init();
RPMStructureSetKeys.init();
}

以创造模式物品栏为例,模组可以选择使用延迟注册机制实现项目的注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SuppressWarnings("unused")
public final class RPMCreativeTabs {
private static final DeferredRegister<CreativeModeTab> REGISTER = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MODID);

public static final RegistryObject<CreativeModeTab> REAL_PEACEFUL_MODE = register(
"real_peaceful_mode", Component.translatable("itemGroup.real_peaceful_mode"), () -> new ItemStack(RPMItems.SpiritBeads.HUGE_SPIRIT_BEAD),
(parameters, output) -> RPMItems.ItemEntry.ALL_ITEMS.forEach(output::accept)
);

public static void init(IEventBus bus) {
REGISTER.register(bus);
}

@SuppressWarnings("SameParameterValue")
private static RegistryObject<CreativeModeTab> register(String name, Component title, Supplier<ItemStack> icon, CreativeModeTab.DisplayItemsGenerator generator) {
return REGISTER.register(name, () -> CreativeModeTab.builder().title(title).icon(icon).displayItems(generator).build());
}
}

这里定义了一个名为”real_peaceful_mode:real_peaceful_mode”的创造模式物品栏,对照本地化文件(以英语en_us.json为例),我们可以看到这个物品栏标签的名字:

1
"itemGroup.real_peaceful_mode": "Real Peaceful Mode"

没错,这是Minecraft原版的本地化系统,最早是自定义格式的.lang文件,如今则是JSON格式。代码中调用时,只需要使用Component.translatable(“<本地化键名>”)即可。

总结

我们的模组框架成功搭建起来了,那么模组开发的基础部分到此为止,下一篇我将讲解一些或核心或边缘的模组功能的实现。模组仍在持续制作中,敬请期待!