diff --git a/docs/1.21-neoforge/README.mdx b/docs/1.21-neoforge/README.mdx new file mode 100644 index 0000000..8ffc88c --- /dev/null +++ b/docs/1.21-neoforge/README.mdx @@ -0,0 +1,13 @@ +--- +hide_table_of_contents: true +hide_title: true +title: 1.21-neoforge +--- + +import Index from '../../README.md' + + + +## 阅读准备 + +这篇指南基于 Minecraft 1.21 版本,并基于 NeoForge 21.0.61,和 Java 21.0.1。这篇指南假设读者拥有一定基于 Java 21 的开发经验,一定量的 Minecraft 游玩经验,和足够通畅的网络环境。这篇指南同时假设读者拥有一台能够正常运行 Minecraft 1.21,并拥有至少 4 GB 空闲内存的计算机。 diff --git a/docs/1.21-neoforge/block-entity/README.mdx b/docs/1.21-neoforge/block-entity/README.mdx new file mode 100644 index 0000000..928519a --- /dev/null +++ b/docs/1.21-neoforge/block-entity/README.mdx @@ -0,0 +1,237 @@ +# 方块实体 + + + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' +import CodeBlock from '@theme/CodeBlock' +import Xiaozhong from '!!raw-loader!./xiaozhong/Xiaozhong.java' + +Minecraft 使用名为方块实体(Block Entity)的机制来实现一些一般情况下方块做不到或很难做到的事情,例如: + +- 持有更复杂的数据(物品、文本等) +- 实现像熔炉那样持续不断地行为 +- 拥有奇妙的渲染特效 + +## 方块实体类型 + +Minecraft 中所有的方块实体均属于 `BlockEntity` 类型。我们首先需要创建我们自己的 `BlockEntity` 类。 + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +public static final class MyMachineEntity extends BlockEntity { + public MyMachineEntity(BlockEntityType type, BlockPos worldPosition, BlockState blockState) { + super(type, worldPosition, blockState); + } +} +``` + + + + {Xiaozhong} + + + +然后我们需要注册一个 `BlockEntityType` 实例,它代表了「一种特定的方块实体」,指明了创建对应方块实体实例的工厂方法,及哪些方块允许持有这种方块实体。 +`BlockEntityType` 由注册表统一管理,我们可以通过 `DeferredRegister` 来注册新的 `BlockEntityType`。 + +使用 `DeferredRegister` 注册 `BlockEntityType`: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +// 注册 BlockEntityType +public static final DeferredRegister> BLOCK_ENTITY_TYPES = DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, "xiaozhong"); +// 注册 xiaozhong:my_machine +public static final DeferredHolder, BlockEntityType> MY_MACHINE_BLOCK_ENTITY; +MY_MACHINE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("my_machine", + () -> BlockEntityType.Builder.of(MyMachineEntity::new, MY_MACHINE.get()).build(DSL.remainderType())); +// 添加监听器 +BLOCK_ENTITY_TYPES.register(modEventBus); +``` + + + + {Xiaozhong} + + + +注意到 `MyMachineEntity::new` 此时会报错。刚才提到的「方块实体实例的工厂方法」就是这个,然而原版要求「这个工厂方法只接受两个参数:`BlockPos` 和 `BlockState`」。我们向 `MyMachineEntity` 添加额外构造器来解决此问题: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +public MyMachineEntity(BlockPos worldPosition, BlockState blockState) { + this(MY_MACHINE_BLOCK_ENTITY.get(), worldPosition, blockState); +} +``` + + + + {Xiaozhong} + + + +:::info + +这个案例也展示了 `DeferredRegister` 的一个优势:可以方便开发者打破这种出现循环依赖的情况。 + +::: + +`BlockEntity` 此时像是一个纯粹的数据容器:我们在我们的 `MyMachineEntity` 中可以随意添加新的成员字段,并视情况决定是否创建对应的访问器。 + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +public static final class MyMachineEntity extends BlockEntity { + public MyMachineEntity(BlockEntityType type, BlockPos worldPosition, BlockState blockState) { + super(type, worldPosition, blockState); + } + + public MyMachineEntity(BlockPos worldPosition, BlockState blockState) { + this(MY_MACHINE_BLOCK_ENTITY.get(), worldPosition, blockState); + } + + private int count = 0; +} +``` + + + + {Xiaozhong} + + + +## 方块类 + +脱离方块的方块实体是不存在的。能容纳方块实体的方块需实现 `EntityBlock` 接口。实现这一接口需实现 `newBlockEntity` 方法。 + +Minecraft 原版为我们提供了 `BaseEntityBlock` 类方便我们实现: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +public static final DeferredRegister BLOCKS = DeferredRegister.Blocks.createBlocks("xiaozhong"); + +public static final RegistryObject MY_MACHINE; +MY_MACHINE = BLOCKS.register("my_machine", + () -> new MyMachine(BlockBehaviour.Properties.ofFullCopy(Blocks.IRON_BLOCK))); + +BLOCKS.register(modEventBus); + +public static final class MyMachine extends BaseEntityBlock { + public static final MapCodec CODEC = simpleCodec(MyMachine::new); + public MyMachine(Properties props) { + super(props); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @NotNull + @Override + public RenderShape getRenderShape(@NotNull BlockState state) { + // 注意:这一方法在 BaseEntityBlock 中默认返回 INVISIBLE, + // 这代表 Minecraft 会跳过该方块的渲染。 + // 为保证正常渲染,我们在此返回 MODEL,代表使用普通方块模型渲染。 + return RenderShape.MODEL; + } + + @Override + public BlockEntity newBlockEntity(@NotNull BlockPos pos, @NotNull BlockState state) { + // 这里返回我们刚刚定义的方块实体 + return new MyMachineEntity(pos, state); + } +} +``` + + + + {Xiaozhong} + + + +## 方块实体刻 + +默认情况下,方块实体并不具备跟随游戏刻刷新(亦即方块实体刻)的能力,若要获得此能力,方块实体所在的那个方块需要明确声明一个所谓的「Ticker」,亦即 `BlockEntityTicker`。这通过覆盖 `EntityBlock` 的 `getTicker` 方法实现。 + +此外,你可以根据 `Level` 是在逻辑服务器上还是逻辑客户端上来返回不同的 Ticker。通常我们在客户端不需要特别的逻辑,因此我们会在 `level.isClientSide()` 返回 `true` 时,令 `getTicker` 返回 `null`。 + +首先在 `MyMachineEntity` 中创建静态方法 `tick`: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +private int count = 0; + +public static void tick(Level level, BlockPos pos, BlockState state, MyMachineEntity entity) { + // 计数。每一个游戏刻中,游戏都会调用一次 tick 方法,所以我们只需要在这里让 count 自增 1 即可。 + entity.count += 1; + // 当计数超过 100 时…… + if (entity.count > 100) { + // 首先重置计数 + entity.count = 0; + // 检查 Level 实例是否存在,以及是不是在服务器上 + if (level != null && !level.isClientSide()) { + // 然后寻找半径 5 方块以内的玩家,最后的 false 表示无视创造或观察模式的玩家 + var player = level.getNearestPlayer(pos.getX(), pos.getY(), pos.getZ(), 5.0, false); + // 如果找到了这样的玩家…… + if (player != null) { + // 向这名玩家的聊天栏发送问候语。 + player.sendSystemMessage(Component.translatable("chat.xiaozhong.welcome")); + } + } + } +} +``` + + + + {Xiaozhong} + + + +然后在我们的方块类中,覆写 `getTicker` 方法: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +@Nullable +@Override +public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + // BaseEntityBlock 提供了 createTickerHelper 帮助你生成 BlockEntityTicker 的实例,这里直接调用并传入 tick 方法引用即可。 + return level.isClientSide() ? null : createTickerHelper(blockEntityType, MY_MACHINE_BLOCK_ENTITY.get(), MyMachineEntity::tick); +} +``` + + + + {Xiaozhong} + + + +以下演示了一个最基础方块实体的实现及的注册流程——没有相应的语言文件,没有模型,什么都没有,甚至没有为该方块注册对应的物品: + +{Xiaozhong} + +这个方块实体的效果是,每 100 个游戏刻检查是否有玩家距离该方块实体距离不到 5 方块,若有,随机抽一位向其打招呼。 + +打开游戏,执行 `/setblock ~ ~-1 ~ xiaozhong:my_machine` 看看效果吧~ diff --git a/docs/1.21-neoforge/block-entity/xiaozhong/Xiaozhong.java b/docs/1.21-neoforge/block-entity/xiaozhong/Xiaozhong.java new file mode 100644 index 0000000..f35fd86 --- /dev/null +++ b/docs/1.21-neoforge/block-entity/xiaozhong/Xiaozhong.java @@ -0,0 +1,98 @@ +package org.teacon.xiaozhong; + +import com.mojang.datafixers.DSL; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; + +import org.jetbrains.annotations.NotNull; + +@Mod("xiaozhong") +public class Xiaozhong { + public static final DeferredRegister BLOCKS = DeferredRegister.Blocks.createBlocks("xiaozhong"); + public static final DeferredRegister> BLOCK_ENTITY_TYPES = DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, "xiaozhong"); + + public static final DeferredHolder MY_MACHINE; + public static final DeferredHolder, BlockEntityType> MY_MACHINE_BLOCK_ENTITY; + + static { + MY_MACHINE = BLOCKS.register("my_machine", + () -> new MyMachine(BlockBehaviour.Properties.ofFullCopy(Blocks.IRON_BLOCK))); + MY_MACHINE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("my_machine", + () -> BlockEntityType.Builder.of(MyMachineEntity::new, MY_MACHINE.get()).build(DSL.remainderType())); + } + + public Xiaozhong(IEventBus modEventBus) { + BLOCKS.register(modEventBus); + BLOCK_ENTITY_TYPES.register(modEventBus); + } + + public static final class MyMachine extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(MyMachine::new); + public MyMachine(Properties props) { + super(props); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @NotNull + @Override + public RenderShape getRenderShape(@NotNull BlockState state) { + return RenderShape.MODEL; + } + + @Override + public BlockEntity newBlockEntity(@NotNull BlockPos pos, @NotNull BlockState state) { + return new MyMachineEntity(pos, state); + } + + @Override + public BlockEntityTicker getTicker(Level level, @NotNull BlockState state, @NotNull BlockEntityType blockEntityType) { + return level.isClientSide() ? null : createTickerHelper(blockEntityType, MY_MACHINE_BLOCK_ENTITY.get(), MyMachineEntity::tick); + } + } + + public static final class MyMachineEntity extends BlockEntity { + public MyMachineEntity(BlockEntityType type, BlockPos worldPosition, BlockState blockState) { + super(type, worldPosition, blockState); + } + + public MyMachineEntity(BlockPos worldPosition, BlockState blockState) { + this(MY_MACHINE_BLOCK_ENTITY.get(), worldPosition, blockState); + } + + private int count = 0; + + public static void tick(Level level, BlockPos pos, BlockState state, MyMachineEntity entity) { + entity.count += 1; + if (entity.count > 100) { + entity.count = 0; + if (level != null && !level.isClientSide()) { + var player = level.getNearestPlayer(pos.getX(), pos.getY(), pos.getZ(), 5.0, false); + if (player != null) { + player.sendSystemMessage(Component.translatable("chat.xiaozhong.welcome")); + } + } + } + } + } +} diff --git a/docs/1.21-neoforge/block-item-objects/README.mdx b/docs/1.21-neoforge/block-item-objects/README.mdx new file mode 100644 index 0000000..1453d75 --- /dev/null +++ b/docs/1.21-neoforge/block-item-objects/README.mdx @@ -0,0 +1,420 @@ +# 方块和物品 + + + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' +import CodeBlock from '@theme/CodeBlock' +import SimpleXiaozhong from '!!raw-loader!./simple/Xiaozhong.java' +import Xiaozhong from '!!raw-loader!./xiaozhong/Xiaozhong.java' + +Minecraft 中不同的方块类型或物品类型对应不同的实例:方块类型对应 `Block` 类,物品类型对应 `Item` 类。开发者可直接使用这两个类构造方块类型或物品类型,也可继承它们,实现自己的方块类或物品类。 + +一个值得注意的物品类是 `BlockItem` 类:它是 `Item` 的子类,用于代表方块对应的物品。`BlockItem` 实现了作为方块的物品需要实现的特性——右键放置方块,因此在声明 `Block` 时通常会一并声明其对应的 `BlockItem`。 + +## 方块和物品属性 + +`Block` 和 `Item` 的构造方法均需传入相应的 `Properties`(方块是 `BlockBehavior.Properties`,物品是 `Item.Properties`)。 + +你可以通过方块的 `Properties` 来指定方块本身的[硬度](https://zh.minecraft.wiki/w/%E6%8C%96%E6%8E%98#.E6.96.B9.E5.9D.97.E7.A1.AC.E5.BA.A6)及[爆炸抗性](https://zh.minecraft.wiki/w/%E7%88%86%E7%82%B8#.E7.88.86.E7.82.B8.E6.8A.97.E6.80.A7)(`strength`)、是否可徒手破坏掉落(`requiresCorrectToolForDrops`)、亮度(`lightLevel`)等。物品的 `Properties` 有一些额外方法可以指定物品的最大堆叠(`stacksTo`)、合成后剩余的物品(`craftRemainder`)等。 + +```java +// 一个普通的物品 +var item = new Item(new Item.Properties()); +// 一个属性和原版石头一致,但需要特定工具挖掘掉落,且方块硬度为 2,爆炸抗性为 1.5 的方块 +var block = new Block(BlockBehaviour.Properties.ofFullCopy(Blocks.STONE).requiresCorrectToolForDrops().strength(2F, 1.5F)); +// 一个与上述方块对应的物品 +var blockItem = new BlockItem(block, new Item.Properties()); +``` + +:::tip + +可以通过查看 `Blocks` 及 `Items` 类(注意类名最后的 `s`)了解原版方块及物品的相关属性。 + +::: + +## 注册系统 + +NeoForge 并未接管原版的注册表,因此我们应使用原版注册表注册新游戏元素,然而该过程极易出错。 + +NeoForge 为我们提供了 `DeferredRegister` 方便我们向 Minecraft 中注册游戏元素。在此基础上,NeoForge 还提供了快速创建方块和物品的 `DeferredRegister` 的方式,在此我们将展示如何使用。 + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +// 为 xiaozhong 命名空间注册物品 +public static final DeferredRegister ITEMS = DeferredRegister.Items.createItems("xiaozhong"); +``` + + + + {SimpleXiaozhong} + + + +`DeferredRegister` 有两个名为 `register` 的重载方法。一个需传入游戏元素的 ID 和相应的 `Supplier`,用于注册相应的实例: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +// xiaozhong:sulfur_dust 对应的物品 +public static final String SULFUR_DUST_ID = "sulfur_dust"; +public static final DeferredHolder SULFUR_DUST_ITEM; + +// 注册物品实例 +SULFUR_DUST_ITEM = ITEMS.register(SULFUR_DUST_ID, + () -> new Item(new Item.Properties())); +``` + + + + {SimpleXiaozhong} + + + +:::info + +`DeferredHolder` 储存着游戏元素的实例——该实例可通过 `get` 方法获得。建议将物品和方块对应的 `DeferredHolder` 以静态字段的方式声明。 + +::: + +`DeferredRegister` 的另一个 `register` 方法需传入 `IEventBus`,用于完成具体的注册(通常在主类的构造方法调用): + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +// 注册所有物品 +ITEMS.register(modEventBus); +``` + + + + {SimpleXiaozhong} + + + +:::info + +将 `IEventBus` 传入 `register` 方法本质上是为注册事件添加监听器——相应的注册事件触发后整个注册流程才会完成。 + +::: + +我们再注册方块和方块对应的 `BlockItem` 物品: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +// 为 xiaozhong 命名空间注册方块 +public static final DeferredRegister BLOCKS = DeferredRegister.Blocks.create("xiaozhong"); + +// xiaozhong:sulfur_block 对应的方块和物品 +public static final String SULFUR_BLOCK_ID = "sulfur_block"; +public static final DeferredHolder SULFUR_BLOCK; +public static final DeferredHolder SULFUR_BLOCK_ITEM; + +// 注册方块实例和方块对应的物品实例,注意 BlockItem 的注册 +SULFUR_BLOCK = BLOCKS.register(SULFUR_BLOCK_ID, + () -> new Block(BlockBehaviour.Properties.of(Material.STONE).requiresCorrectToolForDrops().strength(2F, 1.5F))); +SULFUR_BLOCK_ITEM = ITEMS.register(SULFUR_BLOCK_ID, + () -> new BlockItem(SULFUR_BLOCK.get(), new Item.Properties().tab(CreativeModeTab.TAB_BUILDING_BLOCKS))); + +// 注册所有方块 +BLOCKS.register(modEventBus); +``` + + + + {SimpleXiaozhong} + + + +:::tip + +在游戏中使用 `/give @s [模组 ID]:[物品 ID]` 即可拿到物品,如使用 `/give @s xiaozhong:sulfur_dust` 和 `/give @s xiaozhong:sulfur_block` 等。 + +::: + +## 语言文件 + +仅需在 `LanguageProvider` 的 `add` 方法传入对应的 `Block` 或 `Item` 实例即可。Minecraft 会自动读取对应的翻译标识符。 + +以下为示例: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +// 向 Data Generator 添加 DataProvider +modEventBus.addListener(Xiaozhong::onGatherData); + +public static void onGatherData(GatherDataEvent event) { + var gen = event.getGenerator(); + var packOutput = gen.getPackOutput(); + var helper = event.getExistingFileHelper(); + gen.addProvider(event.includeClient(), new EnglishLanguageProvider(packOutput)); + gen.addProvider(event.includeClient(), new ChineseLanguageProvider(packOutput)); +} + +// 英文语言文件 +public static class EnglishLanguageProvider extends LanguageProvider { + // ... + + @Override + protected void addTranslations() { + // 等价于 this.add("item.xiaozhong.sulfur_dust", "Sulfur Dust") + this.add(SULFUR_DUST_ITEM.get(), "Sulfur Dust"); + // 等价于 this.add("block.xiaozhong.sulfur_block", "Sulfur Block") + this.add(SULFUR_BLOCK.get(), "Sulfur Block"); + } +} + +// 中文语言文件 +public static class ChineseLanguageProvider extends LanguageProvider { + // ... + + @Override + protected void addTranslations() { + // 等价于 this.add("item.xiaozhong.sulfur_dust", "硫粉") + this.add(SULFUR_DUST_ITEM.get(), "硫粉"); + // 等价于 this.add("block.xiaozhong.sulfur_block", "硫磺块") + this.add(SULFUR_BLOCK.get(), "硫磺块"); + } +} +``` + + + + {Xiaozhong} + + + +## 模型文件 + +物品模型通常直接和物品 ID 关联,由位于 `[模组 ID]:item/[物品 ID]` 处的模型文件定义。但对方块模型而言,由于方块存在不同的[方块状态](https://minecraft.fandom.com/zh/wiki/%E6%96%B9%E5%9D%97%E7%8A%B6%E6%80%81),因此需要在 `[模组 ID]:[方块 ID]` 处声明对应的方块状态文件,再在方块状态文件里声明模型位置。 + +幸运的是,无论是模型文件还是方块状态文件均可使用 Data Generator 生成。唯一不能生成的只有纹理——模组开发者需要把纹理放到 `src/main/resources` 下相应的位置。 + +:::tip + +对于常用的方块模型,Data Generator 提供了非常多的预设。当然,模组开发者也可以制作自己的丰富多彩的模型。对 Minecraft 而言,常见的模型制作工具有 Blockbench 等。Blockbench 可在 https://www.blockbench.net/ 下载。 + +::: + +此处展示如何生成最简单的方块模型及物品模型: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +// 向 Data Generator 添加 DataProvider +modEventBus.addListener(Xiaozhong::onGatherData); + +public static void onGatherData(GatherDataEvent event) { + // ... + // 添加模型文件的 DataProvider + gen.addProvider(event.includeClient(), new ModelProvider(packOutput, helper)); + gen.addProvider(event.includeClient(), new StateProvider(packOutput, helper)); +} + +// 物品模型文件 +public static class ModelProvider extends ItemModelProvider { + public ModelProvider(PackOutput gen, ExistingFileHelper helper) { + super(gen, "xiaozhong", helper); + } + + @Override + protected void registerModels() { + // 第一个参数为模型对应的物品 ID,此示例为 sulfur_dust,因此在 xiaozhong:item/sulfur_dust 处生成模型文件 + // 第二个参数为父模型,一般物品的父模型均为 minecraft:item/generated,此处简写为 ResourceLocation.withDefaultNamespace("item/generated") + // 第三个参数及第四个参数为纹理名称及位置,对于当前父模型而言需要指定 layer0 对应的纹理名称,此处为 xiaozhong:item/sulfur_dust + this.singleTexture(SULFUR_DUST_ID, ResourceLocation.withDefaultNamespace("item/generated"), "layer0", ResourceLocation.fromNamespaceAndPath("xiaozhong", "item/" + SULFUR_DUST_ID)); + } +} + +// 方块状态文件及方块模型文件 +public static class StateProvider extends BlockStateProvider { + public StateProvider(PackOutput gen, ExistingFileHelper helper) { + super(gen, "xiaozhong", helper); + } + + @Override + protected void registerStatesAndModels() { + // 此处生成方块状态文件和方块模型文件 + // 第一个参数为模型对应的方块,对应的方块状态文件会在 xiaozhong:sulfur 自动生成 + // 第二个参数为模型,对应的模型文件会在 xiaozhong:block/sulfur_block 处自动生成 + // 自动生成的模型文件中,父模型为 minecraft:block/cube_all,并引用 xiaozhong:block/sulfur_block 处的纹理 + this.simpleBlock(SULFUR_BLOCK.get(), this.cubeAll(SULFUR_BLOCK.get())); + // 此处生成方块对应物品的模型文件 + // 第一个参数为模型对应的方块,对应的模型文件会在 xiaozhong:item/sulfur_block 自动生成,并继承第二个参数代表的模型 + this.simpleBlockItem(SULFUR_BLOCK.get(), this.cubeAll(SULFUR_BLOCK.get())); + /* + // 如欲使用自行添加的位于 foo:bar 的模型文件,第二个参数请通过 getExistingFile 方法生成: + this.simpleBlock(SULFUR_BLOCK.get(), this.models().getExistingFile(ResourceLocation.fromNamespaceAndPath("foo", "bar"))); + this.simpleBlockItem(SULFUR_BLOCK.get(), this.models().getExistingFile(ResourceLocation.fromNamespaceAndPath("foo", "bar"))); + */ + } +} +``` + + + + {Xiaozhong} + + + +## 纹理资源 + +如果此时启动 `runData`,那么大概率会有如下报错产生: + +> `Caused by: java.lang.IllegalArgumentException: Texture xiaozhong:item/sulfur_dust does not exist in any known resource pack` + +这是因为物品对应的 `xiaozhong:item/sulfur_dust` 亦即 `assets/xiaozhong/textures/item/sulfur_dust` 处尚无纹理文件存在。我们只需在 `xiaozhong:item/sulfur_dust` 处添加纹理即可——方块对应的 `xiaozhong:block/sulfur_block` 处的纹理同理。 + +以下是 `xiaozhong:item/sulfur_dust` 和 `xiaozhong:block/sulfur_block` 的纹理示意: + + + + +![sulfur-dust-texture](sulfur-dust-texture.png) + + + + +![sulfur-block-texture](sulfur-block-texture.png) + + + + +:::info + +为保证不同放缩比例下玩家的游戏体验,建议使用长宽均为 16 的像素风格纹理。 + +::: + +:::tip + +纹理既可在 Blockbench 中直接修改,也可使用 GIMP(GNU Image Manipulation Program)等专业图像处理软件处理。GIMP 可在 下载。 + +::: + +以下是游戏内效果: + +![block-item-example](block-item-example.png) + +## 方块掉落物 + +目前的方块无法使用任何工具掉落任何物品,这是因为我们尚未指定方块的战利品表。 + +此处展示如何指定方块的战利品表: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +// 向 Data Generator 添加 DataProvider +modEventBus.addListener(Xiaozhong::onGatherData); + +public static void onGatherData(GatherDataEvent event) { + // ... + // 我们需要获取注册表查询器 + var lookupProvider = event.getLookupProvider(); + // 添加战利品表的 DataProvider + gen.addProvider(event.includeServer(), new LootProvider(packOutput, lookupProvider)); +} + +// 战利品表 +public static class LootProvider extends LootTableProvider { + public LootProvider(PackOutput gen, CompletableFuture lookup) { + super(gen, Set.of(), List.of(new SubProviderEntry(CustomBlockLoot::new, LootContextParamSets.BLOCK)), lookup); + } + + @Override + protected void validate(WritableRegistry registry, ValidationContext context, ProblemReporter.Collector collector) { + // FIXME 需要核实正确写法 + // map.forEach((key, value) -> LootTables.validate(context, key, value)); + } +} + +// 方块战利品表 +public static class CustomBlockLoot extends BlockLootSubProvider { + protected CustomBlockLoot(HolderLookup.Provider lookupProvider) { + super(Set.of(), FeatureFlags.REGISTRY.allFlags(), lookupProvider); + } + + @Override + protected void generate() { + // 此处添加 xiaozhong:sulfur_block 处的战利品表,意为掉落自身对应物品一个 + this.dropSelf(SULFUR_BLOCK.get()); + /* + // 如欲在非精准采集的情况下掉落九个 xiaozhong:sulfur_dust,请使用以下代码: + this.add(SULFUR_BLOCK.get(), block -> createSingleItemTableWithSilkTouch(block, SULFUR_DUST_ITEM.get(), ConstantValue.exactly(9f))); + */ + } + + @Nonnull + @Override + protected Iterable getKnownBlocks() { + // 模组自定义的方块战利品表必须覆盖此方法,以绕过对原版方块战利品表的检查(此处返回该模组的所有方块) + return Iterables.transform(BLOCKS.getEntries(), DeferredHolder::get); + } +} +``` + + + + {Xiaozhong} + + + +## 创造模式物品栏 + +import CTM from '!!raw-loader!./creativemodetab/Xiaozhong.java' + +[创造模式物品栏](https://zh.minecraft.wiki/w/%E7%89%A9%E5%93%81%E6%A0%8F#%E5%88%9B%E9%80%A0%E6%A8%A1%E5%BC%8F%E7%89%A9%E5%93%81%E6%A0%8F)是一些物品的集合,并且如他的名字一样只在创造模式出现。 + +在 1.21 中,标签页需要通过注册表注册。我们可以通过 `DeferredRegister` 注册新标签页,并通过 NeoForge 提供的 `BuildCreativeModeTabContentsEvent` 事件来将我们的新方块和物品放入其中: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +// 创建创造模式标签页的 DeferredRegister +public static DeferredRegister TABS = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, "xiaozhong"); + +public static final String MAIN_TAB_ID = "main_tab"; +public static final DeferredHolder MAIN_TAB; + +// 注册新标签页,并设置标签图标 +MAIN_TAB = TABS.register(MAIN_TAB_ID, + () -> CreativeModeTab.builder().icon(() -> new ItemStack(SULFUR_DUST_ITEM)).build()); + +// 将 DeferredRegister 注册入事件总线 +TABS.register(modEventBus); +// 订阅 BuildCreativeModeTabContentsEvent 事件以填充标签页内容 +modEventBus.addListener(Xiaozhong::buildCreativeTabContent); + +public static void buildCreativeTabContent(BuildCreativeModeTabContentsEvent event) { + if (event.getTab() == MAIN_TAB.get()) { + event.accept(SULFUR_DUST_ITEM.get()); + event.accept(SULFUR_BLOCK_ITEM.get()); + } +} +``` + + + + {CTM} + + \ No newline at end of file diff --git a/docs/1.21-neoforge/block-item-objects/block-item-example.png b/docs/1.21-neoforge/block-item-objects/block-item-example.png new file mode 100644 index 0000000..eebdf72 Binary files /dev/null and b/docs/1.21-neoforge/block-item-objects/block-item-example.png differ diff --git a/docs/1.21-neoforge/block-item-objects/creativemodetab/Xiaozhong.java b/docs/1.21-neoforge/block-item-objects/creativemodetab/Xiaozhong.java new file mode 100644 index 0000000..16921a3 --- /dev/null +++ b/docs/1.21-neoforge/block-item-objects/creativemodetab/Xiaozhong.java @@ -0,0 +1,57 @@ +package org.teacon.xiaozhong; + +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; + +@Mod("xiaozhong") +public class Xiaozhong { + public static DeferredRegister ITEMS = DeferredRegister.Items.createItems("xiaozhong"); + public static DeferredRegister BLOCKS = DeferredRegister.Blocks.createBlocks("xiaozhong"); + public static DeferredRegister TABS = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, "xiaozhong"); + + public static final String SULFUR_DUST_ID = "sulfur_dust"; + public static final DeferredHolder SULFUR_DUST_ITEM; + + public static final String SULFUR_BLOCK_ID = "sulfur_block"; + public static final DeferredHolder SULFUR_BLOCK; + public static final DeferredHolder SULFUR_BLOCK_ITEM; + + public static final String MAIN_TAB_ID = "main_tab"; + public static final DeferredHolder MAIN_TAB; + + static { + SULFUR_DUST_ITEM = ITEMS.register(SULFUR_DUST_ID, + () -> new Item(new Item.Properties())); + SULFUR_BLOCK = BLOCKS.register(SULFUR_BLOCK_ID, + () -> new Block(BlockBehaviour.Properties.ofFullCopy(Blocks.STONE).requiresCorrectToolForDrops().strength(2F, 1.5F))); + SULFUR_BLOCK_ITEM = ITEMS.register(SULFUR_BLOCK_ID, + () -> new BlockItem(SULFUR_BLOCK.get(), new Item.Properties())); + MAIN_TAB = TABS.register(MAIN_TAB_ID, + () -> CreativeModeTab.builder().icon(() -> new ItemStack(SULFUR_DUST_ITEM)).build()); + } + + public Xiaozhong(IEventBus modEventBus) { + ITEMS.register(modEventBus); + BLOCKS.register(modEventBus); + TABS.register(modEventBus); + modEventBus.addListener(Xiaozhong::buildCreativeTabContent); + } + + public static void buildCreativeTabContent(BuildCreativeModeTabContentsEvent event) { + if (event.getTab() == MAIN_TAB.get()) { + event.accept(SULFUR_DUST_ITEM.get()); + event.accept(SULFUR_BLOCK_ITEM.get()); + } + } +} \ No newline at end of file diff --git a/docs/1.21-neoforge/block-item-objects/simple/Xiaozhong.java b/docs/1.21-neoforge/block-item-objects/simple/Xiaozhong.java new file mode 100644 index 0000000..cda467c --- /dev/null +++ b/docs/1.21-neoforge/block-item-objects/simple/Xiaozhong.java @@ -0,0 +1,38 @@ +package org.teacon.xiaozhong; + +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; + +@Mod("xiaozhong") +public class Xiaozhong { + public static final DeferredRegister ITEMS = DeferredRegister.Items.createItems("xiaozhong"); + public static final DeferredRegister BLOCKS = DeferredRegister.Blocks.createBlocks("xiaozhong"); + + public static final String SULFUR_DUST_ID = "sulfur_dust"; + public static final DeferredHolder SULFUR_DUST_ITEM; + + public static final String SULFUR_BLOCK_ID = "sulfur_block"; + public static final DeferredHolder SULFUR_BLOCK; + public static final DeferredHolder SULFUR_BLOCK_ITEM; + + static { + SULFUR_DUST_ITEM = ITEMS.register(SULFUR_DUST_ID, + () -> new Item(new Item.Properties())); + SULFUR_BLOCK = BLOCKS.register(SULFUR_BLOCK_ID, + () -> new Block(BlockBehaviour.Properties.ofFullCopy(Blocks.STONE).requiresCorrectToolForDrops().strength(2F, 1.5F))); + SULFUR_BLOCK_ITEM = ITEMS.register(SULFUR_BLOCK_ID, + () -> new BlockItem(SULFUR_BLOCK.get(), new Item.Properties())); + } + + public Xiaozhong(IEventBus modEventBus) { + ITEMS.register(modEventBus); + BLOCKS.register(modEventBus); + } +} diff --git a/docs/1.21-neoforge/block-item-objects/sulfur-block-texture.png b/docs/1.21-neoforge/block-item-objects/sulfur-block-texture.png new file mode 100644 index 0000000..72956b2 Binary files /dev/null and b/docs/1.21-neoforge/block-item-objects/sulfur-block-texture.png differ diff --git a/docs/1.21-neoforge/block-item-objects/sulfur-dust-texture.png b/docs/1.21-neoforge/block-item-objects/sulfur-dust-texture.png new file mode 100644 index 0000000..e30a7a6 Binary files /dev/null and b/docs/1.21-neoforge/block-item-objects/sulfur-dust-texture.png differ diff --git a/docs/1.21-neoforge/block-item-objects/xiaozhong/Xiaozhong.java b/docs/1.21-neoforge/block-item-objects/xiaozhong/Xiaozhong.java new file mode 100644 index 0000000..fcd0d71 --- /dev/null +++ b/docs/1.21-neoforge/block-item-objects/xiaozhong/Xiaozhong.java @@ -0,0 +1,149 @@ +package org.teacon.xiaozhong; + +import com.google.common.collect.Iterables; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.WritableRegistry; +import net.minecraft.data.PackOutput; +import net.minecraft.data.loot.BlockLootSubProvider; +import net.minecraft.data.loot.LootTableProvider; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.storage.loot.LootTable; +import net.minecraft.world.level.storage.loot.ValidationContext; +import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.model.generators.BlockStateProvider; +import net.neoforged.neoforge.client.model.generators.ItemModelProvider; +import net.neoforged.neoforge.common.data.ExistingFileHelper; +import net.neoforged.neoforge.common.data.LanguageProvider; +import net.neoforged.neoforge.data.event.GatherDataEvent; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +@Mod("xiaozhong") +public class Xiaozhong { + public static DeferredRegister ITEMS = DeferredRegister.Items.createItems("xiaozhong"); + public static DeferredRegister BLOCKS = DeferredRegister.Blocks.createBlocks("xiaozhong"); + + public static final String SULFUR_DUST_ID = "sulfur_dust"; + public static final DeferredHolder SULFUR_DUST_ITEM; + + public static final String SULFUR_BLOCK_ID = "sulfur_block"; + public static final DeferredHolder SULFUR_BLOCK; + public static final DeferredHolder SULFUR_BLOCK_ITEM; + + static { + SULFUR_DUST_ITEM = ITEMS.register(SULFUR_DUST_ID, + () -> new Item(new Item.Properties())); + SULFUR_BLOCK = BLOCKS.register(SULFUR_BLOCK_ID, + () -> new Block(BlockBehaviour.Properties.ofFullCopy(Blocks.STONE).requiresCorrectToolForDrops().strength(2F, 1.5F))); + SULFUR_BLOCK_ITEM = ITEMS.register(SULFUR_BLOCK_ID, + () -> new BlockItem(SULFUR_BLOCK.get(), new Item.Properties())); + } + + public Xiaozhong(IEventBus modEventBus) { + ITEMS.register(modEventBus); + BLOCKS.register(modEventBus); + modEventBus.addListener(Xiaozhong::onGatherData); + } + + public static void onGatherData(GatherDataEvent event) { + var gen = event.getGenerator(); + var packOutput = gen.getPackOutput(); + var helper = event.getExistingFileHelper(); + var lookupProvider = event.getLookupProvider(); + gen.addProvider(event.includeClient(), new EnglishLanguageProvider(packOutput)); + gen.addProvider(event.includeClient(), new ChineseLanguageProvider(packOutput)); + gen.addProvider(event.includeClient(), new ModelProvider(packOutput, helper)); + gen.addProvider(event.includeClient(), new StateProvider(packOutput, helper)); + gen.addProvider(event.includeServer(), new LootProvider(packOutput, lookupProvider)); + } + + public static class EnglishLanguageProvider extends LanguageProvider { + public EnglishLanguageProvider(PackOutput gen) { + super(gen, "xiaozhong", "en_us"); + } + + @Override + protected void addTranslations() { + this.add(SULFUR_DUST_ITEM.get(), "Sulfur Dust"); + this.add(SULFUR_BLOCK.get(), "Sulfur Block"); + } + } + + public static class ChineseLanguageProvider extends LanguageProvider { + public ChineseLanguageProvider(PackOutput gen) { + super(gen, "xiaozhong", "zh_cn"); + } + + @Override + protected void addTranslations() { + this.add(SULFUR_DUST_ITEM.get(), "硫粉"); + this.add(SULFUR_BLOCK.get(), "硫磺块"); + } + } + + public static class ModelProvider extends ItemModelProvider { + public ModelProvider(PackOutput gen, ExistingFileHelper helper) { + super(gen, "xiaozhong", helper); + } + + @Override + protected void registerModels() { + this.singleTexture(SULFUR_DUST_ID, ResourceLocation.withDefaultNamespace("item/generated"), "layer0", ResourceLocation.fromNamespaceAndPath("xiaozhong", "item/" + SULFUR_DUST_ID)); + } + } + + public static class StateProvider extends BlockStateProvider { + public StateProvider(PackOutput gen, ExistingFileHelper helper) { + super(gen, "xiaozhong", helper); + } + + @Override + protected void registerStatesAndModels() { + this.simpleBlock(SULFUR_BLOCK.get(), this.cubeAll(SULFUR_BLOCK.get())); + this.simpleBlockItem(SULFUR_BLOCK.get(), this.cubeAll(SULFUR_BLOCK.get())); + } + } + + public static class LootProvider extends LootTableProvider { + public LootProvider(PackOutput gen, CompletableFuture lookup) { + super(gen, Set.of(), List.of(new SubProviderEntry(CustomBlockLoot::new, LootContextParamSets.BLOCK)), lookup); + } + + @Override + protected void validate(WritableRegistry registry, ValidationContext context, ProblemReporter.Collector collector) { + // FIXME Need proper migration + // map.forEach((key, value) -> LootTables.validate(context, key, value)); + } + } + + public static class CustomBlockLoot extends BlockLootSubProvider { + protected CustomBlockLoot(HolderLookup.Provider lookupProvider) { + super(Set.of(), FeatureFlags.REGISTRY.allFlags(), lookupProvider); + } + + @Override + protected void generate() { + this.dropSelf(SULFUR_BLOCK.get()); + } + + @NotNull + @Override + protected Iterable getKnownBlocks() { + return Iterables.transform(BLOCKS.getEntries(), DeferredHolder::get); + } + } +} diff --git a/docs/1.21-neoforge/concepts/README.mdx b/docs/1.21-neoforge/concepts/README.mdx new file mode 100644 index 0000000..195407c --- /dev/null +++ b/docs/1.21-neoforge/concepts/README.mdx @@ -0,0 +1,308 @@ +# 基本概念 + + + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' +import CodeBlock from '@theme/CodeBlock' +import ModsToml from '!!raw-loader!./neoforge.mods.toml' +import EmptyXiaozhong from '!!raw-loader!./empty/Xiaozhong.java' +import ManualListenerXiaozhong from '!!raw-loader!./manual-listener/Xiaozhong.java' +import AutomaticListenerXiaozhong from '!!raw-loader!./automatic-listener/Xiaozhong.java' +import Xiaozhong from '!!raw-loader!./xiaozhong/Xiaozhong.java' + +以下是本篇指南所用到的基本概念。对部分 Minecraft 模组玩家和资源包 / 数据包创作者来说,一些概念可能已经相对熟悉了。 + +## 资源文件 + +NeoForge MDK 默认从 `src/main/resources` 和 `src/generated/resources` 检索资源文件,并几乎不加改动地复制到生成的模组文件中。 + +:::caution + +`src/generated/resources` 存放的是 Data Generator 自动生成的文件,因此开发者不应该对其中的资源进行手动调整,而仅应通过启动 Data Generator 更新资源,或将自己的资源放置于 `src/main/resources` 目录下。 + +::: + +除去 `pack.mcmeta` 和 `META-INF` 目录外,其他所有文件都应当位于 `assets` 和 `data` 目录下。 + +### 咦,`pack.mcmeta` 不见了? + +NeoForge 会为每一个模组自动生成和当前游戏版本对应的 `pack.mcmeta`,我们无需手动创建。 + +:::tip + +关于 `pack.mcmeta` 的具体格式,请参见 Minecraft Wiki 对[数据包](https://zh.minecraft.wiki/w/%E6%95%B0%E6%8D%AE%E5%8C%85)和[资源包](https://zh.minecraft.wiki/w/%E8%B5%84%E6%BA%90%E5%8C%85)的介绍。 + +::: + +### `META-INF/neoforge.mods.toml` + +通常情况下,所有 NeoForge 模组都需要在 `META-INF` 目录下指定一个 [TOML 格式](https://toml.io/cn/)的 `neoforge.mods.toml` 文件。`neoforge.mods.toml` 文件指定了模组的相关信息。 + +NeoForge MDK 默认提供的 `neoforge.mods.toml` 以注释的形式为其提供了详尽的解释,同时 MDK 会把我们在上一节中 `gradle.properties` 填入的信息自动填入 `neoforge.mods.toml`,因此我们无需手动更改任何内容。 + +以下为 `mods.toml` 的一个示例: + +{ModsToml} + +:::caution + +模组开发者必须通过 `mods.toml` 指定一个协议,否则模组将无法启动。基于 TeaCon 的举办理念,我们鼓励模组开发者采用一个自由或开源的授权协议。 + +::: + +### `assets` 和 `data` + + `assets` 和 `data` 目录下的资源分别对应资源包和数据包的资源。二类资源均拥有对应的[资源路径](https://zh.minecraft.wiki/w/%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4ID)。 + +:::caution + +为方便起见,本篇指南的后续内容均将使用资源路径代指资源,如使用「`minecraft:item/compass` 处的模型文件」代指「名为 `assets/minecraft/models/item/compass.json` 的模型文件」。部分情况下,开发者引用的资源路径需补齐前缀和后缀(使用 `minecraft:textures/block/stone.png` 而非 `minecraft:block/stone`),我们会在类似情况出现时特殊说明,请各位读者加以注意。 + +::: + +以下是 Minecraft 中的一些典型的资源类型: + +| 资源性质 | 资源类型 | 资源前缀 | 资源后缀 | 示例 | +| -------- | -------- | -------------- | -------- | ------------------------------------------------------------ | +| 资源包 | 模型 | `models/` | `.json` | 路径:`minecraft:item/compass`
文件:`assets/minecraft/models/item/compass.json` | +| 资源包 | 纹理 | `textures/` | `.png` | 路径:`minecraft:block/stone`
文件:`assets/minecraft/textures/block/stone.png` | +| 资源包 | 方块状态 | `blockstates/` | `.json` | 路径:`minecraft:dirt`
文件:`assets/minecraft/blockstates/dirt.json` | +| 资源包 | 语言文件 | `lang/` | `.json` | 路径:`minecraft:zh_cn`
文件:`assets/minecraft/lang/zh_cn.json` | +| 数据包 | 配方 | `recipe/` | `.json` | 路径:`minecraft:bread`
文件:`data/minecraft/recipe/bread.json` | +| 数据包 | 战利品表 | `loot_table/` | `.json` | 路径:`minecraft:blocks/ice`
文件:`data/minecraft/loot_table/blocks/ice.json` | + +资源路径在源代码中为 `ResourceLocation`,如 `ResourceLocation.fromNamespaceAndPath("foo", "bar")` 即代表 `foo:bar` 这一资源路径。 + +命名空间如为 `minecraft` 则可使用 `ResourceLocation.withDefaultNamespace("bar")`,代表 `minecraft:bar`。 + +:::caution + +模组 ID 是模组的唯一标识符,也应当是所有和模组相关的资源的[命名空间](https://zh.minecraft.wiki/w/%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4ID):在管理资源时应当尽量使用模组 ID 作为命名空间。本篇指南所有新添加的资源均归属于 `xiaozhong` 命名空间。 + +::: + +## Java 源代码 + +NeoForge MDK 默认从 `src/main/java` 检索 Java 源代码,将其编译并进行适当处理后封装到最终的模组文件中。 + +:::caution + +源代码通常使用 UTF-8 作为文件编码,请调整开发工具的相关设置以确保编码正确性,并保证编译时所使用的编码也是正常的。后一个目标通常可以通过设置 `JAVA_TOOL_OPTIONS` 以及 `GRADLE_OPTS` 两个环境变量的值为 `-Dfile.encoding=UTF-8` 来达成。 + +::: + +### 模组主类 + +通常情况下,所有 NeoForge 模组都需要在源代码中添加一个类作为模组主类。NeoForge MDK 默认内置了 `ExampleMod` 作为主类,实际开发时可将其修改为自己的主类,也可自行删去另建主类。 + +模组的主类需要使用 `@Mod` 注解标识,并在参数中声明模组 ID。以下是一个典型的主类: + +{EmptyXiaozhong} + +## 客户端和服务端 + +模组代码可能同时在两种不同类型的 Minecraft 下运行:一种是单人游玩或多人联机游玩时的玩家客户端,一种是开服时的专用服务端。NeoForge 使用 `Dist` 注解标记两种类型(分别是 `CLIENT` 和 `DEDICATED_SERVER`),并会将 `FMLEnvironment.dist` 标记为相应的值。 + +一些模组代码只会在 Minecraft 玩家客户端运行——Minecraft 本体也是如此。因此开发过程中有时能看到一些类的上方有 `@OnlyIn(Dist.CLIENT)` 标记。这些类只存在于玩家客户端,试图在专用服务端加载这些类将极易导致游戏抛出 `ClassNotFoundException`,进而导致游戏崩溃。 + +:::caution + +**在专用服务端加载标有 `@OnlyIn(Dist.CLIENT)` 的类是模组开发的常见错误**,因开发者绝大多数时间只在玩家客户端测试模组,故无论是新手还是老手均难以避免。 + +有两种办法可以尽力规避此事:一方面,在开发模组时有意识地将所有引用了标有 `@OnlyIn(Dist.CLIENT)` 的代码隔离到特定的类中,并加以特殊标记(如类名包含 `Client` 或位于 `client` 子包下);另一方面,在最终发布前使用 `runServer` 启动选项检查专用服务端在添加模组后是否会崩溃。 + +此外,NeoForge 还允许你在 `@Mod` 注解标识中指定「这个模组主类应在哪个地方运行」。 + +::: + +## 事件系统 + +模组的许多代码都是通过事件系统触发的。和大多数框架的事件系统一样,不同的事件归属于不同的事件总线。 + +NeoForge 的事件总线均为 `IEventBus` 接口的实例。模组开发者能够直接接触到的事件总线有两种: + +* NeoForge 总线:一般处理游戏启动时能够触发的事件,可通过 `NeoForge.EVENT_BUS` 获得。 +* 模组总线:一般处理游戏未启动时也会触发的事件,可通过在模组主类构造器中额外声明一 `IEventBus` 参数获得。 + +:::info + +区分一个事件属于何种总线可以通过判断是否实现了 `IModBusEvent` 接口确定:实现了 `IModBusEvent` 接口的事件经由模组总线触发,否则经由 NeoForge 总线触发。 + +::: + +### 注册监听器 + +模组开发者可通过直接调用事件总线的 `addListener` 方法注册监听器。以下为使用事件监听器注册玩家登录事件的例子: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +// 通常在模组主类的构造方法注册事件监听器 +NeoForge.EVENT_BUS.addListener(PlayerLoggedInHandler::onLoggedIn); +/* +// 如果只希望在玩家客户端注册事件,请检查 FMLEnvironment.dist 的值 +if (FMLEnvironment.dist == Dist.CLIENT) { + NeoForge.EVENT_BUS.addListener(PlayerLoggedInHandler::onLoggedIn); +} +*/ + +public static class PlayerLoggedInHandler { + public static void onLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + // 检查到玩家登录后,向玩家发送一条「Welcome to xiaozhong!」的消息 + var player = event.getEntity(); + player.sendSystemMessage(Component.literal("Welcome to xiaozhong!")); + } +} +``` + + + + {ManualListenerXiaozhong} + + + +模组开发者也可通过 `@EventBusSubscriber` 注解自动注册。以下是一个使用该注解的例子,与上面的例子等价: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +// 为监听器的外部类添加 @EventBusSubscriber 注解 +@EventBusSubscriber(bus = EventBusSubscriber.Bus.GAME) +/* +// 如果只希望在玩家客户端注册事件,请添加 value 参数 +@EventBusSubscriber(bus = EventBusSubscriber.Bus.GAME, value = Dist.CLIENT) +*/ +public static class PlayerLoggedInHandler { + // 监听器需为 public static 方法,并使用 @SubscribeEvent 注解 + // 监听器的方法名可自由选取,方法参数为对应的事件,在事件触发时作为参数传入 + @SubscribeEvent + public static void onLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + // 检查到玩家登录后,向玩家发送一条「Welcome to xiaozhong!」的消息 + var player = event.getEntity(); + player.sendSystemMessage(Component.literal("Welcome to xiaozhong!")); + } +} +``` + + + + {AutomaticListenerXiaozhong} + + + +## 文本与国际化 + +Minecraft 的所有用于展示的文本均为 `Component` 的实例。 + +此外,还有可以修改内容的 `MutableComponent`,可以搭配 `withStyle` 和 `append` 方法调整文本的样式: + +```java +player.sendSystemMessage(Component.literal("Welcome to xiaozhong!")); +player.sendSystemMessage(Component.literal("Welcome to xiaozhong!").withStyle(ChatFormatting.GREEN)); +player.sendSystemMessage(Component.literal("Welcome to ").append(Component.literal("xiaozhong").withStyle(ChatFormatting.GREEN)).append("!")); +``` + +![text-example](text-example.png) + +如果使用 `Component.literal`,则玩家无论选择了何种语言,看到的文字都是相同的。`Component.translatable` 可使文字在不同的语言下不同: + +```java +var ironBars = Component.translatable("block.minecraft.iron_bars"); +var ironIngot = Component.translatable("item.minecraft.iron_ingot"); +player.sendSystemMessage(Component.literal("16 x ").append(ironBars).append(" <= 6 x ").append(ironIngot)); +``` + +![translation-example](translation-example.png) + +:::info + +如果使用 `Component.translatable`,则游戏会去寻找[语言文件](https://zh.minecraft.wiki/w/%E8%B5%84%E6%BA%90%E5%8C%85#.E8.AF.AD.E8.A8.80)中的翻译标识符并按照语言文件的规则翻译。如果翻译失败则游戏将直接显示翻译标识符本身。模组开发者也可以指定自己的语言文件,并放在 `[模组 ID]:[语言代码]` 亦即 `assets/[模组 ID]/lang/[语言代码].json` 处,常见的语言代码有 `en_us` 及 `zh_cn` 等。语言文件亦可使用 Data Generator 生成。 + +::: + +:::caution + +我们鼓励自定义的翻译标识符包含模组 ID 本身(如 `chat.xiaozhong.welcome` 便要比 `chat.welcome` 来得更好),以避免和其他模组或 Minecraft 原版定义的翻译标识符冲突。 + +::: + +## Data Generator + +Data Generator 是 Minecraft 原版提供的用于自动生成资源文件的机制。NeoForge 拓展了这一机制,以方便模组开发者从繁冗的 JSON 文件里解脱出来。 + +模组开发者需监听 `GatherDataEvent` 事件,并在监听器中添加自己的 `DataProvider`。该事件将在启动 Data Generator 后触发,并将自动生成的结果放入 `src/generated/resources` 中。 + +Minecraft 原版便提供了很多不同的 `DataProvider` 实例,NeoForge 还对其进行了扩展:如对应方块状态及模型的 `BlockStateProvider`,对应物品模型的 `ItemModelProvider`,对应语言文件的 `LanguageProvider` 等。模组开发者需继承它们,并覆盖对应的方法。 + +这里以语言文件的自动生成为例,演示 Data Generator 的相关代码: + + + + +```java title="src/main/java/org/teacon/xiaozhong/Xiaozhong.java" +// 添加监听器 +NeoForge.EVENT_BUS.addListener(Xiaozhong::onLoggedIn); +modEventBus.addListener(Xiaozhong::onGatherData); + +public static void onLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + var player = event.getEntity(); + // 发送一条 chat.xiaozhong.welcome 对应的消息 + // Data Generator 生成了含有 chat.xiaozhong.welcome 的语言文件 + player.sendSystemMessage(Component.translatable("chat.xiaozhong.welcome")); +} + +public static void onGatherData(GatherDataEvent event) { + // Data Generator 启动时,该方法便会调用,新添加的 DataProvider 也开始工作 + // 从而在 src/generated/resources 下的 xiaozhong:en_us 和 xiaozhong:zh_cn 两处生成语言文件 + var gen = event.getGenerator(); + var packOutput = gen.getPackOutput(); + gen.addProvider(event.includeClient(), new EnglishLanguageProvider(packOutput)); + gen.addProvider(event.includeClient(), new ChineseLanguageProvider(packOutput)); +} + +// 英文语言文件 +public static class EnglishLanguageProvider extends LanguageProvider { + public EnglishLanguageProvider(PackOutput packOutput) { + // 前三个参数分别是 Data Generator 本身,模组 ID,以及语言代码 + // 语言代码对应语言文件的资源路径,此处为 xiaozhong:en_us + super(packOutput, "xiaozhong", "en_us"); + } + + @Override + protected void addTranslations() { + this.add("chat.xiaozhong.welcome", "Welcome to xiaozhong!"); + } +} + +// 中文语言文件 +public static class ChineseLanguageProvider extends LanguageProvider { + public ChineseLanguageProvider(PackOutput packOutput) { + // 前三个参数分别是 Data Generator 本身,模组 ID,以及语言代码 + // 语言代码对应语言文件的资源路径,此处为 xiaozhong:zh_cn + super(packOutput, "xiaozhong", "zh_cn"); + } + + @Override + protected void addTranslations() { + this.add("chat.xiaozhong.welcome", "欢迎来到正山小种!"); + } +} +``` + + + + {Xiaozhong} + + + +:::caution + +在更新了 Data Generator 相关代码后,模组开发者需及时通过 `runData` 重新运行 Data Generator,以更新 `src/generated/resources` 下的资源。 + +::: diff --git a/docs/1.21-neoforge/concepts/automatic-listener/Xiaozhong.java b/docs/1.21-neoforge/concepts/automatic-listener/Xiaozhong.java new file mode 100644 index 0000000..98dd62a --- /dev/null +++ b/docs/1.21-neoforge/concepts/automatic-listener/Xiaozhong.java @@ -0,0 +1,21 @@ +package org.teacon.xiaozhong; + +import net.minecraft.network.chat.Component; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; + +@Mod("xiaozhong") +public class Xiaozhong { + public Xiaozhong() {} + + @EventBusSubscriber(bus = EventBusSubscriber.Bus.GAME) + public static class PlayerLoggedInHandler { + @SubscribeEvent + public static void onLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + var player = event.getEntity(); + player.sendSystemMessage(Component.literal("Welcome to xiaozhong!")); + } + } +} diff --git a/docs/1.21-neoforge/concepts/empty/Xiaozhong.java b/docs/1.21-neoforge/concepts/empty/Xiaozhong.java new file mode 100644 index 0000000..c18fb43 --- /dev/null +++ b/docs/1.21-neoforge/concepts/empty/Xiaozhong.java @@ -0,0 +1,8 @@ +package org.teacon.xiaozhong; + +import net.neoforged.fml.common.Mod; + +@Mod("xiaozhong") +public class Xiaozhong { + public Xiaozhong() {} +} diff --git a/docs/1.21-neoforge/concepts/manual-listener/Xiaozhong.java b/docs/1.21-neoforge/concepts/manual-listener/Xiaozhong.java new file mode 100644 index 0000000..2ef9970 --- /dev/null +++ b/docs/1.21-neoforge/concepts/manual-listener/Xiaozhong.java @@ -0,0 +1,20 @@ +package org.teacon.xiaozhong; + +import net.minecraft.network.chat.Component; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; + +@Mod("xiaozhong") +public class Xiaozhong { + public Xiaozhong() { + NeoForge.EVENT_BUS.addListener(PlayerLoggedInHandler::onLoggedIn); + } + + public static class PlayerLoggedInHandler { + public static void onLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + var player = event.getEntity(); + player.sendSystemMessage(Component.literal("Welcome to xiaozhong!")); + } + } +} diff --git a/docs/1.21-neoforge/concepts/neoforge.mods.toml b/docs/1.21-neoforge/concepts/neoforge.mods.toml new file mode 100644 index 0000000..b6b0b83 --- /dev/null +++ b/docs/1.21-neoforge/concepts/neoforge.mods.toml @@ -0,0 +1,17 @@ +modLoader="javafml" # 模组所使用的加载器,此处固定 +loaderVersion="[1,)" # 加载器版本号,通常和 Forge 的大版本号有关 +license="All rights reserved" # 模组所采用的授权协议 + +[[mods]] # 模组本体信息 +modId="xiaozhong" # 模组 ID +version="${file.jarVersion}" # 模组的版本号,此处固定 +authors="TeaConMC" # 模组的作者,可在此填写自己的常用名称 +displayName="Xiaozhong" # 模组名称,通常和 build.gradle 所写相同 +description="The example mod for xiaozhong" # 模组的相关介绍,可以多行 + +[[dependencies.xiaozhong]] # 模组的相关依赖,通常会写上对 NeoForge 版本的依赖 +modId="forge" # 相关依赖的模组 ID +type=required # 相关依赖的类型:必须(required)、可选(optional)、不建议使用(discourgaed)、不兼容(incompatible) +versionRange="[21,)" # 相关依赖的版本号范围 +ordering="NONE" # 相关依赖和模组本体的加载顺序,也可以是 BEFORE 或 AFTER +side="BOTH" # 相关依赖是否一定要在玩家客户端或专用服务端出现,也可以是 CLIENT 或 SERVER diff --git a/docs/1.21-neoforge/concepts/text-example.png b/docs/1.21-neoforge/concepts/text-example.png new file mode 100644 index 0000000..12c1fe5 Binary files /dev/null and b/docs/1.21-neoforge/concepts/text-example.png differ diff --git a/docs/1.21-neoforge/concepts/translation-example.png b/docs/1.21-neoforge/concepts/translation-example.png new file mode 100644 index 0000000..15da605 Binary files /dev/null and b/docs/1.21-neoforge/concepts/translation-example.png differ diff --git a/docs/1.21-neoforge/concepts/xiaozhong/Xiaozhong.java b/docs/1.21-neoforge/concepts/xiaozhong/Xiaozhong.java new file mode 100644 index 0000000..9923d23 --- /dev/null +++ b/docs/1.21-neoforge/concepts/xiaozhong/Xiaozhong.java @@ -0,0 +1,53 @@ +package org.teacon.xiaozhong; + +import net.minecraft.data.PackOutput; +import net.minecraft.network.chat.Component; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.common.data.LanguageProvider; +import net.neoforged.neoforge.data.event.GatherDataEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; + + +@Mod("xiaozhong") +public class Xiaozhong { + public Xiaozhong(IEventBus modEventBus) { + NeoForge.EVENT_BUS.addListener(Xiaozhong::onLoggedIn); + modEventBus.addListener(Xiaozhong::onGatherData); + } + + public static void onLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + var player = event.getEntity(); + player.sendSystemMessage(Component.translatable("chat.xiaozhong.welcome")); + } + + public static void onGatherData(GatherDataEvent event) { + var gen = event.getGenerator(); + var packOutput = gen.getPackOutput(); + gen.addProvider(event.includeClient(), new EnglishLanguageProvider(packOutput)); + gen.addProvider(event.includeClient(), new ChineseLanguageProvider(packOutput)); + } + + public static class EnglishLanguageProvider extends LanguageProvider { + public EnglishLanguageProvider(PackOutput packOutput) { + super(packOutput, "xiaozhong", "en_us"); + } + + @Override + protected void addTranslations() { + this.add("chat.xiaozhong.welcome", "Welcome to xiaozhong!"); + } + } + + public static class ChineseLanguageProvider extends LanguageProvider { + public ChineseLanguageProvider(PackOutput packOutput) { + super(packOutput, "xiaozhong", "zh_cn"); + } + + @Override + protected void addTranslations() { + this.add("chat.xiaozhong.welcome", "欢迎来到正山小种!"); + } + } +} diff --git a/docs/1.21-neoforge/localization-with-json/README.mdx b/docs/1.21-neoforge/localization-with-json/README.mdx new file mode 100644 index 0000000..756904b --- /dev/null +++ b/docs/1.21-neoforge/localization-with-json/README.mdx @@ -0,0 +1,64 @@ +# 使用 JSON 语言文件 + + + +我们在之前的教程中,创建了一些方块与物品并创建了语言文件。但除了前文中使用 `LanguageProvider`,在 Minecraft 中,我们也可以直接手动编写语言文件。 + +## 非本地化名称与本地化名称 + +我们之前创建的物品、方块的例如 `item.xiaozhong.sulfur_dust` 的名称,都是一串代码,这种名称叫**非本地化名称**。而我们平时在 Minecraft 中看到的好看好记的物品、方块的名字,叫**本地化名称**。 + +将非本地化名称转换为本地化名称的过程,叫做**本地化**。 + +:::tip +实际上,本地化覆盖的范围不只有这一点,这是不严谨的说法。 +::: + +## 将我们的物品、方块本地化 + +在Minecraft中本地化名主要是写在一个 `json` 文件里的。例如这样: + +```json +{ + "key.name": "name", + "key.information": "information", + "modid": "teacon" +} +``` + +可见,我们的语言文件是一个**『键值对』**。『键』就是我们物品、方块的非本地化名称,『值』就是我们物品、方块的本地化名称。 + +### 简体中文 + +对于简体中文,只需要在 `src/main/resources/assets//lang/` 创建一个叫 `zh_cn.json` 的文件,在『键』的位置填上未本地化时 Minecraft 显示的例如 `item.xiaozhong.sulfur_dust` 的名称,『值』的位置填上你想要的本地化名称即可 + +```json +{ + "item.xiaozhong.sulfur_dust": "硫粉", + + "block.xiaozhong.sulfur_block": "硫磺块" +} +``` + +### 英语(美国) + +当然,仅有中文的语言文件还不够,因为如果没有英语(美国)的语言文件,使用其它语言时,还是会显示非本地化名称,所以,我们还要创建英语(美国)的语言文件。 + +在 `src/main/resources/assets//lang/` 创建一个叫 `en_us.json` 的文件,按照先前的写法编写即可。 + +```json +{ + "item.xiaozhong.sulfur_dust": "Sulfur Dust", + + "block.xiaozhong.sulfur_block": "Sulfur Block" +} +``` + +:::tip +当目前使用的语言文件没有某个非本地化名称对应的本地化名称时,Minecraft 将会使用该非本地化名称在 `en_us.json` 中的本地化名称。 +如果在 `en_us.json` 中依然没有找到对应的本地化名称,则直接以非本地化名称显示。 +::: + +### 文言文和其他语言 + +相应的,对于文言文,在同样的目录下创建 `lzh.json` 即可,其他语言亦如此。 \ No newline at end of file diff --git a/docs/1.21-neoforge/preparations/MDK-1.21-ModDevGradle-main.zip b/docs/1.21-neoforge/preparations/MDK-1.21-ModDevGradle-main.zip new file mode 100644 index 0000000..eeb215d Binary files /dev/null and b/docs/1.21-neoforge/preparations/MDK-1.21-ModDevGradle-main.zip differ diff --git a/docs/1.21-neoforge/preparations/README.mdx b/docs/1.21-neoforge/preparations/README.mdx new file mode 100644 index 0000000..ba4d5a6 --- /dev/null +++ b/docs/1.21-neoforge/preparations/README.mdx @@ -0,0 +1,150 @@ +# 准备工作 + + + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' +import CodeBlock from '@theme/CodeBlock' +import GradleProperties from '!!raw-loader!./gradle.properties' +import BuildGradleKotlin from '!!raw-loader!./build.gradle.kts' + +除去必要的硬件条件外,读者需要从 下载模组开发套件(Mod Development Kit,简称 MDK)。 + + +如果你下载的是形如 `MDK-1.21-ModDevGradle-main.zip` 的完整文件,那么恭喜你,下载成功了:找个合适的地方解压这个压缩包吧。 + +:::tip +下载 MDK 有困难的读者可以考虑[点这里](./MDK-1.21-ModDevGradle-main.zip)下载。 +::: + +## 模组 ID + +除去一个帅气的模组名称外,每个模组都需要一个全局唯一的模组 ID。 + +:::caution + +模组 ID 应当仅由数字(`0-9`)、小写字母(`a-z`)、和下划线(`_`)组成,最长 63 个字符,且尽量避免冲突:`industrialcraft2` 或者 `industrial_craft_2` 作为模组 ID 看起来就比 `ic2` 好很多(当然,如果你是 `ic2` 的作者,那当我没说)。 + +::: + +本篇指南的模组 ID 统一使用 `xiaozhong`。 + +## 配置文件 + +NeoForge MDK 的配置文件是 `build.gradle` 和 `gradle.properties`。 + +对 Gradle 熟悉的读者应该注意到了这些是 Gradle 的核心配置文件。 + +:::tip + +如欲进一步了解 Gradle,可参阅 [Gradle 官方用户指南](https://docs.gradle.org/current/userguide/userguide.html)。此外,Gradle 还提供了使用 Kotlin 编写构建脚本的支持,如欲进一步了解,可参阅 [Gradle Kotlin DSL 入门](https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotlin_dsl)以及本篇指南的[使用 Kotlin 配置项目](#使用-kotlin-配置项目)一节。 + +::: + +主要需要修改的地方只有一处:`gradle.properties`。 + + + + +```properties title="gradle.properties" +# Minecraft 官方对源代码编译后进行了混淆,因此为还原方法名、字段名、类名、以及包名以方便模组开发,NeoForge MDK 需要参照一个映射表。 +# 官方映射表未包含变量名的相关数据,而一个名叫「Parchment」的项目正好填补了这一空白。 +# NeoForge MDK 默认会自动使用 Parchment 提供的数据,我们则需要在这里指定 Parchment 的版本号。 +parchment_minecraft_version=1.21 +parchment_mappings_version=2024.06.23 +# Environment Properties +# You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge +# The Minecraft version must agree with the Neo version to get a valid artifact +minecraft_version=1.21 +# The Minecraft version range can use any release version of Minecraft as bounds. +# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly +# as they do not follow standard versioning conventions. +minecraft_version_range=[1.21,1.21.1) +# The Neo version must agree with the Minecraft version to get a valid artifact +neo_version=21.0.61-beta +# The Neo version range can use any version of Neo as bounds +neo_version_range=[21.0.61-beta,) +# The loader version range can only use the major version of FML as bounds +loader_version_range=[4,) + +# 模组的唯一识别 ID,必须和 @Mod(后面会提到)中填入的 ID 相同。 +# 关于模组 ID 的要求,请参考上文。 +mod_id=xiaozhong +# 模组的大名。 +mod_name=正山小种 +# 模组的授权协议,默认为 ARR,即用户无权使用你的模组做任何事。 +# 您可以在 https://choosealicense.com/ 了解更多可能的选项。 +# 我们建议您在这里使用 SPDX 收录的许可证代码,以方便机器自动识别。 +mod_license=All Rights Reserved +# 模组版本号。我们在此建议使用语义化版本(Semantic Versioning)。 +# 关于语义化版本的更多信息,可参考:https://semver.org/ +mod_version=1.0.0 +# 模组的 Maven 组名,通常和包名相同。 +# 扩展阅读:https://maven.apache.org/guides/mini/guide-naming-conventions.html +mod_group_id=org.teacon.xiaozhong +# 模组作者列表 +mod_authors=YourNameHere, OtherNameHere +# 模组简介。你可以使用 \n 表示换行。 +mod_description=Example mod description.\nNewline characters can be used and will be replaced properly. +``` + + + + {GradleProperties} + + + + +配置文件修改完成后,将 `build.gradle` 所在目录使用文本编辑器或 IDE 打开,等待进度条加载完成即可。 + +:::info + +NeoForge 官方倾向于支持的开发工具有:Eclipse、IntelliJ IDEA、和 Visual Studio Code。我们鼓励使用 IntelliJ IDEA()作为开发工具。 + +::: + +:::caution + +文本编辑器或 IDE 会自动完成全部配置(对 IntelliJ IDEA 而言进度会在 Build 侧边栏显示),但如果网络环境欠佳,配置进度将会十分缓慢,甚至可能出错。 + +如果反复尝试均无法配置成功,可尝试删除**用户根目录**的 `.gradle` 目录重试。 + +::: + +## MDK 任务 + +Gradle 是一个基于任务的项目管理系统。通过 NeoForge MDK 我们可以执行许多不同的 Gradle 任务,但最常用的任务如下: + +* `clean`:清理和 NeoForge MDK 有关的自动生成的部分文件。 +* `build`:生成模组文件(生成的文件可在 `build/libs` 找到,可用于发布)。 + +### 启动选项 + +NeoForge MDK 会为我们自动生成四套启动选项。其中: + +- `runClient` 用于启动 Minecraft 玩家客户端 +- `runServer` 用于启动专用服务端 +- `runGameTestServer` 用于游戏测试 +- `runData` 则用于启动 Data Generator。 + +:::info + +Data Generator 是 Minecraft 原版的一种机制,可以用于自动生成资源文件。 + +我们[稍后](./concepts#data-generator)会用到 Data Generator 这一机制。 + +::: + +常见的开发工具均深度集成了 Gradle 的支持(或有对应的插件),无论是执行 Gradle 任务还是通过启动选项启动乃至调试游戏均可在开发工具内部进行。 + +:::tip + +IntelliJ IDEA 用户可打开 Gradle 侧边栏,并在 `Tasks` 下找到上述任务并双击执行。在 IntelliJ IDEA 执行 `genIntellijRuns` 任务后,右上角便会出现四个相应的启动选项。 + +![idea-run-example.png](idea-run-example.png) + +::: + +## 使用 Kotlin 配置项目 + +*即将到来* \ No newline at end of file diff --git a/docs/1.21-neoforge/preparations/gradle.properties b/docs/1.21-neoforge/preparations/gradle.properties new file mode 100644 index 0000000..fa8826c --- /dev/null +++ b/docs/1.21-neoforge/preparations/gradle.properties @@ -0,0 +1,45 @@ +# Sets default memory used for gradle commands. Can be overridden by user or command line properties. +org.gradle.jvmargs=-Xmx1G +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true + +#read more on this at https://github.com/neoforged/ModDevGradle?tab=readme-ov-file#better-minecraft-parameter-names--javadoc-parchment +# you can also find the latest versions at: https://parchmentmc.org/docs/getting-started +parchment_minecraft_version=1.21 +parchment_mappings_version=2024.06.23 +# Environment Properties +# You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge +# The Minecraft version must agree with the Neo version to get a valid artifact +minecraft_version=1.21 +# The Minecraft version range can use any release version of Minecraft as bounds. +# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly +# as they do not follow standard versioning conventions. +minecraft_version_range=[1.21,1.21.1) +# The Neo version must agree with the Minecraft version to get a valid artifact +neo_version=21.0.61-beta +# The Neo version range can use any version of Neo as bounds +neo_version_range=[21.0.0-beta,) +# The loader version range can only use the major version of FML as bounds +loader_version_range=[4,) + +## Mod Properties + +# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} +# Must match the String constant located in the main mod class annotated with @Mod. +mod_id=xiaozhong +# The human-readable display name for the mod. +mod_name=正山小种 +# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. +mod_license=All Rights Reserved +# The mod version. See https://semver.org/ +mod_version=1.0.0 +# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. +# This should match the base package used for the mod sources. +# See https://maven.apache.org/guides/mini/guide-naming-conventions.html +mod_group_id=org.teacon.xiaozhong +# The authors of the mod. This is a simple text string that is used for display purposes in the mod list. +mod_authors=YourNameHere, OtherNameHere +# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. +mod_description=Example mod description.\nNewline characters can be used and will be replaced properly. \ No newline at end of file diff --git a/docs/1.21-neoforge/preparations/idea-run-example.png b/docs/1.21-neoforge/preparations/idea-run-example.png new file mode 100644 index 0000000..c1f0ecb Binary files /dev/null and b/docs/1.21-neoforge/preparations/idea-run-example.png differ diff --git a/docs/1.21-neoforge/world-gen/README.mdx b/docs/1.21-neoforge/world-gen/README.mdx new file mode 100644 index 0000000..4e0b011 --- /dev/null +++ b/docs/1.21-neoforge/world-gen/README.mdx @@ -0,0 +1,61 @@ +# 世界生成 + + + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' +import CodeBlock from '@theme/CodeBlock' +import AddTree from '!!raw-loader!./xiaozhong/neoforge/biome_modifier/add_tree.json' +import CGoldTree from '!!raw-loader!./xiaozhong/worldgen/configured_feature/c_gold_tree.json' +import GoldTree from '!!raw-loader!./xiaozhong/worldgen/placed_feature/gold_tree.json' + +Minecraft 将世界生成划分为了若干个阶段,其中模组开发者们经常关注的阶段有下列几个: + +- 生物群系生成:游戏会在这一阶段中决定世界的每一个位置上都是什么生物群系。 +- 地形生成:游戏会在这一阶段中决定世界的整体地势地貌。 +- 地表生成:游戏会在这一阶段中根据生物群系等信息,为光秃秃的地表覆盖上一层「衣服」,例如沙漠的沙子、平原的草方块等 +- 大型结构生成:游戏会在这一阶段中随机放置少量大型建筑(村庄、要塞、神殿、等) +- 地物生成:游戏会在这一阶段中随机放置小型的地表、地下装饰,如树木、竹子、大型蘑菇、晶洞、各类矿石等。 + +我们接下来将通过几个实例来说明「什么时候该用什么」。 + +## 生成矿物,树木,或是··· + +由于大部分地物都可以使用数据包定制,所以配置地物本身不再赘述,此处仅推荐一个[工具](https://misode.github.io/)用于制作地物。 + +可能已经有读者尝试过并感到一头雾水:怎么把他们加到已有的生物群系里而不是必须要重新写一个生物群系? + +NeoForge 提供的 Biome Modifier 机制可以让我们实现这一目标。 + +参照已有的实现类,我们将使用 `AddFeaturesBiomeModifier`。 + +他们将以这样的格式组织: + + xiaozhong + ├─ neoforge + │ └─ biome_modifier + │ └─ add_tree.json + └─ worldgen + ├─ configured_feature + │ └─ c_gold_tree.json + └─ placed_feature + └─ gold_tree.json + + + + {AddTree} + + + {CGoldTree} + + + {GoldTree} + + + +最终我们会在游戏内看到这样的内容: + +![gold_tree](gold_tree.png) \ No newline at end of file diff --git a/docs/1.21-neoforge/world-gen/gold_tree.png b/docs/1.21-neoforge/world-gen/gold_tree.png new file mode 100644 index 0000000..14216ba Binary files /dev/null and b/docs/1.21-neoforge/world-gen/gold_tree.png differ diff --git a/docs/1.21-neoforge/world-gen/xiaozhong/neoforge/biome_modifier/add_tree.json b/docs/1.21-neoforge/world-gen/xiaozhong/neoforge/biome_modifier/add_tree.json new file mode 100644 index 0000000..007d9ea --- /dev/null +++ b/docs/1.21-neoforge/world-gen/xiaozhong/neoforge/biome_modifier/add_tree.json @@ -0,0 +1,6 @@ +{ + "type": "neoforge:add_features", + "biomes": "#minecraft:is_forest", + "features": "xiaozhong:gold_tree", + "step": "vegetal_decoration" +} \ No newline at end of file diff --git a/docs/1.21-neoforge/world-gen/xiaozhong/worldgen/configured_feature/c_gold_tree.json b/docs/1.21-neoforge/world-gen/xiaozhong/worldgen/configured_feature/c_gold_tree.json new file mode 100644 index 0000000..2703340 --- /dev/null +++ b/docs/1.21-neoforge/world-gen/xiaozhong/worldgen/configured_feature/c_gold_tree.json @@ -0,0 +1,39 @@ +{ + "type": "minecraft:tree", + "config": { + "minimum_size": { + "type": "minecraft:two_layers_feature_size" + }, + "dirt_provider": { + "type": "minecraft:simple_state_provider", + "state": { + "Name": "minecraft:stone" + } + }, + "trunk_provider": { + "type": "minecraft:simple_state_provider", + "state": { + "Name": "minecraft:blackstone" + } + }, + "foliage_provider": { + "type": "minecraft:simple_state_provider", + "state": { + "Name": "minecraft:glowstone" + } + }, + "trunk_placer": { + "type": "minecraft:straight_trunk_placer", + "base_height": 5, + "height_rand_a": 2, + "height_rand_b": 0 + }, + "foliage_placer": { + "type": "minecraft:blob_foliage_placer", + "radius": 2, + "offset": 0, + "height": 3 + }, + "decorators": [] + } +} diff --git a/docs/1.21-neoforge/world-gen/xiaozhong/worldgen/placed_feature/gold_tree.json b/docs/1.21-neoforge/world-gen/xiaozhong/worldgen/placed_feature/gold_tree.json new file mode 100644 index 0000000..bee94d0 --- /dev/null +++ b/docs/1.21-neoforge/world-gen/xiaozhong/worldgen/placed_feature/gold_tree.json @@ -0,0 +1,47 @@ +{ + "feature": "xiaozhong:c_gold_tree", + "placement": [ + { + "type": "minecraft:count", + "count": { + "type": "minecraft:weighted_list", + "distribution": [ + { + "weight": 9, + "data": 10 + }, + { + "weight": 1, + "data": 11 + } + ] + } + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:surface_water_depth_filter", + "max_water_depth": 0 + }, + { + "type": "minecraft:heightmap", + "heightmap": "OCEAN_FLOOR" + }, + { + "type": "minecraft:biome" + }, + { + "type": "minecraft:block_predicate_filter", + "predicate": { + "type": "minecraft:would_survive", + "state": { + "Name": "minecraft:birch_sapling", + "Properties": { + "stage": "0" + } + } + } + } + ] +} diff --git a/docusaurus.config.js b/docusaurus.config.js index eabd4f7..261b557 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -29,7 +29,7 @@ const config = { routeBasePath: '/', sidebarPath: require.resolve('./src/sidebars.js'), editUrl({ docPath }) { - return 'https://github.com/teaconmc/Xiaozhong/blob/1.19-forge/docs/' + docPath + return 'https://github.com/teaconmc/Xiaozhong/blob/1.21-neoforge/docs/' + docPath } }, theme: { @@ -58,15 +58,20 @@ const config = { navbar: { title: '正山小种 - Forge 模组开发指南', items: [ + { + position: 'right', + to: '1.21-neoforge', + label: '1.21-NeoForge', + }, { position: 'right', to: '1.19.x', - label: '1.19.x', + label: '1.19.x-Forge', }, { position: 'right', to: '1.18.x', - label: '1.18.x', + label: '1.18.x-Forge', }, { position: 'right', diff --git a/src/pages/index.tsx b/src/pages/index.tsx index f9f643d..1b4cbd0 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -2,5 +2,5 @@ import React from 'react' import {Redirect} from '@docusaurus/router' export default function Index() { - return + return } diff --git a/src/sidebars.js b/src/sidebars.js index a22ec0d..218d727 100644 --- a/src/sidebars.js +++ b/src/sidebars.js @@ -3,6 +3,23 @@ /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { 'xiaozhong': [{ + type: 'category', + label: '正山小种 - 1.21 + NeoForge', + collapsed: false, + collapsible: false, + link: { + type: 'doc', + id: '1.21-neoforge/README' + }, + items: [ + '1.21-neoforge/preparations/README', + '1.21-neoforge/concepts/README', + '1.21-neoforge/block-item-objects/README', + '1.21-neoforge/localization-with-json/README', + '1.21-neoforge/block-entity/README', + '1.21-neoforge/world-gen/README', + ], + }, { type: 'category', label: '正山小种 - 1.19.x', collapsed: false,