From e6ac0d4b0161e9a4a62cec35c1fbf422e667ccd9 Mon Sep 17 00:00:00 2001 From: src_resources Date: Sun, 8 Oct 2023 18:03:47 +0800 Subject: [PATCH] Add the translation section with drafted Chinese translations for NeoForged Documentation Signed-off-by: src_resources --- docs/translation/index.md | 22 + .../zh_CN/advanced/accesstransformers.md | 112 +++++ docs/translation/zh_CN/blockentities/ber.md | 28 ++ docs/translation/zh_CN/blockentities/index.md | 135 ++++++ docs/translation/zh_CN/blocks/index.md | 49 ++ docs/translation/zh_CN/blocks/states.md | 94 ++++ docs/translation/zh_CN/concepts/events.md | 140 ++++++ .../zh_CN/concepts/internationalization.md | 67 +++ docs/translation/zh_CN/concepts/lifecycle.md | 74 +++ docs/translation/zh_CN/concepts/registries.md | 194 ++++++++ docs/translation/zh_CN/concepts/resources.md | 19 + docs/translation/zh_CN/concepts/sides.md | 119 +++++ docs/translation/zh_CN/contributing.md | 4 + .../zh_CN/datagen/client/localization.md | 40 ++ .../zh_CN/datagen/client/modelproviders.md | 419 +++++++++++++++++ .../zh_CN/datagen/client/sounds.md | 83 ++++ docs/translation/zh_CN/datagen/index.md | 86 ++++ .../zh_CN/datagen/server/advancements.md | 67 +++ .../datagen/server/datapackregistries.md | 129 ++++++ docs/translation/zh_CN/datagen/server/glm.md | 30 ++ .../zh_CN/datagen/server/loottables.md | 144 ++++++ .../zh_CN/datagen/server/recipes.md | 194 ++++++++ docs/translation/zh_CN/datagen/server/tags.md | 121 +++++ .../zh_CN/datastorage/capabilities.md | 175 +++++++ docs/translation/zh_CN/datastorage/codecs.md | 438 ++++++++++++++++++ .../zh_CN/datastorage/saveddata.md | 42 ++ docs/translation/zh_CN/forgedev/index.md | 109 +++++ .../zh_CN/forgedev/prguidelines.md | 97 ++++ .../zh_CN/gameeffects/particles.md | 125 +++++ docs/translation/zh_CN/gameeffects/sounds.md | 110 +++++ .../translation/zh_CN/gettingstarted/index.md | 119 +++++ .../zh_CN/gettingstarted/modfiles.md | 165 +++++++ .../zh_CN/gettingstarted/structuring.md | 81 ++++ .../zh_CN/gettingstarted/versioning.md | 56 +++ docs/translation/zh_CN/gui/menus.md | 337 ++++++++++++++ docs/translation/zh_CN/gui/screens.md | 334 +++++++++++++ docs/translation/zh_CN/index.md | 114 +++++ docs/translation/zh_CN/items/bewlr.md | 34 ++ docs/translation/zh_CN/items/index.md | 73 +++ docs/translation/zh_CN/legacy/index.md | 29 ++ docs/translation/zh_CN/legacy/porting.md | 22 + docs/translation/zh_CN/misc/config.md | 135 ++++++ docs/translation/zh_CN/misc/debugprofiler.md | 46 ++ docs/translation/zh_CN/misc/gametest.md | 262 +++++++++++ docs/translation/zh_CN/misc/keymappings.md | 151 ++++++ docs/translation/zh_CN/misc/updatechecker.md | 63 +++ docs/translation/zh_CN/networking/entities.md | 35 ++ docs/translation/zh_CN/networking/index.md | 20 + .../zh_CN/networking/simpleimpl.md | 118 +++++ .../ambientocclusion_annotated.png | Bin 0 -> 98157 bytes .../rendering/modelextensions/facedata.md | 114 +++++ .../rendering/modelextensions/rendertypes.md | 80 ++++ .../rendering/modelextensions/transforms.md | 76 +++ .../rendering/modelextensions/visibility.md | 52 +++ .../rendering/modelloaders/bakedmodel.md | 57 +++ .../zh_CN/rendering/modelloaders/index.md | 26 ++ .../rendering/modelloaders/itemoverrides.md | 49 ++ .../zh_CN/rendering/modelloaders/transform.md | 37 ++ .../zh_CN/resources/client/index.md | 15 + .../zh_CN/resources/client/models/index.md | 24 + .../resources/client/models/itemproperties.md | 62 +++ .../zh_CN/resources/client/models/tinting.md | 32 ++ .../zh_CN/resources/server/advancements.md | 165 +++++++ .../zh_CN/resources/server/conditional.md | 179 +++++++ .../translation/zh_CN/resources/server/glm.md | 145 ++++++ .../zh_CN/resources/server/index.md | 14 + .../zh_CN/resources/server/loottables.md | 110 +++++ .../zh_CN/resources/server/recipes/custom.md | 128 +++++ .../zh_CN/resources/server/recipes/incode.md | 62 +++ .../zh_CN/resources/server/recipes/index.md | 102 ++++ .../resources/server/recipes/ingredients.md | 175 +++++++ .../zh_CN/resources/server/tags.md | 120 +++++ 72 files changed, 7484 insertions(+) create mode 100644 docs/translation/index.md create mode 100644 docs/translation/zh_CN/advanced/accesstransformers.md create mode 100644 docs/translation/zh_CN/blockentities/ber.md create mode 100644 docs/translation/zh_CN/blockentities/index.md create mode 100644 docs/translation/zh_CN/blocks/index.md create mode 100644 docs/translation/zh_CN/blocks/states.md create mode 100644 docs/translation/zh_CN/concepts/events.md create mode 100644 docs/translation/zh_CN/concepts/internationalization.md create mode 100644 docs/translation/zh_CN/concepts/lifecycle.md create mode 100644 docs/translation/zh_CN/concepts/registries.md create mode 100644 docs/translation/zh_CN/concepts/resources.md create mode 100644 docs/translation/zh_CN/concepts/sides.md create mode 100644 docs/translation/zh_CN/contributing.md create mode 100644 docs/translation/zh_CN/datagen/client/localization.md create mode 100644 docs/translation/zh_CN/datagen/client/modelproviders.md create mode 100644 docs/translation/zh_CN/datagen/client/sounds.md create mode 100644 docs/translation/zh_CN/datagen/index.md create mode 100644 docs/translation/zh_CN/datagen/server/advancements.md create mode 100644 docs/translation/zh_CN/datagen/server/datapackregistries.md create mode 100644 docs/translation/zh_CN/datagen/server/glm.md create mode 100644 docs/translation/zh_CN/datagen/server/loottables.md create mode 100644 docs/translation/zh_CN/datagen/server/recipes.md create mode 100644 docs/translation/zh_CN/datagen/server/tags.md create mode 100644 docs/translation/zh_CN/datastorage/capabilities.md create mode 100644 docs/translation/zh_CN/datastorage/codecs.md create mode 100644 docs/translation/zh_CN/datastorage/saveddata.md create mode 100644 docs/translation/zh_CN/forgedev/index.md create mode 100644 docs/translation/zh_CN/forgedev/prguidelines.md create mode 100644 docs/translation/zh_CN/gameeffects/particles.md create mode 100644 docs/translation/zh_CN/gameeffects/sounds.md create mode 100644 docs/translation/zh_CN/gettingstarted/index.md create mode 100644 docs/translation/zh_CN/gettingstarted/modfiles.md create mode 100644 docs/translation/zh_CN/gettingstarted/structuring.md create mode 100644 docs/translation/zh_CN/gettingstarted/versioning.md create mode 100644 docs/translation/zh_CN/gui/menus.md create mode 100644 docs/translation/zh_CN/gui/screens.md create mode 100644 docs/translation/zh_CN/index.md create mode 100644 docs/translation/zh_CN/items/bewlr.md create mode 100644 docs/translation/zh_CN/items/index.md create mode 100644 docs/translation/zh_CN/legacy/index.md create mode 100644 docs/translation/zh_CN/legacy/porting.md create mode 100644 docs/translation/zh_CN/misc/config.md create mode 100644 docs/translation/zh_CN/misc/debugprofiler.md create mode 100644 docs/translation/zh_CN/misc/gametest.md create mode 100644 docs/translation/zh_CN/misc/keymappings.md create mode 100644 docs/translation/zh_CN/misc/updatechecker.md create mode 100644 docs/translation/zh_CN/networking/entities.md create mode 100644 docs/translation/zh_CN/networking/index.md create mode 100644 docs/translation/zh_CN/networking/simpleimpl.md create mode 100644 docs/translation/zh_CN/rendering/modelextensions/ambientocclusion_annotated.png create mode 100644 docs/translation/zh_CN/rendering/modelextensions/facedata.md create mode 100644 docs/translation/zh_CN/rendering/modelextensions/rendertypes.md create mode 100644 docs/translation/zh_CN/rendering/modelextensions/transforms.md create mode 100644 docs/translation/zh_CN/rendering/modelextensions/visibility.md create mode 100644 docs/translation/zh_CN/rendering/modelloaders/bakedmodel.md create mode 100644 docs/translation/zh_CN/rendering/modelloaders/index.md create mode 100644 docs/translation/zh_CN/rendering/modelloaders/itemoverrides.md create mode 100644 docs/translation/zh_CN/rendering/modelloaders/transform.md create mode 100644 docs/translation/zh_CN/resources/client/index.md create mode 100644 docs/translation/zh_CN/resources/client/models/index.md create mode 100644 docs/translation/zh_CN/resources/client/models/itemproperties.md create mode 100644 docs/translation/zh_CN/resources/client/models/tinting.md create mode 100644 docs/translation/zh_CN/resources/server/advancements.md create mode 100644 docs/translation/zh_CN/resources/server/conditional.md create mode 100644 docs/translation/zh_CN/resources/server/glm.md create mode 100644 docs/translation/zh_CN/resources/server/index.md create mode 100644 docs/translation/zh_CN/resources/server/loottables.md create mode 100644 docs/translation/zh_CN/resources/server/recipes/custom.md create mode 100644 docs/translation/zh_CN/resources/server/recipes/incode.md create mode 100644 docs/translation/zh_CN/resources/server/recipes/index.md create mode 100644 docs/translation/zh_CN/resources/server/recipes/ingredients.md create mode 100644 docs/translation/zh_CN/resources/server/tags.md diff --git a/docs/translation/index.md b/docs/translation/index.md new file mode 100644 index 000000000..962702352 --- /dev/null +++ b/docs/translation/index.md @@ -0,0 +1,22 @@ +Translations +============ + +# Translations of NeoForged Documentation + +The following is a list of translated versions of NeoForged Documentation so as to offer a better reading experience for readers in different language regions. + +:::caution +Note that the translations may not be 100% appropriate due to the ability limit of translators or untracked updates of original English documentation. If there's trouble during your reading with the content, try refering to the original documentation for solution and any corrects are welcome relating to bridging the translation gaps. + +If you have any questions with translated documentation, contact the maintainer of the translation first. +::: + +# List of Translations + +### [Chinese (Simplified)][zh_CN] +* Maintainer: [src_resources][zh_CN-maintainer] +* Repository: [link][zh_CN-repo] + +[zh_CN]: ./zh_CN/index.md +[zh_CN-maintainer]: https://github.com/srcres258 +[zh_CN-repo]: https://github.com/srcres258/neo-doc diff --git a/docs/translation/zh_CN/advanced/accesstransformers.md b/docs/translation/zh_CN/advanced/accesstransformers.md new file mode 100644 index 000000000..49bbd10f8 --- /dev/null +++ b/docs/translation/zh_CN/advanced/accesstransformers.md @@ -0,0 +1,112 @@ +访问转换器 +========= + +访问转换器(简称AT)允许扩大可见性并修改类、方法和字段的`final`标志。它们允许模组开发者访问和修改其控制之外的类中不可访问的成员。 + +[规范文档][specs]可以在Minecraft Forge GitHub上查看。 + +添加AT +------ + +在你的模组项目中添加一个访问转换器就像在`build.gradle`中添加一行一样简单: + +```groovy +// 此代码块也是指定映射版本的位置 +minecraft { + accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg') +} +``` + +添加或修改访问转换器后,必须刷新Gradle项目才能使转换生效。 + +在开发过程中,AT文件可以位于上面一行指定的任何位置。然而,当在非开发环境中加载时,Forge只会在JAR文件中搜索`META-INF/accesstransformer.cfg`的确切路径。 + +注释 +---- + +`#`之后直到行尾的所有文本都将被视为注释,不会被解析。 + +访问修饰符 +--------- + +访问修饰符指定给定目标将转换为什么样的新成员可见性。按可见性降序: + +* `public` - 对其包内外的所有类可见 +* `protected` - 仅对包内和子类中的类可见 +* `default` - 仅对包内的类可见 +* `private` - 仅对类内部可见 + +一个特殊的修饰符`+f`和`-f`可以附加到前面提到的修饰符中,以分别添加或删除`final`修饰符,这可以在应用时防止子类化、方法重写或字段修改。 + +!!! 警告 + 指令只修改它们直接引用的方法;任何重写方法都不会进行访问转换。建议确保转换后的方法没有限制可见性的未转换重写,这将导致JVM抛出错误(Error)。 + + 可以安全转换的方法示例有`private`方法、`final`方法(或`final`类中的方法)和`static`方法。 + +目标和指令 +--------- + +!!! 重要 + 在Minecraft类上使用访问转换器时,字段和方法必须使用SRG名称。 + +### 类 +转换为目标类: +``` + +``` +内部类是通过将外部类的完全限定名称和内部类的名称与分隔符`$`组合来表示的。 + +### 字段 +转换为目标字段: +``` + +``` + +### 方法 +目标方法需要一种特殊的语法来表示方法参数和返回类型: +``` + () +``` + +#### 指定类型 + +也称为“描述符”:有关更多技术细节,请参阅[Java虚拟机规范,SE 8,第4.3.2和4.3.3节][jvmdescriptors]。 + +* `B` - `byte`,有符号字节 +* `C` - `char`,UTF-16 Unicode字符 +* `D` - `double`,双精度浮点值 +* `F` - `float`,单精度浮点值 +* `I` - `integer`,32位整数 +* `J` - `long`,64位整数 +* `S` - `short`,有符号short +* `Z` - `boolean`,`true`或`false`值 +* `[` - 代表数组的一个维度 + * 例如:`[[S`指`short[][]` +* `L;` - 代表一个引用类型 + * 例如:`Ljava/lang/String;`指`java.lang.String`引用类型 _(注意左斜杠的使用而非句点)_ +* `(` - 代表方法描述符,应在此处提供参数,如果不存在参数,则不提供任何参数 + * 例如:`(I)Z`指的是一个需要整数参数并返回布尔值的方法 +* `V` - 指示方法不返回值,只能在方法描述符的末尾使用 + * 例如:`()V`指的是一个没有参数且不返回任何值的方法 + +示例 +---- + +``` +# 将Crypt中的ByteArrayToKeyFunction接口转换为public +public net.minecraft.util.Crypt$ByteArrayToKeyFunction + +# 将MinecraftServer中的'random'转换为protected并移除final修饰符 +protected-f net.minecraft.server.MinecraftServer f_129758_ #random + +# 将Util中的'makeExecutor'方法转换为public, +# 接受一个String并返回一个ExecutorService +public net.minecraft.Util m_137477_(Ljava/lang/String;)Ljava/util/concurrent/ExecutorService; #makeExecutor + +# 将UUIDUtil中的'leastMostToIntArray'方法转换为public +# 接受两个long参数并返回一个int[] +public net.minecraft.core.UUIDUtil m_235872_(JJ)[I #leastMostToIntArray +``` + +[specs]: https://github.com/MinecraftForge/AccessTransformers/blob/master/FMLAT.md +[jvmdescriptors]: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2 diff --git a/docs/translation/zh_CN/blockentities/ber.md b/docs/translation/zh_CN/blockentities/ber.md new file mode 100644 index 000000000..90e4a722b --- /dev/null +++ b/docs/translation/zh_CN/blockentities/ber.md @@ -0,0 +1,28 @@ +BlockEntityRenderer +=================== + +`BlockEntityRenderer`(简称`BER`)用于以静态烘焙模型(JSON、OBJ、B3D等)无法表示的方式渲染方块。方块实体渲染器要求方块具有`BlockEntity`。 + +创建一个BER +---------- + +要创建BER,请创建一个继承自`BlockEntityRenderer`的类。它采用一个泛型参数来指定方块的`BlockEntity`类。该泛型参数用于BER的`render`方法。 + +对于任意一个给定的`BlockEntityType`,仅存在一个BER。因此,特定于存档中单个实例的值应该存储在传递给渲染器的方块实体中,而不是存储在BER本身中。例如,如果将逐帧递增的整数存储在BER中,则对于该存档中该类型的每个方块实体也会逐帧递增。 + +### `render` + +为了渲染方块实体,每帧都调用此方法。 + +#### 参数 +* `blockEntity`: 这是正在渲染的方块实体的实例。 +* `partialTick`: 在帧的摩擦过程中,从上一次完整刻度开始经过的时间量。 +* `poseStack`: 一个栈,包含偏移到方块实体当前位置的四维矩阵条目。 +* `bufferSource`: 能够访问顶点Consumer的渲染缓冲区。 +* `combinedLight`: 方块实体上当前亮度值的整数。 +* `combinedOverlay`: 设置为方块实体的当前overlay的整数,通常为`OverlayTexture#NO_OVERLAY`或655,360。 + +注册一个BER +---------- + +要注册BER,你必须订阅模组事件总线上的`EntityRenderersEvent$RegisterRenderers`事件,并调用`#registerBlockEntityRenderer`。 diff --git a/docs/translation/zh_CN/blockentities/index.md b/docs/translation/zh_CN/blockentities/index.md new file mode 100644 index 000000000..5b1b32a8c --- /dev/null +++ b/docs/translation/zh_CN/blockentities/index.md @@ -0,0 +1,135 @@ +# 方块实体 + +`BlockEntities`类似于绑定到某一方块的简化的`Entities`。 +它们能用于存储动态数据、执行基于游戏刻的任务和动态渲染。 +原版Minecraft中的一些例子是处理箱子的物品栏、熔炉的熔炼逻辑或信标的区域效果。 +模组中存在更高级的示例,例如采石场(如BC)、分拣机(如IC2)、管道(如BC)和显示器(如OC)。(括号内容为译者注。) + +!!! 注意 + `BlockEntities`并不是万能的解决方案,如果使用错误,它们可能会导致游戏卡顿。 + 如果可能的话,尽量避免使用。 + +## 注册 + +方块实体是动态创建和删除的,因此它们本身不是注册表对象。 + +为了创建`BlockEntity`,你需要继承`BlockEntity`类。这样,另一个对象被替代性地注册以方便创建和引用动态对象的*类型*。对于`BlockEntity`,这些对象被称为`BlockEntityType`。 + +`BlockEntityType`可以像任何其他注册表对象一样进行[注册][registration]。若要构造`BlockEntityType`,可以通过`BlockEntityType$Builder#of`使用其Builder形式。这需要两个参数:`BlockEntityType$BlockEntitySupplier`,它接受`BlockPos`和`BlockState`来创建关联`BlockEntity`的新实例,以及该`BlockEntity`可以附加到的`Block`的可变参数。构建该`BlockEntityType`是通过调用`BlockEntityType$Builder#build`来完成的。其接受一个`Type`,表示用于引用某个`DataFixer`中的此注册表对象的类型安全引用。由于`DataFixer`是用于模组的可选系统,因此其也可用`null`代替。 + +```java +// 对于某个类型为DeferredRegister>的REGISTER +public static final RegistryObject> MY_BE = REGISTER.register("mybe", () -> BlockEntityType.Builder.of(MyBE::new, validBlocks).build(null)); + +// 在MyBE(一个BlockEntity的子类)中 +public MyBE(BlockPos pos, BlockState state) { + super(MY_BE.get(), pos, state); +} +``` + +## 创建一个`BlockEntity` + +要创建`BlockEntity`并将其附加到`Block`,`EntityBlock`接口必须在你的`Block`子类上实现。方法`EntityBlock#newBlockEntity(BlockPos, BlockState)`必须实现并返回一个你的`BlockEntity`的新实例。 + +## 将数据存储到你的`BlockEntity` + +为了保存数据,请重写以下两个方法: +```java +BlockEntity#saveAdditional(CompoundTag tag) + +BlockEntity#load(CompoundTag tag) +``` +每当包含`BlockEntity`的`LevelChunk`从标签加载/保存到标签时,都会调用这些方法。 +使用它们以读取和写入你的方块实体类的字段。 + +!!! 注意 + 每当你的数据发生改变时,你需要调用`BlockEntity#setChanged`;否则,保存存档时可能会跳过包含你的`BlockEntity`的`LevelChunk`。 + +!!! 重要 + 调用`super`方法非常重要! + + 标签名称`id`、`x`、`y`、`z`、`ForgeData`和`ForgeCaps`均由`super`方法保留。 + +## 计时的`BlockEntity` + +如果你需要一个计时的`BlockEntity`,例如为了跟踪冶炼过程中的进度,则必须在`EntityBlock`中实现并重写另一个方法:`EntityBlock#getTicker(Level, BlockState, BlockEntityType)`。这可以根据用户所处的逻辑端实现不同的计时器,或者只实现一个通用计时器。无论哪种情况,都必须返回`BlockEntityTicker`。由于这是一个功能性的接口,因此它可以转而采用一个表示计时器的方法: + +```java +// 在某个Block子类内 +@Nullable +@Override +public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + return type == MyBlockEntityTypes.MYBE.get() ? MyBlockEntity::tick : null; +} + +// 在MyBlockEntity内 +public static void tick(Level level, BlockPos pos, BlockState state, MyBlockEntity blockEntity) { + // 处理一些事情 +} +``` + +!!! 注意 + 这个方法在每个游戏刻都会调用;因此,你应该避免在这里进行复杂的计算。如果可能的话,你应该每X个游戏刻进行更复杂的计算。(一秒钟内的游戏刻数量可能低于20(二十),但不会更高) + +## 向客户端同步数据 + +有三种方法可以将数据同步到客户端:在区块加载时同步、在方块更新时同步以及使用自定义网络消息同步。 + +### 在LevelChunk加载时同步 + +为此你需要重写 +```java +BlockEntity#getUpdateTag() + +IForgeBlockEntity#handleUpdateTag(CompoundTag tag) +``` +同样,这非常简单,第一个方法收集应该发送到客户端的数据,而第二个方法处理这些数据。如果你的`BlockEntity`不包含太多数据,你可以使用[将数据存储到你的`BlockEntity`][storing-data]小节之外的方法。 + +!!! 重要 + 为方块实体同步过多/无用的数据可能会导致网络拥塞。你应该通过在客户端需要时仅发送客户端需要的信息来优化网络使用。例如,在更新标签中发送方块实体的物品栏通常是没有必要的,因为这可以通过其[`AbstractContainerMenu`][menu]进行同步。 + +### 在方块更新时同步 + +这个方法有点复杂,但同样,你只需要重写两个或三个方法。 +下面是它的一个简易的实现示例: +```java +@Override +public CompoundTag getUpdateTag() { + CompoundTag tag = new CompoundTag(); + //将你的数据写入标签 + return tag; +} + +@Override +public Packet getUpdatePacket() { + // 将从#getUpdateTag得到标签 + return ClientboundBlockEntityDataPacket.create(this); +} + +// 可以重写IForgeBlockEntity#onDataPacket。默认地,其将遵从#load。 +``` +静态构造器`ClientboundBlockEntityDataPacket#create`接受: + +* 该`BlockEntity`。 +* 从该`BlockEntity`中获取`CompoundTag`的可选函数。默认情况下,其使用`BlockEntity#getUpdateTag`。 + +现在,要发送数据包,必须在服务端上发出更新通知。 +```java +Level#sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) +``` +`pos`应为你的`BlockEntity`的位置。 +对于`oldState`和`newState`,你可以传递那个位置的`BlockState`。 +`flags`是一个应含有`2`的位掩码(bitmask),其将向客户端同步数据。有关更多信息以及flags的其余信息,参见`Block`。flag `2`与`Block#UPDATE_CLIENTS`相同。 + +### 使用自定义网络消息同步 + +这种同步方式可能是最复杂的,但通常是最优化的,因为你可以确保只有需要同步的数据才是真正同步的。在尝试之前,你应该先查看[`Networking`][networking]部分,尤其是[`SimpleImpl`][simple_impl]。一旦你创建了自定义网络消息,你就可以使用`SimpleChannel#send(PacketDistributor$PacketTarget, MSG)`将其发送给所有加载了该`BlockEntity`的用户。 + +!!! 警告 + 进行安全检查很重要,当消息到达玩家时,`BlockEntity`可能已经被销毁/替换!你还应该检查区块是否已加载(`Level#hasChunkAt(BlockPos)`)。 + +[registration]: ../concepts/registries.md#methods-for-registering +[storing-data]: #storing-data-within-your-blockentity +[menu]: ../gui/menus.md +[networking]: ../networking/index.md +[simple_impl]: ../networking/simpleimpl.md diff --git a/docs/translation/zh_CN/blocks/index.md b/docs/translation/zh_CN/blocks/index.md new file mode 100644 index 000000000..5239cae04 --- /dev/null +++ b/docs/translation/zh_CN/blocks/index.md @@ -0,0 +1,49 @@ +方块 +==== + +显然,方块是Minecraft世界的关键。它们构成了所有的地形、结构和机器。如果你有兴趣制作一个模组,那么你必然可能会想添加一些方块。本页将指导你创建方块,以及你可以使用它们做的一些事情。 + +创建一个方块 +----------- + +### 基础方块 + +对于不需要特殊功能的简单方块(比如圆石、木板等),不必自定义一个类。你可以通过使用`BlockBehaviour$Properties`对象实例化`Block`类来创建一个方块。该`BlockBehaviour$Properties`对象可以调用`BlockBehaviour$Properties#of`创建,并且可以通过调用其方法进行自定义。例如: + +- `strength` - 硬度控制着断块所需的时间。它是一个任意值。作为参考,石头的硬度为1.5,泥土的硬度为0.5。如果该方块不能被破坏,则应使用-1.0的硬度,`Blocks#BEDROCK`的定义是一个例子。抗性控制块的防爆性。作为参考,石头的抗性为6.0,泥土的抗性为0.5。 +- `sound` - 控制方块在点击、破坏或放置时发出的音效。其需要一个`SoundType`参数,请参阅[音效][sounds]页面了解更多详细信息。 +- `lightLevel` - 控制方块的亮度。其接受一个带有`BlockState`参数的函数,该函数返回从0到15的某一个值。 +- `friction` - 控制方块的动摩擦系数。作为参考,冰的动摩擦系数为0.98。 + +所有这些方法都是*可链接*的,这意味着你可以串联地调用它们。有关此方面的示例,请参见`Blocks`类。 + +!!! 注意 + `CreativeModeTab`未针对方块定义setter。如果方块有与之关联的物品(例如`BlockItem`),则由[`BuildCreativeModeTabContentsEvent`][creativetabs]处理。此外,也没有针对翻译键的setter,因为它是通过`Block#getDescriptionId`从注册表名称生成的。 + +### 进阶方块 + +当然,上面只允许创建非常基本的方块。如果你想添加一些功能,比如玩家交互,那么需要一个自定义的方块类。然而,`Block`类有很多方法,并且不幸的是,并不是每一个方法都能在这里用文档完全表述。请参阅本节中的其余页面,以了解你可以对方块进行的操作。 + +注册一个方块 +----------- + +方块必须经过[注册][registering]后才能发挥作用。 + +!!! 重要 + 存档中的方块和物品栏中的“方块”是非常不同的东西。存档中的方块由`BlockState`表示,其行为由一个`Block`类的实例定义。同时,物品栏中的物品是由`Item`控制的`ItemStack`。作为`Block`和`Item`二者之间的桥梁,有一个`BlockItem`类。`BlockItem`是`Item`的一个子类,它有一个字段`block`,其中包含对它所代表的`Block`的引用。`BlockItem`将“方块”的一些行为定义为物品,例如右键单击如何放置方块。存在一个没有其`BlockItem`的`Block`也是可能的。(例如`minecraft:water`是一个方块,但不是一个物品。因此,不可能将其作为一个物品保存在物品栏中。) + + 当一个方块被注册时,也*仅仅*意味着一个方块被注册了。该方块不会自动具有`BlockItem`。要为块创建基本的`BlockItem`,应该将`BlockItem`的注册表名称设置为其`Block`的注册表名称。`BlockItem`的自定义子类也可以使用。一旦为方块注册了`BlockItem`,就可以使用`Block#asItem`来获取它。如果该方块没有`BlockItem`,`Block#asItem`将返回`Items#AIR`,因此,如果你不确定你正在使用的方块是否有`BlockItem`,请检查其`Block#asItem`是否返回`Items#AIR`。 + +#### 选择性地注册方块 + +在过去,有一些模组允许用户在配置文件中禁用方块/物品。但是,你不应该这样做。允许注册的方块数量没有限制,所以请在你的模组中注册所有方块!如果你想通过配置文件禁用一个方块,你应该禁用其配方。如果要禁用创造模式物品栏中的方块,请在[`BuildCreativeModeTabContentsEvent`][creativetabs]中构建内容时使用`FeatureFlag`。 + +延伸阅读 +------- + +有关方块属性的信息,例如用于栅栏、墙等原版方块的属性,请参见[方块状态][blockstates]部分。 + +[sounds]: ../gameeffects/sounds.md +[creativetabs]: ../items/index.md#creative-tabs +[registering]: ../concepts/registries.md#methods-for-registering +[blockstates]: states.md diff --git a/docs/translation/zh_CN/blocks/states.md b/docs/translation/zh_CN/blocks/states.md new file mode 100644 index 000000000..472a761ea --- /dev/null +++ b/docs/translation/zh_CN/blocks/states.md @@ -0,0 +1,94 @@ +方块状态 +======= + +旧版本的行为 +----------- + +在Minecraft 1.7及以前的版本中,需要存储没有BlockEntity的位置或状态数据的方块使用**元数据(metadata)**。元数据是与方块一起存储的额外数字,允许方块不同的旋转、朝向,甚至完全独立的行为。 + +然而,元数据系统是令人费解且有限度的,因为它只存储为方块ID旁边的一个数字,离开了代码中注释的内容后便没有任何意义。例如,要实现可以面向某个方向并且位于方块空间(例如楼梯)的上半部分或下半部分的方块,下列操作被执行: + +```Java +switch (meta) { + case 0: { ... } // 面向南方且位于方块的下半部分 + case 1: { ... } // 面向南方且位于方块的上半部分 + case 2: { ... } // 面向北方且位于方块的下半部分 + case 3: { ... } // 面向北方且位于方块的上半部分 + // ... etc. ... +} +``` + +因为这些数字本身没有任何意义,所以除非能够访问源代码和注释,否则没有人能够知道它们代表什么。 + +状态简介 +------- + +在Minecraft 1.8及以上版本中,元数据系统和方块ID系统被弃用,最终被**方块状态系统**取代。方块状态系统从方块的其他行为中抽象出方块属性的细节。 + +方块的每个*属性*都由`Property`的一个实例来描述。方块属性的示例包括乐器(`EnumProperty`)、朝向(`DirectionProperty`)、充能状态(`Property`)等。每个属性都具有由`Property`参数化后的类型`T`的值。 + +可以构建从`Block`和`Property`的Map到与它们相关联的值的唯一的对。这个唯一的对被称为`BlockState`。 + +以前无意义的元数据值系统被更容易解释和处理的方块属性系统所取代。以前,朝向东方并充能或按下的石头按钮由“带有元数据`9`的`minecraft:stone_button`”表示。现在,其用“`minecraft:stone_button[facing=east,powered=true]`”来表示。 + +方块状态系统的正确用法 +-------------------- + +`BlockState`系统是一个灵活而强大的系统,但它也有局限性。`BlockState`是不可变的,其属性的所有组合都是在游戏启动时生成的。这意味着拥有一个具有许多属性和可能值的`BlockState`会减慢游戏的加载速度,并让任何试图理解你的方块逻辑的人感到困惑。 + +并非所有方块和情况都需要使用`BlockState`;只有方块的最基本属性才应该被放入`BlockState`,而任何其他情况都最好有一个`BlockEntity`或是一个区分开的`Block`。要始终考虑是否确实需要出于你的目的而使用方块状态。 + +!!! 注意 + 一个很好的经验法则是:**如果它有不同的名称,那么它应该是一个单独的方块**。 + +一个案例是制作椅子方块:椅子的*朝向*应该是一个*属性*,而*不同类型的木材*应该被分成不同的块。朝向东方的“橡木椅子”(`oak_chair[facing=east]`)与朝向西方的“云杉椅子”(`oak_chair[facing=east]`)不同。 + +实现方块状态 +----------- + +在你的Block类中,为你的方块所拥有的所有属性创建或引用`static final` `Property`对象。你尽可以创建自己的`Property`实现,但本文没有介绍实现的方法。原版代码提供了几个方便的实现: + +* `IntegerProperty` + * 实现了`Property`。定义具有整数值的一个属性。 + * 通过调用`IntegerProperty#create(String propertyName, int minimum, int maximum)`创建。 +* `BooleanProperty` + * 实现了`Property`。定义具有`true`或`false`值的一个属性。 + * 通过调用`BooleanProperty#create(String propertyName)`创建。 +* `EnumProperty>` + * 实现了`Property`定义具有某一枚举类的值的一个属性。 + * 通过调用`EnumProperty#create(String propertyName, Class enumClass)`创建。 + * 也能够仅使用枚举值的一个子集(例如16个`DyeColor`中的4个)。请参阅`EnumProperty#create`的重载。 +* `DirectionProperty` + * 这是`EnumProperty`的一个便利实现。 + * Several convenience predicates are also provided. For example, to get a property that represents the cardinal directions, call `DirectionProperty.create("", Direction.Plane.HORIZONTAL)`; to get the X directions, `DirectionProperty.create("", Direction.Axis.X)`. + * 也提供了一些便利的Predicate。例如,要获得一个表示基本方向的属性,请调用`DirectionProperty.create("", Direction.Plane.HORIZONTAL)`;要获得仅X方向的,请调用`DirectionProperty.create("", Direction.Axis.X)`。 + +类`BlockStateProperties`包含共有的原版属性,应该尽可能先使用或引用这些属性,而不是创建自己的属性。 + +当你拥有所需的`Property<>`对象时,请重写你的Block类中的`Block#createBlockStateDefinition(StateDefinition$Builder)`。在该方法中,使用你希望这个方块具有的每个`Property`作为参数调用`StateDefinition$Builder#add(...);`。 + +每个方块也将有一个自动为你选择的“默认”状态。你可以通过从构造函数中调用`Block#registerDefaultState(BlockState)`方法来更改此“默认”状态。放置方块后,它将变为此“默认”状态。如`DoorBlock`的一个案例: + +```Java +this.registerDefaultState( + this.stateDefinition.any() + .setValue(FACING, Direction.NORTH) + .setValue(OPEN, false) + .setValue(HINGE, DoorHingeSide.LEFT) + .setValue(POWERED, false) + .setValue(HALF, DoubleBlockHalf.LOWER) +); +``` + +如果你希望更改放置方块时使用的`BlockState`,可以重写`Block#getStateForPlacement(BlockPlaceContext)`。例如,这可以用来根据玩家放置方块时所站的位置来设置方块的方向。 + +由于`BlockState`是不可变的,并且其属性的所有组合都是在游戏启动时生成的,因此调用`BlockState#setValue(Property, T)`将只需转到`Block`的`StateHolder`并请求具有你所需的一系列值的`BlockState`。 + +因为所有可能的`BlockState`都是在启动时生成的,所以你可以自由地使用引用相等运算符(`==`)来检查两个`BlockState`是否相等。 + +使用`BlockState` +---------------- + +你可以通过调用`BlockState#getValue(Property)`来获取某个属性的值,并将要获取值的属性传递给它。如果你想获得一个具有不同的一系列值的`BlockState`,只需使用该属性及其值调用`BlockState#setValue(Property, T)`。 + +你可以使用`Level#setBlockAndUpdate(BlockPos, BlockState)`和`Level#getBlockState(BlockPos)`在存档中获取并放置`BlockState`。如果你正在放置一个`Block`,请调用`Block#defaultBlockState()`以获得“默认”状态,并使用对`BlockState#setValue(Property, T)`的后续调用,如上所述,以保存所需状态。 diff --git a/docs/translation/zh_CN/concepts/events.md b/docs/translation/zh_CN/concepts/events.md new file mode 100644 index 000000000..8f4c93013 --- /dev/null +++ b/docs/translation/zh_CN/concepts/events.md @@ -0,0 +1,140 @@ +事件 +==== + +Forge使用事件总线以允许模组拦截来自各种原版和模组行为的事件。 + +例如:右键单击原版的木棍时,一个事件可被触发以用于执行操作。 + +用于大多数事件的主事件总线位于`MinecraftForge#EVENT_BUS`。在`FMLJavaModLoadingContext#getModEventBus`中还有另一个用于特定于模组事件的事件总线,你应该只在特定情况下使用它。关于该事件总线的更多信息可以在下面找到。 + +每个事件都在其中一条总线上触发:大多数事件在主要的Forge事件总线上触发,但也有一些在特定于模组的事件总线上触发。 + +事件处理器是某个已注册到事件总线的方法。 + +创建一个事件处理器 +---------------- + +事件处理器方法只有一个参数,不返回结果。该方法可以是静态的,也可以是实例化的,具体取决于实现。 + +事件处理器可以使用`IEventBus#addListener`直接注册,或对于泛型事件(`GenericEvent`的子类)使用`IEventBus#addGenericListener`直接注册。任一监听器注册方法接收表示方法引用的Consumer。泛型事件处理器还需要指定泛型的具体类型。事件处理器必须在模组主类的构造函数中注册。 + +```java +// 在模组主类ExampleMod中 + +// 该事件位于模组事件总线上 +private void modEventHandler(RegisterEvent event) { + // Do things here +} + +// 该事件位于Forge事件总线上 +private static void forgeEventHandler(AttachCapabilitiesEvent event) { + // ... +} + +// 在模组构造函数内 +modEventBus.addListener(this::modEventHandler); +forgeEventBus.addGenericListener(Entity.class, ExampleMod::forgeEventHandler); +``` + +### 实例化的已注释的事件处理器 + +该事件处理器监听`EntityItemPickupEvent`,正如名称所述,每当`Entity`拾取一件物品时,该事件就会被发布到事件总线。 + +```java +public class MyForgeEventHandler { + @SubscribeEvent + public void pickupItem(EntityItemPickupEvent event) { + System.out.println("Item picked up!"); + } +} +``` + +要注册这个事件处理器,请使用`MinecraftForge.EVENT_BUS.register(...)`并向其传递事件处理器所在类的一个实例。如果要将此处理器注册到特定于模组的事件总线,则应使用`FMLJavaModLoadingContext.get().getModEventBus().register(...)`。 + +### 静态的已注释的事件处理器 + +事件处理器也可以是静态的。处理事件的方法仍然使用`@SubscribeEvent`进行注释。与实例化的事件处理器的唯一区别是它也被标记为`static`。要注册静态的事件处理器,传入类的实例是不行的。必须传入类本身。例如: + +```java +public class MyStaticForgeEventHandler { + @SubscribeEvent + public static void arrowNocked(ArrowNockEvent event) { + System.out.println("Arrow nocked!"); + } +} +``` + +其必须像这样注册:`MinecraftForge.EVENT_BUS.register(MyStaticForgeEventHandler.class)`。 + +### 自动注册静态的事件处理器 + +类可以使用`@Mod$EventBusSubscriber`进行注释。当`@Mod`类本身被构造时,这样的类会自动注册到`MinecraftForge#EVENT_BUS`。这实质上相当于在`@Mod`类的构造函数的末尾添加`MinecraftForge.EVENT_BUS.register(AnnotatedClass.class);`。 + +你可以向`@Mod$EventBusSubscriber`注释指明所要监听的总线。建议你也指定mod id,因为注释在处理的过程中可能无法确定它,以及你所注册的总线,因为它作为一个保障可以确保你所注册的是正确的总线。你还可以指定要加载此事件处理器的`Dist`或物理端。这可用于保证不在dedicated服务器上加载客户端特定的事件处理器。 + +下面是静态事件处理器监听`RenderLevelStageEvent`的示例,该处理器将仅在客户端上调用: + +```java +@Mod.EventBusSubscriber(modid = "mymod", bus = Bus.FORGE, value = Dist.CLIENT) +public class MyStaticClientOnlyEventHandler { + @SubscribeEvent + public static void drawLast(RenderLevelStageEvent event) { + System.out.println("Drawing!"); + } +} +``` + +!!! 注意 + 这不会注册类的实例;它注册类本身(即事件处理方法必须是静态的)。 + +事件的取消 +--------- + +如果一个事件可以被取消,它将带有`@Cancelable`注释,并且方法`Event#isCancelable()`将返回`true`。可取消事件的取消状态可以通过调用`Event#setCanceled(boolean canceled)`来修改,其中传递布尔值`true`意为取消事件,传递布尔值`false`被解释为“不取消”事件。但是,如果无法取消事件(如`Event#isCancelable()`所定义),则无论传递的布尔值如何,都将抛出`UnsupportedOperationException`,因为不可取消事件事件的取消状态被认为是不可变的。 + +!!! 重要 + 并非所有事件都可以取消!试图取消不可取消的事件将导致抛出未经检查的`UnsupportedOperationException`,可能将导致游戏崩溃!在尝试取消某个事件之前,请始终使用`Event#isCancelable()`检查该事件是否可以取消! + +事件的结果 +--------- + +某些事件具有`Event$Result`。结果可以是以下三种情况之一:`DENY`(停止事件)、`DEFAULT`(使用默认行为)和`ALLOW`(强制执行操作,而不管最初是否执行)。事件的结果可以通过调用`#setResult`并用一个`Event$Result`来设置。并非所有事件都有结果;带有结果的事件将用`@HasResult`进行注释。 + +!!! 重要 + 不同的事件可能以不同的方式处理结果,在使用事件的结果之前请参阅事件的JavaDoc。 + +事件处理优先级 +------------- + +事件处理方法(用`@SubscribeEvent`标记)具有优先级。你可以通过设置注释的`priority`值来安排事件处理方法的优先级。优先级可以是`EventPriority`枚举的任何值(`HIGHEST`、`HIGH`、`NORMAL`、`LOW`和`LOWEST`)。优先级为`HIGHEST`的事件处理器首先执行,然后按降序执行,直到最后执行的`LOWEST`为止。 + +子事件 +------ + +许多事件本身都有不同的变体。这些变体事件可以不尽相同,但都基于一个共同的因素(例如`PlayerEvent`),也可以是具有多个阶段的事件(例如`PotionBrewEvent`)。请注意,如果你监听父类事件,你的事件处理方法也将收到其*所有*子类事件。 + +模组事件总线 +----------- + +模组事件总线主要用于监听模组应该初始化的生命周期事件。模组总线上的每个事件类型都需要实现`IModBusEvent`。其中许多事件也是并行运行的(多线程——译者注),因此多个模组可以同时被初始化。这意味着你不能在这些事件中直接执行来自其他模组的代码。为此,请使用`InterModComms`系统。 + +以下是在模组事件总线上的模组初始化期间调用的四个最常用的生命周期事件: + +* `FMLCommonSetupEvent` +* `FMLClientSetupEvent`和`FMLDedicatedServerSetupEvent` +* `InterModEnqueueEvent` +* `InterModProcessEvent` + +!!! 注意 + `FMLClientSetupEvent`和`FMLDedicatedServerSetupEvent`仅在各自的分发版本(物理端——译者注)上调用。 + +这四个生命周期事件都是并行运行的,因为它们都是`ParallelDispatchEvent`的子类。如果你想在任何`ParallelDispatchEvent`期间在主线程上运行运行代码,可以使用`#enqueueWork`来执行此操作。 + +除了生命周期事件之外,还有一些在模组事件总线上触发的杂项事件,你可以在其中注册、设置或初始化各种事情。与生命周期事件相比,这些事件中的大多数不是并行运行的。举几个例子: + +* `RegisterColorHandlersEvent` +* `ModelEvent$BakingCompleted` +* `TextureStitchEvent` +* `RegisterEvent` + +一个很好的经验法则是:当事件应该在模组初始化期间处理时,就在模组事件总线上触发事件。 diff --git a/docs/translation/zh_CN/concepts/internationalization.md b/docs/translation/zh_CN/concepts/internationalization.md new file mode 100644 index 000000000..3e8272030 --- /dev/null +++ b/docs/translation/zh_CN/concepts/internationalization.md @@ -0,0 +1,67 @@ +国际化与本地化 +============= + +国际化(Internationalization),简称I18n,是一种设计代码的方式,以便不需要进行任何更改即可适应各种语言。本地化(Localization)是使显示的文本适应用户语言的过程。 + +I18n是使用 _翻译键_ 来实现的。翻译键是一个字符串,用于指定一段不使用特定语言的可显示文本。例如,`block.minecraft.dirt`是引用泥土方块名称的翻译键。这样,可显示文本可被引用,而不必考虑特定的语言。这些代码不需要任何更改即可适应新的语言。 + +本地化将在游戏的语言设置中进行。在Minecraft客户端中,语言环境由语言设置指定。在dedicated服务端上,唯一支持的语言设置是`en_us`。可用语言地区的列表可以在[Minecraft Wiki][langs]上找到。 + +语言文件 +------- + +语言文件由`assets/[namespace]/lang/[locale].json`定位(例如,`examplemod`提供的所有美国英语翻译都在`assets/examplemod/lang/en_us.json`中)。文件格式只是从翻译键到值的json映射。文件必须使用UTF-8编码。可以使用[转换器][converter]将旧的.lang文件转换为json。 + +```js +{ + "item.examplemod.example_item": "Example Item Name", + "block.examplemod.example_block": "Example Block Name", + "commands.examplemod.examplecommand.error": "Example Command Errored!" +} +``` + +对方块和物品的用法 +----------------- + +Block、Item和其他一些Minecraft类都内置了用于显示其名称的翻译键。这些转换键是通过重写`#getDescriptionId`指定的。Item还具有`#getDescriptionId(ItemStack)`,重写该方法后可以根据所给ItemStack NBT提供不同的翻译键。 + +默认情况下,`#getDescriptionId`将返回以`block.`或`item.`为前缀的方块或物品的注册表名称,冒号由句点代替。默认情况下,`BlockItem`覆盖此方法以获取其对应的`Block`的翻译密钥。例如,ID为`examplemod:example_item`的物品实际上需要语言文件中的以下行: + +```js +{ + "item.examplemod.example_item": "Example Item Name" +} +``` + +!!! 注意 + 翻译键的唯一目的是国际化。不要把它们用于代码的逻辑处理部分。请改用注册表名称。 + + +本地化相关方法 +------------- + +!!! 警告 + 一个常见的问题是让服务端为客户端进行本地化。服务端只能在自己的语言设置中进行本地化,这不一定与所连接的客户端的语言设置相匹配。 + + 为了尊重客户端的语言设置,服务端应该让客户端使用`TranslatableComponent`或其他保留语言中性翻译键的方法在自己的语言设置中本地化文本。 + +### `net.minecraft.client.resources.language.I18n` (仅客户端) + +**这个I18n类仅在Minecraft客户端上有效!**它旨在由仅在客户端上运行的代码使用。尝试在服务端上使用它会引发异常并崩溃。 + +- `get(String, Object...)`使用格式采取客户端的语言设置进行本地化。第一个参数是翻译键,其余的是`String.format(String, Object...)`的格式化参数。 + +### `TranslatableContents` + +`TranslatableContents`是一个经过惰性的本地化和格式化的`ComponentContents`。它在向玩家发送消息时非常有用,因为它将在玩家自己的语言设置中自动本地化。 + +`TranslatableContents(String, Object...)`构造函数的第一个参数是翻译键,其余参数用于格式化。唯一支持的格式说明符是`%s`和`%1$s`、`%2$s`、`%3$s`等。格式化参数可能是将插入到格式化结果文本中并保留其所有属性的`Component`。 + +通过传入`TranslatableContents`的参数,可以使用`Component#translatable`创建`MutableComponent`。它也可以使用`MutableComponent#create`通过传入`ComponentContents`本身来创建。 + +### `TextComponentHelper` + +- `createComponentTranslation(CommandSource, String, Object...)`根据接收者创建本地化并格式化的`MutableComponent`。如果接收者是一个原版客户端,那么本地化和格式化就很容易完成。如果没有,本地化和格式化将使用包含`TranslatableContents`的`Component`惰性地进行。只有当服务端允许原版客户端连接时,这才有用。 + +[langs]: https://minecraft.fandom.com/wiki/Language#Languages +[converter]: https://tterrag.com/lang2json/ diff --git a/docs/translation/zh_CN/concepts/lifecycle.md b/docs/translation/zh_CN/concepts/lifecycle.md new file mode 100644 index 000000000..c221dcdcd --- /dev/null +++ b/docs/translation/zh_CN/concepts/lifecycle.md @@ -0,0 +1,74 @@ +模组生命周期 +=========== + +在模组加载过程中,各种生命周期事件在模组特定的事件总线上触发。在这些事件期间许多操作被执行,例如[注册对象][registering]、准备[数据生成][datagen]或[与其他模组通信][imc]。 + +事件监听器应使用`@EventBusSubscriber(bus = Bus.MOD)`或在模组构造函数中被注册: + +```Java +@Mod.EventBusSubscriber(modid = "mymod", bus = Mod.EventBusSubscriber.Bus.MOD) +public class MyModEventSubscriber { + @SubscribeEvent + static void onCommonSetup(FMLCommonSetupEvent event) { ... } +} + +@Mod("mymod") +public class MyMod { + public MyMod() { + FMLModLoadingContext.get().getModEventBus().addListener(this::onCommonSetup); + } + + private void onCommonSetup(FMLCommonSetupEvent event) { ... } +} +``` + +!!! 警告 + 大多数生命周期事件都是并行触发的(多线程——译者注):所有模组都将同时接收相同的事件。 + + 模组必须注意线程安全,就像调用其他模组的API或访问原版系统一样。延迟代码,以便稍后通过`ParallelDispatchEvent#enqueueWork`执行。 + +注册表事件 +--------- + +注册表事件是在模组实例构造之后激发的。注册表事件有三种:`NewRegistryEvent`、`DataPackRegistryEvent$NewRegistry`和`RegisterEvent`。这些事件在模组加载期间同步触发。 + +`NewRegistryEvent`允许模组开发者使用`RegistryBuilder`类注册自己的自定义注册表。 + +`DataPackRegistryEvent$NewRegistry`允许模组开发者通过提供`Codec`对JSON中的对象进行编码和解码来注册自定义数据包注册表。 + +`RegisterEvent`用于[将对象注册到注册表中][registering]。每个注册表都会触发该事件。 + +数据生成 +------- + +如果游戏被设置为运行[数据生成器][datagen],那么`GatherDataEvent`将是最后一个触发的事件。此事件用于将模组的数据提供者注册到其关联的数据生成器。此事件也是同步触发的。 + +通用初始化 +--------- + +`FMLCommonSetupEvent`用于物理客户端和物理服务端通用的操作,例如注册[Capability][capabilities]。 + +单端初始化 +--------- + +单端初始化事件在其各自的[物理端][sides]触发:物理客户端上触发`FMLClientSetupEvent`,dedicated服务端上触发`FMLDedicatedServerSetupEvent`。这就是应该进行各物理端特定的初始化的地方,例如注册客户端键盘绑定。 + +InterModComms +------------- + +这是模组间可以相互通信以实现跨模组兼容性的地方。有两个相关的事件:`InterModEnqueueEvent`和`InterModProcessEvent`。 + +`InterModComms`是负责为模组间交换消息的类。其方法在生命周期事件期间可以安全调用,因为它有`ConcurrentMap`支持。 + +在`InterModEnqueueEvent`期间,使用`InterModComms#sendTo`以向不同的模组发送消息。这些方法接收所发消息的目的模组的mod id、与消息数据相关的键以及持有消息数据的Supplier。此外,还可以指定消息的发送者,但默认情况下,它将是调用者的mod id。 + +之后在`InterModProcessEvent`期间,使用`InterModComms#getMessages`获取所有接收到的消息的Stream。提供的mod id几乎总是先前调用发送消息方法的模组的mod id。此外,可以指定一个Predicate来对消息键进行过滤。这将返回一个带有`IMCMessages`的Stream,其中包含数据的发送方、数据的接收方、数据键以及所提供的数据本身。 + +!!! 注意 + 还有另外两个生命周期事件:`FMLConstructModEvent`,在模组实例构造之后但在`RegisterEvent`之前直接触发;`FMLLoadCompleteEvent`,在`InterModComms`事件之后触发,用于模组加载过程完成时。 + +[registering]: ./registries.md#methods-for-registering +[capabilities]: ../datastorage/capabilities.md +[datagen]: ../datagen/index.md +[imc]: ./lifecycle.md#intermodcomms +[sides]: ./sides.md diff --git a/docs/translation/zh_CN/concepts/registries.md b/docs/translation/zh_CN/concepts/registries.md new file mode 100644 index 000000000..59fc70ba4 --- /dev/null +++ b/docs/translation/zh_CN/concepts/registries.md @@ -0,0 +1,194 @@ +注册表 +====== + +注册是获取模组的对象(如物品、方块、音效等)并使其为游戏所知的过程。注册东西很重要,因为如果没有注册,游戏将根本不知道这些对象,这将导致无法解释的行为和崩溃。 + +游戏中的大多数注册相关事项都由Forge注册表处理。注册表是一个与为键分配值的Map的行为类似的对象。Forge使用带有[`ResourceLocation`][ResourceLocation]键的注册表来注册对象。这允许`ResourceLocation`充当对象的“注册表名称”。 + +每种类型的可注册对象都有自己的注册表。要查看由Forge封装的所有注册表,请参阅`ForgeRegistries`类。注册表中的所有注册表名称必须是唯一的。但是,不同注册表中的名称不会发生冲突。例如,有一个`Block`注册表和一个`Item`注册表。一个方块和一个物品可以用相同的名称`example:thing`注册而不冲突;但是,如果两个不同的方块(或物品)以相同的名称被注册,则第二个对象将覆盖第一个对象。 + +注册的方式 +--------- + +有两种正确的方式来注册对象:`DeferredRegister`类和`RegisterEvent`生命周期事件。 + +### DeferredRegister + +`DeferredRegister`是注册对象的推荐方式。它包容静态初始化的使用与便利,同时也避免与之相关的问题。它只需维护一系列的Supplier,并在`RegisterEvent`期间注册这些Supplier所提供的对象。(Supplier是Java 8加入的新语法。——译者注) + +以下是一个模组注册一个自定义方块的案例: + +```java +private static final DeferredRegister BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, MODID); + +public static final RegistryObject ROCK_BLOCK = BLOCKS.register("rock", () -> new Block(BlockBehaviour.Properties.of().mapColor(MapColor.STONE))); + +public ExampleMod() { + BLOCKS.register(FMLJavaModLoadingContext.get().getModEventBus()); +} +``` + +### `RegisterEvent` + +`RegisterEvent`是注册对象的第二种方式。在模组构造函数之后和加载configs之前,该[事件][event]会为每个注册表激发。对象通过调用`#register`并传入注册表键、注册表对象的名称和对象本身而得以注册。还有一个额外的`#register`重载,它接收一个已使用的助手来注册具有给定名称的对象。建议使用此方法以避免不必要的对象创建。 + +案例如下:(事件处理器已被注册到*模组事件总线*) + +```java +@SubscribeEvent +public void register(RegisterEvent event) { + event.register(ForgeRegistries.Keys.BLOCKS, + helper -> { + helper.register(new ResourceLocation(MODID, "example_block_1"), new Block(...)); + helper.register(new ResourceLocation(MODID, "example_block_2"), new Block(...)); + helper.register(new ResourceLocation(MODID, "example_block_3"), new Block(...)); + // ... + } + ); +} +``` + +### 未被Forge封装的注册表 + +并非所有的注册表都由Forge封装。这些可以是静态注册表,如`LootItemConditionType`,使用起来是安全的。还有动态注册表,如`ConfiguredFeature`和其他一些世界生成注册表,它们通常以JSON表示。`DeferredRegister#create`有一个重载,允许模组开发者指定原版注册表所创建的`RegistryObject`的注册表键。注册表方法和模组事件总线的附加与其他`DeferredRegister`相同。 + +!!! 重要 + 动态注册表对象**只能**通过数据文件(如JSON)被注册。它们**不能**在代码中被注册。 + +```java +private static final DeferredRegister REGISTER = DeferredRegister.create(Registries.LOOT_CONDITION_TYPE, "examplemod"); + +public static final RegistryObject EXAMPLE_LOOT_ITEM_CONDITION_TYPE = REGISTER.register("example_loot_item_condition_type", () -> new LootItemConditionType(...)); +``` + +!!! 注意 + 有些类无法自行注册。相反,`*Type`类被注册,并在前者的构造函数中被使用。例如,[`BlockEntity`][blockentity]具有`BlockEntityType`,`Entity`具有`EntityType`。这些`*Type`类是工厂,它们只是根据需要创建包含类型。 + + 这些工厂是通过使用它们的`*Type$Builder`类创建的。例如:(`REGISTER`指的是`DeferredRegister`) + ```java + public static final RegistryObject> EXAMPLE_BLOCK_ENTITY = REGISTER.register( + "example_block_entity", () -> BlockEntityType.Builder.of(ExampleBlockEntity::new, EXAMPLE_BLOCK.get()).build(null) + ); + ``` + +引用已注册的对象 +--------------- + +已注册的对象在创建和注册时不应存储在字段中。每当为相应的注册表触发`RegisterEvent`时,它们应总是新创建并注册的。这是为了允许在未来版本的Forge中动态加载和卸载模组。 + +已注册的对象必须始终通过`RegistryObject`或带有`@ObjectHolder`的字段引用。 + +### 使用RegistryObjects + +一旦注册对象可用,就可以使用`RegistryObjects`检索对这些对象的引用。`DeferredRegister`使用它们来返回对已注册对象的引用。在为其注册表触发`RegisterEvent`后,它们的引用以及带有`@ObjectHolder`注释的字段都将被更新。 + +要获取`RegistryObject`,请使用可注册对象的`IForgeRegistry`和一个`ResourceLocation`调用`RegistryObject#create`。亦可使用自定义注册表,方式是向其提供注册表名称。请将`RegistryObject`存储在一个`public static final`字段中,并在需要该已注册对象时调用`#get`。 + +使用`RegistryObject`的一个案例: + +```java +public static final RegistryObject BOW = RegistryObject.create(new ResourceLocation("minecraft:bow"), ForgeRegistries.ITEMS); + +// 假设'neomagicae:mana_type'是一个合法的注册表,且'neomagicae:coffeinum'是该注册表中一个合法的对象 +public static final RegistryObject COFFEINUM = RegistryObject.create(new ResourceLocation("neomagicae", "coffeinum"), new ResourceLocation("neomagicae", "mana_type"), "neomagicae"); +``` + +### 使用@ObjectHolder + +通过使用`@ObjectHolder`注释类或字段,并提供足够的信息来构造`ResourceLocation`以标识特定注册表中的特定对象,可以将注册表中的已注册对象注入`public static`字段。 + +使用`@ObjectHolder`的规则如下: + +* 若类被使用`@ObjectHolder`注释,则如果未明确定义,其值将是该类中所有字段的默认命名空间 +* 若类被使用`@Mod`注释,则如果未明确定义,modid将是其中所有已注释字段的默认命名空间 +* 若符合下列条件,该类中的一个字段将会被考虑注入: + * 其至少包含修饰符`public static`; + * 该**字段**被`@ObjectHolder`注释,并且: + * name值已被显式指明;并且 + * registry name值已被显式指明 + * _如果某个字段没有相应的注册表(registry name)或名称(name),则会引发编译时异常。_ +* _如果最终的`ResourceLocation`不完整或无效(路径中存在无效字符),则会引发异常。_ +* 如果没有发生其他错误或异常,则该字段将被注入 +* 如果以上所有规则都不适用,则不会采取任何操作(并且日志可能会输出一条信息) + +被`@ObjectHolder`注释的字段会在`RegisterEvent`为其注册表激发之后注入其值,与`RegistryObjects`的引用的更新同时发生。 + +!!! 注意 + 如果要注入对象时该对象不存在于注册表中,那么日志会记录一条调试信息,并且不会注入任何值。 + +由于这些规则相当复杂,案例如下: + +```java +class Holder { + @ObjectHolder(registryName = "minecraft:enchantment", value = "minecraft:flame") + public static final Enchantment flame = null; // 注释存在。[public static]是必需的。[final]是可选的。 + // Registry name已被显式指明:"minecraft:enchantment" + // Resource location已被显式指明:"minecraft:flame" + // 将注入:[Enchantment]注册表中的"minecraft:flame" + + public static final Biome ice_flat = null; // 该字段无注释。 + // 因此,该字段被忽略。 + + @ObjectHolder("minecraft:creeper") + public static Entity creeper = null; // 注释存在。[public static]是必需的。 + // 该字段未指明注册表。 + // 因此,其将引发编译时异常。 + + @ObjectHolder(registryName = "potion") + public static final Potion levitation = null; // 注释存在。[public static]是必需的。[final]是可选的。 + // Registry name已被显式指明:"minecraft:potion" + // Resource location未在该字段中指明 + // 因此,其将引发编译时异常。 +} +``` + +创建自定义的Forge注册表 +---------------------- + +自定义注册表通常只是一个简单的键值映射。这是一种常见的风格;然而,它强制对存在的注册表进行严格的依赖。它还要求任何需要在端位之间同步的数据都必须手动完成。自定义Forge注册表为创建软依赖项提供了一个简单的替代方案,同时提供了更好的管理手段和端位之间的自动同步(除非另有说明)。由于这些对象也使用Forge注册表,注册也以同样的方式标准化。 + +自定义Forge注册表是在`RegistryBuilder`的帮助下通过`NewRegistryEvent`或`DeferredRegister`创建的。`RegistryBuilder`类接受多种参数(例如注册表的名称、id范围以及注册表上发生的不同事件的各种回调)。`NewRegistryEvent`完成激发后,新的注册表将被注册到`RegistryManager`。 + +任何新创建的注册表都应该使用其关联的[注册方法][registration]来注册关联的对象。 + +### 使用NewRegistryEvent + +使用`NewRegistryEvent`时,用`RegistryBuilder`调用`#create`将返回一个用Supplier包装的注册表。`NewRegistryEvent`在模组事件总线处理完毕后,这个Supplier注册表就可以访问了。在`NewRegistryEvent`被处理完毕之前试图从Supplier获取该自定义注册表将得到`null`值。 + +#### 新的数据包注册表 + +可以使用模组事件总线上的`DataPackRegistryEvent$NewRegistry`事件添加新的数据包注册表。注册表是通过`#dataPackRegistry`创建的,方法是传入表示注册表名称的`ResourceKey`和用于对JSON中的数据进行编码和解码的`Codec`。可以提供可选的`Codec`来将数据包注册表同步到客户端。 + +!!! 重要 + 数据包注册表不能用`DeferredRegister`创建。它们只能通过这个事件创建。 + +### 使用DeferredRegister + +`DeferredRegister`方法又是上述事件的另一个包装。一旦使用`#create`重载在常量字段中创建了`DeferredRegister`(该重载接受注册表名称和mod id),就可以通过`DeferredRegistry#makeRegistry`构建注册表。该方法接受了由Supplier提供的包含任何其他配置的`RegistryBuilder`。默认情况下,该方法已调用`#setName`。由于此方法可以在任何时候返回,因此会返回由Supplier提供的`IForgeRegistry`版本。在激发NewRegistryEvent之前试图从Supplier获取自定义注册表将得到`null`值。 + +!!! 重要 + 在通过`#register`将`DeferredRegister`添加到模组事件总线之前,必须调用`DeferredRegister#makeRegistry`。`#makeRegistry`也使用`#register`方法在`NewRegistryEvent`期间创建注册表。 + +处理缺失的注册表条目 +------------------ + +在某些情况下,每当更新模组或删除模组(更可能的情况)时,某些注册表对象将不复存在。可以通过第三个注册表事件指定操作来处理丢失的映射:`MissingMappingsEvent`。在该事件中,既可以通过给定注册表项和mod id的`#getMappings`获取丢失映射的列表,也可以通过给定注册项的`#getAllMappings`获取所有映射。 + +!!! 重要 + `MissingMappingsEvent`在**Forge**事件总线上触发。 + +对于每个映射(`Mapping`),可以选择四种映射类型之一来处理丢失的条目: + +| 操作 | 描述 | +| :---: | :--- | +| IGNORE | 忽略丢失的条目并丢弃映射。 | +| WARN | 在日志中生成警告。 | +| FAIL | 阻止世界加载。 | +| REMAP | 将条目重新映射到已注册的非null对象。 | + +如果未指定任何操作,则默认操作为通过通知用户丢失的条目以及用户是否仍要加载世界。除了重新映射之外的所有操作都将防止任何其他注册表对象取代现有id,以防止相关条目被添加回游戏中。 + +[ResourceLocation]: ./resources.md#resourcelocation +[registration]: #methods-for-registering +[event]: ./events.md +[blockentity]: ../blockentities/index.md diff --git a/docs/translation/zh_CN/concepts/resources.md b/docs/translation/zh_CN/concepts/resources.md new file mode 100644 index 000000000..b5717ec18 --- /dev/null +++ b/docs/translation/zh_CN/concepts/resources.md @@ -0,0 +1,19 @@ +资源 +==== + +资源是游戏使用的额外数据,存储在数据文件中,而不是代码中。Minecraft有两个主要的资源系统:一个在逻辑客户端上,用于模型、纹理和本地化等视觉效果,称为`assets`(资源),另一个在用于游戏的逻辑服务端上,如配方和战利品表,称为`data`(数据)。[资源包(Resource pack)][respack]控制前者,而[数据包(Datapack)][datapack]控制后者。 + +在默认的模组开发工具包中,assets和data目录位于项目的`src/main/resources`目录下。 + +如果启用了多个资源包或数据包,它们会被合并。通常,堆栈顶部包中的文件会覆盖下面的文件;但是,对于某些文件,例如本地化文件和标签,数据实际上是按内容合并的。模组在其`resources`目录中定义资源和数据包,但它们被视为“模组资源”包的子集。不能禁用模组资源包,但它们可以被其他资源包覆盖。可以使用原版的`/datapack`命令禁用模组数据包。 + +所有资源都应该有遵循蛇形命名法(Snake Case)的路径和文件名(小写,使用“_”表示单词边界),这在1.11及更高版本中得到了强制执行。 + +`ResourceLocation` +------------------ + +Minecraft使用`ResourceLocation`识别资源。`ResourceLocation`包含两部分:命名空间和路径。它通常指向`assets///`处的资源,其中`ctx`是特定于上下文的路径片段,取决于`ResourceLocation`的使用方式。当从字符串中写入/读取为`ResourceLocation`时,它被视为`:`。如果省略了`:`,那么当字符串被读取为`ResourceLocation`时,命名空间将始终默认为`minecraft`。模组应该将其资源放入与其mod id同名的命名空间中(例如,id为`examplemod`的模组应该分别将其资源放置在`assets/examplemod`和`data/examplemod`中,指向这些文件的`ResourceLocation`看起来像`examplemod:`。)。这不是要求,并且在某些情况下,可能希望使用不同的(或者甚至不止一个)命名空间。`ResourceLocation`也在资源系统之外使用,因为它们恰好是唯一标识对象(例如[注册表][])的好方法。 + +[respack]: ../resources/client/index.md +[datapack]: ../resources/server/index.md +[registries]: ./registries.md diff --git a/docs/translation/zh_CN/concepts/sides.md b/docs/translation/zh_CN/concepts/sides.md new file mode 100644 index 000000000..102ad34df --- /dev/null +++ b/docs/translation/zh_CN/concepts/sides.md @@ -0,0 +1,119 @@ +Minecraft中的端位 +================ + +为Minecraft开发模组时需要理解的一个非常重要的概念是两个端位:*客户端*和*服务端*。关于端位有很多常见的误解和错误,这可能会导致bug,而这些bug虽然可能不会破坏游戏,但是一定能够产生意想不到的、往往不可预测的影响。 + +不同种类的端位 +------------- + +当我们说“客户端”或“服务端”时,我们通常会对所谈论的游戏的哪个部分有相当直观的理解。毕竟,客户端是用户交互的对象,服务端是用户连接多人游戏的地方。很简单,对吧? + +而事实是,即使有两个这样的术语,也可能存在一些歧义。在这里,我们消除了“客户端”和“服务端”的四个可能含义的歧义: + +* 物理客户端 - 无论何时从启动器启动Minecraft,*物理客户端*都是运行的整个程序。在游戏的图形化、可交互的生命周期中运行的所有线程、进程和服务都是物理客户端的一部分。 +* 物理服务端 - 通常被称为dedicated服务端,*物理服务端*是在你启动任何类型的`minecraft_server.jar`时运行的整个程序,该程序不会显示可用于游玩的GUI。 +* 逻辑服务端 - *逻辑服务端*运行游戏逻辑:生物的生成,天气,物品栏、生命值、AI的更新以及其他所有游戏机制。逻辑服务端存在于物理服务端中,但它也可以与逻辑客户端一起在物理客户端中运行,作为一个单机世界。逻辑服务端始终在名为`Server Thread`的线程中运行。 +* 逻辑客户端 - *逻辑客户端*接受玩家的输入并将其转发到逻辑服务端。此外,它还从逻辑服务端接收信息,并以图形方式呈现给玩家。逻辑客户端在`Render Thread`中运行,但通常会派生出几个其他线程来处理音频和方块渲染批处理等事务。 + +在MinecraftForge代码库中,物理端由一个名为`Dist`的枚举表示,而逻辑端则由一个名为`LogicalSide`的枚举表示。 + +进行特定端位的操作 +----------------- + +### `Level#isClientSide` + +这种boolean检查将是你最常用的检查端位的方法。在`Level`对象上查询此字段将建立该Level所属的**逻辑**端。也就是说,如果此字段为`true`,则该Level当前正在逻辑客户端上运行。如果该字段为`false`,则表示该Level正在逻辑服务端上运行。因此,物理服务端在该字段中总是包含`false`,但我们不能假设`false`意味着物理服务端,因为该字段对于物理客户端(换句话说,单机世界)内的逻辑服务端也可能是`false`。 + +当你需要确定是否应该运行游戏逻辑和其他机制时,请使用这种检查方式。例如,如果你想在玩家每次点击你的方块时伤害他们,或者让你的机器将泥土处理成钻石,你只有在确保`#isClientSide`为`false`后才能这样做。在最好的情况下,将游戏逻辑应用于逻辑客户端可能会导致去同步(幽灵实体、去同步状态等),在最坏的情况下会导致崩溃。 + +这种检查应该成为习惯。你很少需要除`DistExecutitor`以外的其他方式来确定端位和调整行为。 + +### `DistExecutor` + +考虑到客户端和服务端的模组都使用同一个“通用”的jar,以及将物理端分离为两个jar,我们想到了一个重要的问题:我们该如何使用只存在于某一个物理端的代码?`net.minecraft.client`下的所有代码仅存在于物理客户端上。如果你编写的任何类以任何方式引用了上述包下的类型名称,那么当在不存在这些类型名称的环境中加载相应的类时,它们将导致游戏崩溃。初学者的一个非常常见的错误是在他们的方块或方块实体类中调用`Minecraft.getInstance().()`,一旦加载这些类,就会导致任何物理服务端崩溃。 + +我们如何解决这个问题?幸运的是,FML有一个`DistExecutor`,它提供了各种方法来在不同的物理端运行不同的方法,或者只在某一物理端运行单个方法。 + +!!! 注意 + 对FML基于**物理**端进行检查的理解尤为重要。单机世界(包含逻辑服务端+物理客户端的逻辑客户端)将始终使用`Dist.CLIENT`! + +`DistExecutor`的工作原理是接收所提供的执行方法的Supplier,通过利用[JVM指令`invokedynamic`][invokedynamic]有效地防止类加载。被执行的方法应该是静态的并且在不同的类中。此外,如果这个静态方法没有参数,则应使用该方法的引用,而不是一个执行方法的Supplier。 + +`DistExecutor`中有两个主要方法:`#runWhenOn`和`#callWhenOn`。方法接受的参数为将被执行的方法和该方法应该运行的物理端,该方法(将被执行的方法)既可有返回值,也可无返回值。 + +这两种方法被进一步细分为`#safe*`和`#unsafe*`变体。安全(safe)和不安全(unsafe)这两种命名方式其实差强人意。主要区别在于,在开发环境中,`#safe*`方法将验证所提供的执行方法是否是返回的对另一个类的方法引用的lambda,否则将抛出错误。在产品环境中,`#safe*`和`#unsafe*`在功能上是相同的。 + +```java +// 在一个客户端类中:ExampleClass +public static void unsafeRunMethodExample(Object param1, Object param2) { + // ... +} + +public static Object safeCallMethodExample() { + // ... +} + +// 在一个通用类中 +DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> ExampleClass.unsafeRunMethodExample(var1, var2)); + +DistExecutor.safeCallWhenOn(Dist.CLIENT, () -> ExampleClass::safeCallMethodExample); + +``` + +!!! 警告 + 由于`invokedynamic`在Java 9+中的工作方式发生了变化,`DistExecutor`方法的所有`#safe*`变体都会在开发环境中抛出封装在`BootstrapMethodError`中的原始异常。应该使用`#unsafe*`变体或对[`FMLEnvironment#dist`][dist]的检查作为替代。 + +### 线程组 + +如果`Thread.currentThread().getThreadGroup() == SidedThreadGroups.SERVER`为true,则很可能当前线程位于逻辑服务端上。否则,它很可能在逻辑客户端上。当你无法访问`Level`对象以检查`isClientSide`时,这对于检索**逻辑**端非常有用。它通过查看当前运行的线程组来*猜测*你处于哪个逻辑端。因为这是一种猜测,所以只有在用尽其他选项时才应该使用这种方法。在几乎所有情况下,你应该优先检查`Level#isClientSide`。 + +### `FMLEnvironment#dist`和`@OnlyIn` + +`FMLEnvironment#dist`表示当前你的代码正在运行的**物理**端。由于它是在启动时确定的,所以它不依赖于猜测来返回结果。然而,在这方面的用例并不是很多。 + +使用`@OnlyIn(Dist)`注释对方法或字段进行注释会向加载器表明,应该将相应的成员在非指定的**物理**端中从定义里完全剥离。通常,这些只有在浏览反编译的Minecraft代码时才能看到,暗示着Mojang混淆器删除了的方法。**没有**理由直接使用此注释。请改用`DistExecutor`或检查`FMLEnvironment#dist`。 + +常见错误 +-------- + +### 跨逻辑端访问 + +每当你想将信息从一个逻辑端发送到另一个逻辑端时,必须**始终**使用网络数据包。即便在单机场景中,将数据从逻辑服务端直接传输到逻辑客户端是非常诱人的。 + +实际上,这通常是通过静态字段无意中完成的。由于在单机场景中,逻辑客户端和逻辑服务端共享相同的JVM,因此向静态字段写入和从静态字段读取的线程都会导致各种竞争条件以及与线程相关的经典问题。 + +通过从逻辑服务端上运行或可以运行的公共代码访问仅物理客户端的类(如`Minecraft`),也可能会明确地犯下这个错误。对于在物理客户端中调试的初学者来说,这个错误很容易被忽略。代码会在那里工作,但它会立即在物理服务端上崩溃。 + + +编写单端模组 +----------- + +在最近的版本中,Minecraft Forge从mods.toml中删除了一个“sidedness”属性。这意味着无论你的模组是加载在物理客户端还是物理服务端上,它们都可以工作。因此,对于单端模组,你通常会在`DistExecutor#safeRunWhenOn`或`DistExecutor#unsafeRunWhen`中注册事件处理程序,而不是直接调用模组构造函数中的相关注册方法。基本上,如果你的模组加载在错误的一端,它应该什么都不做,不监听任何事件,等等。单端模组本质上不应该注册方块、物品……因为它们也需要在另一端可用。 + +此外,如果你的模组是单端的,它通常不会禁止用户加入缺乏该模组的服务端。因此,你应该将mods.toml中的`displayTest`属性设置为任何必要的值。 + +```toml +[[mods]] + # ... + + # MATCH_VERSION表示如果客户端和服务端上的版本不同,你的模组将导致红X。这是默认行为,如果你的模组有服务端和客户端元素,这就是你应该使用的。 + # IGNORE_SERVER_VERSION表示如果你的模组出现在服务端上但不在客户端上,它不会导致红X。如果你的模组是一个仅限服务端的模组,这就是你应该使用的。 + # IGNORE_ALL_VERSION表示如果你的模组出现在客户端或服务端上,它不会导致红X。这是一个特殊情况,只有当你的模组没有服务端成分时才应该使用。 + # NONE表示没有在你的模组上设置显示检测。你需要自己完成此操作,有关详细信息,请参阅IExtensionPoint.DisplayTest。你可以使用此值定义任何你想要的方案。 + # 重要提示:这不是关于你的模组加载在哪个环境(客户端或dedicated服务端)上的说明。你的模组必然会加载(也许什么都不做!)。 + displayTest="IGNORE_ALL_VERSION" # 如果未指定任何内容,则MATCH_VERSION为默认值 (#可选) +``` + +如果要使用自定义显示检测,则`displayTest`选项应设置为`NONE`,并且应注册`IExtensionPoint$displayTest`扩展: + +```java +//确保另一个网络端上缺失的模组不会导致客户端将服务端显示为不兼容 +ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> new IExtensionPoint.DisplayTest(() -> NetworkConstants.IGNORESERVERONLY, (a, b) -> true)); +``` + +这告诉客户端它应该忽略服务端版本不存在,服务端不应该告诉客户端这个模组应该存在。因此,这个代码片段适用于仅客户端和服务端的模组。 + + +[invokedynamic]: https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-6.html#jvms-6.5.invokedynamic +[dist]: #fmlenvironmentdist-and-onlyin +[structuring]: ../gettingstarted/modfiles.md#modstoml diff --git a/docs/translation/zh_CN/contributing.md b/docs/translation/zh_CN/contributing.md new file mode 100644 index 000000000..746e478e1 --- /dev/null +++ b/docs/translation/zh_CN/contributing.md @@ -0,0 +1,4 @@ +向文档做出贡献 +================================== + +贡献指南尚在编写中。你可以等待一段时间再做出贡献。 diff --git a/docs/translation/zh_CN/datagen/client/localization.md b/docs/translation/zh_CN/datagen/client/localization.md new file mode 100644 index 000000000..c3eb1942a --- /dev/null +++ b/docs/translation/zh_CN/datagen/client/localization.md @@ -0,0 +1,40 @@ +语言生成 +======== + +可以通过子类化`LanguageProvider`并实现`#addTranslations`为模组生成[语言文件][lang]。创建的每个`LanguageProvider`子类代表一个单独的[locale](`en_us`代表美式英语,`es_es`代表西班牙语等)。实现后,必须将提供者[添加][datagen]到`DataGenerator`中。 + +```java +// 在模组事件总线上 +@SubscribeEvent +public void gatherData(GatherDataEvent event) { + event.getGenerator().addProvider( + // 告诉生成器仅在生成客户端资源时运行 + event.includeClient(), + // 美式英语的本地化 + output -> new MyLanguageProvider(output, MOD_ID, "en_us") + ); +} +``` + +`LanguageProvider` +------------------ + +每个语言提供者都是一个简单的字符串映射,其中每个翻译键都映射到一个本地化名称。可以使用`#add`添加翻译键映射。此外,还有一些方法使用`Block`、`Item`、`ItemStack`、`Enchantment`、`MobEffect`和`EntityType`的翻译键。 + +```java +// 在LanguageProvider#addTranslations中 +this.addBlock(EXAMPLE_BLOCK, "Example Block"); +this.add("object.examplemod.example_object", "Example Object"); +``` + +!!! 提示 + 包含非美式英语字母数字值的本地化名称可以按原样提供。提供者会自动将字符翻译为等效的unicode,供游戏读取。 + + ```java + // 编码为'Example with a d\u00EDacritic' + this.addItem("example.diacritic", "Example with a díacritic"); + ``` + +[lang]: ../../concepts/internationalization.md +[locale]: https://minecraft.fandom.com/wiki/Language#Languages +[datagen]: ../index.md#data-providers diff --git a/docs/translation/zh_CN/datagen/client/modelproviders.md b/docs/translation/zh_CN/datagen/client/modelproviders.md new file mode 100644 index 000000000..800cc6111 --- /dev/null +++ b/docs/translation/zh_CN/datagen/client/modelproviders.md @@ -0,0 +1,419 @@ +模型生成 +======== + +默认情况下,可以为模型或方块状态生成[模型][Models]。每种都提供了一种生成必要JSON的方法(`ModelBuilder#toJson`用于模型,`IGeneratedBlockState#toJson`用于方块状态)。实现后,必须将[关联的提供者][provider] [添加][datagen]到`DataGenerator`中。 + +```java +// 在模组事件总线上 +@SubscribeEvent +public void gatherData(GatherDataEvent event) { + DataGenerator gen = event.getGenerator(); + ExistingFileHelper efh = event.getExistingFileHelper(); + + gen.addProvider( + // 告诉生成器仅在生成客户端资源时运行 + event.includeClient(), + output -> new MyItemModelProvider(output, MOD_ID, efh) + ); + gen.addProvider( + event.includeClient(), + output -> new MyBlockStateProvider(output, MOD_ID, efh) + ); +} +``` + +模型文件 +------- + +`ModelFile`充当提供者引用或生成的所有模型的基础。每个模型文件存储相对于`models`子目录的位置,并可以断言该文件是否存在。 + +### 现存的模型文件 + +`ExistingModelFile`是`ModelFile`的子类,它通过[`ExistingFileHelper#exists`][efh]检查模型是否已存在于`models`子目录中。所有未生成的模型通常通过`ExistingModelFile`引用。 + +### 未检查的模型文件 + +`UncheckedModelFile`是`ModelFile`的一个子类,它假定指定的模型存在于某个位置。 + +!!! 注意 + 不应存在使用`UncheckedModelFile`引用模型的情况。如果存在,则`ExistingFileHelper`无法正确跟踪关联的资源。 + +模型生成器 +--------- + +`ModelBuilder`表示要生成的`ModelFile`。它包含了关于模型的所有数据:它的父级、面、纹理、变换、照明和[加载器][loader]。 + +!!! 提示 + 虽然可以生成复杂的模型,但建议事先使用建模软件构建这些模型。然后,数据提供者可以生成具有通过父复杂模型中定义的引用应用的特定纹理的子模型。 + +生成器的父级(通过`ModelBuilder#parent`)可以是任何`ModelFile`:生成的或现有的。一旦创建了生成器,生成的文件就会添加到`ModelProvider`中。生成器本身可以作为父级传入,也可以提供`ResourceLocation`。 + +!!! 警告 + 如果在传递`ResourceLocation`时父模型不是在子模型之前生成的,则将引发异常。 + +模型中的每个元素(通过`ModelBuilder#element`)都被定义为使用两个三维点(分别为`ElementBuilder#from`和`#to`)的立方体,其中每个轴都被限制为值`[-16,32]`(包括-16和32)。多维数据集的每个面(`ElementBuilder#face`)都可以指定面何时被剔除(`FaceBuilder#cullface`)、[色调索引][color](`FaceBuilder#tintindex`)、来自`textures`键的纹理引用(`FaceBuilder#texture`)、纹理上的UV坐标(`FaceBuilder#uvs`)以及以90度间隔旋转(`FaceBuilder#rotation`)。 + +!!! 注意 + 建议在任何轴上元素超过`[0,16]`界限的方块模型分离为多个方块,例如多方块结构,以避免照明和剔除问题。 + +每个立方体还可以围绕指定点(`RotationBuilder#origin`)以22.5度的间隔(`RotationBuilder#angle`)为给定轴(`RotationBuilder#axis`)旋转(`ElementBuilder#rotation`)。立方体也可以相对于整个模型缩放所有面(`RotationBuilder#rescale`)。多维数据集还可以确定是否应渲染其阴影(`ElementBuilder#shade`)。 + +每个模型都定义了一个纹理键列表(`ModelBuilder#texture`),该列表指向一个位置或引用。然后,通过使用`#`前缀,可以在任何元素中引用每个键(`example`的纹理键可以在使用`#example`元素中引用)。位置指定纹理在`assets//textures/.png`中的位置。引用由作为当前模型的子级的任何模型使用,作为以后定义纹理的键。 + +对于任何定义的透视图(在第一人称的左手、在图形用户界面、在地面等),还可以对模型进行转换(`ModelBuilder#transforms`)。对于任何透视图(`TransformsBuilder#transform`),都可以设置旋转(`TransformVecBuilder#rotation`)、平移(`TransformVecBuilder#translation`)和缩放(`TransformVecBuilder#scale`)。 + +最后,模型可以设置是否在某个存档(`ModelBuilder#ao`)中使用环境遮挡,以及从哪个位置从`ModelBuilder#guiLight`对模型进行明暗处理。 + +### `BlockModelBuilder` + +`BlockModelBuilder`表示要生成的方块模型。除了`ModelBuilder`之外,还可以生成对整个模型的转换(`BlockModelBuilder#rootTransform`)。根可以围绕某个原点(`RootTransformBuilder#origin`)单独或全部在一个变换(`RootTransformBuilder#transform`)中进行平移(`RootTransformBuilder#transform`)、旋转(`RootTransformBuilder#rotation`、`RootTransformBuilder#postRotation`)和缩放(`RootTransformBuilder#origin`)。 + +### `ItemModelBuilder` + +`ItemModelBuilder`表示要生成的物品模型。除了`ModelBuilder`之外,还可以生成[overrides](`OverrideBuilder#override`)。应用于模型的每个重写都可以应用表示给定属性的条件,该属性必须高于指定值(`OverrideBuilder#predicate`)。如果满足条件,则将呈现指定的模型(`OverrideBuilder#model`),而不是此模型。 + +模型提供者 +--------- + +`ModelProvider`子类负责生成构造的`ModelBuilder`。提供者接收生成器、mod id、要在其中生成的`models`文件夹中的子目录、`ModelBuilder`工厂和现有文件助手。每个提供器子类都必须实现`#registerModels`。 + +提供器包含创建`ModelBuilder`或为获取纹理或模型引用提供便利的基本方法: + +方法 | 描述 +:---: | :--- +`getBuilder` | Creates a new `ModelBuilder` within the provider's subdirectory for the given mod id. +`withExistingParent` | Creates a new `ModelBuilder` for the given parent. Should be used when the parent is not generated by the builder. +`mcLoc` | Creates a `ResourceLocation` for the path in the `minecraft` namespace. +`modLoc` | Creates a `ResourceLocation` for the path in the given mod id's namespace. + +此外,还有几个助手可以使用普通模板轻松生成通用模型。大多数是方块模型,只有少数是通用的。 + +!!! 注意 + 尽管模型在一个特定的子目录中,但**并不**意味着该模型不能被另一个子目录中的模型引用。通常,它表示该模型用于该类型的对象。 + +### `BlockModelProvider` + +`BlockModelProvider`用于通过`block`文件夹中的`BlockModelBuilder`生成方块模型。方块模型通常应为`minecraft:block/block`或其子模型之一的父模型,以便与物品模型一起使用。 + +!!! 注意 + 方块模型及其物品模型对应物通常不是通过`BlockModelProvider`和`ItemModelProvider`的直接子类生成的,而是通过[`BlockStateProvider`][blockstateprovider]生成的。 + +### `ItemModelProvider` + +`ItemModelProvider`用于通过`item`文件夹中的`ItemModelBuilder`生成块模型。大多数物品模型的父级为`item/generated`,并使用`layer0`来指定其纹理,这可以使用`#singleTexture`来完成。 + +!!! 注意 + `item/generated`可以支持堆叠在一起的五个纹理层:`layer0`、`layer1`、`layer2`、`layer3`和`layer4`。 + +```java +// 在某个ItemModelProvider#registerModels中 + +// 将会生成'assets//models/item/example_item.json' +// 父级将是'minecraft:item/generated' +// 对于纹理键'layer0' +// 其将会在'assets//textures/item/example_item.png' +this.basicItem(EXAMPLE_ITEM.get()); +``` + +!!! 注意 + 方块的物品模型通常应作为现有方块模型的父级,而不是为物品生成单独的模型。 + +方块状态提供者 +------------- + +`BlockStateProvider`负责为所述方块生成`blockstates`中的[方块状态JSON][blockstate]、`models/block`中的方块模型以及`models/item`中的物品模型。提供器接收数据生成器、mod id和现有的文件助手。每个`BlockStateProvider`子类都必须实现`#registerStatesAndModels`。 + +提供者包含用于生成方块状态JSON和方块模型的基本方法。物品模型必须单独生成,因为方块状态JSON可以定义多个模型以在不同的上下文中使用。然而,在处理更复杂的任务时,模组开发者应该注意一些常见的方法: + +方法 | 描述 +:---: | :--- +`models` | 获取用于生成物品方块模型的[`BlockModelProvider`][blockmodels]。 +`itemModels` | 获取用于生成物品方块模型的[`ItemModelProvider`][itemmodels]。 +`modLoc` | 为给定mod id的命名空间中的路径创建`ResourceLocation`。 +`mcLoc` | 为`minecraft`命名空间中的路径创建`ResourceLocation`。 +`blockTexture` | 引用`textures/block`中与方块同名的纹理。 +`simpleBlockItem` | 为给定关联模型文件的方块创建物品模型。 +`simpleBlockWithItem` | 使用方块模型作为其父级,为方块模型和物品模型创建单个方块状态。 + +方块状态JSON由变量或条件组成。每个变量或条件都引用一个`ConfiguredModelList`:`ConfiguredModel`的列表。每个配置的模型都包含模型文件(通过`ConfiguredModel$Builder#modelFile`)、90度间隔的X和Y旋转(分别通过`#rotationX`和`rotationY`)、当模型通过方块状态JSON旋转时纹理是否可以旋转(通过`#uvLock`),以及与列表中其他模型相比出现的模型的权重(通过`#weight`)。 + +生成器(`ConfiguredModel#builder`)还可以通过使用`#nextModel`创建下一个模型并重复设置直到调用`#build`来创建`ConfiguredModel`的数组。 + +### `VariantBlockStateBuilder` + +可以使用`BlockStateProvider#getVariantBuilder`生成变量。每个变体都指定了一个[属性][properties]列表(`PartialBlockstate`),当该列表与存档中的`BlockState`匹配时,将显示从相应模型列表中选择的模型。如果存在未被定义的任何变体覆盖的`BlockState`,则抛出异常。对于任何`BlockState`,只有一种变体可以为true。 + +`PartialBlockstate`通常使用以下三种方法之一进行定义: + +方法 | 描述 +:---: | :--- +`partialState` | 创建要定义的`PartialBlockstate`。 +`forAllStates` | 定义一个函数,其中给定的`BlockState`可以由`ConfiguredModel`的数组表示。 +`forAllStatesExcept` | 定义一个类似于`#forAllStates`的函数;但是,它还指定了哪些属性不会影响渲染的模型。 + +对于`PartialBlockstate`,可以指定定义的属性(`#with`)。 配置的模型可以设置(`#setModels`),附加到现有模型(`#addModels`),或构建(`#modelForState`,然后是`ConfiguredModel$Builder#addModel`,而不是`#ConfiguredModel$Builder#build`)。 + +```java +// 在某个BlockStateProvider#registerStatesAndModels中 + +// EXAMPLE_BLOCK_1:拥有属性BlockStateProperties#AXIS +this.getVariantBuilder(EXAMPLE_BLOCK_1) // 获取变量生成器 + .partialState() // 构建部分状态 + .with(AXIS, Axis.Y) // 当 BlockState AXIS = Y 时 + .modelForState() // 当 AXIS = Y 时设置模型 + .modelFile(yModelFile1) // 可以显示'yModelFile1' + .nextModel() // 当 AXIS = Y 时添加另一个模型 + .modelFile(yModelFile2) // 可以显示'yModelFile2' + .weight(2) // 此时将显示'yModelFile2' 2/3 + .addModel() // 完成当 AXIS = Y 时的模型 + .with(AXIS, Axis.Z) // 当 BlockState AXIS = Z 时 + .modelForState() // 当 AXIS = Z 时设置模型 + .modelFile(hModelFile) // 可以显示'hModelFile' + .addModel() // 完成当 AXIS = Z 时的模型 + .with(AXIS, Axis.X) // 当 BlockState AXIS = X 时 + .modelForState() // 当 AXIS = X 时设置模型 + .modelFile(hModelFile) // 可以显示'hModelFile' + .rotationY(90) // 绕Y轴将'hModelFile'旋转90度 + .addModel(); // 完成当 AXIS = X 时的模型 + +// EXAMPLE_BLOCK_2:拥有属性BlockStateProperties#HORIZONTAL_FACING +this.getVariantBuilder(EXAMPLE_BLOCK_2) // 获取变量生成器 + .forAllStates(state -> // 对于全部可能的状态 + ConfiguredModel.builder() // 创建配置模型生成器 + .modelFile(modelFile) // 可以显示'modelFile' + .rotationY((int) state.getValue(HORIZONTAL_FACING).toYRot()) // 根据变量需求将'modelFile'绕Y轴旋转 + .build() // 创建配置模型的数组 + ); + +// EXAMPLE_BLOCK_3:拥有属性BlockStateProperties#HORIZONTAL_FACING, BlockStateProperties#WATERLOGGED +this.getVariantBuilder(EXAMPLE_BLOCK_3) // 获取变量生成器 + .forAllStatesExcept(state -> // 对于全部HORIZONTAL_FACING状态 + ConfiguredModel.builder() // 创建配置模型生成器 + .modelFile(modelFile) // 可以显示'modelFile' + .rotationY((int) state.getValue(HORIZONTAL_FACING).toYRot()) // 根据变量需求将'modelFile'绕Y轴旋转 + .build(), // 创建配置模型的数组 + WATERLOGGED); // 忽略WATERLOGGED属性 +``` + +### `MultiPartBlockStateBuilder` + +可以使用`BlockStateProvider#getMultipartBuilder`生成多部分。每个部分(`MultiPartBlockStateBuilder#part`)指定一组属性条件,当与存档中的`BlockState`匹配时,将显示模型列表中的模型。所有与`BlockState`匹配的条件组将显示它们所选的模型。 + +对于任何部分(通过`ConfiguredModel$Builder#addModel`获得),当属性是指定值之一时,可以添加条件(通过`#condition`)。条件必须全部成功,或者当设置了`#useOr`时,必须至少有一个成功。只要当前分组只包含其他组而不包含单个条件,就可以对条件进行分组(通过`#nestedGroup`)。条件组可以使用`#endNestedGroup`留下,给定的部分可以通过`#end`完成。 + +```java +// 在某个BlockStateProvider#registerStatesAndModels中 + +// 红石线 +this.getMultipartBuilder(REDSTONE) // 获取多部分生成器 + .part() // 创建一个部分 + .modelFile(redstoneDot) // 可以显示'redstoneDot' + .addModel() // 'redstoneDot'被显示,当... + .useOr() // 这些条件中至少一个为true + .nestedGroup() // 当所有组合条件均为true时,true + .condition(WEST_REDSTONE, NONE) // 当WEST_REDSTONE为NONE时,true + .condition(EAST_REDSTONE, NONE) // 当EAST_REDSTONE为NONE时,true + .condition(SOUTH_REDSTONE, NONE) // 当SOUTH_REDSTONE为NONE时,true + .condition(NORTH_REDSTONE, NONE) // 当NORTH_REDSTONE为NONE时,true + .endNestedGroup() // 结束组合 + .nestedGroup() // 当所有组合条件均为true时,true + .condition(EAST_REDSTONE, SIDE, UP) // 当EAST_REDSTONE为SIDE或UP时,true + .condition(NORTH_REDSTONE, SIDE, UP) // 当NORTH_REDSTONE为SIDE或UP时,true + .endNestedGroup() // 结束组合 + .nestedGroup() // 当所有组合条件均为true时,true + .condition(EAST_REDSTONE, SIDE, UP) // 当EAST_REDSTONE为SIDE或UP时,true + .condition(SOUTH_REDSTONE, SIDE, UP) // 当SOUTH_REDSTONE为SIDE或UP时,true + .endNestedGroup() // 结束组合 + .nestedGroup() // 当所有组合条件均为true时,true + .condition(WEST_REDSTONE, SIDE, UP) // 当WEST_REDSTONE为SIDE或UP时,true + .condition(SOUTH_REDSTONE, SIDE, UP) // 当SOUTH_REDSTONE为SIDE或UP时,true + .endNestedGroup() // 结束组合 + .nestedGroup() // 当所有组合条件均为true时,true + .condition(WEST_REDSTONE, SIDE, UP) // 当WEST_REDSTONE为SIDE或UP时,true + .condition(NORTH_REDSTONE, SIDE, UP) // 当NORTH_REDSTONE为SIDE或UP时,true + .endNestedGroup() // 结束组合 + .end() // 结束该部分 + .part() // 创建一个部分 + .modelFile(redstoneSide0) // 可以显示'redstoneSide0' + .addModel() // 'redstoneSide0'被显示,当... + .condition(NORTH_REDSTONE, SIDE, UP) // NORTH_REDSTONE为SIDE或UP + .end() // 结束该部分 + .part() // 创建一个部分 + .modelFile(redstoneSideAlt0) // 可以显示'redstoneSideAlt0' + .addModel() // 'redstoneSideAlt0'被显示,当... + .condition(SOUTH_REDSTONE, SIDE, UP) // SOUTH_REDSTONE为SIDE或UP + .end() // 结束该部分 + .part() // 创建一个部分 + .modelFile(redstoneSideAlt1) // 可以显示'redstoneSideAlt1' + .rotationY(270) // 将'redstoneSideAlt1'绕Y轴旋转270度 + .addModel() // 'redstoneSideAlt1'被显示,当... + .condition(EAST_REDSTONE, SIDE, UP) // EAST_REDSTONE为SIDE或UP + .end() // 结束该部分 + .part() // 创建一个部分 + .modelFile(redstoneSide1) // 可以显示'redstoneSide1' + .rotationY(270) // 将'redstoneSide1'绕Y轴旋转270度 + .addModel() // 'redstoneSide1'被显示,当... + .condition(WEST_REDSTONE, SIDE, UP) // WEST_REDSTONE为SIDE或UP + .end() // 结束该部分 + .part() // 创建一个部分 + .modelFile(redstoneUp) // 可以显示'redstoneUp' + .addModel() // 'redstoneUp'被显示,当... + .condition(NORTH_REDSTONE, UP) // NORTH_REDSTONE为UP + .end() // 结束该部分 + .part() // 创建一个部分 + .modelFile(redstoneUp) // 可以显示'redstoneUp' + .rotationY(90) // 将'redstoneUp'绕Y轴旋转90度 + .addModel() // 'redstoneUp'被显示,当... + .condition(EAST_REDSTONE, UP) // EAST_REDSTONE为UP + .end() // 结束该部分 + .part() // 创建一个部分 + .modelFile(redstoneUp) // 可以显示'redstoneUp' + .rotationY(180) // 将'redstoneUp'绕Y轴旋转180度 + .addModel() // 'redstoneUp'被显示,当... + .condition(SOUTH_REDSTONE, UP) // SOUTH_REDSTONE为UP + .end() // 结束该部分 + .part() // 创建一个部分 + .modelFile(redstoneUp) // 可以显示'redstoneUp' + .rotationY(270) // 将'redstoneUp'绕Y轴旋转270度 + .addModel() // 'redstoneUp'被显示,当... + .condition(WEST_REDSTONE, UP) // WEST_REDSTONE为UP + .end(); // 结束该部分 +``` + +模型加载器生成器 +--------------- + +还可以为给定的`ModelBuilder`生成自定义模型加载器。自定义模型加载器子类`CustomLoaderBuilder`,可以通过`#customLoader`应用于`ModelBuilder`。传入的工厂方法创建了一个新的加载器生成器,可以对其进行配置。完成所有更改后,自定义加载器可以通过`CustomLoaderBuilder#end`返回到`ModelBuilder`。 + +模型生成器 | 工厂方法 | 描述 +:---: | :---: | :--- +`DynamicFluidContainerModelBuilder` | `#begin` | 为特定的流体生成一个桶模型。 +`CompositeModelBuilder` | `#begin` | 生成一个由模型组成的模型。 +`ItemLayersModelBuilder` | `#begin` | 生成一个`item/generated`模型的Forge实现。 +`SeparateTransformsModelBuilder` | `#begin` | 生成一个模型,其修改基于特定的[变换][transform]。 +`ObjModelBuilder` | `#begin` | 生成一个[OBJ模型][obj]。 + +```java +// 对于某个BlockModelBuilder生成器 +builder.customLoader(ObjModelBuilder::begin) // 自定义加载器'forge:obj' + .modelLocation(modLoc("models/block/model.obj")) // 设置OBJ模型位置 + .flipV(true) // 在提供的.mtl纹理中翻转V坐标 + .end() // 完成自定义加载器配置 +.texture("particle", mcLoc("block/dirt")) // 将粒子纹理设置为泥土 +.texture("texture0", mcLoc("block/dirt")); // 将'texture0'纹理设置为泥土 +``` + +自定义模型加载器生成器 +-------------------- + +可以通过扩展`CustomLoaderBuilder`来创建自定义加载器生成器。构造函数仍然可以具有`protected`的可见性,其中`ResourceLocation`硬编码为通过`ModelEvent$RegisterGeometryLoaders#register`注册的加载器id。然后,可以通过静态工厂方法或构造函数(如果设置为`public`)初始化生成器。 + +```java +public class ExampleLoaderBuilder> extends CustomLoaderBuilder { + public static > ExampleLoaderBuilder begin(T parent, ExistingFileHelper existingFileHelper) { + return new ExampleLoaderBuilder<>(parent, existingFileHelper); + } + + protected ExampleLoaderBuilder(T parent, ExistingFileHelper existingFileHelper) { + super(new ResourceLocation(MOD_ID, "example_loader"), parent, existingFileHelper); + } +} +``` + +Afterwards, any configurations specified by the loader should be added as chainable methods. + +```java +// 在ExampleLoaderBuilder中 +public ExampleLoaderBuilder exampleInt(int example) { + // 设置int + return this; +} + +public ExampleLoaderBuilder exampleString(String example) { + // 设置string + return this; +} +``` + +If any additional configuration is specified, `#toJson` should be overridden to write the additional properties. + +```java +// 在ExampleLoaderBuilder中 +@Override +public JsonObject toJson(JsonObject json) { + json = super.toJson(json); // 处理基础加载器属性 + // 编码自定义加载器属性 + return json; +} +``` + +自定义模型提供者 +--------------- + +自定义模型提供者需要`ModelBuilder`子类和`ModelProvider`子类,前者定义要生成的模型的基础,后者生成模型。 + +`ModelBuilder`子类包含任何特殊属性,这些属性可以专门应用于这些类型的模型(物品模型可以具有重写)。如果添加了任何附加属性,则需要重写`#toJson`以写入附加信息。 + +```java +public class ExampleModelBuilder extends ModelBuilder { + // ... +} +``` + +`ModelProvider`子类不需要特殊的逻辑。构造函数应硬编码`models`文件夹和`ModelBuilder`中的子目录,以表示要生成的模型。 + +```java +public class ExampleModelProvider extends ModelProvider { + + public ExampleModelProvider(PackOutput output, String modid, ExistingFileHelper existingFileHelper) { + // 如果'#getBuilder'中未指定'modid',则模型将生成到'assets//models/example' + super(output, modid, "example", ExampleModelBuilder::new, existingFileHelper); + } +} +``` + +自定义模型Consumer +----------------- + +自定义模型Consumer,如[`BlockStateProvider`][blockstateprovider],可以通过手动生成模型来创建。应指定用于生成模型的`ModelProvider`子类并使其可用。 + +```java +public class ExampleModelConsumerProvider implements IDataProvider { + + public ExampleModelConsumerProvider(PackOutput output, String modid, ExistingFileHelper existingFileHelper) { + this.example = new ExampleModelProvider(output, modid, existingFileHelper); + } +} +``` + +一旦数据提供者运行,就可以使用`ModelProvider#generateAll`生成`ModelProvider`子类中的模型。 + +```java +// 在ExampleModelConsumerProvider中 +@Override +public CompletableFuture run(CachedOutput cache) { + // 填入模型提供者 + CompletableFuture exampleFutures = this.example.generateAll(cache); // 生成模型 + + // 运行逻辑并创建CompletableFuture以写入文件 + // ... + + // 假设我们有一个新的CompletableFuture providerFuture + return CompletableFuture.allOf(exampleFutures, providerFuture); +} +``` + +[provider]: #model-providers +[models]: ../../resources/client/models/index.md +[datagen]: ../index.md#data-providers +[efh]: ../index.md#existing-files +[loader]: #custom-model-loader-builders +[color]: ../../resources/client/models/tinting.md#blockcoloritemcolor +[overrides]: ../../resources/client/models/itemproperties.md +[blockstateprovider]: #block-state-provider +[blockstate]: https://minecraft.fandom.com/wiki/Tutorials/Models#Block_states +[blockmodels]: #blockmodelprovider +[itemmodels]: #itemmodelprovider +[properties]: ../../blocks/states.md#implementing-block-states +[transform]: ../../rendering/modelloaders/transform.md +[obj]: ../../rendering/modelloaders/index.md#wavefront-obj-models diff --git a/docs/translation/zh_CN/datagen/client/sounds.md b/docs/translation/zh_CN/datagen/client/sounds.md new file mode 100644 index 000000000..1073fac29 --- /dev/null +++ b/docs/translation/zh_CN/datagen/client/sounds.md @@ -0,0 +1,83 @@ +音效定义生成 +=========== + +通过子类化`SoundDefinitionsProvider`并实现`#registerSounds`,可以为模组生成`sounds.json`文件。实现后,必须将提供者[添加][datagen]到`DataGenerator`中。 + +```java +// 在模组事件总线上 +@SubscribeEvent +public void gatherData(GatherDataEvent event) { + event.getGenerator().addProvider( + // 告诉生成器仅在生成客户端资源时运行 + event.includeClient(), + output -> new MySoundDefinitionsProvider(output, MOD_ID, event.getExistingFileHelper()) + ); +} +``` + +添加一个音效 +----------- + +可以通过`#add`指定音效名称和定义来生成音效定义。音效名称可以从[`SoundEvent`][soundevent]、`ResourceLocation`或字符串中提供。 + +!!! 警告 + 提供的音效名称将始终假定命名空间是提供给提供者的构造函数的mod id。没有对音效名称的命名空间执行验证! + +### `SoundDefinition` + +可以使用`#definition`创建`SoundDefinition`。定义包含用于定义音效实例的数据。 + +定义指定了一些方法: + +方法 | 描述 +:---: | :--- +`with` | 添加选择定义时可能播放的音效。 +`subtitle` | 设置定义的翻译键。 +`replace` | 当为`true`时,将删除其他`sounds.json`为该定义定义的音效,而不是附加到该定义。 + +### `SoundDefinition$Sound` + +提供给`SoundDefinition`的音效可以使用`SoundDefinitionsProvider#sound`指定。这些方法采用音效的引用和`SoundType`(如果已指定)。 + +`SoundType`可以是两个值之一: + +音效类型 | 定义 +:---: | :--- +`SOUND` | 指定位于`assets//sounds/.ogg`的音效的一个引用。 +`EVENT` | 指定由`sounds.json`定义的另一个音效的名称的引用。 + +从`SoundDefinitionsProvider#sound`创建的每个`Sound`都可以指定关于如何加载和播放所提供音效的其他配置: + +方法 | 描述 +:---: | :--- +`volume` | 设置音效的音量大小,必须大于`0`。 +`pitch` | 设置音效的音高大小,必须大于`0`。 +`weight` | 设置音效被选定时播放音效的可能性。 +`stream` | 当为`true`时,从文件中读取音效,而不是将音效加载到内存中。推荐用于长音效:背景音乐、音乐唱片等。 +`attenuationDistance` | 设置可以听到音效的所距离的方块数。 +`preload` | 当为`true`时,一旦加载资源包,就会立即将音效加载到内存中。 + +```java +// 在某个SoundDefinitionsProvider#registerSounds中 +this.add(EXAMPLE_SOUND_EVENT, definition() + .subtitle("sound.examplemod.example_sound") // 设置翻译键 + .with( + sound(new ResourceLocation(MODID, "example_sound_1")) // 设置第一个音效 + .weight(4) // 具有4 / 5 = 80%的播放机率 + .volume(0.5), // 将调用此音效的所有音量缩放一半 + sound(new ResourceLocation(MODID, "example_sound_2")) // 设置第二个音效 + .stream() // 流播该音效 + ) +); + +this.add(EXAMPLE_SOUND_EVENT_2, definition() + .subtitle("sound.examplemod.example_sound") // 设置翻译键 + .with( + sound(EXAMPLE_SOUND_EVENT.getLocation(), SoundType.EVENT) // 从'EXAMPLE_SOUND_EVENT'添加音效 + .pitch(0.5) // 将调用此音效的所有音高缩放一半 + ) +); +``` + +[datagen]: ../index.md#data-providers +[soundevent]: ../../gameeffects/sounds.md#creating-sound-events diff --git a/docs/translation/zh_CN/datagen/index.md b/docs/translation/zh_CN/datagen/index.md new file mode 100644 index 000000000..35bb5aea7 --- /dev/null +++ b/docs/translation/zh_CN/datagen/index.md @@ -0,0 +1,86 @@ +数据生成 +======== + +数据生成器是以编程方式生成模组的资源(asset)和数据(data)的一种方式。它允许在代码中定义这些文件的内容并自动生成它们,而不必担心细节。 + +数据生成器系统由主类`net.minecraft.data.Main`加载。可以传递不同的命令行参数来自定义收集了哪些模组的数据,考虑了哪些现有文件等。负责数据生成的类是`net.minecraft.data.DataGenerator`。 + +MDK的`build.gradle`中的默认配置添加了用于运行数据生成器的`runData`任务。 + +现存的文件 +--------- +对未为数据生成而生成的纹理或其他数据文件的所有引用都必须引用系统上的现有文件。这是为了确保所有引用的纹理都在正确的位置,这样就可以找到并更正拼写错误。 + +`ExistingFileHelper`是负责验证这些数据文件是否存在的类。可以从`GatherDataEvent#getExistingFileHelper`中检索实例。 + +`--existing `参数允许在验证文件是否存在时使用指定的文件夹及其子文件夹。此外,`--existing-mod `参数允许将加载的模组的资源用于验证。默认情况下,只有普通的数据包和资源可用于`ExistingFileHelper`。 + +生成器模式 +--------- + +数据生成器可以配置为运行4个不同的数据生成,这些数据生成是通过命令行参数配置的,并且可以通过`GatherDataEvent#include***`方法进行检查。 + +* __Client Assets__ + * 在`assets`中生成仅客户端文件:f方块/物品模型、方块状态JSON、语言文件等。 + * __`--client`__, `#includeClient` +* __Server Data__ + * 在`data`中生成仅服务端文件:配方、进度、标签等。 + * __`--server`__, `#includeServer` +* __Development Tools__ + * 运行一些开发工具:将SNBT转换为NBT,反之亦然,等等。 + * __`--dev`__, `#includeDev` +* __Reports__ + * 转储所有已注册的方块、物品、命令等。 + * __`--reports`__, `#includeReports` + +所有的生成器都可以使用`--all`包含在内。 + +数据提供者 +--------- + +数据提供者是实际定义将生成和提供哪些数据的类。所有数据提供者都实现`DataProvider`。Minecraft对大多数asset和data都有抽象实现,因此模组开发者只需要扩展和覆盖指定的方法。 + +当创建数据生成器时,在模组事件总线上触发`GatherDataEvent`,并且可以从事件中获取`DataGenerator`。使用`DataGenerator#addProvider`创建和注册数据提供者。 + +### 客户端资源(Assets) +* [`net.minecraftforge.common.data.LanguageProvider`][langgen] - 针对[语言设置][lang];实现`#addTranslations` +* [`net.minecraftforge.common.data.SoundDefinitionsProvider`][soundgen] - 针对[`sounds.json`][sounds];实现`#registerSounds` +* [`net.minecraftforge.client.model.generators.ModelProvider`][modelgen] - 针对[模型];实现`#registerModels` + * [`ItemModelProvider`][itemmodelgen] - 针对物品模型 + * [`BlockModelProvider`][blockmodelgen] - 针对方块模型 +* [`net.minecraftforge.client.model.generators.BlockStateProvider`][blockstategen] - 针对方块状态JSON以及其方块和物品模型;实现`#registerStatesAndModels` + +### 服务端数据(Data) + +**这些类在`net.minecraftforge.common.data`包之下**: + +* [`GlobalLootModifierProvider`][glmgen] - 针对[全局战利品修改器][glm];实现`#start` +* [`DatapackBuiltinEntriesProvider`][datapackregistriesgen] - 针对数据包注册表对象;向构造函数传递`RegistrySetBuilder` + +**这些类在`net.minecraft.data`包之下**: + +* [`loot.LootTableProvider`][loottablegen] - 针对[战利品表][loottable];向构造函数传递`LootTableProvider$SubProviderEntry` +* [`recipes.RecipeProvider`][recipegen] - 针对[配方]以及其解锁的进度;实现`#buildRecipes` +* [`tags.TagsProvider`][taggen] - 针对[标签];实现`#addTags` +* [`advancements.AdvancementProvider`][advgen] - 针对[进度];向构造函数传递`AdvancementSubProvider` + +[langgen]: ./client/localization.md +[lang]: https://minecraft.fandom.com/wiki/Language +[soundgen]: ./client/sounds.md +[sounds]: https://minecraft.fandom.com/wiki/Sounds.json +[modelgen]: ./client/modelproviders.md +[models]: ../resources/client/models/index.md +[itemmodelgen]: ./client/modelproviders.md#itemmodelprovider +[blockmodelgen]: ./client/modelproviders.md#blockmodelprovider +[blockstategen]: ./client/modelproviders.md#block-state-provider +[glmgen]: ./server/glm.md +[glm]: ../resources/server/glm.md +[datapackregistriesgen]: ./server/datapackregistries.md +[loottablegen]: ./server/loottables.md +[loottable]: ../resources/server/loottables.md +[recipegen]: ./server/recipes.md +[recipes]: ../resources/server/recipes/index.md +[taggen]: ./server/tags.md +[tags]: ../resources/server/tags.md +[advgen]: ./server/advancements.md +[advancements]: ../resources/server/advancements.md diff --git a/docs/translation/zh_CN/datagen/server/advancements.md b/docs/translation/zh_CN/datagen/server/advancements.md new file mode 100644 index 000000000..6f818b01b --- /dev/null +++ b/docs/translation/zh_CN/datagen/server/advancements.md @@ -0,0 +1,67 @@ +进度生成 +======== + +可以通过构建新的`AdvancementProvider`并提供`AdvancementSubProvider`来为模组生成[进度][Advancements]。进度既可以手动创建和提供,也可以为方便起见,使用`Advancement$Builder`创建。该提供者必须被[添加][datagen]到`DataGenerator`中。 + +!!! 注意 + Forge为`AdvancementProvider`提供了一个名为`ForgeAdvancementProvider`的扩展,它可以更好地集成以生成进度。因此,本文档将使用`ForgeAdvancementProvider`和子提供者接口`ForgeAdvancementProvider$AdvancementGenerator`。 + +```java +// 在模组事件总线上 +@SubscribeEvent +public void gatherData(GatherDataEvent event) { + event.getGenerator().addProvider( + // 告诉生成器仅在生成服务端资源时运行 + event.includeServer(), + output -> new ForgeAdvancementProvider( + output, + event.getLookupProvider(), + event.getExistingFileHelper(), + // 生成进度的子提供者 + List.of(subProvider1, subProvider2, /*...*/) + ) + ); +} +``` + +`ForgeAdvancementProvider$AdvancementGenerator` +----------------------------------------------- + +`ForgeAdvancementProvider$AdvancementGenerator`负责生成进度,包含一个接受注册表查找的方法、写入器(`Consumer`)和现有文件助手.. + +```java +// 在ForgeAdvancementProvider$AdvancementGenerator的某个子类中,或作为一个lambda引用 + +@Override +public void generate(HolderLookup.Provider registries, Consumer writer, ExistingFileHelper existingFileHelper) { + // 在此处构建进度 +} +``` + +`Advancement$Builder` +--------------------- + +`Advancement$Builder`是一个方便的实现,用于创建要生成的`Advancement`。它允许定义父级进度、显示信息、进度完成时的奖励以及解锁进度的要求。只需指定要求即可创建`Advancement`。 + +尽管不是必需的,但有许多方法很重要: + +方法 | 描述 +:---: | :--- +`parent` | 设置此进度直接链接到的进度。可以指定进度的名称,也可以指定进度本身(如果它是由模组开发者生成的)。 +`display` | 设置要显示在聊天、toast和进度屏幕上的信息。 +`rewards` | 设置此进度完成时获得的奖励。 +`addCriterion` | 为此进度添加一个条件。 +`requirements` | 指定是所有条件都必须返回true,还是至少有一个条件必须返回true。可以使用额外的重载来混合和匹配这些操作。 + +一旦准备好构建`Advancement$Builder`,就应该调用`#save`方法,该方法接受写入器、进度的注册表名以及用于检查提供的父级是否存在的文件助手。 + +```java +// 在某个ForgeAdvancementProvider$AdvancementGenerator#generate(registries, writer, existingFileHelper)中 +Advancement example = Advancement.Builder.advancement() + .addCriterion("example_criterion", triggerInstance) // 该进度如何解锁 + .save(writer, name, existingFileHelper); // 将数据加入生成器 +``` + +[advancements]: ../../resources/server/advancements.md +[datagen]: ../index.md#data-providers +[conditional]: ../../resources/server/conditional.md diff --git a/docs/translation/zh_CN/datagen/server/datapackregistries.md b/docs/translation/zh_CN/datagen/server/datapackregistries.md new file mode 100644 index 000000000..82190759b --- /dev/null +++ b/docs/translation/zh_CN/datagen/server/datapackregistries.md @@ -0,0 +1,129 @@ +数据包注册表对象生成 +================== + +Datapack registry objects can be generated for a mod by constructing a new `DatapackBuiltinEntriesProvider` and providing a `RegistrySetBuilder` with the new objects to register. The provider must be [added][datagen] to the `DataGenerator`. +通过构造新的`DatapackBuiltinEntriesProvider`并为`RegistrySetBuilder`提供要注册的新对象,可以为模组生成数据包注册表对象。该提供者必须被[添加][datagen]到`DataGenerator`中。 + +!!! 注意 + `DatapackBuiltinEntriesProvider`是`RegistriesDatapackGenerator`之上的一个Forge扩展,它可以正确处理引用现有数据包注册表对象而不会分解条目。因此,本文档将使用`DatapackBuiltinEntriesProvider`。 + +```java +// 在模组事件总线上 +@SubscribeEvent +public void gatherData(GatherDataEvent event) { + event.getGenerator().addProvider( + // 告诉生成器仅在生成服务端资源时运行 + event.includeServer(), + output -> new DatapackBuiltinEntriesProvider( + output, + event.getLookupProvider(), + // 包含要生成的数据包注册表对象的生成器 + new RegistrySetBuilder().add(/* ... */), + // 用于生成的数据包注册表对象的mod id集合 + Set.of(MOD_ID) + ) + ); +} +``` + +`RegistrySetBuilder` +-------------------- + +`RegistrySetBuilder`负责构建游戏中使用的所有数据包注册表对象。生成器可以为注册表添加一个新条目,然后注册表可以将对象注册到该注册表中。 + +首先,可以通过调用构造函数来初始化`RegistrySetBuilder`的新实例。然后,可以调用`#add`方法(它接受注册表的`ResourceKey`,一个包含`BootstapContext`的`RegistryBootstrap` Consumer来注册对象,以及一个可选的`Lifecycle`参数来指示注册表的当前生命周期状态)来处理特定注册表进行注册。 + +```java +new RegistrySetBuilder() + // 创建已配置的特性 + .add(Registries.CONFIGURED_FEATURE, bootstrap -> { + // 在此处注册已配置的特性 + }) + // 创建已放置的特性 + .add(Registries.PLACED_FEATURE, bootstrap -> { + // 在此处注册已放置的特性 + }); +``` + +!!! 注意 + 通过Forge创建的数据包注册表也可以通过传递相关的`ResourceKey`来使用该生成器生成它们的对象。 + +使用`BootstapContext`注册 +------------------------- + +生成器提供的`BootstapContext`中的`#register`方法可用于注册对象。它采用`ResourceKey`表示对象的注册表名称、要注册的对象,以及一个可选的`Lifecycle`参数来指示注册表对象的当前生命周期状态。 + +```java +public static final ResourceKey> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create( + Registries.CONFIGURED_FEATURE, + new ResourceLocation(MOD_ID, "example_configured_feature") +); + +// 在某个恒定的位置或参数中 +new RegistrySetBuilder() + // 创建已配置的特性 + .add(Registries.CONFIGURED_FEATURE, bootstrap -> { + // 在此处注册已配置的特性 + bootstrap.register( + // 已配置的特性的资源键 + EXAMPLE_CONFIGURED_FEATURE, + new ConfiguredFeature<>( + Feature.ORE, // 创建一个矿物特性 + new OreConfiguration( + List.of(), // 不做任何事情 + 8 // 在最多8个矿脉中 + ) + ) + ); + }) + // 创建已放置的特性 + .add(Registries.PLACED_FEATURE, bootstrap -> { + // 在此处注册已放置的特性 + }); +``` + +### Datapack Registry Object Lookup + +有时,数据包注册表对象可能希望使用其他数据包注册表对象或包含数据包注册表对象的标签。在这种情况下,你可以使用`BootstapContext#lookup`查找另一个数据包注册表以获得`HolderGetter`。从那里,你可以通过`#getOrThrow`传递相关的键,获得数据包注册表对象的`Holder$Reference`或标签的`HolderSet$Named`。 + +```java +public static final ResourceKey> EXAMPLE_CONFIGURED_FEATURE = ResourceKey.create( + Registries.CONFIGURED_FEATURE, + new ResourceLocation(MOD_ID, "example_configured_feature") +); + +public static final ResourceKey EXAMPLE_PLACED_FEATURE = ResourceKey.create( + Registries.PLACED_FEATURE, + new ResourceLocation(MOD_ID, "example_placed_feature") +); + +// 在某个恒定的位置或参数中 +new RegistrySetBuilder() + // 创建已配置的特性 + .add(Registries.CONFIGURED_FEATURE, bootstrap -> { + // 在此处注册已配置的特性 + bootstrap.register( + // 已配置的特性的资源键 + EXAMPLE_CONFIGURED_FEATURE, + new ConfiguredFeature(/* ... */) + ); + }) + // 创建已放置的特性 + .add(Registries.PLACED_FEATURE, bootstrap -> { + // 在此处注册已放置的特性 + + // 获取已配置的特性的注册表 + HolderGetter> configured = bootstrap.lookup(Registries.CONFIGURED_FEATURE); + + bootstrap.register( + // 已放置的特性的资源键 + EXAMPLE_PLACED_FEATURE, + new PlacedFeature( + configured.getOrThrow(EXAMPLE_CONFIGURED_FEATURE), // 获取已配置的特性 + List.of() // 并对于放置位置不做任何事情 + ) + ) + }); +``` + +[datagen]: ../index.md#data-providers \ No newline at end of file diff --git a/docs/translation/zh_CN/datagen/server/glm.md b/docs/translation/zh_CN/datagen/server/glm.md new file mode 100644 index 000000000..ae0388ac2 --- /dev/null +++ b/docs/translation/zh_CN/datagen/server/glm.md @@ -0,0 +1,30 @@ +全局战利品修改器生成 +================== + +可以通过子类化`GlobalLootModifierProvider`并实现`#start`来为模组生成[全局战利品修改器(GLM)][glm]。每个GLM都可以通过调用`#add`并指定要序列化的修改器和[修改器实例][instance]的名称来添加生成。实现后,该提供者必须被[添加][datagen]到`DataGenerator`中。 + +```java +// 在模组事件总线上 +@SubscribeEvent +public void gatherData(GatherDataEvent event) { + event.getGenerator().addProvider( + // 告诉生成器仅在生成服务端资源时运行 + event.includeServer(), + output -> new MyGlobalLootModifierProvider(output, MOD_ID) + ); +} + +// 在某个GlobalLootModifierProvider#start中 +this.add("example_modifier", new ExampleModifier( + new LootItemCondition[] { + WeatherCheck.weather().setRaining(true).build() // 当下雨时执行 + }, + "val1", + 10, + Items.DIRT +)); +``` + +[glm]: ../../resources/server/glm.md +[instance]: ../../resources/server/glm.md#igloballootmodifier +[datagen]: ../index.md#data-providers diff --git a/docs/translation/zh_CN/datagen/server/loottables.md b/docs/translation/zh_CN/datagen/server/loottables.md new file mode 100644 index 000000000..e907d2974 --- /dev/null +++ b/docs/translation/zh_CN/datagen/server/loottables.md @@ -0,0 +1,144 @@ +战利品表生成 +=========== + +可以通过构造新的`LootTableProvider`并提供`LootTableProvider$SubProviderEntry`来为模组生成[战利品表][loottable]。该提供者必须被[添加][datagen]到`DataGenerator`中。 + +```java +// 在模组事件总线上 +@SubscribeEvent +public void gatherData(GatherDataEvent event) { + event.getGenerator().addProvider( + // 告诉生成器仅在生成服务端资源时运行 + event.includeServer(), + output -> new MyLootTableProvider( + output, + // 指定需要生成的表的注册表名称,或者可留空 + Collections.emptySet(), + // 生成战利品的子提供者 + List.of(subProvider1, subProvider2, /*...*/) + ) + ); +} +``` + +`LootTableSubProvider` +---------------------- + +每个`LootTableProvider$SubProviderEntry`接受一个提供的`LootTableSubProvider`,该`LootTableSubProvider`为给定的`LootContextParamSet`生成战利品表。`LootTableSubProvider`包含一个方法,该方法采用编写器(`BiConsumer`)来生成表。 + +```java +public class ExampleSubProvider implements LootTableSubProvider { + + // 用于为包装Supplier创建工厂方法 + public ExampleSubProvider() {} + + // 用于生成战利品表的方法 + @Override + public void generate(BiConsumer writer) { + // 在此处通过调用writer#accept生成战利品表 + } +} +``` + +The table can then be added to `LootTableProvider#getTables` for any available `LootContextParamSet`: + +```java +// 在将会传递给LootTableProvider构造函数的列表中 +new LootTableProvider.SubProviderEntry( + ExampleSubProvider::new, + // 'empty'参数集的战利品表生成器 + LootContextParamSets.EMPTY +) +``` + +### `BlockLootSubProvider`和`EntityLootSubProvider`子类 + +对于`LootContextParamSets#BLOCK`和`#ENTITY`,有一些特殊类型(分别为`BlockLootSubProvider`和`EntityLootSubProvider`),它们提供了额外的帮助方法来创建和验证是否存在战利品表。 + +`BlockLootSubProvider`的构造函数接受一个物品列表和一个`FeatureFlagSet`,前者是耐爆炸的,用于确定如果方块爆炸,是否可以生成战利品表,后者用于确定是否启用了该方块,以便为其生成战利品表。 + +```java +// 在某个BlockLootSubProvider子类中 +public MyBlockLootSubProvider() { + super(Collections.emptySet(), FeatureFlags.REGISTRY.allFlags()); +} +``` + +`EntityLootSubProvider`的构造函数接受一个`FeatureFlagSet`,它确定是否启用了实体类型,以便为其生成战利品表。 + +```java +// 在某个EntityLootSubProvider子类中 +public MyEntityLootSubProvider() { + super(FeatureFlags.REGISTRY.allFlags()); +} +``` + +要使用它们,所有注册的对象必须分别提供给`BlockLootSubProvider#getKnownBlocks`和`EntityLootSubProvider#getKnownEntityTypes`。这些方法是为了确保Iterable中的所有对象都有一个战利品表。 + +!!! 提示 + 如果`DeferredRegister`用于注册模组的对象,则可以通过`DeferredRegister#getEntries`向`#getKnown*`方法提供条目: + + ```java + // 在针对某个DeferredRegister BLOCK_REGISTRAR的某个BlockLootSubProvider子类中 + @Override + protected Iterable getKnownBlocks() { + return BLOCK_REGISTRAR.getEntries() // 获取所有已注册的条目 + .stream() // 流播所有已包装的对象 + .flatMap(RegistryObject::stream) // 如果可行,获取该对象 + ::iterator; // 创建该Iterable + } + ``` + +战利品表本身可以通过实现`#generate`方法来添加。 + +```java +// 在某个BlockLootSubProvider子类中 +@Override +public void generate() { + // 在此处添加战利品表 +} +``` + +战利品表生成器 +------------- + +要生成战利品表,它们被`LootTableSubProvider`接受为`LootTable$Builder`。之后,在`LootTableProvider$SubProviderEntry`中设置指定的`LootContextParamSet`,然后通过`#build`生成。在构建之前,生成器可以指定影响战利品表功能的条目、条件和修改器。 + +!!! 注意 + 战利品表的功能非常广泛,因此本文档不会对其进行全面介绍。取而代之的是,将对每个组件进行简要描述。每个组件的特定子类型可以使用IDE找到。它们的实现将留给读者练习。 + +### LootTable + +战利品表是基本对象,可以使用`LootTable#lootTable`将其转换为所需的`LootTable$Builder`。战利品表可以通过池列表(通过`#withPool`)以及修改这些池的结果物品的功能(通过`#apply`)来构建,池列表按指定的顺序应用。 + +### LootPool + +战利品池代表一个执行操作的组,并且可以使用`LootPool#lootPool`生成`LootPool$Builder`。每个战利品池都可以指定定义池中操作的条目(通过`#add`)、定义是否应该执行池中的操作的条件(通过`#when`)以及修改条目的结果物品的功能(通过`#apply`)。每个池可以按指定次数执行(通过`#setRolls`)。此外,还可以指定奖金执行(通过`#setBonusRolls`),这取决于执行者的运气。 + +### LootPoolEntryContainer + +战利品条目定义了选择时要执行的操作,通常是生成物品。每个条目都有一个关联的[已注册的][registered]`LootPoolEntryType`。它们也有自己的关联生成器,为`LootPoolEntryContainer$Builder`的子类型。多个条目可以同时执行(通过`#append`)或顺序执行,直到一个条目失败为止(通过`#then`)。此外,条目可以在失败时默认为另一个条目(通过`#otherwise`)。 + +### LootItemCondition + +战利品条件定义了执行某些操作所需满足的要求。每个条件都有一个关联的[已注册的][registered]`LootItemConditionType`。它们也有自己的关联生成器,为`LootItemCondition$Builder`的子类型。默认情况下,所有指定的战利品条件都必须返回true才能执行操作。战利品条件也可以指定为只有一个必须返回true(通过`#or`)。此外,条件的结果输出可以反转(通过`#invert`)。 + +### LootItemFunction + +战利品函数在将执行结果传递给输出之前会对其进行修改。每个函数都有一个关联的[已注册的][registered]`LootItemFunctionType`。它们也有自己的关联生成器,为`LootItemFunction$Builder`的子类型。 + +#### NbtProvider + +NBT提供者是由`CopyNbtFunction`定义的一种特殊类型的函数。它们定义了从何处提取标记信息。每个提供者都有一个关联的[已注册的][registered]`LootNbtProviderType`。 + +### NumberProvider + +数字提供者决定战利品池执行的次数。每个提供者都有一个关联的[已注册的][registered]`LootNumberProviderType`。 + +#### ScoreboardNameProvider + +记分牌提供者是由`ScoreboardValue`定义的一种特殊类型的数字提供者。他们定义了记分牌的名称,以获取要执行的掷数。每个提供者都有一个关联的[已注册的][registered]`LootScoreProviderType`。 + +[loottable]: ../../resources/server/loottables.md +[datagen]: ../index.md#data-providers +[registered]: ../../concepts/registries.md#registries-that-arent-forge-registries diff --git a/docs/translation/zh_CN/datagen/server/recipes.md b/docs/translation/zh_CN/datagen/server/recipes.md new file mode 100644 index 000000000..7574269d9 --- /dev/null +++ b/docs/translation/zh_CN/datagen/server/recipes.md @@ -0,0 +1,194 @@ +配方生成 +======== + +可以通过子类化`RecipeProvider`并实现`#buildRecipes`来为模组生成配方。一旦Consumer接受`FinishedRecipe`视图,就会提供一个用于生成数据的配方。`FinishedRecipe`既可以手动创建和提供,也可以为方便起见,使用`RecipeBuilder`创建。 + +实现后,该提供者必须被[添加][datagen]到`DataGenerator`。 + +```java +// 在模组事件总线上 +@SubscribeEvent +public void gatherData(GatherDataEvent event) { + event.getGenerator().addProvider( + // 告诉生成器仅在生成服务端资源时运行 + event.includeServer(), + MyRecipeProvider::new + ); +} +``` + +`RecipeBuilder` +--------------- + +`RecipeBuilder`是一个方便的实现,用于创建要生成的`FinishedRecipe`。它提供了解锁、分组、保存和获取配方结果的基本定义。这分别通过`#unlockedBy`、`#group`、`#save`和`#getResult`来完成。 + +!!! 重要 + 原版配方生成器中不支持配方中的[`ItemStack`输出][stack]。对于现有的原版配方序列化器,必须以不同的方式构建`FinishedRecipe`才能生成此数据。 + +!!! 警告 + 正在生成的物品结果必须指定有效的`RecipeCategory`;否则,将引发`NullPointerException`。 + +除[`SpecialRecipeBuilder`]外的所有配方构建器都需要指定一个进度标准。如果玩家以前使用过配方,则所有配方都会生成解锁配方的标准。然而,必须指定一个额外的标准,允许玩家在没有任何先验知识的情况下获得配方。如果指定的任何标准为真,则玩家将获得配方书的配方。 + +!!! 提示 + 配方标准通常使用`InventoryChangeTrigger`在用户物品栏中存在某些物品时解锁配方。 + +### ShapedRecipeBuilder + +`ShapedRecipeBuilder`用于生成有序配方。该生成器可以通过`#shaped`进行初始化。保存前可以指定配方组、输入符号模式、配料的符号定义和配方解锁条件。 + +```java +// 在RecipeProvider#buildRecipes(writer)中 +ShapedRecipeBuilder builder = ShapedRecipeBuilder.shaped(RecipeCategory.MISC, result) + .pattern("a a") // 创建配方图案 + .define('a', item) // 定义符号代表什么 + .unlockedBy("criteria", criteria) // 该配方如何解锁 + .save(writer); // 将数据加入生成器 +``` + +#### 附加验证检查 + +有序配方在构建前进行了一些额外的验证检查: + +* 图案必须被定义且接受多于一个物品。 +* 所有图案行的宽度必须相同。 +* 一个符号不能被定义多次。 +* 空格字符(`' '`)被保留用于表示格中无物品,因此无法被定义。 +* 图案必须使用用户定义的全部符号。 + +### ShapelessRecipeBuilder + +`ShapelessRecipeBuilder`用于生成无序配方。该生成器可以通过`#shapeless`进行初始化。保存前可以指定配方组、输入原料和配方解锁条件。 + +```java +// 在RecipeProvider#buildRecipes(writer)中 +ShapelessRecipeBuilder builder = ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, result) + .requires(item) // 将物品加入配方 + .unlockedBy("criteria", criteria) // 该配方如何解锁 + .save(writer); // 将数据加入生成器 +``` + +### SimpleCookingRecipeBuilder + +`SimpleCookingRecipeBuilder`用于生成熔炼、高炉熔炼、烟熏和篝火烹饪配方。此外,使用`SimpleCookingSerializer`的自定义烹饪配方也可以是使用该生成器生成的数据。生成器可以分别通过`#smelting`、`#blasting`、`#smoking`、`#campfireCooking`或`#cooking`进行初始化。保存前可以指定配方组和配方解锁条件。 + +```java +// 在RecipeProvider#buildRecipes(writer)中 +SimpleCookingRecipeBuilder builder = SimpleCookingRecipeBuilder.smelting(input, RecipeCategory.MISC, result, experience, cookingTime) + .unlockedBy("criteria", criteria) // 该配方如何解锁 + .save(writer); // 将数据加入生成器 +``` + +### SingleItemRecipeBuilder + +`SingleItemRecipeBuilder`用于生成切石配方。此外,使用类似`SingleItemRecipe$Serializer`的序列化器的自定义但物品配方也可以是使用该生成器生成的数据。生成器可以分别通过`#stonecutting`或通过构造函数进行初始化。保存前可以指定配方组和配方解锁条件。 + +```java +// 在RecipeProvider#buildRecipes(writer)中 +SingleItemRecipeBuilder builder = SingleItemRecipeBuilder.stonecutting(input, RecipeCategory.MISC, result) + .unlockedBy("criteria", criteria) // 该配方如何解锁 + .save(writer); // 将数据加入生成器 +``` + +非`RecipeBuilder`生成器 +----------------------- + +由于缺少前面提到的所有配方所使用的功能,一些配方生成器没有实现`RecipeBuilder`。 + +### SmithingTransformRecipeBuilder + +`SmithingTransformRecipeBuilder`用于生成转换物品的锻造配方。此外,使用序列化器(如`SmithingTransformRecipe$Serializer`)的自定义配方也可以是使用此生成器生成的数据。生成器可以分别通过`#smithing`或通过构造函数进行初始化。保存前可以指定配方解锁条件。 + +```java +// 在RecipeProvider#buildRecipes(writer)中 +SmithingTransformRecipeBuilder builder = SmithingTransformRecipeBuilder.smithing(template, base, addition, RecipeCategory.MISC, result) + .unlocks("criteria", criteria) // 该配方如何解锁 + .save(writer, name); // 将数据加入生成器 +``` + +### SmithingTrimRecipeBuilder + +`SmithingTrimRecipeBuilder`用于生成盔甲装饰的锻造配方。此外,使用类似`SmithingTrimRecipe$Serializer`的序列化器的自定义升级配方也可以是使用该生成器生成的数据。生成器可以分别通过`#smithingTrim`或通过构造函数进行初始化。保存前可以指定配方解锁条件。 + +```java +// 在RecipeProvider#buildRecipes(writer)中 +SmithingTrimRecipe builder = SmithingTrimRecipe.smithingTrim(template, base, addition, RecipeCategory.MISC) + .unlocks("criteria", criteria) // 该配方如何解锁 + .save(writer, name); // 将数据加入生成器 +``` + +### SpecialRecipeBuilder + +`SpecialRecipeBuilder` is used to generate empty JSONs for dynamic recipes that cannot easily be constrained to the recipe JSON format (dying armor, firework, etc.). The builder can be initialized via `#special`. + +```java +// 在RecipeProvider#buildRecipes(writer)中 +SpecialRecipeBuilder.special(dynamicRecipeSerializer) + .save(writer, name); // 将数据加入生成器 +``` + +条件性配方 +--------- + +[条件性配方][conditional]也可以是通过`ConditionalRecipe$Builder`生成的数据。生成器可以使用`#builder`获得。 + +每个配方的条件可以通过首先调用`#addCondition`,然后在指定所有条件后调用`#addRecipe`来指定。这个过程可以重复多次,只要程序员愿意。 + +在指定了所有配方后,可以在最后使用`#generateAdvancement`为每个配方添加进度。或者,可以使用`#setAdvancement`设置条件性进度。 + +```java +// 在RecipeProvider#buildRecipes(writer)中 +ConditionalRecipe.builder() + // 为该配方添加条件 + .addCondition(...) + // 添加当条件为true时返回的配方 + .addRecipe(...) + + // 为下一个配方添加接下来的条件 + .addCondition(...) + // 添加当条件为true时返回的下一个配方 + .addRecipe(...) + + // 创建条件性进度,其使用上面配方中的条件和所解锁的进度 + .generateAdvancement() + .build(writer, name); +``` + +### IConditionBuilder + +为了简化向条件配方添加条件而不必手动构造每个条件实例,扩展的`RecipeProvider`可以实现`IConditionBuilder`。该接口添加了可以轻松构造条件实例的方法。 + +```java +// 在ConditionalRecipe$Builder#addCondition中 +( + // 如果'examplemod:example_item' + // 或(OR)'examplemod:example_item2'存在 + // 并且(AND) + // 非FALSE(NOT FALSE) + + // Methods are defined by IConditionBuilder + and( + or( + itemExists("examplemod", "example_item"), + itemExists("examplemod", "example_item2") + ), + not( + FALSE() + ) + ) +) +``` + +自定义配方序列化器 +----------------- + +自定义配方序列化器可以是通过创建可以构造`FinishedRecipe`的生成器生成的数据。完成的配方将配方数据及其所解锁的进度(如果存在)编码为JSON。此外,还指定了配方的名称和序列化器,以了解在加载时向何处写入以及可以解码对象的内容。构造完`FinishedRecipe`后,只需将其传递给`RecipeProvider#buildRecipes`提供的`Consumer`。 + +!!! 提示 + `FinishedRecipe`足够灵活,任何对象转换都可以是数据生成的,而不仅仅是物品。 + +[datagen]: ../index.md#data-providers +[ingredients]: ../../resources/server/recipes/ingredients.md#forge-types +[stack]: ../../resources/server/recipes/index.md#recipe-itemstack-result +[conditional]: ../../resources/server/conditional.md +[special]: #specialrecipebuilder diff --git a/docs/translation/zh_CN/datagen/server/tags.md b/docs/translation/zh_CN/datagen/server/tags.md new file mode 100644 index 000000000..ba30fe7f3 --- /dev/null +++ b/docs/translation/zh_CN/datagen/server/tags.md @@ -0,0 +1,121 @@ +标签生成 +======== + +可以通过子类化`TagsProvider`并实现`#addTags`来为模组生成[标签][Tags]。实现后,该提供者必须被[添加][datagen]到`DataGenerator`中。 + +```java +// 在模组事件总线上 +@SubscribeEvent +public void gatherData(GatherDataEvent event) { + event.getGenerator().addProvider( + // 告诉生成器仅在生成服务端资源时运行 + event.includeServer(), + // 扩展net.minecraftforge.common.data.BlockTagsProvider + output -> new MyBlockTagsProvider( + output, + event.getLookupProvider(), + MOD_ID, + event.getExistingFileHelper() + ) + ); +} +``` + +`TagsProvider` +-------------- + +标签提供者有两种用于生成标签的方法:通过`#tag`创建带有对象和其他标签的标签,或通过`#getOrCreateRawBuilder`使用其他对象类型的标签生成标签数据。 + +!!! 注意 + 通常,提供者不会直接调用`#getOrCreateRawBuilder`,除非注册表包含来自不同注册表的对象表示(方块具有物品表示以获得物品栏中的方块)。 + +当调用`#tag`时,将创建一个`TagAppender`,它充当要添加到标签中的元素的可链接Consumer: + +方法 | 描述 +:---: | :--- +`add` | 通过对象的资源键将对象添加到标签中。 +`addOptional` | 通过对象的名称将对象添加到标签中。如果对象不存在,则加载时将跳过该对象。 +`addTag` | 通过标签键将标签添加到标签中。内部标签中的所有元素现在都是外部标签的一部分。 +`addOptionalTag` | 通过标签的名称将标签添加到标签中。如果标签不存在,则加载时将跳过该标签。 +`replace` | 当为`true`时,从其他数据包添加到此标签的所有先前加载的条目都将被丢弃。如果在这个数据包之后加载了一个数据包,那么它仍然会将条目附加到标签中。 +`remove` | 通过对象或标签的名称或键从标签中删除对象或标签。 + +```java +// 在某个TagProvider#addTags中 +this.tag(EXAMPLE_TAG) + .add(EXAMPLE_OBJECT) // 向该标签添加一个对象 + .addOptional(new ResourceLocation("othermod", "other_object")) // 向该标签添加一个来自其他模组的对象 + +this.tag(EXAMPLE_TAG_2) + .addTag(EXAMPLE_TAG) // 向该标签添加一个标签 + .remove(EXAMPLE_OBJECT) // 从该标签中移除一个对象 +``` + +!!! 重要 + 如果模组的标签软依赖于另一个模组的标签(另一个模组可能在运行时存在,也可能不存在),则应使用可选方法引用其他模组的标签。 + +### Existing Providers + +Minecraft包含一些用于某些注册表的标签提供者,这些注册表可以被子类化。此外,一些提供者包含额外的辅助方法,以便更容易地创建标签。 + +注册表对象类型 | 标签提供者 +:---: | :--- +`Block` | `BlockTagsProvider`\* +`Item` | `ItemTagsProvider` +`EntityType` | `EntityTypeTagsProvider` +`Fluid` | `FluidTagsProvider` +`GameEvent` | `GameEventTagsProvider` +`Biome` | `BiomeTagsProvider` +`FlatLevelGeneratorPreset` | `FlatLevelGeneratorPresetTagsProvider` +`WorldPreset` | `WorldPresetTagsProvider` +`Structure` | `StructureTagsProvider` +`PoiType` | `PoiTypeTagsProvider` +`BannerPattern` | `BannerPatternTagsProvider` +`CatVariant` | `CatVariantTagsProvider` +`PaintingVariant` | `PaintingVariantTagsProvider` +`Instrument` | `InstrumentTagsProvider` +`DamageType` | `DamageTypeTagsProvider` + +\* `BlockTagsProvider`是一个由Forge添加的`TagsProvider`。 + +#### `ItemTagsProvider#copy` + +方块具有用于在物品栏中获取它们的物品表示。因此,许多方块标签也可以是物品标签。为了容易地生成与方块标签具有相同条目的物品标签,可以使用`#copy`方法,该方法接受要从中复制的方块标签和要复制到的物品标签。 + +```java +// 在ItemTagsProvider#addTags中 +this.copy(EXAMPLE_BLOCK_TAG, EXAMPLE_ITEM_TAG); +``` + +自定义标签提供者 +--------------- + +可以通过`TagsProvider`子类创建自定义标签提供者,该子类接受注册表键来为其生成标签。 + +```java +public RecipeTypeTagsProvider(PackOutput output, CompletableFuture registries, ExistingFileHelper fileHelper) { + super(output, Registries.RECIPE_TYPE, registries, MOD_ID, fileHelper); +} +``` + +### Intrinsic Holder Tags Providers + +一种特殊类型的`TagProvider`是`IntrinsicHolderTagsProvider`。当通过`#tag`使用此提供者创建标签时,可以使用对象本身通过`#add`将自己添加到标签中。为此,在构造函数中提供了一个函数,将对象转换为其`ResourceKey`。 + +```java +// `IntrinsicHolderTagsProvider`的子类型 +public AttributeTagsProvider(PackOutput output, CompletableFuture registries, ExistingFileHelper fileHelper) { + super( + output, + ForgeRegistries.Keys.ATTRIBUTES, + registries, + attribute -> ForgeRegistries.ATTRIBUTES.getResourceKey(attribute).get(), + MOD_ID, + fileHelper + ); +} +``` + +[tags]: ../../resources/server/tags.md +[datagen]: ../index.md#data-providers +[custom]: ../../concepts/registries.md#creating-custom-forge-registries diff --git a/docs/translation/zh_CN/datastorage/capabilities.md b/docs/translation/zh_CN/datastorage/capabilities.md new file mode 100644 index 000000000..fea74d04b --- /dev/null +++ b/docs/translation/zh_CN/datastorage/capabilities.md @@ -0,0 +1,175 @@ +Capability系统 +============== + +Capability允许以动态和灵活的方式公开Capability,而不必直接实现许多接口。 + +一般来说,每个Capability都以接口的形式提供了一个Capability。 + +Forge为BlockEntity、Entity、ItemStack、Level和LevelChunk添加了Capability支持,这些Capability可以通过事件附加它们,也可以通过重写你自己的对象实现中的Capability方法来公开。这将在接下来的章节中进行更详细的解释。 + +Forge提供的Capability +--------------------- + +Forge提供三种Capability:`IItemHandler`、`IFluidHandler`和`IEnergyStorage`。 + +`IItemHandler`公开了一个用于处理物品栏Slot的接口。它可以应用于BlockEntity(箱子、机器等)、Entity(额外的玩家Slot、生物/生物物品栏/袋子)或ItemStack(便携式背包等)。它用一个自动化友好的系统取代了旧的`Container`和`WorldlyContainer`。 + +`IFluidHandler`公开了一个用于处理流体物品栏的接口。它也可以应用于BlockEntitiy、Entity或ItemStack。 + +`IEnergyStorage`公开了一个用于处理能源容器的接口。它可以应用于BlockEntity、Entity或ItemStack。它基于TeamCoFH的RedstoneFlux API。 + +使用现存的Capability +------------------- + +如前所述,BlockEntity、Entity和ItemStack通过`ICapabilityProvider`接口实现了Capability提供者Capability。此接口添加了方法`#getCapability`,该方法可用于查询相关提供者对象中存在的Capability。 + +为了获得一个Capability,你需要通过它的唯一实例来引用它。在`IItemHandler`的情况下,此Capability主要存储在`ForgeCapabilities#ITEM_HANDLER`中,但也可以使用`CapabilityManager#get`获取其他实例引用。 + +```java +public static final Capability ITEM_HANDLER = CapabilityManager.get(new CapabilityToken<>(){}); +``` + +当被调用时,`CapabilityManager#get`为你的相关类型提供一个非null的Capability。匿名的`CapabilityToken`允许Forge保持软依赖系统,同时仍然拥有获得正确Capability所需的泛型信息。 + +!!! 重要 + 即使你在任何时候都可以使用非null的Capability,但这并不意味着该Capability本身是可用的或已注册的。这可以通过`Capability#isRegistered`进行检查。 + +`#getCapability`方法有另一个参数,类型为`Direction`,可用于请求那一面的特定实例。如果传递`null`,则可以假设请求来自方块内,或者来自某个侧面没有意义的地方,例如不同的维度。在这种情况下,将请求一个不关侧面的一个通用的Capability实例。`#getCapability`的返回类型将对应于传递给方法的Capability中声明的类型的`LazyOptional`。对于物品处理器Capability,其为`LazyOptional`。如果该Capability不适用于特定的提供者,它将返回一个空的`LazyOptional`。 + +公开一个Capability +------------------ + +为了公开一个Capability,你首先需要一个底层Capability类型的实例。请注意,你应该为每个保有该Capability的对象分配一个单独的实例,因为该Capability很可能与所包含的对象绑定。 + +在`IItemHandler`的情况下,默认实现使用`ItemStackHandler`类来指定多个Slot,该类在构造函数中有一个可选参数。然而,应避免依赖这些默认实现的存在,因为Capability系统的目的是防止在不存在Capability的情况下出现加载错误,因此如果Capability已注册,则应在检查测试之后对实例化进行保护(请参阅上一节中关于`CapabilityManager#get`的备注)。 + +一旦你拥有了自己的Capability接口实例,你将希望通知Capability系统的用户你公开了此Capability,并提供接口引用的`LazyOptional`。这是通过重写`#getCapability`方法来完成的,并将Capability实例与你要公开的Capability进行比较。如果你的机器根据被查询的一侧有不同的Slot,你可以使用`side`参数进行测试。对于实体和物品栈,此参数可以忽略,但仍然可以将侧面作为上下文,例如玩家上的不同护甲Slot(`Direction#UP`暴露玩家的头盔Slot),或物品栏中的周围方块(`Direction#WEST`暴露熔炉的输入Slot)。不要忘记回到`super`,否则现有的附加Capability将停止工作。 + +在提供者生命周期结束时,必须通过`LazyOptional#invalidate`使Capability失效。对于拥有的BlockEntitiy和Entity,`LazyOptional`可以在`#invalidateCaps`内失效。对于非拥有者提供者,提供失效过程的Runnable应传递到`AttachCapabilitiesEvent#addListener`中。 + +```java +// 在你BlockEntity子类中的某处 +LazyOptional inventoryHandlerLazyOptional; + +// 被提供的对象(例如:() -> inventoryHandler) +// 确保惰性,因为初始化只应在需要时发生 +inventoryHandlerLazyOptional = LazyOptional.of(inventoryHandlerSupplier); + +@Override +public LazyOptional getCapability(Capability cap, Direction side) { + if (cap == ForgeCapabilities.ITEM_HANDLER) { + return inventoryHandlerLazyOptional.cast(); + } + return super.getCapability(cap, side); +} + +@Override +public void invalidateCaps() { + super.invalidateCaps(); + inventoryHandlerLazyOptional.invalidate(); +} +``` + +!!! 提示 + 如果给定对象上只公开了一个Capability,则可以使用`Capability#orEmpty`作为if/else语句的替代语句。 + + ```java + @Override + public LazyOptional getCapability(Capability cap, Direction side) { + return ForgeCapabilities.ITEM_HANDLER.orEmpty(cap, inventoryHandlerLazyOptional); + } + ``` + +`Item`是一种特殊情况,因为它们的Capability提供者存储在`ItemStack`上。相反的是,应该通过`Item#initCapabilities`附加提供者。其应该在物品栈的生命周期中保持你的Capability。 + +强烈建议在代码中使用直接检查来测试Capability,而不是试图依赖Map或其他数据结构,因为每个游戏刻都可以由许多对象进行Capability测试,并且它们需要尽可能快,以避免减慢游戏速度。 + +Capability的附加 +---------------- + +如前所述,可以使用`AttachCapabilitiesEvent`将Capability附加到现有提供者、`Level`和`LevelChunk`。同一事件用于所有可以提供Capability的对象。`AttachCapabilitiesEvent`有5个有效的泛型类型,提供以下事件: + +* `AttachCapabilitiesEvent`: 仅为实体触发。 +* `AttachCapabilitiesEvent`: 仅为方块实体触发。 +* `AttachCapabilitiesEvent`: 仅为物品栈触发。 +* `AttachCapabilitiesEvent`: 仅为存档触发。 +* `AttachCapabilitiesEvent`: 仅为存档区块触发。 + +泛型类型不能比上述类型更具体。例如:如果要将Capability附加到`Player`,则必须订阅`AttachCapabilitiesEvent`,然后在附加Capability之前确定所提供的对象是`Player`。 + +在所有情况下,该事件都有一个方法`#addCapability`,可用于将Capability附加到目标对象。不是将Capability本身添加到列表中,而是添加Capability提供者,这些提供者有机会仅从某些面返回Capability。虽然提供者只需要实现`ICapabilityProvider`,但如果该Capability需要持久存储数据,则可以实现`ICapabilitySerializable`,该Capability除了返回Capability外,还将提供标签保存/加载Capability。 + +有关如何实现`ICapabilityProvider`的信息,请参阅[公开一个Capability][expose]部分。 + +创建你自己的Capability +--------------------- + +Capability可通过以下两种方式之一被注册:`RegisterCapabilitiesEvent`或`@AutoRegisterCapability`。 + +### RegisterCapabilitiesEvent + +通过向`#register`方法提供Capability类型的类,可以使用`RegisterCapabilitiesEvent`注册Capability。该事件在模组事件总线上[被处理][handled]。 + +```java +@SubscribeEvent +public void registerCaps(RegisterCapabilitiesEvent event) { + event.register(IExampleCapability.class); +} +``` + +### @AutoRegisterCapability + +Capability也可通过使用`@AutoRegisterCapability`注释以被注册。 + +```java +@AutoRegisterCapability +public interface IExampleCapability { + // ... +} +``` + +LevelChunk和BlockEntity的Capability的持久化 +------------------------------------------ + +与Level、Entity和ItemStack不同,LevelChunk和BlockEntity只有在标记为脏时才会写入磁盘。因此,LevelChunk或BlockEntity具有持久状态的Capability实现应确保无论何时其状态发生变化,其所有者都被标记为脏。 + +`ItemStackHandler`通常用于BlockEntity中的物品栏,它有一个可重写的方法`void onContentsChanged(int slot)`,用于将BlockEntity标记为脏。 + +```java +public class MyBlockEntity extends BlockEntity { + + private final IItemHandler inventory = new ItemStackHandler(...) { + @Override + protected void onContentsChanged(int slot) { + super.onContentsChanged(slot); + setChanged(); + } + } + + // ... +} +``` + +向客户端同步数据 +--------------- + +默认情况下,Capability数据不会发送到客户端。为了改变这一点,模组必须使用数据包管理自己的同步代码。 + +在三种不同的情况下,你可能希望发送同步数据包,所有这些情况都是可选的: + +1. 当实体在存档中生成或放置方块时,你可能希望与客户端共享初始化指定的值。 +2. 当存储的数据发生更改时,你可能需要通知部分或全部正在监视的客户端。 +3. 当新客户端开始查看实体或方块时,你可能希望将现有数据通知它。 + +有关实现网络数据包的更多信息,请参阅[网络][network]页面。 + +在玩家死亡时的持久化 +------------------- + +默认情况下,Capability数据不会在死亡时持续存在。为了改变这一点,在重生过程中克隆玩家实体时,必须手动复制数据。 + +这可以通过`PlayerEvent$Clone`完成,方法是从原始实体读取数据并将其分配给新实体。在这种情况下,`#isWasDeath`方法可以用于区分死后重生和从末地返回。这一点很重要,因为从末地返回时数据已经存在,因此在这种情况下必须注意不要重复值。 + +[expose]: #exposing-a-capability +[handled]: ../concepts/events.md#creating-an-event-handler +[network]: ../networking/index.md diff --git a/docs/translation/zh_CN/datastorage/codecs.md b/docs/translation/zh_CN/datastorage/codecs.md new file mode 100644 index 000000000..d630374f2 --- /dev/null +++ b/docs/translation/zh_CN/datastorage/codecs.md @@ -0,0 +1,438 @@ +# 编解码器(Codecs) + +编解码器(Codecs)是源于Mojang的[DataFixerUpper]的一个序列化工具,用于描述对象如何在不同格式之间转换,例如JSON的`JsonElement`和NBT的`Tag`。 + +## 编解码器的使用 + +编解码器主要用于将Java对象编码或序列化为某种数据格式类型,并将格式化的数据对象解码或反序列化为其关联的Java类型。这通常分别使用`Codec#encodeStart`和`Codec#parse`来完成。 + +### DynamicOps + +为了确定要编码和解码的中间文件格式,`#encodeStart`和`#parse`都需要一个`DynamicOps`实例来定义该格式中的数据。 + +[DataFixerUpper]库包含`JsonOps`,用于对存储在[`Gson`的][gson]`JsonElement`实例中的JSON数据进行编码。`JsonOps`支持两个版本的`JsonElement`序列化:定义标准JSON文件的`JsonOps#INSTANCE`和允许将数据压缩为单个字符串的`JsonOps#COMPRESSED`。 + +```java +// 让exampleCodec代表一个Codec +// 让exampleObject是一个ExampleJavaObject +// 让exampleJson是一个JsonElement + +// 将Java对象编码为常规的JsonElement +exampleCodec.encodeStart(JsonOps.INSTANCE, exampleObject); + +// 将Java对象编码为压缩的JsonElement +exampleCodec.encodeStart(JsonOps.COMPRESSED, exampleObject); + +// 将JsonElement解码为Java对象 +// 假设JsonElement被普通地转换 +exampleCodec.parse(JsonOps.INSTANCE, exampleJson); +``` + +Minecraft还提供了`NbtOps`来对存储在`Tag`实例中的NBT数据进行编解码。其可以使用`NbtOps#INSTANCE`被引用。 + +```java +// 让exampleCodec代表一个Codec +// 让exampleObject是一个ExampleJavaObject +// 让exampleNbt是一个Tag + +// 将Java对象编码为Tag +exampleCodec.encodeStart(JsonOps.INSTANCE, exampleObject); + +// 将Tag解码为Java对象 +exampleCodec.parse(JsonOps.INSTANCE, exampleNbt); +``` + +#### 格式的转换 + +`DynamicOps`还可以单独用于在两种不同的编码格式之间进行转换。这可以使用`#convertTo`并提供`DynamicOps`格式和要转换的编码对象来完成。 + +```java +// 将Tag转换为JsonElement +// 让exampleTag是一个Tag +JsonElement convertedJson = NbtOps.INSTANCE.convertTo(JsonOps.INSTANCE, exampleTag); +``` + +### DataResult + +使用编解码器编码或解码的数据返回一个`DataResult`,它保存转换后的实例或一些错误数据,具体取决于转换是否成功。转换成功后,`#result`提供的`Optional`将包含成功转换的对象。如果转换失败,`#error`提供的`Optional`将包含`PartialResult`,其中包含错误消息和部分转换的对象,具体取决于编解码器。 + +此外,`DataResult`上有许多方法可用于将结果或错误转换为所需格式。例如,`#resultOrPartial`将返回一个`Optional`,其中包含成功时的结果,以及失败时部分转换的对象。该方法接收字符串Consumer,以确定如何报告错误消息(如果存在)。 + +```java +// 让exampleCodec代表一个Codec +// 让exampleJson是一个JsonElement + +// 将JsonElement解码为Java对象 +DataResult result = exampleCodec.parse(JsonOps.INSTANCE, exampleJson); + +result + // 获取结果或部分结果(当错误时),并报告错误消息 + .resultOrPartial(errorMessage -> /* 处理错误消息 */) + // 如果结果或部分结果存在,做一些事情 + .ifPresent(decodedObject -> /* 处理解码后的对象 */); +``` + +## 现存的编解码器 + +### 原始类型 + +`Codec`类包含某些已定义的原始类型的编解码器的静态实例。 + +Codec | Java类型 +:---: | :--- +`BOOL` | `Boolean` +`BYTE` | `Byte` +`SHORT` | `Short` +`INT` | `Integer` +`LONG` | `Long` +`FLOAT` | `Float` +`DOUBLE` | `Double` +`STRING` | `String` +`BYTE_BUFFER` | `ByteBuffer` +`INT_STREAM` | `IntStream` +`LONG_STREAM` | `LongStream` +`PASSTHROUGH` | `Dynamic`\* +`EMPTY` | `Unit`\*\* + +\* `Dynamic`是一个对象,它包含以支持的`DynamicOps`格式编码的值。这些通常用于将编码对象格式转换为其他编码对象格式。 + +\*\* `Unit`是一个用于表示`null`对象的对象。 + +### 原版和Forge + +Minecraft和Forge为经常编码和解码的对象定义了许多编解码器。一些示例包括`ResourceLocation`的`ResourceLocation#CODEC`,`DateTimeFormatter#ISO_INSTANT`格式的`Instant`的`ExtraCodecs#INSTANT_ISO8601`,以及`CompoundTag`的`CompoundTag#CODEC`。 + +!!! 警告 + `CompoundTag`无法使用`JsonOps`解码JSON中的数字列表。转换时,`JsonOps`将数字设置为其最窄的类型。`ListTag`强制为其数据指定一个特定类型,因此具有不同类型的数字(例如,`64`将是`byte`,`384`为`short`)将在转换时引发错误。 + +原版和Forge注册表也具有注册表所包含对象类型的编解码器(例如`Registry#BLOCK`或`ForgeRegistries#BLOCKS`具有`Codec`)。`Registry#byNameCodec`和`IForgeRegistry#getCodec`将把注册表对象编码为其注册表名称,或者如果被压缩,则编码为整数标识符。原版注册表还有一个`Registry#holderByNameCodec`,它编码为注册表名称,并解码为`Holder`中包装的注册表对象。 + +## 创建编解码器 + +可以创建用于对任何对象进行编码和解码的编解码器。为了便于理解,将展示等效的编码JSON。 + +### 记录 + +编解码器可以通过使用记录来定义对象。每个记录编解码器都定义了具有显式命名字段的任何对象。创建记录编解码器的方法有很多,但最简单的是通过`RecordCodecBuilder#create`。 + +`RecordCodecBuilder#create` takes in a function which defines an `Instance` and returns an application (`App`) of the object. A correlation can be drawn to creating a class *instance* and the constructors used to *apply* the class to the constructed object. +`RecordCodecBuilder#create`接受一个定义`Instance`的函数,并返回对象的应用(`App`)。一个为创建类*实例*和用于将该类*应用*于所构造对象的构造函数的关联可被绘制。 + +```java +// 要为其创建编解码器的某个对象 +public class SomeObject { + + public SomeObject(String s, int i, boolean b) { /* ... */ } + + public String s() { /* ... */ } + + public int i() { /* ... */ } + + public boolean b() { /* ... */ } +} +``` + +#### 字段 + +一个`Instance`可以使用`#group`定义多达16个字段。每个字段都必须是一个应用,定义为其创建对象的实例和对象的类型。满足这一要求的最简单方法是使用`Codec`,设置要解码的字段的名称,并设置用于编码字段的getter。 + +如果字段是必需的,则可以使用`#fieldOf`从`Codec`创建字段;如果字段被包装在`Optional`或默认值中,则使用`#optionalFieldOf`创建字段。任一方法都需要一个字符串,该字符串包含编码对象中字段的名称。然后,可以使用`#forGetter`设置用于对字段进行编码的getter,接受一个给定对象并返回字段数据的函数。 + +从那里,可以通过`#apply`应用生成的产品,以定义实例应如何构造应用的对象。为了方便起见,分组字段应该按照它们在构造函数中出现的顺序列出,这样函数就可以简单地作为构造函数方法引用。 + +```java +public static final Codec RECORD_CODEC = RecordCodecBuilder.create(instance -> // 给定一个实例 + instance.group( // 定义该实例内的字段 + Codec.STRING.fieldOf("s").forGetter(SomeObject::s), // 字符串 + Codec.INT.optionalFieldOf("i", 0).forGetter(SomeObject::i), // 整数,当字段不存在时默认为0 + Codec.BOOL.fieldOf("b").forGetter(SomeObject::b) // 布尔值 + ).apply(instance, SomeObject::new) // 定义如何创建该对象 +); +``` + +```js +// 已编码的SomeObject +{ + "s": "value", + "i": 5, + "b": false +} + +// 另一个已编码的SomeObject +{ + "s": "value2", + // i被忽略,默认为0 + "b": true +} +``` + +### 转换器 + +编解码器可以通过映射方法转换为等效或部分等效的表示。每个映射方法都有两个函数:一个将当前类型转换为新类型,另一个将新类型转换回当前类型。这是通过`#xmap`函数完成的。 + +```java +// A类 +public class ClassA { + + public ClassB toB() { /* ... */ } +} + +// 另一个等效的类 +public class ClassB { + + public ClassA toA() { /* ... */ } +} + +// 假设有一个编解码器A_CODEC +public static final Codec B_CODEC = A_CODEC.xmap(ClassA::toB, ClassB::toA); +``` + +如果一个类型是部分等效的,这意味着在转换过程中存在一些限制,则存在返回`DataResult`的映射函数,每当达到异常或无效状态时,该函数可用于返回错误状态。 + +A是否完全等效于B | B是否完全等效于A | 转换方法 +:---: | :---: | :--- +是 | 是 | `#xmap` +是 | 否 | `#flatComapMap` +否 | 是 | `#comapFlatMap` +否 | 否 | `#flatXMap` + +```java +// 给定一个字符串编码器用于转换为一个整数 +// 并非所有字符串都能成为整数(A不完全等效于B) +// 所有整数都能成为字符串(B完全等效于A) +public static final Codec INT_CODEC = Codec.STRING.comapFlatMap( + s -> { // 返回含有错误或失败的数据结果 + try { + return DataResult.success(Integer.valueOf(s)); + } catch (NumberFormatException e) { + return DataResult.error(s + " is not an integer."); + } + }, + Integer::toString // 常规函数 +); +``` + +```js +// 将会返回5 +"5" + +// 将会产生错误,不是一个整数 +"value" +``` + +#### 范围编解码器 + +范围编解码器是`#flatXMap`的实现,如果值不包含在设置的最小值和最大值之间,则返回错误`DataResult`。如果超出界限,该值仍将作为部分结果提供。分别通过`#intRange`、`#floatRange`和`#doubleRange`实现了整数(int)、浮点数(float)和双精度小数(double)。 + +```java +public static final Codec RANGE_CODEC = Codec.intRange(0, 4); +``` + +```js +// 将会合法,在[0, 4]范围内 +4 + +// 将会产生错误,在[0, 4]范围外 +5 +``` + +### 默认值 + +如果编码或解码的结果失败,则可以通过`Codec#orElse`或`Codec#orElseGet`提供默认值。 + +```java +public static final Codec DEFAULT_CODEC = Codec.INT.orElse(0); // Can also be a supplied value via #orElseGet +``` + +```js +// 不是一个整数,默认为0 +"value" +``` + +### Unit + +提供代码内的值并编码为空的编解码器可以使用`Codec#unit`来表示。如果编解码器在数据对象中使用了不可编码的条目,这将非常有用。 + +```java +public static final Codec> UNIT_CODEC = Codec.unit( + () -> ForgeRegistries.BLOCKS // 也可以是一个原始值 +); +``` + +```js +// 此处无内容,将会返回方块注册表编解码器 +``` + +### List + +对象列表的编解码器可以通过`Codec#listOf`从对象编解码器生成。 + +```java +// BlockPos#CODEC是一个Codec +public static final Codec> LIST_CODEC = BlockPos.CODEC.listOf(); +``` + +```js +// 已编码的List +[ + [1, 2, 3], // BlockPos(1, 2, 3) + [4, 5, 6], // BlockPos(4, 5, 6) + [7, 8, 9] // BlockPos(7, 8, 9) +] +``` + +使用列表编解码器解码的列表对象存储在**不可变**列表中。如果需要可变列表,则应将[转换器][transformer]应用于列表编解码器。 + +### Map + +键和值对象映射(Map)的编解码器可以通过`Codec#unboundedMap`从两个编解码器生成。无边界映射可以指定任何基于字符串或经过字符串转换的值作为键。 + +```java +// BlockPos#CODEC是一个Codec +public static final Codec> MAP_CODEC = Codec.unboundedMap(Codec.STRING, BlockPos.CODEC); +``` + +```js +// 已编码的Map +{ + "key1": [1, 2, 3], // key1 -> BlockPos(1, 2, 3) + "key2": [4, 5, 6], // key2 -> BlockPos(4, 5, 6) + "key3": [7, 8, 9] // key3 -> BlockPos(7, 8, 9) +} +``` + +使用无界映射编解码器解码的映射对象存储在**不可变**映射中。如果需要一个可变映射,则应该将[转换器][transformer]应用于映射编解码器。 + +!!! 警告 + 无界映射仅支持对字符串进行编码/解码的键。键值[对][pair]列表编解码器可以用来绕过这个限制。 + +### Pair + +对象对的编解码器可以通过`Codec#pair`从两个编解码器生成。 + +成对编解码器通过首先解码成对中的左对象,然后取编码对象的剩余部分并从中解码右对象来解码对象。因此,编解码器必须在解码后表达关于编码对象的某些内容(例如[记录][records]),或者必须将它们扩充为`MapCodec`,并通过`#codec`转换为常规编解码器。这通常可以通过使编解码器成为某个对象的[字段][field]来实现。 + +```java +public static final Codec> PAIR_CODEC = Codec.pair( + Codec.INT.fieldOf("left").codec(), + Codec.STRING.fieldOf("right").codec() +); +``` + +```js +// 已编码的Pair +{ + "left": 5, // fieldOf查询'left'键以获取左对象 + "right": "value" // fieldOf查询'right'键以获取右对象 +} +``` + +!!! 提示 + 可以使用[转换器][transformer]应用的键值对列表对具有非字符串键的映射编解码器进行编码/解码。 + +### Either + +用于编码/解码某些对象数据的两种不同方法的编解码器可以通过`Codec#either`从两个编解码器生成。 + +Either编解码器尝试使用第一编解码器对对象进行解码。如果失败,它将尝试使用第二个编解码器进行解码。如果也失败了,那么`DataResult`将只包含第二个编解码器失败的错误。 + +```java +public static final Codec> EITHER_CODEC = Codec.either( + Codec.INT, + Codec.STRING +); +``` + +```js +// 已编码的Either$Left +5 + +// 已编码的Either$Right +"value" +``` + +!!! 提示 + 这可以与[转换器][transformer]结合使用,从两种不同的编码方法中获取特定对象。 + +### Dispatch + +编解码器可以具有子解码器,子解码器可以通过`Codec#dispatch`基于某个指定类型对特定对象进行解码。这通常用于包含编解码器的注册表中,例如规则测试或方块放置器。 + +Dispatch编解码器首先尝试从某个字符串关键字(通常为`type`)中获取编码类型。从那里,类型被解码,为用于解码实际对象的特定编解码器调用getter。如果用于解码对象的`DynamicOps`压缩了其映射,或者对象编解码器本身没有扩充为`MapCodec`(例如记录或已部署的基本类型),则需要将对象存储在`value`键中。否则,对象将在与其余数据相同的级别上进行解码。 + +```java +// 定义我们的对象 +public abstract class ExampleObject { + + // 定义用于指定要编码的对象类型的方法 + public abstract Codec type(); +} + +// 创建存储字符串的简单对象 +public class StringObject extends ExampleObject { + + public StringObject(String s) { /* ... */ } + + public String s() { /* ... */ } + + public Codec type() { + // 一个已注册的注册表对象 + // "string": + // Codec.STRING.xmap(StringObject::new, StringObject::s) + return STRING_OBJECT_CODEC.get(); + } +} + +// 创建存储字符串和整数的复杂对象 +public class ComplexObject extends ExampleObject { + + public ComplexObject(String s, int i) { /* ... */ } + + public String s() { /* ... */ } + + public int i() { /* ... */ } + + public Codec type() { + // 一个已注册的注册表对象 + // "complex": + // RecordCodecBuilder.create(instance -> + // instance.group( + // Codec.STRING.fieldOf("s").forGetter(ComplexObject::s), + // Codec.INT.fieldOf("i").forGetter(ComplexObject::i) + // ).apply(instance, ComplexObject::new) + // ) + return COMPLEX_OBJECT_CODEC.get(); + } +} + +// 假设有一个IForgeRegistry> DISPATCH +public static final Codec = DISPATCH.getCodec() // 获取Codec> + .dispatch( + ExampleObject::type, // 从特定对象获取编解码器 + Function.identity() // 从注册表获取编解码器 + ); +``` + +```js +// 简单对象 +{ + "type": "string", // 对于StringObject + "value": "value" // MapCodec不需要编解码器类型参数,需要字段 +} + +// 复杂对象 +{ + "type": "complex", // 对于ComplexObject + + // MapCodec不需要编解码器类型参数,可被内联 + "s": "value", + "i": 0 +} +``` + +[DataFixerUpper]: https://github.com/Mojang/DataFixerUpper +[gson]: https://github.com/google/gson +[transformer]: #transformer-codecs +[pair]: #pair +[records]: #records +[field]: #fields diff --git a/docs/translation/zh_CN/datastorage/saveddata.md b/docs/translation/zh_CN/datastorage/saveddata.md new file mode 100644 index 000000000..d4e56d64f --- /dev/null +++ b/docs/translation/zh_CN/datastorage/saveddata.md @@ -0,0 +1,42 @@ +Saved Data +========== + +Saved Data(SD)系统是存档Capability功能的替代方案,可以按存档附加数据。 + +声明 +---- + +Each SD implementation must subtype the `SavedData` class. There are two important methods to be aware of: +每个SD实现都必须继承`SavedData`类。有两种重要方法需要注意: + +* `save`:允许实现将NBT数据写入该存档。 +* `setDirty`:在更改数据后必须调用的方法,以通知游戏有需要写入的更改。如果未调用,将不会调用`#save`,并且现有数据将持久存在。 + +附加到存档 +--------- + +任何`SavedData`都是动态加载和/或附加到一个存档的。因此,如果一个`SavedData`从来没有在一个存档上创建过,那么它就不存在了。 + +`SavedData`是从`DimensionDataStorage`创建和加载的,借助`ServerChunkCache#getDataStorage`或`ServerLevel#getDataStorage`都可以访问该存储。从那里,您可以通过调用`DimensionDataStorage#computeIfAbsent`来获取或创建SD的实例。这将尝试获取SD的当前实例(如果存在),或者创建一个新实例并加载所有可用数据。 + +`DimensionDataStorage#computeIfAbsent`接受三个参数:一个将NBT数据加载到SD并返回它的函数,一个构造SD新实例的Supplier,以及存储在所实现的存档的`data`文件夹中的`.dat`文件的名称。 + +例如,如果一个SD在下界中被命名为"example",那么一个文件将在`.//DIM-1/data/example.dat`创建并且将这样实现: + +```java +// 在某个类中 +public ExampleSavedData create() { + return new ExampleSavedData(); +} + +public ExampleSavedData load(CompoundTag tag) { + ExampleSavedData data = this.create(); + // 加载saved data + return data; +} + +// 在该类的某个方法中 +netherDataStorage.computeIfAbsent(this::load, this::create, "example"); +``` + +要在多个存档之间保持SD,应将SD连接到主世界,其可以从`MinecraftServer#overworld`获得。主世界是唯一一个从未完全卸载的维度,因此非常适合在其上存储多存档数据。 diff --git a/docs/translation/zh_CN/forgedev/index.md b/docs/translation/zh_CN/forgedev/index.md new file mode 100644 index 000000000..f1f605cb6 --- /dev/null +++ b/docs/translation/zh_CN/forgedev/index.md @@ -0,0 +1,109 @@ +入门 +==== + +如果你已经决定为Forge做出贡献,你将不得不采取一些特殊的步骤来开始开发。一个简单的模组开发环境不足以直接使用Forge的代码库。相反,你可以使用以下指南来帮助你进行设置,并开始改进Forge! + +fork或克隆(clone)仓库 +---------------------- + +像你会发现的大多数主要开源项目一样,Forge托管在[GitHub][github]上。如果你以前为另一个项目做过贡献,你就会知道这个过程,可以直接跳到下一节。 + +对于那些通过Git进行协作的初学者来说,以下是两个简单的步骤。 + +!!! 注意 + 本指南假设你已经设置了GitHub帐户。如果没有,请访问GitHub的[注册页面][register]创建帐户。此外,本指南不是关于git使用的教程。如果你正在努力上手git,请先查询其他资料。 + +### Forking + +首先,你必须通过单击右上角的“fork”按钮来“fork”[MinecraftForge仓库][forgerepo]。如果你在一个组织中,请选择要托管该fork的帐户。 + +fork仓库是必要的,因为不是每个GitHub用户都可以自由访问每个仓库。相反,你可以创建原始仓库的副本,以便稍后通过所谓的Pull Request贡献你的更改,稍后你将了解更多信息。 + +### Cloning + +在fork仓库之后,是时候获得本地访问权限来实际进行一些更改了。为此,你需要将存储库克隆到本地计算机上。 + +使用你最喜欢的git客户端,只需将你的fork克隆到你选择的目录中。作为一般示例,这里有一个命令行片段,它应该适用于所有正确配置的系统,并将仓库克隆到当前目录下名为“MinecraftForge”的目录中(请注意,你必须将``替换为你的用户名): + +```git clone https://github.com//MinecraftForge``` + +# 检出到正确的分支 + +fork和克隆存储库是Forge开发的唯一强制性步骤。但是,为了简化为你创建Pull Request的过程,最好使用分支。 + +建议为你计划提交的每个PR创建并检出一个分支。这样,你就可以在工作于旧补丁的同时,随时了解Forge对新PR的最新更改。 + +完成此步骤后,你就可以开始设置开发环境了。 + +设置开发环境 +----------- + +根据你喜欢的IDE,你必须遵循一组不同的推荐步骤才能成功设置开发环境。 + +### Eclipse + +由于Eclipse工作区的工作方式,ForgeGradle可以完成大部分相关工作,让你开始使用Forge工作区。 + +1. 打开终端/命令提示符,然后导航到克隆的fork的目录。 +2. 输入`./gradlew setup`并按回车。等到ForgeGradle完成。 +3. 输入`./gradlew genEclipseRuns`并按回车。再次等到ForgeGradle完成。 +4. 打开你的Eclipse工作区并转到`File -> Import -> General -> Existing Gradle Project`. +5. 在打开的对话框中,在“项目根目录”("Project root directory")选项中浏览到仓库目录。 +6. 点击“完成”按钮完成导入。 + +这就是让你启动并运行Eclipse所需的全部内容。运行测试模组不需要额外的步骤。只需像在任何其他项目中一样点击“运行”("Run"),然后选择适当的运行配置。 + +### IntelliJ IDEA + +JetBrains的旗舰IDE提供了对[Gradle][gradle]的强大集成支持:Forge的首选构建系统。然而,由于Minecraft模组开发的一些特点,需要额外的步骤才能使一切正常工作。 + +#### IDEA 2021及以后版本 +1. IntelliJ IDEA 2021,启动! + - 如果你已经打开了另一个项目,请使用 文件 -> 关闭项目 选项关闭该项目。 +2. 在“欢迎使用IntelliJ IDEA”窗口的项目选项卡中,单击右上角的“打开”按钮,然后选择你之前克隆的MinecraftForge文件夹。 +3. 如有提示,点击“信任项目”。 +4. IDEA导入项目并索引其文件后,运行Gradle设置任务。你可以通过以下方式执行此操作: + - 打开屏幕右侧的Gradle侧边栏,然后打开forge项目树,选择任务(Tasks),然后选择其他(other),然后双击 forge -> 任务 -> 其他 -> “设置”(`setup`) 中的`setup`任务(也可能显示为`MinecraftForge[Setup]`)。 +5. 生成运行配置 + - 打开屏幕右侧的Gradle侧边栏,然后打开forge项目树,选择任务,然后选择其他,双击 forge -> 任务 -> forgegradle runs -> `genIntellijRuns` 中的`genIntellijRuns`任务(也可能显示为`MinecraftForge[genIntellijRuns]`)。 +- 如果在进行任何更改之前在构建过程中遇到许可错误,运行`updateLicenses`任务可能会有所帮助。这个任务也可以在 Forge -> Tasks -> other 中找到。 + +#### IDEA 2019-2020 +IDEA 2021和这些版本的设置之间有一些细微的差异。 + +1. 导入Forge的`build.gradle`作为IDEA项目。为此,只需从`Welcome to IntelliJ IDEA`启动屏幕中单击`Import Project`,然后选择`build.gradle`文件。 +1. IDEA导入项目并索引文件后,运行Gradle设置任务。两者之一: + 1. 打开屏幕右侧的Gradle侧边栏,然后打开`forge`项目树,选择`Tasks`,然后选择`other`,双击`setup`任务(也可能显示为`MinecraftForge[Setup]`)。或者: + 1. 按CTRL键两次,然后在弹出的`Run`命令窗口中键入`gradle setup`。 + +然后,你可以使用`forge_client`Gradle任务(`Tasks -> fg_runs -> forge_client`)运行Forge:右键单击任务并根据需要选择`Run`或`Debug`。 + +你现在应该能够使用你对Forge和原版代码库所做的更改来使用你的模组。 + +做出更改并提交Pull Request +------------------------- + +一旦你设置了你的开发环境,是时候对Forge的代码库进行一些更改了。然而,在编辑项目代码时,你必须避免一些陷阱。 + +最重要的是,如果你想编辑Minecraft的源代码,你必须只在“Forge”子项目中这样做。“Clean”项目中的任何更改都会干扰ForgeGradle和生成补丁。这可能会带来灾难性的后果,并可能使你的环境完全无用。如果你希望拥有完美的体验,请确保你只在“Forge”项目中编辑代码! + +### 生成补丁 + +在你对代码库进行了更改并对其进行了彻底测试之后,你可以继续生成补丁。只有当你在Minecraft代码库中工作时(即在“Forge”项目中),这才是必要的,但这一步骤对于你的更改在其他地方工作至关重要。Forge的工作原理是只将更改后的内容注入原版Minecraft,因此需要以适当的格式提供这些更改。值得庆幸的是,ForgeGradle能够生成变更集供你提交。 + +要启动补丁生成,只需从IDE或命令行运行`genPatches`Gradle任务。完成后,你可以提交所有更改(确保没有添加任何不必要的文件)并提交Pull Request! + +### Pull Requests + +将你的贡献添加到Forge之前的最后一步是Pull Request(简称PR)。这是一个将fork的更改合并到活动代码库中的正式请求。创建PR很容易。只需转到[这个GitHub页面][submitpr]并按照建议的步骤进行操作。现在,对于分支的良好设置是有回报的,因为你可以准确地选择要提交的更改。 + +!!! 注意 + Pull Request受规则约束;并不是每一个请求都会被盲目接受。关注[本文档][contribute]以获取更多信息并确保你的PR达到最佳质量!如果你想最大限度地提高你的PR被接受的机会,请遵循这些[PR准则][guidelines]! + +[github]: https://www.github.com +[register]: https://www.github.com/join +[forgerepo]: https://www.github.com/MinecraftForge/MinecraftForge +[gradle]: https://www.gradle.org +[submitpr]: https://github.com/MinecraftForge/MinecraftForge/compare +[contribute]: https://github.com/MinecraftForge/MinecraftForge/blob/1.13.x/CONTRIBUTING.md +[guidelines]: ./prguidelines.md diff --git a/docs/translation/zh_CN/forgedev/prguidelines.md b/docs/translation/zh_CN/forgedev/prguidelines.md new file mode 100644 index 000000000..5e4904264 --- /dev/null +++ b/docs/translation/zh_CN/forgedev/prguidelines.md @@ -0,0 +1,97 @@ +Pull Request准则 +================ + +模组是在Forge之上构建的,但有些事情Forge不支持,这限制了模组的功能。 +当模组开发者遇到类似的情况时,他们可以对Forge进行更改以支持它,并将该更改作为Pull Request提交到Github上。 + +为了充分利用你和Forge团队的时间,建议在准备Pull Request时遵循一些粗略的指导原则。在编写一个好的Pull Request时,以下几点是需要记住的最重要的方面。 + +到底什么是Forge? +---------------- + +在较高级别上,Forge是Minecraft之上的一个模组兼容性层。 +早期的模组直接编辑了Minecraft的代码(就像现在的coremod一样),但当他们编辑相同的东西时,他们会遇到冲突。当一个模组以其他模组无法预料的方式改变行为时(就像现在的coremod一样),他们也遇到了问题,导致了神秘的问题和很多头痛。 + +通过使用Forge之类的东西,模组可以集中常见的更改并避免冲突。 +Forge还包括通用模组功能的支持结构,如Capability、注册表和其他允许模组更好地协同工作的功能。 + +在编写一个好的Forge Pull Request时,你还必须知道Forge在较低级别上是什么。 +Forge中有两种主要类型的代码:Minecraft补丁和Forge代码。 + +补丁 +---- + +补丁是作为对Minecraft源代码的直接更改应用的,且致力于尽可能最小化。 +每次Minecraft代码更改时,都需要仔细查看所有Forge补丁,并将其正确应用于新代码。 +这意味着,改变很多事情的大型补丁很难维护,因此Forge的目标是避免这些补丁,并使补丁尽可能小。 +除了确保代码有意义之外,对补丁的审查将侧重于最小化大小。 + +制作小补丁有很多策略,评论通常会指出更好的方法。 +Forge补丁程序通常插入一行触发事件或代码挂钩,如果事件满足某些条件,就会影响之后的代码。 +这允许大多数代码存在于补丁之外,从而使补丁保持小而简单。 + +有关创建补丁的更多详细信息,[请参阅GitHub wiki][patches]。 + +Forge代码 +--------- + +除了补丁之外,Forge代码只是普通的Java代码。它可以是事件代码、兼容性功能,也可以是任何不直接编辑Minecraft代码的东西。 +当Minecraft更新时,Forge代码必须像其他一切一样更新。然而,它要容易得多,因为它没有直接纠缠在Minecraft代码中。 + +因为这个代码是独立的,所以没有像补丁那样的大小限制。 + +除了确保代码有意义之外,评审(review)还将侧重于使代码干净:使用正确的格式和Java文档。 + +解释你自己 +--------- + +所有Pull Request都需要回答一个问题:为什么这是必要的? +添加到Forge中的任何代码都需要维护,而更多的代码意味着更可能出现错误,因此添加代码需要有充分的理由。 + +一个常见的Pull Request问题是没有提供解释,或者给出了理论上如何使用Pull Request的神秘例子。 +这只会延迟Pull Request过程。 +对一般情况有一个明确的解释是好的,但也给出了一个具体的例子,说明你的模组如何需要这个Pull Request。 + +有时有更好的方法来做你想做的事情,或者一种完全不需要Pull Request的方法。在完全排除这些可能性之前,代码更改不能被接受。 + +证明它有效 +--------- + +你提交给Forge的代码应该能完美地工作,这取决于你是否能说服评审人员。 + +最好的方法之一是在Forge中添加一个示例模组或JUnit测试,利用你的新代码并展示它的工作原理。 + +要使用示例模组设置和运行Forge环境,请参阅[这个指南][forgeenv]。 + +Forge中的突破性变化 +------------------ + +Forge不能做出破坏依赖它的模组的更改。 +这意味着Pull Request必须确保它们不会破坏与以前Forge版本的二进制兼容性。 +破坏二进制兼容性的更改称为“突破性变化”。 + +关于这个有一些例外: + +* Forge在新的Minecraft版本开始时接受突破性变化,因为Minecraft本身已经为模组开发者造成了突破性变化。 +* 有时需要在该时间窗口之外进行紧急更改,但这种情况很少见,可能会给改装后的Minecraft社区中的每个人带来依赖性头痛。 + +在这些特殊时间之外,不接受具有突破性变化的Pull Request。它们必须适应以支持旧的行为,或者等待下一个Minecraft版本。 + +有耐心、有礼貌、有同情心 +---------------------- + +在提交Pull Request时,你通常必须通过代码审查并进行多次更改,才能获得最佳的Pull Request。 +请记住,代码审查并不是针对你的判断。代码中的错误不是针对个人的。没有人是完美的,这就是我们合作的原因。 + +消极也无济于事。威胁放弃你的Pull Request,转而编写一个coremod,只会让人们感到不安,并使修改后的生态系统变得更糟。 +重要的是,在一起工作时,你要考虑到审查你的Pull Request的人的最佳意图,不要把事情看得太个人化。 + +审查(Review) +-------------- + +如果你尽最大努力理解Pull Request流程的缓慢和完美主义本质,我们也会尽最大努力了解你的观点。 + +在你的Pull Request经过审查并尽所有人所能进行清理后,它将被Lex标记为最终审查,Lex对项目中包含的内容拥有最终发言权。 + +[patches]: https://github.com/MinecraftForge/MinecraftForge/wiki/If-you-want-to-contribute-to-Forge#conventions-for-coding-patches-for-a-minecraft-class-javapatch +[forgeenv]: ./index.md diff --git a/docs/translation/zh_CN/gameeffects/particles.md b/docs/translation/zh_CN/gameeffects/particles.md new file mode 100644 index 000000000..5fc5c86aa --- /dev/null +++ b/docs/translation/zh_CN/gameeffects/particles.md @@ -0,0 +1,125 @@ +粒子效果 +======= + +粒子是游戏中的一种效果,用于打磨游戏,以更好地提高沉浸感。由于它们的创建和引用方法,其有用性也需要非常谨慎地对待。 + +创建一个粒子 +----------- + +粒子被分解为仅用于显示粒子的[**仅客户端**][sides]实现和用于引用来自服务端的粒子或同步数据的通用实现。 + +| 类 | 物理端 | 描述 | +| :--- | :---: | :--- | +| ParticleType | BOTH | 粒子类型定义的注册表对象,用于引用任一端位的粒子 | +| ParticleOptions | BOTH | 用于将来自网络或命令的信息同步到相关客户端的数据保持器 | +| ParticleProvider | CLIENT | 由`ParticleType`注册的工厂,用于从关联的`ParticleOptions`构造`Particle`。 | +| Particle | CLIENT | 要在关联客户端上显示的可渲染逻辑 | + +### ParticleType + +`ParticleType`是定义特定粒子类型的注册表对象,并提供对两端位特定粒子的可用引用。因此,每个`ParticleType`都必须[注册][registration]。 + +每个`ParticleType`都有两个参数:一个`overrideLimiter`,用于确定粒子是否在不考虑距离的情况下渲染,以及一个`ParticleOptions$Deserializer`,用于读取客户端上发送的`ParticleOptions`。由于基类`ParticleType`是抽象类,因此需要实现一个方法:`#codec`。其表示如何对与该类型相关的`ParticleOptions`进行编码和解码。 + +!!! 注意 + `ParticleType#codec`仅在用于原版实现的生物群系编解码器中使用。 + +在大多数情况下,不需要将任何粒子数据发送到客户端。对于这些例子,更容易创建`SimpleParticleType`的新实例:一个对`ParticleType`和`ParticleOptions`的实现,除了类型之外,它不向客户端发送任何自定义数据。除了红石粉之外,对于着色和依赖方块/物品的粒子而言,大多数原版实现还使用`SimpleParticleType`。 + +!!! 重要 + 如果仅在客户端上引用,则生成粒子时`ParticleType`非必要。但是,有必要使用`ParticleEngine`中的任何预构建逻辑,或者从服务端生成粒子。 + +### ParticleOptions + +`ParticleOptions`表示每个粒子所接收的数据。它还用于发送通过服务端生成的粒子的数据。所有粒子生成方法都接受一个`ParticleOptions`,这样它就知道粒子的类型以及与生成方法关联的数据。 + +`ParticleOptions`被拆分为三种方法: + +| 方法 | 描述 | +| :--- | :--- | +| getType | 获取粒子的类型定义,或`ParticleType` +| writeToNetwork | 将粒子数据写入服务端上的缓冲区以发送到客户端 +| writeToString | 将粒子数据写入字符串 + +这些对象要么是根据需要动态构建的,要么是作为`SimpleParticleType`的结果而产生的单体。 + +#### ParticleOptions$Deserializer + +要在客户端上接收`ParticleOptions`,或引用命令中的数据,必须通过`ParticleOptions$Deserializer`对粒子数据进行反序列化。`ParticleOptions$Deserializer`中的每个方法都对等`ParticleOptions`的编码方法: + +| 方法 | ParticleOptions编码器 | 描述 | +| :--- | :---: | :--- | +| fromCommand | writeToString | 从字符串(通常是从命令)中解码粒子数据。 | +| fromNetwork | writeToNetwork | 解码客户端缓冲区中的粒子数据。 | + +当需要发送自定义粒子数据时,此对象会传递到`ParticleType`的构造函数中。 + +### Particle + +`Particle`提供将所述数据绘制到屏幕上所需的渲染逻辑。要创建任何`Particle`,必须实现两个方法: + +| 方法 | 描述 | +| :--- | :--- | +| render | 将粒子渲染到屏幕上。 | +| getRenderType | 获取粒子的渲染类型。 | + +用于渲染纹理的`Particle`的一个常见子类是`TextureSheetParticle`。虽然需要实现`#getRenderType`,但无论设置了什么纹理sprite,都将在粒子的位置进行渲染。 + +#### ParticleRenderType + +`ParticleRenderType`是`RenderType`的一个变体,它为该类型的每个粒子构造启动和拆卸阶段,然后通过`Tesselator`同时渲染所有粒子。粒子可以使用六种不同的渲染类型。 + +| 渲染类型 | 描述 | +| :--- | :--- | +| TERRAIN_SHEET | 渲染纹理位于可用方块内的粒子。 | +| PARTICLE_SHEET_OPAQUE | 渲染纹理不透明且位于可用粒子内的粒子。 | +| PARTICLE_SHEET_TRANSLUCENT | 渲染纹理为半透明且位于可用粒子内的粒子。 | +| PARTICLE_SHEET_LIT | 与`PARTICLE_SHEET_OPAQUE`相同,但不使用粒子着色器。 | +| CUSTOM | 提供混合和深度遮罩的设置,但不提供将在`Particle#render`中实现的渲染功能。 | +| NO_RENDER | 粒子将永远不会渲染。 | + +实现自定义渲染类型将留给读者练习。 + +### ParticleProvider + +最后,粒子通常是通过`ParticleProvider`创建的。工厂有一个单一的方法`ParticleProvider`,用于在给定粒子数据、客户端存档、位置和移动增量的情况下创建粒子。由于`Particle`不受任何特定`ParticleType`的约束,因此可以根据需要在不同的工厂中重复使用。 + +必须通过订阅**模组事件总线**上的`RegisterParticleProvidersEvent`以注册`ParticleProvider`。在事件中,可以通过向方法提供工厂实例,通过`#registerSpecial`注册工厂。 + +!!! 重要 + `RegisterParticleProvidersEvent`应仅在客户端上调用,因此在某些客户端类中被单端化独立,并被`DistExecutor`或`@EventBusSubscriber`引用。 + +#### ParticleDescription、SpriteSet、以及SpriteParticleRegistration + +有三种粒子渲染类型不能使用上述注册方法: `PARTICLE_SHEET_OPAQUE`、`PARTICLE_SHEET_TRANSLUCENT`和`PARTICLE_SHEET_LIT`。这是因为这三种粒子渲染类型都使用由`ParticleEngine`直接加载的sprite集。因此,所提供的纹理必须通过不同的方法获得和注册。这将假设你的粒子是`TextureSheetParticle`的子类型,因为这是该逻辑的唯一原版实现。 + +要将纹理添加到粒子,必须将一个新的JSON文件添加到`assets//particles`。这被称为`ParticleDescription`。该文件的名称将代表工厂所附加的`ParticleType`的注册表名称。每个粒子JSON都是一个对象。该对象存储单个关键的`textures`,该键包含`ResourceLocation`的一个数组。此处表示的任何`:`纹理都将指向`assets//textures/particle/.png`处的纹理。 + +```js +{ + "textures": [ + // Will point to a texture located in + // assets/mymod/textures/particle/particle_texture.png + "mymod:particle_texture", + // Textures should by ordered by drawing order + // e.g. particle_texture will render first, then particle_texture2 + // after some time + "mymod:particle_texture2" + ] +} +``` + +若要引用一个粒子纹理,`TextureSheetParticle`的子类型应采用`SpriteSet`或从`SpriteSet`获得的`TextureAtlasSprite`。`SpriteSet`包含一个纹理列表,这些纹理引用了我们的`ParticleDescription`定义的sprite。`SpriteSet`有两个方法,这两个方法都以不同的方法获取`TextureAtlasSprite`。第一种方法接受两个整数。其背后的实现允许sprite在老化时进行纹理更改。第二种方法接受一个`Random`实例,从sprite集中获取随机纹理。可以使用`SpriteSet`中的一个辅助方法在`TextureSheetParticle`中设置sprite:`#pickSprite`使用拾取纹理的随机方法,`#setSpriteFromAge`使用两个整数的百分比方法拾取纹理。 + +要注册这些粒子纹理,需要向`RegisterParticleProvidersEvent#registerSpriteSet`方法提供一个`SpriteParticleRegistration`。此方法接收一个`SpriteSet`,其中包含粒子的相关sprite集,并创建一个`ParticleProvider`来创建粒子。最简单的实现方法可以通过在某个类上实现`ParticleProvider`并让构造函数接受`SpriteSet`来完成。然后,`SpriteSet`可以正常地传递给粒子。 + +!!! 注意 + 如果你注册的是仅包含一个纹理的`TextureSheetParticle`子类型,则可以转而向`#registerSprite`方法提供`ParticleProvider$Sprite`,其与`ParticleProvider`具有基本相同的功能接口方法。 + +生成一个粒子 +----------- + +粒子可以在任一存档实例中生成。但是,每一端都有一种特定的方式来生成粒子。如果在`ClientLevel`上,可以调用`#addParticle`来生成粒子,或者可以调用`#addAlwaysVisibleParticle`以生成从任何距离可见的粒子。如果在`ServerLevel`上,则可以调用`#sendParticles`向客户端发送数据包以生成粒子。在服务端上调用两个`ClientLevel`方法将会一无所获。 + +[sides]: ../concepts/sides.md +[registration]: ../concepts/registries.md#methods-for-registering diff --git a/docs/translation/zh_CN/gameeffects/sounds.md b/docs/translation/zh_CN/gameeffects/sounds.md new file mode 100644 index 000000000..f62ed3a80 --- /dev/null +++ b/docs/translation/zh_CN/gameeffects/sounds.md @@ -0,0 +1,110 @@ +音效 +==== + +术语 +---- + +| 术语 | 描述 | +|----------------|----------------| +| 音效事件 | 触发音效效果的东西。例子包括`minecraft:block.anvil.hit`或`botania:spreader_fire`。 | +| 音效类别 | 音效的类别,例如`player`、`block`或只不过是`master`。音效设置GUI中的滑块展示这些类别。 | +| 音效文件 | 字面意义上的磁盘上播放的文件:一个.ogg文件。 | + +`sounds.json` +------------- + +此JSON定义音效事件,并定义它们播放的音效文件、字幕等。音效事件用[`ResourceLocation`][loc]标识。`sounds.json`应该位于资源命名空间的根目录(`assets//sounds.json`),且在该命名空间中定义音效事件(`assets//soundes.json`在名称空间`namespace`中定义音效事件。)。 + +原版[wiki][]上提供了完整的规范,但这个例子强调了重要的部分: + +```js +{ + "open_chest": { + "subtitle": "mymod.subtitle.open_chest", + "sounds": [ "mymod:open_chest_sound_file" ] + }, + "epic_music": { + "sounds": [ + { + "name": "mymod:music/epic_music", + "stream": true + } + ] + } +} +``` + +在顶级对象的下面,每个键都对应一个音效事件。请注意,没有给出命名空间,因为它取自JSON本身的命名空间。每个事件指定启用字幕时要显示的本地化翻译键。最后,指定要播放的实际音效文件。请注意,该值是一个数组;如果指定了多个音效文件,则每当触发音效事件时,游戏将随机选择一个播放。 + +这两个示例代表了指定音效文件的两种不同方式。[wiki]有精确的细节,但一般来说,长音效文件(如背景音乐或音乐光盘)应该使用第二种形式,因为"stream"参数告诉Minecraft不要将整个音效文件加载到内存中,而是从磁盘流形式传输。第二种形式还可以指定音效文件的音量、音高和重量。 + +在所有情况下,命名空间`namespace`和路径`path`的音效文件路径都是`assets//sounds/.ogg`。因此,`mymod:open_chest_sound_file`指向`assets/mymod/sounds/open_chest_sound_file.ogg`,而`mymod:music/epic_music`指向`assets/mymod/sounds/music/epic_music.ogg`。 + +`sounds.json`可以是[数据生成][datagen]的。 + +创建音效事件 +----------- + +为了引用服务端上的音效,必须创建一个在`sounds.json`中包含相应条目的`SoundEvent`。然后必须对`SoundEvent`进行[注册][registration]。通常,用于创建音效事件的位置应设置为其注册表名称。 + +`SoundEvent`作为对音效的一个引用,并被传递以播放它们。如果一个模组有API,应该在API中公开它的`SoundEvent`。 + +!!! 注意 + 只要音效在`sounds.json`中被注册,它就仍然可以在逻辑客户端上被引用,而不管是否存在引用其的`SoundEvent`。 + +播放音效 +------- + +原版有很多播放音效的方法,有时很难清楚该用哪种。 + +请注意,每个方法都要接受一个`SoundEvent`,即上面注册的事件。此外,术语 *“服务端行为”* 和 *“客户端行为”* 指其分别的[**逻辑**端][side]。 + +### `Level` + +1. `playSound(Player, BlockPos, SoundEvent, SoundSource, volume, pitch)` + - 简单地转发到[重载 (2)](#level-playsound-pxyzecvp),在给定的`BlockPos`的每个坐标上加0.5。 + +2. `playSound(Player, double x, double y, double z, SoundEvent, SoundSource, volume, pitch)` + - **客户端行为**: 如果传入的玩家是*客户端*玩家,则向客户端玩家播放该音效事件。 + - **服务端行为**: 向附近的所有人播放音效事件,除了传入的玩家以外。玩家可以为`null`。 + - **用法**: 行为之间的对应关系意味着这两个方法将从一些玩家启动的代码中调用,这些代码将同时在两逻辑端运行:逻辑客户端处理向用户播放,逻辑服务端处理其他所有听到它的人,而不向原始用户重新播放。它们还可以用于在服务端端的任何位置播放任何音效,方法是在逻辑服务端上调用它并传入`null`玩家,从而让每个人都能听到。 + +3. `playLocalSound(double x, double y, double z, SoundEvent, SoundSource, volume, pitch, distanceDelay)` + - **客户端行为**: 只是在客户端存档播放音效事件。如果`distanceDelay`为`true`,则根据音效与玩家的距离来延迟音效。 + - **服务端行为**: 不做任何事情。 + - **用法**: 此方法仅适用于客户端,因此对于在自定义数据包中发送的音效或其他仅客户端效果类型的音效非常有用。打雷就用了该方法。 + +### `ClientLevel` + +1. `playLocalSound(BlockPos, SoundEvent, SoundSource, volume, pitch, distanceDelay)` + - 简单地转发到`Level`的[overload (3)](#level-playsound-xyzecvpd),在给定的`BlockPos`的每个坐标上加0.5。 + +### `Entity` + +1. `playSound(SoundEvent, volume, pitch)` + - 简单地转发到`Level`的[overload (2)](#level-playsound-pxyzecvp),将玩家传递为`null`。 + - **客户端行为**: 不做任何事情。 + - **服务端行为**: 向该实体所在位置的所有人播放音效事件。 + - **用法**: 在服务端从任何非玩家实体发出任何音效。 + +### `Player` + +1. `playSound(SoundEvent, volume, pitch)` (overriding the one in [`Entity`](#entity-playsound-evp)) + - 简单地转发到`Level`的[overload (2)](#level-playsound-pxyzecvp),将玩家传递为`null`。 + - **客户端行为**: 不做任何事情,参见[`LocalPlayer`](#localplayer-playsound-evp)中的重载。 + - **服务端行为**: 向附近*除了*该玩家以外的所有人播放该音效。 + - **用法**: 参见[`LocalPlayer`](#localplayer-playsound-evp)。 + +### `LocalPlayer` + +1. `playSound(SoundEvent, volume, pitch)` (overriding the one in [`Player`](#player-playsound-evp)) + - 简单地转发到`Level`的[overload (2)](#level-playsound-pxyzecvp),将玩家传递为`this`。 + - **客户端行为**: 仅仅播放该音效事件。 + - **服务端行为**: 该方法仅客户端适用。 + - **用法**: 就像`Level`中的方法一样,玩家类中的这两个重写似乎是针对在两端同时运行的代码。客户端处理向用户播放音效,而服务端处理其他所有听到音效的人,而不向原始用户重新播放。 + +[loc]: ../concepts/resources.md#resourcelocation +[wiki]: https://minecraft.fandom.com/wiki/Sounds.json +[datagen]: ../datagen/client/sounds.md +[registration]: ../concepts/registries.md#methods-for-registering +[sides]: ../concepts/sides.md diff --git a/docs/translation/zh_CN/gettingstarted/index.md b/docs/translation/zh_CN/gettingstarted/index.md new file mode 100644 index 000000000..8315d2d24 --- /dev/null +++ b/docs/translation/zh_CN/gettingstarted/index.md @@ -0,0 +1,119 @@ +Forge入门 +========= + +如果你之前从未制作过一个Forge模组,本节将提供设置Forge开发环境所需的最少信息。其余的文档是关于从这里开始的内容。 + +先决条件 +-------- + +* 安装Java 17开发包(JDK)和64位JVM。Forge推荐并官方支持[Eclipse Temurin][jdk]。 + + !!! 警告 + 确保你正在使用64位的JVM。一种检查方式是在终端中运行`java -version`。使用32位的JVM会导致在使用[ForgeGradle]的过程中出现问题。 + +* 熟练使用一款集成开发环境(IDE)。 + * 建议使用一款集成了Gradle功能的IDE。 + +从零开始模组开发 +---------------- + +1. 从[Forge文件站][files]下载Mod开发包(MDK)。点击“Mdk”,等待一段时间之后点击右上角的“Skip”按钮。如果可能的话,推荐下载最新版本的Forge。 +1. 解压所下载的MDK到一个空文件夹中。它会成为你的模组的目录,且现在应该已包含一些gradle文件和一个含有example模组的`src`子目录。 + + !!! 注意 + 许多文件可以在不同的模组中重复使用。这些文件是: + + * `gradle`子目录 + * `build.gradle` + * `gradlew` + * `gradlew.bat` + * `settings.gradle` + + `src`子目录不需要跨工作区进行复制;但是,如果稍后创建java(`src/main/java`)和resource(`src/main/resources`),则可能需要刷新Gradle项目。 + +1. 打开你选择的IDE: + * Forge只明确支持在Eclipse和IntelliJ IDEA上进行开发,但还有其他针对Visual Studio代码的运行配置。无论如何,从Apache NetBeans到Vim/Emacs的任何开发环境都可被使用。 + * Eclipse和IntelliJ IDEA的Gradle集成,都是已默认安装和启用的,将在导入或打开时处理其余的初始工作区设置。这包括从Mojang、MinecraftForge等下载必要的软件包。如果你使用Visual Studio,则需要安装“Gradle for Java”插件。 + * Gradle将需要被调用来重新评估项目中对其相关文件的几乎所有更改(如`build.gradle`、`settings.gradle`等等)。有些IDE带有“刷新”按钮来完成此操作;然而,它也可以通过在终端上运行`gradlew`来完成。 +1. 为你选择的IDE生成运行配置: + * **Eclipse**: 运行`genEclipseRuns`任务。 + * **IntelliJ IDEA**: 运行`genIntellijRuns`任务。如果发生了"module not specified"错误,请将[`ideaModule`属性][config]设置为你的'main'模块(通常为`${project.name}.main`)。 + * **Visual Studio Code**: 运行`getVSCodeRuns`任务。 + * **Other IDEs**: 你可以通过`gradle run*`来直接运行这些配置(如`runClient`、`runServer`、`runData`、`runGameTestServer`)。这对于已提供支持的IDE同样有效。 + +自定义你的模组信息 +----------------- + +编辑`build.gradle`文件以自定义你的模组的构建方式(如文件名称、artifact版本等等)。 + +!!! 重要 + 除非你知道你在做什么,否则**不要**编辑`settings.gradle`。该文件指定[ForgeGradle]所上传的仓库。 + +### 建议的`build.gradle`自定义项目 + +#### Mod Id替换 + +将包括[mods.toml和主mod文件][modfiles]在内的所有出现的examplemo替换为你的模组的mod id。这还包括通过设置`base.archivesName`(通常设置为你的mod id)来更改你构建的文件的名称。 + +```gradle +// 在某个build.gradle文件中 +base.archivesName = 'mymod' +``` + +#### Group Id + +`group`属性应该设置为你的顶级程序包,其应为你拥有的域名或你的电子邮件地址: + +类型 | 值 | 顶级程序包 +:---: | :---: | :--- +域名 | example.com | `com.example` +子域名 | example.github.io | `io.github.example` +电子邮箱地址 | example@gmail.com | `com.gmail.example` + +```gradle +// 在某个build.gradle文件中 +group = 'com.example' +``` + +java源文件(`src/main/java`)中的包现在也应该符合这种结构,更深层的包表示mod id: + +```text +com +- example (在group属性中所指定的顶级程序包) + - mymod (mod id) + - MyMod.java (重命名后的ExampleMod.java) +``` + +#### 版本 + +将`version`属性设置为你的模组的当前版本。我们推荐采用[Maven版本号命名格式][mvnver]。 + +```gradle +// 在某个build.gradle文件中 +version = '1.19.4-1.0.0.0' +``` + +### 额外配置 + +额外配置可在[ForgeGradle]文档中找到。 + +构建并测试你的模组 +----------------- + +1. 要构建你的模组,请运行`gradlew build`。这将在`build/libs`输出一个默认名为`[archivesBaseName]-[version].jar`的文件。这个文件可以被放在已安装了Forge的Minecraft的`mods`文件夹中,也可以被分发。 +1. 要在测试环境中运行你的模组,你既可以使用已生成的运行配置,也可以运行功能类似的Gradle任务(例如`gradlew runClient`)。这将使用任何所指定的源码集从run文件夹中启动Minecraft。默认的MDK包括`main`源码集,因此任何在`src/main/java`中编写的源代码都会被应用。 +1. 如果你想要运行dedicated服务端,无论是通过运行配置,还是通过`gradlew runServer`,服务端都会立刻宕机。你需要通过编辑run文件夹中的`eula.txt`文件同意Minecraft EULA。一旦同意后,服务器就会加载,之后就可以通过直连`localhost`进行访问了。 + +!!! 注意 + 在服务端环境测试你的模组是必要的。这包括[只针对客户端的模组][client],因为在加载到服务端后它们不应该做任何事。 + +[jdk]: https://adoptium.net/temurin/releases?version=17 "Eclipse Temurin 17 Prebuilt Binaries" +[ForgeGradle]: https://docs.minecraftforge.net/en/fg-6.x + +[files]: https://files.minecraftforge.net "Forge Files distribution site" +[config]: https://docs.minecraftforge.net/en/fg-6.x/configuration/runs/ + +[modfiles]: ./modfiles.md +[packaging]: ./structuring.md#packaging +[mvnver]: ./versioning.md +[client]: ../concepts/sides.md#writing-one-sided-mods diff --git a/docs/translation/zh_CN/gettingstarted/modfiles.md b/docs/translation/zh_CN/gettingstarted/modfiles.md new file mode 100644 index 000000000..f3a88a721 --- /dev/null +++ b/docs/translation/zh_CN/gettingstarted/modfiles.md @@ -0,0 +1,165 @@ +模组文件 +======= + +模组文件负责确定哪些文件会被打包到你模组的JAR文件中,在“Mods”菜单中显示哪些信息,以及你的模组如何被加载到游戏中。 + +mods.toml +--------- + +`mods.toml`定义你的一个或多个模组的元数据。它也包含一些附加信息,这些信息将在Mods菜单中被展示,并决定你的模组如何被加载进游戏。 + +该文件采用[Tom's Obvious Minimal Language][toml](简称TOML)格式。这个文件必须保存在你所使用的源码集的resource目录中的`META-INF`文件夹下(例如对于`main`源码集,其路径为`src/main/resources/META-INF/mods.toml`)。`mods.toml`文件看起来长这样: + +```toml +modLoader="javafml" +loaderVersion="[46,)" + +license="All Rights Reserved" +issueTrackerURL="https://github.com/MinecraftForge/MinecraftForge/issues" +showAsResourcePack=false + +[[mods]] + modId="examplemod" + version="1.0.0.0" + displayName="Example Mod" + updateJSONURL="https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json" + displayURL="https://minecraftforge.net" + logoFile="logo.png" + credits="I'd like to thank my mother and father." + authors="Author" + description=''' + Lets you craft dirt into diamonds. This is a traditional mod that has existed for eons. It is ancient. The holy Notch created it. Jeb rainbowfied it. Dinnerbone made it upside down. Etc. + ''' + displayTest="MATCH_VERSION" + +[[dependencies.examplemod]] + modId="forge" + mandatory=true + versionRange="[46,)" + ordering="NONE" + side="BOTH" + +[[dependencies.examplemod]] + modId="minecraft" + mandatory=true + versionRange="[1.20]" + ordering="NONE" + side="BOTH" +``` + +`mods.toml`被分为三个部分:非模组特定属性,与模组文件相关联;模组特定属性,对每个模组都有单独的小节;以及依赖配置,对每个模组依赖都有单独的小节。下面将解释与`mods.toml`文件相关的各个属性,其中`required`表示必须指定一个值,否则将引发异常。 + +### 非模组特定属性 + +非模组特定属性是与JAR文件本身相关的属性,指明如何加载模组和任何附加的全局元数据。 + +属性 | 类型 | 缺省值 | 描述 | 样例 +:--- | :---: | :---: | :---: | :--- +`modLoader` | string | **必需** | 模组所使用的语言加载器。可用于支持额外的语言结构,如为主文件定义的Kotlin对象,或确定入口点的不同方法,如接口或方法。Forge提供Java加载器`"javafml"`和低/无代码加载器`"lowcodefml"`。 | `"javafml"` +`loaderVersion` | string | **必需** | 可接受的语言加载器版本范围,以[Maven版本范围][mvr]表示。对于`javafml`和`lowcodefml`,其版本是Forge版本的主版本号。 | `"[46,)"` +`license` | string | **必需** | 该JAR文件中的模组所遵循的许可证。建议将其设置为你正在使用的[SPDX标识符][spdx]和/或许可证的链接。你可以访问 https://choosealicense.com/ 以帮助选取你想使用的许可证。 | `"MIT"` +`showAsResourcePack` | boolean | `false` | 当为`true`时,模组的资源会以一个单独的资源包的形式在“资源包”菜单中展示,而不是与“模组资源”包融为一体。 | `true` +`services` | array | `[]` | 表示你的模组所**使用**的一系列服务的数组。这是从Forge的Java平台模块系统实现中为模组创建的模块的一部分。 | `["net.minecraftforge.forgespi.language.IModLanguageProvider"]` +`properties` | table | `{}` | 替换属性表。`StringSubstitutor`使用它将`${file.}`替换为相应的值。该功能目前仅用于替换模组特定属性中的`version`。 | 由`${file.example}`引用的`{ "example" = "1.2.3" }` +`issueTrackerURL` | string | *无* | 指向报告与追踪模组问题的地点的URL。 | `"https://forums.minecraftforge.net/"` + +!!! 重要 + `services`属性在功能上等效于在指定[在模块中的`uses`指令][uses],该指令允许加载给定类型的服务。 + +### 模组特定属性 + +模组特定属性通过`[[mods]]`头与指定的模组绑定。其本质是一个[表格数组(Array of Tables)][array];直到下一个头之前的所有键/值对都会被关联到那个模组。 + +```toml +# examplemod1的属性 +[[mods]] +modId = "examplemod1" + +# examplemod2的属性 +[[mods]] +modId = "examplemod2" +``` + +属性 | 类型 | 缺省值 | 描述 | 样例 +:--- | :---: | :---: | :---: | :--- +`modId` | string | **必需** | 代表这个模组的唯一标识符。该标识符必须匹配`^[a-z][a-z0-9_]{1,63}$`(一个长度在[2,64]闭区间内的字符串;以小写字母开头;由小写字母、数字或下划线组成)。 | `"examplemod"` +`namespace` | string | `modId`的值 | 该模组的一个重载命名空间。该命名空间必须匹配`^[a-z][a-z0-9_.-]{1,63}$`(一个长度在[2,64]闭区间内的字符串;以小写字母开头;由小写字母、数字、下划线、点或短横线组成)。目前无作用。 | `"example"` +`version` | string | `"1"` | 该模组的版本,最好符合[Maven版本号命名格式][mvnver]。当设置为`${file.jarVersion}`时,它将被替换为JAR清单文件中`Implementation-Version`属性的值(在开发环境下默认显示为`0.0NONE`)。 | `"1.20-1.0.0.0"` +`displayName` | string | `modId`的值 | 该模组的更具可读性的名字。用于将模组展示到屏幕上时(如模组列表、模组不匹配)。 | `"Example Mod"` +`description` | string | `"MISSING DESCRIPTION"` | 在模组列表中展示的该模组的描述。建议使用一个[多行文字字符串][multiline]。 | `"This is an example."` +`logoFile` | string | *无* | 在模组列表中展示的该模组的logo图像文件的名称和扩展名。该logo必须位于JAR文件的根目录或直接位于源码集的根目录。 | `"example_logo.png"` +`logoBlur` | boolean | `true` | 决定使用`GL_LINEAR*`(true)或`GL_NEAREST*`(false)渲染`logoFile`。 | `false` +`updateJSONURL` | string | *无* | 被[更新检查器][update]用来检查你所使用的模组是否为最新版本的指向一个JSON文件的URL。 | `"https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json"` +`features` | table | `{}` | 参见 '[features]'。 | `{ java_version = "17" }` +`modproperties` | table | `{}` | 与本模组相关联的一个键/值对表。目前尚未被Forge使用,但主要被模组使用。 | `{ example = "value" }` +`modUrl` | string | *无* | 指向本模组下载界面的URL。目前无作用。 | `"https://files.minecraftforge.net/"` +`credits` | string | *无* | 在模组列表中展示的致谢声明。 | `"The person over here and there."` +`authors` | string | *无* | 在模组列表中展示的本模组的作者。 | `"Example Person"` +`displayURL` | string | *无* | 在模组列表中展示的本模组的展示页面(项目主页)。 | `"https://minecraftforge.net/"` +`displayTest` | string | `"MATCH_VERSION"` | 参见 '[sides]'。 | `"NONE"` + +#### 功能 + +功能系统允许模组在加载系统时要求某些设置、软件或硬件可用。当某个功能不满足时,模组加载将失败,并将要求通知给用户。目前,Forge提供以下功能: + +功能 | 描述 | 样例 +:---: | :---: | :--- +`java_version` | 可支持的Java版本范围,以[Maven版本范围][mvr]表示。该范围须能够支持Minecraft所使用的Java版本。 | `"[17,)"` + +### 依赖配置 + +模组可以指定它们的依赖项,这些依赖项在加载模组之前由Forge检查。这些配置是使用[表格数组(Array of Tables)][array]`[[dependencies.]]`创建的,其中`modid`是所依赖的模组的标识符。 + +属性 | 类型 | 缺省值 | 描述 | 样例 +:--- | :---: | :---: | :---: | :--- +`modId` | string | **必需** | 被添加为依赖的模组的标识符。 | `"example_library"` +`mandatory` | boolean | **必需** | 当依赖未满足时游戏是否崩溃。 | `true` +`versionRange` | string | `""` | 可接受的语言加载器版本范围,以[Maven版本范围][mvr]表示。空字符串表示匹配所有版本。 | `"[1, 2)"` +`ordering` | string | `"NONE"` | 定义本模组是否必须在所依赖的模组之前(`"BEFORE"`)或之后(`"AFTER"`)加载。`"NONE"`表示不规定顺序。 | `"AFTER"` +`side` | string | `"BOTH"` | 所依赖模组必须位于的[端位][dist]:`"CLIENT"`、`"SERVER"`或`"BOTH"`。 | `"CLIENT"` +`referralUrl` | string | *无* | 指向依赖下载界面的URL。目前无作用。 | `"https://library.example.com/"` + +!!! 警告 + 两个模组的`ordering`可能会因循环依赖而造成崩溃:例如模组A必须在模组B之前(`"BEFORE"`)加载,而模组B也必须在模组A之前(`"BEFORE"`)加载。 + +模组入口点 +---------- + +现在我们已经填写了`mods.toml`,我们需要提供一个对模组进行编程的入口点。入口点本质上是执行模组的起点。入口点本身由`mods.toml`中使用的语言加载器决定。 + +### `javafml`和`@Mod` + +`javafml`是Forge为Java编程语言提供的语言加载器。入口点是通过使用带有`@Mod`注释的公共类来定义的。`@Mod`的值必须包含`mods.toml`中指定的一个Mod id。从那里,所有初始化逻辑(例如[注册事件][events]、添加[`DeferredRegister`][registration])都可以在类的构造函数中写明。模组总线可以从`FMLJavaModLoadingContext`获得。 + +```java +@Mod("examplemod") // 必须匹配mods.toml +public class Example { + + public Example() { + // 此处初始化逻辑 + var modBus = FMLJavaModLoadingContext.get().getModEventBus(); + + // ... + } +} +``` + +### `lowcodefml` + +`lowcodefml`是一种语言加载器,用于将数据包和资源包作为模组形式分发,而无需代码形式的入口点。它被指定为`lowcodefml`而不是`nocodefml`,用于将来可能需要的最少量代码的小添加。 + +[toml]: https://toml.io/ +[mvr]: https://maven.apache.org/enforcer/enforcer-rules/versionRanges.html +[spdx]: https://spdx.org/licenses/ +[modsp]: #mod-specific-properties +[uses]: https://docs.oracle.com/javase/specs/jls/se17/html/jls-7.html#jls-7.7.3 +[serviceload]: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/ServiceLoader.html#load(java.lang.Class) +[array]: https://toml.io/en/v1.0.0#array-of-tables +[mvnver]: ./versioning.md +[multiline]: https://toml.io/en/v1.0.0#string +[update]: ../misc/updatechecker.md +[features]: #features +[sides]: ../concepts/sides.md#writing-one-sided-mods +[dist]: ../concepts/sides.md#different-kinds-of-sides +[events]: ../concepts/events.md +[registration]: ../concepts/registries.md#deferredregister diff --git a/docs/translation/zh_CN/gettingstarted/structuring.md b/docs/translation/zh_CN/gettingstarted/structuring.md new file mode 100644 index 000000000..ed86f32f8 --- /dev/null +++ b/docs/translation/zh_CN/gettingstarted/structuring.md @@ -0,0 +1,81 @@ +规划你的模组结构 +============== + +结构分明的模组有利于维护和做出贡献,并提供对底层代码库的更清晰理解。下面列举了由Java、Minecraft和Forge提出的一些建议。 + +!!! 注意 + 你不必遵循以下建议;你可以以任何你认为合适的方式规划你的模组。然而,我们仍强烈建议这样做。 + +程序包 +------ + +在规划你的模组时,选择一个独特的、顶级的程序包结构。许多程序员会对不同的类、接口等使用相同的名称。Java允许类具有相同的名称,只要它们位于不同的包中。因此,如果两个类具有相同的名称和相同的包,则只有一个会被加载,这很可能导致游戏崩溃。 + +``` +a.jar + - com.example.ExampleClass +b.jar + - com.example.ExampleClass // 这个类不会被正常加载 +``` + +当涉及到加载模块时,这一点更为重要。如果在不同的模块中有两个同名包下的类文件,这将导致模组加载器**在启动时崩溃**,因为模组模块会被导出到游戏和其他模组中。 + +``` +module A + - package X + - class I + - class J +module B + - package X // 此包将导致模组加载器崩溃,因为已经有一个模块将包X导出 + - class R + - class S + - class T +``` + +正因如此,你的顶级程序包应该是你自己的东西:域名、电子邮件地址、网站的子域等。它甚至可以是你的名字或用户名,只要你能保证它在预期目标中是唯一可识别的。 + +类型 | 值 | 顶级程序包 +:---: | :---: | :--- +域名 | example.com | `com.example` +子域名 | example.github.io | `io.github.example` +电子邮箱地址 | example@gmail.com | `com.gmail.example` + +下一个级别的包应该是你的mod id(例如`com.example.examplemod`,其中`examplemod`是mod id)。这将保证,除非你有两个id相同的模组(这种情况永远不会发生),否则你的包在加载时不会出现任何问题。 + +你可以在[Oracle的教程页面][naming]上找到一些其他命名约定。 + +### 子包的组织 + +除了顶级包以外,强烈建议将你的模组的类拆分为不同的子包。关于如何做到这一点,主要有两种方法: + +* **按功能分组**: 将具有共同目的的类归入同一个子包。例如,方块相关的类可被置于`block`或`blocks`子包下,实体相关的类可被置于`entity`或`entities`子包下等等。Mojang就在使用这种结构,单词用的是单数形式(`block`、`entity`)。 +* **按逻辑分组**: 将具有共同逻辑的类归入同一个子包。例如,如果你正在创建一种新配方,你可以将它的方块、菜单、物品等等都放在`feature.crafting_table`子包下。 + +#### 客户端、服务端和数据相关的子包 + +通常,仅用于给定端位或运行时的代码都应该在单独的子包中与其他类隔离。例如,与[数据生成][datagen]相关的代码应该放在`data`子包中,而仅与dedicated服务器相关的代码应该在`server`子包中。 + +然而,强烈建议在`client`子包中隔离[仅限客户端][sides]的代码。这是因为dedicated服务器不应有任何权限访问Minecraft中仅限客户端的包。因此,拥有一个专用的包将提供一个不错的健全性检查,以保证你的模组中的代码没有越过端位的行为。 + +类的命名规则 +----------- + +一个普适的类命名方案可以让你更容易地读懂类的目的或查找某个特定的类。 + +类的名称通常以其类型作为后缀,例如: + +* 一个叫作`PowerRing`的`Item` -> `PowerRingItem`。 +* 一个叫作`NotDirt`的`Block` -> `NotDirtBlock`。 +* 为`Oven`设计的一个菜单 -> `OvenMenu`。 + +!!! 注意 + Mojang通常对除实体以外的所有类命名时都遵循类似的结构。而实体只用它们的名字来表示(例如`Pig`、`Zombie`等)。 + +选择仅用一个方法而非多个 +---------------------- + +执行特定任务的方法有很多:注册对象、监听事件等。通常建议使用单一方法来完成给定的任务以保持一致。这在改善了代码格式的同时,也避免了可能发生的任何奇怪的交互或冗余(例如,你的事件监听器执行了两次)。 + +[naming]: https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html +[datagen]: ../datagen/index.md +[sides]: ../concepts/sides.md diff --git a/docs/translation/zh_CN/gettingstarted/versioning.md b/docs/translation/zh_CN/gettingstarted/versioning.md new file mode 100644 index 000000000..2a68eedc0 --- /dev/null +++ b/docs/translation/zh_CN/gettingstarted/versioning.md @@ -0,0 +1,56 @@ +版本号 +====== + +在一般项目中,语义式的版本号(格式为`MAJOR.MINOR.PATCH`)被经常使用。然而,在长期性地修改的情况下,使用格式`MCVERSION-MAJORMOD.MAJORAPI.MINOR.PATCH`可能更有利于将模组的创世性的修改与API变更性的修改区分开来。 + +!!! 重要 + Forge使用[Maven版本范围][cmpver]来比较版本字符串,这与Semantic Versioning 2.0.0规范不完全兼容,例如“prerelease”标签。 + +样例 +---- + +以下是在不同情形下能递增各种变量的示例列表。 + +* `MCVERSION` + * 始终与该模组所适用的Minecraft版本相匹配。 +* `MAJORMOD` + * 移除物品、方块、方块实体等。 + * 改变或移除之前存在的机制。 + * 升级到新的Minecraft版本。 +* `MAJORAPI` + * 更改枚举的顺序或变量。 + * 更改方法的返回类型。 + * 一并移除公共方法。 +* `MINOR` + * 添加物品、方块、方块实体等。 + * 添加新机制。 + * 废弃公共方法。(这不是一次`MAJORAPI`递增,因为它并未改变API。) +* `PATCH` + * Bug修复。 + +当递增任何变量时,所有更小级别的变量都应重置为`0`。例如,如果`MINOR`递增,`PATCH`将变为`0`。如果`MAJORMOD`递增,则所有其他变量将变为`0`。 + +### 项目初始阶段 + +如果你正处于模组的初始开发阶段(在任何正式发布之前),`MAJORMOD`和`MAJORAPI`应始终为`0`。只有`MINOR`和`PATCH`应该在每次构建你的模组时更新。一旦你构建了一个官方版本(大多数情况下应使用稳定的API),你应该将`MAJORMOD`增加到版本`1.0.0.0`。有关任何进一步的开发阶段,请参阅本文档的[预发布][pre]和[候选发布][rc]部分。 + +### 多个Minecraft版本 + +如果模组升级到新版本的Minecraft,而旧版本将只会得到bug修复,则`PATCH`变量应根据升级前的版本进行更新。如果模组针对旧版本和新版本的Minecraft都仍在积极开发中,建议将该版本附加到**所有**两个Minecraft版本号之后。例如,如果模组由于Minecraft版本的更改而升级到`3.0.0.0`版本,那么旧版本的模组也应该更新到`3.0.0.0`。又例如,旧版本将变成`1.7.10-3.0.0.0`版本,而新版本将变成`1.8-3.0.0.0`版本。如果在为新的Minecraft版本构建时模组本身并没有任何更改,那么除了Minecraft版本之外的所有变量都应该保持不变。 + +### 最终发布 + +当放弃对某个Minecraft版本的支持时,针对该版本的最后一个模组构建版本应该有`-final`后缀。这意味着模组对于所表示的`MCVERSION`将不再支持,玩家应该升级到模组所支持的新版本的Minecraft,以继续接收更新和bug修复。 + +### 预发布 + +(本指南不使用`-pre`,因为在撰写本文时,它不是`-beta`的有效别名。)请注意,已经发布的版本和首次发布之前的版本不能进入预发布;变量(主要是`MINOR`,但`MAJORAPI`和`MAJORMOD`也可以预发布)应该在添加`-beta`后缀之前进行相应的更新。首次发布之前的版本只是在建版本。 + +### 候选发布 + +候选发布在实际版本更替之前充当预发布。这些版本应该附加`-rcX`,其中`X`是候选版本的数量,理论上,只有在修复bug时才应该增加。已经发布的版本无法接收候选版本;在添加`-rc`后缀之前,应该相应地更新变量(主要是`MINOR`,但`MAJORAPI`和`MAJORMOD`也可以预发布)。当作为稳定构建版本发布候选版本时,它既可以与上一个候选版本完全相同,也可以有更多的bug修复。 + +[semver]: https://semver.org/ +[cmpver]: https://maven.apache.org/ref/3.5.2/maven-artifact/apidocs/org/apache/maven/artifact/versioning/ComparableVersion.html +[pre]: #pre-releases +[rc]: #release-candidates diff --git a/docs/translation/zh_CN/gui/menus.md b/docs/translation/zh_CN/gui/menus.md new file mode 100644 index 000000000..749afb34a --- /dev/null +++ b/docs/translation/zh_CN/gui/menus.md @@ -0,0 +1,337 @@ +# 菜单(Menus) + +菜单(Menus)是图形用户界面(GUI)的一种后端类型;它们处理与某些代表的数据持有者交互所涉及的逻辑。菜单本身不是数据持有者。它们是允许用户间接修改内部数据持有者状态的视图。因此,数据持有者不应直接耦合到任何菜单,而应传入数据引用以便调用和修改。 + +## `MenuType` + +菜单是动态创建和删除的,因此不是注册表对象。因此,另一种工厂对象被注册,以方便创建和引用菜单的*类型*。对于菜单,其为`MenuType`。 + +`MenuType`必须被[注册][registered]。 + +### `MenuSupplier` + +`MenuType`是通过将`MenuSupplier`和`FeatureFlagSet`传递给其构造函数来创建的。`MenuSupplier`表示一个函数,该函数接收容器的id和查看菜单的玩家的物品栏,并返回一个新创建的[`AbstractContainerMenu`][acm]。 + +```java +// 对于某个类型为DeferredRegister>的REGISTER +public static final RegistryObject> MY_MENU = REGISTER.register("my_menu", () -> new MenuType(MyMenu::new, FeatureFlags.DEFAULT_FLAGS)); + +// 在MyMenu,一个AbstractContainerMenu的子类中 +public MyMenu(int containerId, Inventory playerInv) { + super(MY_MENU.get(), containerId); + // ... +} +``` + +!!! 注意 + 容器id对于单个玩家是唯一的。这意味着,两个不同玩家上的相同容器id将代表两个不同的菜单,即使他们正在查看相同的数据持有者。 + +`MenuSupplier`通常负责在客户端上创建一个菜单,其中包含用于存储来自服务端数据持有者的同步信息并与之交互的伪数据引用。 + +### `IContainerFactory` + +如果需要有关客户端的其他信息(例如数据持有者在世界中的位置),则可以使用子类`IContainerFactory`。除了容器id和玩家物品栏之外,这还提供了一个`FriendlyByteBuf`,它可以存储从服务端发送的附加信息。`MenuType`可以通过`IForgeMenuType#create`使用`IContainerFactory`创建。 + +```java +// 对于某个类型为DeferredRegister>的REGISTER +public static final RegistryObject> MY_MENU_EXTRA = REGISTER.register("my_menu_extra", () -> IForgeMenuType.create(MyMenu::new)); + +// 在MyMenuExtra,一个AbstractContainerMenu的子类中 +public MyMenuExtra(int containerId, Inventory playerInv, FriendlyByteBuf extraData) { + super(MY_MENU_EXTRA.get(), containerId); + // 从buffer中存储附加信息 + // ... +} +``` + +## `AbstractContainerMenu` + +所有菜单都是从`AbstractContainerMenu`继承而来的。菜单包含两个参数,即表示菜单本身类型的[`MenuType`][mt]和表示当前访问者的菜单唯一标识符的容器id。 + +!!! 重要 + 玩家一次只能打开100个唯一的菜单。 + +每个菜单应该包含两个构造函数:一个用于初始化服务端上的菜单,另一个用于启动客户端上的菜单。用于初始化客户端菜单的构造函数是提供给`MenuType`的构造函数。服务端菜单构造函数包含的任何字段都应该具有客户端菜单构造函数的一些默认值。 + +```java +// 客户端菜单构造函数 +public MyMenu(int containerId, Inventory playerInventory) { + this(containerId, playerInventory); +} + +// 服务端菜单构造函数 +public MyMenu(int containerId, Inventory playerInventory) { + // ... +} +``` + +每个菜单实现必须实现两个方法:`#stillValid`和[`#quickMoveStack`][qms]。 + +### `#stillValid`和`ContainerLevelAccess` + +`#stillValid`确定菜单是否应该为给定的玩家保持打开状态。这通常指向静态的`#stillValid`,它接受一个`ContainerLevelAccess`、该玩家和该菜单所附的`Block`。客户端菜单必须始终为该方法返回`true`,而静态的`#stillValid`默认为该方法。该实现检查玩家是否在数据存储对象所在的八个方块内。 + +`ContainerLevelAccess`提供封闭范围内方块的当前存档和位置。在服务端上构建菜单时,可以通过调用`ContainerLevelAccess#create`创建新的访问。客户端菜单构造函数可以传入`ContainerLevelAccess#NULL`,这将不起任何作用。 + +```java +// 客户端菜单构造函数 +public MyMenuAccess(int containerId, Inventory playerInventory) { + this(containerId, playerInventory, ContainerLevelAccess.NULL); +} + +// 服务端菜单构造函数 +public MyMenuAccess(int containerId, Inventory playerInventory, ContainerLevelAccess access) { + // ... +} + +// 假设该菜单已绑定到RegistryObject MY_BLOCK +@Override +public boolean stillValid(Player player) { + return AbstractContainerMenu.stillValid(this.access, player, MY_BLOCK.get()); +} +``` + +### 数据的同步 + +一些数据需要同时出现在服务端和客户端上才能显示给玩家。为此,菜单实现了数据同步的基本层,以便在当前数据与上次同步到客户端的数据不匹配时进行同步。对于玩家来说,这是每个tick都会检查的。 + +Minecraft默认支持两种形式的数据同步:通过`Slot`进行的`ItemStack`同步和通过`DataSlot`进行的整数同步。`Slot`和`DataSlot`是保存对数据存储的引用的视图,假设操作有效,玩家可以在屏幕中修改这些数据存储。这些可以通过`#addSlot`和`#addDataSlot`在菜单的构造函数中添加。 + +!!! 注意 + 由于`Slot`使用的`Container`已被Forge弃用,取而代之的是使用[`IItemHandler`功能][cap],因此其余解释将围绕使用功能变体:`SlotItemHandler`展开。 + +`SlotItemHandler`包含四个参数:`IItemHandler`表示物品栈所在的物品栏,该Slot具体表示的物品栈索引,以及该Slot左上角将在屏幕上呈现的相对于`AbstractContainerScreen#leftPos`和`#topPos`的x和y位置。客户端菜单构造函数应该始终提供相同大小的物品栏的空实例。 + +在大多数情况下,菜单中包含的任何Slot都会首先添加,然后是玩家的物品栏,最后以玩家的快捷栏结束。要从菜单中访问任何单独的`Slot`,必须根据添加Slot的顺序计算索引。 + +`DataSlot`是一个抽象类,它应该实现getter和setter来引用存储在数据存储对象中的数据。客户端菜单构造函数应始终通过`DataSlot#standalone`提供一个新实例。 + +每次初始化新菜单时,都应该重新创建上述内容以及Slot。 + +!!! 警告 + 尽管`DataSlot`存储一个整数(int),但由于它在网络上发送数值的方式,它实际上被限制为**short**类型(-32768到32767)。该整数(int)的16个高比特位被忽略。 + +```java +// 假设我们有一个来自大小为5的数据对象的物品栏 +// 假设我们在每次初始化服务端菜单时都构造了一个DataSlot + +// 客户端菜单构造函数 +public MyMenuAccess(int containerId, Inventory playerInventory) { + this(containerId, playerInventory, new ItemStackHandler(5), DataSlot.standalone()); +} + +// 服务端菜单构造函数 +public MyMenuAccess(int containerId, Inventory playerInventory, IItemHandler dataInventory, DataSlot dataSingle) { + // 检查数据物品栏大小是否为某个固定值 + // 然后,为数据物品栏添加Slot + this.addSlot(new SlotItemHandler(dataInventory, /*...*/)); + + // 为玩家物品栏添加Slot + this.addSlot(new Slot(playerInventory, /*...*/)); + + // 为被处理的整数添加Slot + this.addDataSlot(dataSingle); + + // ... +} +``` + +#### `ContainerData` + +如果需要将多个整数同步到客户端,则可以使用一个`ContainerData`来引用这些整数。此接口用作索引查找,以便每个索引表示不同的整数。如果通过`#addDataSlots`将`ContainerData`添加到菜单中,则也可以在数据对象本身中构造`ContainerData`。该方法为接口指定量的数据创建一个新的`DataSlot`。客户端菜单构造函数应始终通过`SimpleContainerData`提供一个新实例。 + +```java +// 假设我们有一个大小为3的ContainerData + +// 客户端菜单构造函数 +public MyMenuAccess(int containerId, Inventory playerInventory) { + this(containerId, playerInventory, new SimpleContainerData(3)); +} + +// 服务端菜单构造函数 +public MyMenuAccess(int containerId, Inventory playerInventory, ContainerData dataMultiple) { + // 检查ContainerData大小是否为某个固定值 + checkContainerDataCount(dataMultiple, 3); + + // 为被处理的整数添加Slot + this.addDataSlots(dataMultiple); + + // ... +} +``` + +!!! 警告 + 由于`ContainerData`委托`DataSlot`,这些整数也被限制为**short**(-32768到32767)。 + +#### `#quickMoveStack` + +`#quickMoveStack`是任何菜单都必须实现的第二个方法。每当物品栈被Shift单击或快速移出其当前Slot,直到物品栈完全移出其上一个Slot,或者物品栈没有其他位置可去时,就会调用此方法。该方法返回正在快速移动的Slot中物品栈的一个副本。 + +物品栈通常使用`#moveItemStackTo`在Slot之间移动,它将物品栈移动到第一个可用的Slot中。它接受要移动的物品栈、尝试将物品栈移动到的第一个Slot的索引(包括)、最后一个Slot的索引,以及是以从第一个到最后一个(当`false`时)还是从最后一个到第一个(当`true`时)的顺序检查Slot。 + +在Minecraft的实现中,这种方法的逻辑相当一致: + +```java +// 假设我们有一个大小为5的数据物品栏 +// 该物品栏有4个输入(索引1 - 4)并输出到一个结果Slot(索引0) +// 我们也有27个玩家物品栏Slot和9个快捷栏Slot +// 这样,真正的Slot索引按如下编排: +// - 数据物品栏:结果(0),输入(1 - 4) +// - 玩家物品栏(5 - 31) +// - 玩家快捷栏(32 - 40) +@Override +public ItemStack quickMoveStack(Player player, int quickMovedSlotIndex) { + // 快速移动的Slot的物品栈 + ItemStack quickMovedStack = ItemStack.EMPTY; + // 快速移动的Slot + Slot quickMovedSlot = this.slots.get(quickMovedSlotIndex) + + // 如果该Slot在合理范围内且不为空 + if (quickMovedSlot != null && quickMovedSlot.hasItem()) { + // 获取原始物品栈以用于移动 + ItemStack rawStack = quickMovedSlot.getItem(); + // 将Slot物品栈设置为该原始物品栈的副本 + quickMovedStack = rawStack.copy(); + + /* + 以下快速移动逻辑可以简化为:如果在数据物品栏中,尝试移动到玩家物品栏/快捷栏, + 反之亦然,对于无法转换数据的容器(例如箱子)。 + */ + + // 如果快速移动在数据物品栏的结果Slot上进行 + if (quickMovedSlotIndex == 0) { + // 尝试将结果Slot移入玩家物品栏/快捷栏 + if (!this.moveItemStackTo(rawStack, 5, 41, true)) { + // 如果无法移动,就不再进行快速移动 + return ItemStack.EMPTY; + } + + // 执行Slot的快速移动逻辑 + slot.onQuickCraft(rawStack, quickMovedStack); + } + // 否则如果快速移动在玩家物品栏或快捷栏Slot上进行 + else if (quickMovedSlotIndex >= 5 && quickMovedSlotIndex < 41) { + // 尝试将物品栏/快捷栏Slot移入数据物品栏输入Slot + if (!this.moveItemStackTo(rawStack, 1, 5, false)) { + // 如果无法移动且在玩家物品栏Slot内,尝试移入快捷栏 + if (quickMovedSlotIndex < 32) { + if (!this.moveItemStackTo(rawStack, 32, 41, false)) { + // 如果无法移动,就不再进行快速移动 + return ItemStack.EMPTY; + } + } + // 否则就尝试将快捷栏移入玩家物品栏Slot + else if (!this.moveItemStackTo(rawStack, 5, 32, false)) { + // 如果无法移动,就不再进行快速移动 + return ItemStack.EMPTY; + } + } + } + // 否则如果快速移动在数据物品栏的输入Slot上进行,尝试将其移入玩家物品栏/快捷栏 + else if (!this.moveItemStackTo(rawStack, 5, 41, false)) { + // 如果无法移动,就不再进行快速移动 + return ItemStack.EMPTY; + } + + if (rawStack.isEmpty()) { + // 如果原始物品栈已完全移出当前Slot,将该Slot置空 + quickMovedSlot.set(ItemStack.EMPTY); + } else { + // 否则,通知该Slot物品栈数量已改变 + quickMovedSlot.setChanged(); + } + + /* + 如果菜单不表示可以转换物品栈的容器(例如箱子),则可以删除以下if语句和 + Slot#onTake调用。 + */ + if (rawStack.getCount() == quickMovedStack.getCount()) { + // 如果原始物品栈不能被移动到另一个Slot,就不再进行快速移动 + return ItemStack.EMPTY; + } + // 执行剩余物品栈的移动后逻辑 + quickMovedSlot.onTake(player, rawStack); + } + + return quickMovedStack; // 返回该Slot物品栈 +} +``` + +## 打开菜单 + +一旦注册了菜单类型,菜单本身已经完成,并且一个[屏幕(Screen)][screen]已被附加,玩家就可以打开菜单。可以通过在逻辑服务端上调用`NetworkHooks#openScreen`来打开菜单。该方法让玩家打开菜单,服务端端菜单的`MenuProvider`,如果需要将额外数据同步到客户端,还可以选择`FriendlyByteBuf`。 + +!!! 注意 + 只有在使用[`IContainerFactory`][icf]创建菜单类型时,才应使用带有`FriendlyByteBuf`参数的`NetworkHooks#openScreen`。 + +#### `MenuProvider` + +`MenuProvider`是一个包含两个方法的接口:`#createMenu`和`#getDisplayName`,前者创建菜单的服务端实例,后者返回一个包含要传递到[屏幕(Screen)][screen]的菜单标题的组件。`#createMenu`方法包含三个参数:菜单的容器id、打开菜单的玩家的物品栏以及打开菜单的玩家。 + +使用`SimpleMenuProvider`可以很容易地创建`MenuProvider`,它采用方法引用来创建服务端菜单和菜单标题。 + +```java +// 在某种实现中 +NetworkHooks.openScreen(serverPlayer, new SimpleMenuProvider( + (containerId, playerInventory, player) -> new MyMenu(containerId, playerInventory), + Component.translatable("menu.title.examplemod.mymenu") +)); +``` + +### 常见的实现 + +菜单通常在某种玩家交互时打开(例如,当右键单击方块或实体时)。 + +#### 方块的实现 + +方块块通常通过重写`BlockBehaviour#use`来实现菜单。如果在逻辑客户端上,则交互返回`InteractionResult#SUCCESS`。否则,它将打开菜单并返回`InteractionResult#CONSUME`。 + +应通过重写`BlockBehaviour#getMenuProvider`来实现`MenuProvider`。原版方法使用这个来显示旁观者模式下的菜单。 + +```java +// 在某个Block的子类中 +@Override +public MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos) { + return new SimpleMenuProvider(/* ... */); +} + +@Override +public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult result) { + if (!level.isClientSide && player instanceof ServerPlayer serverPlayer) { + NetworkHooks.openScreen(serverPlayer, state.getMenuProvider(level, pos)); + } + return InteractionResult.sidedSuccess(level.isClientSide); +} +``` + +!!! 注意 + 这是实现逻辑的最简单的方法,而不是唯一的方法。如果你希望方块仅在特定条件下打开菜单,则需要提前将一些数据同步到客户端,以便在不满足条件的情况下返回`InteractionResult#PASS`或`#FAIL`。 + +#### 生物的实现 + +Mob通常通过重写`Mob#mobInteract`来实现菜单。这与方块实现类似,唯一的区别是`Mob`本身应该实现`MenuProvider`以支持旁观者模式下的显示。 + +```java +public class MyMob extends Mob implements MenuProvider { + // ... + + @Override + public InteractionResult mobInteract(Player player, InteractionHand hand) { + if (!this.level.isClientSide && player instanceof ServerPlayer serverPlayer) { + NetworkHooks.openScreen(serverPlayer, this); + } + return InteractionResult.sidedSuccess(this.level.isClientSide); + } +} +``` + +!!! 注意 + 再次说明,这是实现逻辑的最简单的方法,而不是唯一的方法。 + +[registered]: ../concepts/registries.md#methods-for-registering +[acm]: #abstractcontainermenu +[mt]: #menutype +[qms]: #quickmovestack +[cap]: ../datastorage/capabilities.md#forge-provided-capabilities +[screen]: ./screens.md +[icf]: #icontainerfactory diff --git a/docs/translation/zh_CN/gui/screens.md b/docs/translation/zh_CN/gui/screens.md new file mode 100644 index 000000000..f4c4daeff --- /dev/null +++ b/docs/translation/zh_CN/gui/screens.md @@ -0,0 +1,334 @@ +# 屏幕(Screens) + +屏幕通常是Minecraft中所有图形用户界面(GUI)的基础:接收用户输入,在服务端上验证,并将生成的操作同步回客户端。它们可以与[菜单(Menus)][menus]相结合,为类似物品栏的视图创建通信网络,也可以是独立的,模组开发者可以通过自己的[网络][network]实现来处理。 + +屏幕由许多部分组成,因此很难完全理解Minecraft中的“屏幕”到底是什么。因此,在讨论屏幕本身之前,本文档将介绍屏幕的每个组件及其应用方式。 + +## 相对坐标 + +每当渲染任何东西时,都需要有一些标识符来指定它将出现的位置。通过大量的抽象,Minecraft的大多数渲染调用都在坐标平面中采用x、y和z值。x值从左到右递增,y从上到下递增,z从远到近递增。但是,坐标并不是固定在指定的范围内。它们可以根据屏幕的大小和选项中指定的比例进行更改。因此,在渲染时必须格外小心,以确保坐标值正确缩放到可更改的屏幕大小。 + +关于如何将坐标相对化的信息将在[屏幕][screen]部分中呈现。 + +!!! 重要 + 如果选择使用固定坐标或不正确地缩放屏幕,则渲染的对象可能看起来很奇怪或错位。检查坐标是否正确相对化的一个简单方法是单击视频设置中的“Gui比例”按钮。在确定GUI渲染的比例时,此值用作显示器宽度和高度的除数。 + +## Gui图形 + +Minecraft渲染的任何GUI通常都是使用`GuiGraphics`完成的。`GuiGraphics`是几乎所有渲染方法的第一个参数;它包含渲染常用对象的基本方法。它们分为五类:彩色矩形、字符串、纹理、物品和提示信息。还有一种用于呈现组件片段的附加方法(`#enableScissor`/`#disableScissor`)。`GuiGraphics`还公开了`PoseStack`,它应用了正确渲染组件所需的转换。此外,颜色采用[ARGB][argb]格式。 + +### 彩色矩形 + +彩色矩形是通过位置颜色着色器绘制的。有三种类型的彩色矩形可以绘制。 + +首先,有一条彩色的水平和垂直一像素宽的线,分别为`#hLine`和`#vLine`。`#hLine`接受两个x坐标,定义左侧和右侧(包括)、顶部y坐标和颜色。`#vLine`接受左侧的x坐标、定义顶部和底部(包括)的两个y坐标以及颜色。 + +其次,还有`#fill`方法,它在屏幕上绘制一个矩形。Line方法在内部调用此方法。其接受左x坐标、上y坐标、右x坐标、下y坐标和颜色。 + +最后,还有`#fillGradient`方法,它绘制一个具有垂直梯度的矩形。这包括右x坐标、下y坐标、左x坐标、上y坐标、z坐标以及底部和顶部的颜色。 + +### 字符串 + +字符串是通过其`Font`绘制的,通常由它们自己的普通、透视和偏移模式的着色器组成。可以渲染两种对齐的字符串,每种都有一个后阴影:左对齐字符串(`#drawString`)和居中对齐字符串(`#drawCenteredString`)。这两者都采用了字符串将被渲染的字体、要绘制的字符串、分别表示字符串左侧或中心的x坐标、顶部的y坐标和颜色。 + +!!! 注意 + 字符串通常应作为[`Component`][component]传入,因为它们处理各种用例,包括方法的另外两个重载。 + +### 纹理 + +纹理是通过blitting的方式绘制的,因此方法名为`#blit`,为此,它复制图像的比特并将其直接绘制到屏幕上。这些是通过位置纹理着色器绘制的。虽然有许多不同的`#blit`重载,但我们只讨论两个静态的`#blit`。 + +第一个静态`#blit`取六个整数,并假设渲染的纹理位于256 x 256 PNG文件上。它接受左侧x和顶部y屏幕坐标,PNG中的左侧x和底部y坐标,以及要渲染的图像的宽度和高度。 + +!!! 注意 + 必须指定PNG文件的大小,以便可以规范化坐标以获得关联的UV值。 + +第一个`#blit`所调用的另一个静态`#blit`将参数扩展为九个整数,仅假设图像位于PNG文件上。它获取左侧x和顶部y屏幕坐标、z坐标(称为blit偏移)、PNG中的左侧x和上部y坐标、要渲染的图像的宽度和高度以及PNG文件的宽度和高。 + +#### Blit偏移 + +渲染纹理时的z坐标通常设置为blit偏移。偏移量负责在查看屏幕时对渲染进行适当分层。z坐标较小的渲染在背景中渲染,反之亦然,z坐标较大的渲染在前景中渲染。z偏移量可以通过`#translate`直接设置在`PoseStack`本身上。一些基本的偏移逻辑在`GuiGraphics`的某些方法(例如物品渲染)中内部应用。 + +!!! 重要 + 设置blit偏移时,必须在渲染对象后重置它。否则,屏幕内的其他对象可能会在不正确的层中渲染,从而导致图形问题。建议在平移前推动当前姿势,然后在偏移处完成所有渲染后弹出。 + +## Renderable + +`Renderable`本质上是被渲染的对象。其中包括屏幕、按钮、聊天框、列表等。`Renderable`只有一个方法:`#render`。这需要用于将十五渲染到屏幕上的`GuiGraphics`,以正确渲染可渲染的、缩放到相对屏幕大小的鼠标的x和y位置,以及游戏刻增量(自上一帧以来经过了多少游戏刻)。 + +一些常见的可渲染文件是屏幕和“小部件”:通常在屏幕上渲染的可交互元素,如`Button`、其子类型`ImageButton`和用于在屏幕上输入文本的`EditBox`。 + +## GuiEventListener + +在Minecraft中呈现的任何屏幕都实现了`GuiEventListener`。`GuiEventListener`负责处理用户与屏幕的交互。其中包括来自鼠标(移动、单击、释放、拖动、滚动、鼠标悬停)和键盘(按下、释放、键入)的输入。每个方法都返回关联的操作是否成功影响了屏幕。按钮、聊天框、列表等小工具也实现了这个界面。 + +### ContainerEventHandler + +与`GuiEventListener`几乎同义的是它们的子类型:`ContainerEventHandler`。它们负责处理包含小部件的屏幕上的用户交互,管理当前聚焦的内容以及相关交互的应用方式。`ContainerEventHandler`添加了三个附加功能:可交互的子项、拖动和聚焦。 + +事件处理器包含用于确定元素交互顺序的子级。在鼠标事件处理器(不包括拖动)期间,鼠标悬停的列表中的第一个子级将执行其逻辑。 + +用鼠标拖动元素,通过`#mouseClicked`和`#mouseReleased`实现,可以提供更精确的执行逻辑。 + +聚焦允许在事件执行期间,例如在键盘事件或拖动鼠标期间,首先检查并处理特定的子项。焦点通常通过`#setFocused`设置。此外,可以使用`#nextFocusPath`循环可交互的子级,根据传入的`FocusNavigationEvent`选择子级。 + +!!! 注意 + 屏幕通过`AbstractContainerEventHandler`实现了`ContainerEventHandler`和`GuiComponent`,添加了setter和getter逻辑用于拖动和聚焦子级。 + +## NarratableEntry + +`NarratableEntry`是可以通过Minecraft的无障碍讲述功能进行讲述的元素。每个元素可以根据悬停或选择的内容提供不同的叙述,通常按焦点、悬停以及所有其他情况进行优先级排序。 + +`NarratableEntry`有三种方法:一种是确定元素的优先级(`#narrationPriority`),一种是决定是否说出讲述(`#isActive`),最后一种是将讲述提供给相关的输出(说出或读取)(`#updateNarration`)。 + +!!! 注意 + Minecraft中的所有小部件都是`NarratableEntry`,因此如果使用可用的子类型,通常不需要手动实现。 + +## 屏幕子类型 + +利用以上所有知识,可以构建一个简单的屏幕。为了更容易理解,屏幕的组件将按通常遇到的顺序提及。 + +首先,所有屏幕都包含一个`Component`,其表示屏幕的标题。此组件通常由其子类型之一绘制到屏幕上。它仅用于讲述消息的基本屏幕。 + +```java +// 在某个Screen子类中 +public MyScreen(Component title) { + super(title); +} +``` + +### 初始化 + +一旦屏幕被初始化,就会调用`#init`方法。`#init`方法将屏幕内的初始设置从`ItemRenderer`和`Minecraft`实例设置为游戏缩放的相对宽度和高度。任何设置,如添加小部件或预计算相对坐标,都应该用这种方法完成。如果调整游戏窗口的大小,屏幕将通过调用`#init`方法重新初始化。 + +有三种方法可以将小部件添加到屏幕中,每种方法都有各自的用途: + +方法 | 描述 +:---: | :--- +`#addWidget` | 添加一个可交互和讲述但不被渲染的小部件。 +`#addRenderableOnly` | 添加一个只会被渲染的小部件;它既不可互动,也不可被讲述。 +`#addRenderableWidget` | 添加一个可交互、讲述和被渲染的小部件。 + +通常,`#addRenderableWidget`将是最常用的。 + +```java +// 在某个Screen子类中 +@Override +protected void init() { + super.init(); + + // 添加小部件和已预计算的值 + this.addRenderableWidget(new EditBox(/* ... */)); +} +``` + +### 计时屏幕 + +屏幕也会使用`#tick`方法计时来执行某种级别的客户端逻辑以进行渲染。最常见的例子是`EditBox`的光标闪烁。 + +```java +// 在某个Screen子类中 +@Override +public void tick() { + super.tick(); + + // 在editBox中为EditBox添加计时逻辑 + this.editBox.tick(); +} +``` + +### 输入处理 + +由于屏幕是`GuiEventListener`的子类型,输入处理器也可以被覆盖,例如用于处理特定[按键][keymapping]上的逻辑。 + +### 屏幕的渲染 + +最后,屏幕是通过作为`Renderable`子类型提供的`#render`方法进行渲染的。如前所述,`#render`方法绘制屏幕必须渲染每一帧的所有内容,如背景、小部件、提示文本等。默认情况下,`#render`方法仅将小部件渲染到屏幕上。 + +在通常不由子类型处理的屏幕中渲染的两件最常见的事情是背景和提示文本。 + +背景可以使用`#renderBackground`进行渲染,其中一种方法在无法渲染屏幕后面的级别时,每当渲染屏幕时,都会将v偏移值作为选项背景。 + +提示文本通过`GuiGraphics#renderTooltip`或`GuiGraphics#renderComponentTooltip`进行渲染,它们可以接受正在渲染的文本组件、可选的自定义提示文本示组件以及提示文本应在屏幕上渲染的x/y相对坐标。 + +```java +// 在某个Screen子类中 + +// mouseX和mouseY指示鼠标光标在屏幕上的缩放坐标 +@Override +public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + // 通常首先渲染背景 + this.renderBackground(graphics); + + // 在此处渲染在小部件之前渲染的内容(背景纹理) + + // 然后是窗口小部件,如果这是Screen的直接子项 + super.render(graphics, mouseX, mouseY, partialTick); + + // 在小部件之后渲染的内容(工具提示) +} +``` + +### 屏幕的关闭 + +当屏幕关闭时,有两种方法处理屏幕的关闭:`#onClose`和`#removed`。 + +每当用户做出关闭当前屏幕的输入时,就会调用`#onClose`。此方法通常用作回调,以销毁和保存屏幕本身中的任何内部进程。这包括向服务端发送数据包。 + +`#removed`在屏幕更改并被释放到垃圾收集器之前被调用。这将处理任何尚未重置回屏幕打开前初始状态的内容。 + +```java +// 在某个Screen子类中 + +@Override +public void onClose() { + // 在此处停止任何处理器 + + // 最后调用,以防干扰重写后的方法体 + super.onClose(); +} + +@Override +public void removed() { + // 在此处重置初始状态 + + // 最后调用,以防干扰重写后的方法体 + super.removed() +;} +``` + +## `AbstractContainerScreen` + +如果一个屏幕直接连接到[菜单(Menu)][menus],那么其应改为继承`AbstractContainerScreen`。`AbstractContainerScreen`充当菜单的渲染器和输入处理程序,包含用于与Slot同步和交互的逻辑。因此,通常只需要重写或实现两个方法就可以拥有一个可工作的容器屏幕。同样,为了更容易理解,容器屏幕的组件将按通常遇到的顺序提及。 + +`AbstractContainerScreen`通常需要三个参数:打开的容器菜单(用泛型`T`表示)、玩家物品栏(仅用于显示名称)和屏幕本身的标题。在这里,可以设置多个定位字段: + +字段 | 描述 +:---: | :--- +`imageWidth` | 用于背景的纹理的宽度。这通常位于256 x 256的PNG中,默认值为176。 +`imageHeight` | 用于背景的纹理的高度。这通常位于256 x 256的PNG中,默认值为166。 +`titleLabelX` | 将渲染屏幕标题的位置的相对x坐标。 +`titleLabelY` | 将渲染屏幕标题的位置的相对y坐标。 +`inventoryLabelX` | 将渲染玩家物品栏名称的位置的相对x坐标。 +`inventoryLabelY` | 将渲染玩家物品栏名称的位置的相对y坐标。 + +!!! 重要 + 在上一节中提到应该在`#init`方法中设置预先计算的相对坐标。这仍然保持正确,因为这里提到的值不是预先计算的坐标,而是静态值和相对坐标。 + + 图像值是静态的且不变,因为它们表示背景纹理大小。为了在渲染时更容易,在`#init`方法中预先计算了两个附加值(`leftPos`和`topPos`),该方法标记了将渲染背景的左上角。标签坐标相对于这些值。 + + `leftPos`和`topPos`也被用作渲染背景的方便方式,因为它们已经表示要传递到`#blit`方法中的位置。 + +```java +// 在某个AbstractContainerScreen子类中 +public MyContainerScreen(MyMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + + this.titleLabelX = 10; + this.inventoryLabelX = 10; + + /* + * 如果'imageHeight'已更改,则还必须更改'inventoryLabelY',因为该值取决于'imageHeight'值。 + */ +} +``` + +### 屏幕的访问 + +当菜单被传递给屏幕时,菜单中的任何值(通过Slot、数据Slot或自定义系统)都可以通过`menu`字段访问。 + +### 容器的计时 + +当玩家活着并通过`#containerTick`查看屏幕时,容器屏幕在`#tick`方法中计时。这基本上取代了容器屏幕中的`#tick`,其最常见的用法是在配方书中计时。 + +```java +// 在某个AbstractContainerScreen子类中 +@Override +protected void containerTick() { + super.containerTick(); + + // 在此处对某些事计时 +} +``` + +### 容器屏幕的渲染 + +容器屏幕通过三种方法进行渲染:`#renderBg`,用于渲染背景纹理;`#renderLabels`,用于在背景顶部渲染任何文本;以及`#render`,除了提供灰色背景和提示文本外,还包含前两种方法。 + +从`#render`开始,最常见的重写(通常是唯一的情况)是添加背景,调用super来渲染容器屏幕,以及最后在其顶部渲染提示文本。 + +```java +// 在某个AbstractContainerScreen子类中 +@Override +public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + this.renderBackground(graphics); + super.render(graphics, mouseX, mouseY, partialTick); + + /* + * 该方法由容器屏幕添加,用于渲染悬停在其上的任何Slot的提示文本。 + */ + this.renderTooltip(graphics, mouseX, mouseY); +} +``` + +在super中,`#renderBg`被调用以渲染屏幕的背景。最标准的代表是使用三个方法调用:两个用于设置,一个用于绘制背景纹理。 + +```java +// 在某个AbstractContainerScreen子类中 + +// 背景纹理的位置(assets//) +private static final ResourceLocation BACKGROUND_LOCATION = new ResourceLocation(MOD_ID, "textures/gui/container/my_container_screen.png"); + +@Override +protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { + /* + * 将背景纹理渲染到屏幕上。'leftPos'和'topPos'应该已经表示纹理应该渲染 + * 的左上角,因为它是根据'imageWidth'和'imageHeight'预计算的。两个零 + * 表示256 x 256 PNG文件中的整数u/v坐标。 + */ + graphics.blit(BACKGROUND_LOCATION, this.leftPos, this.topPos, 0, 0, this.imageWidth, this.imageHeight); +} +``` + +最后,调用`#renderLabels`来渲染背景上方但提示文本下方的任何文本。这个简单的调用使用字体来绘制相关的组件。 + +```java +// 在某个AbstractContainerScreen子类中 +@Override +protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { + super.renderLabels(graphics, mouseX, mouseY); + + // 假设我们有个组件'label' + // 'label'在'labelX'和'labelY'处被绘制 + graphics.drawString(this.font, this.label, this.labelX, this.labelY, 0x404040); +} +``` + +!!! 注意 + 渲染标签时,**不**需要指定`leftPos`和`topPos`偏移量。这些已经在`PoseStack`中进行了转换,因此该方法中的所有内容都是相对于这些坐标绘制的。 + +## 注册一个AbstractContainerScreen + +要将`AbstractContainerScreen`与菜单一起使用,需要对其进行注册。这可以通过调用[**模组事件总线**][modbus]上的`FMLClientSetupEvent`中的`MenuScreens#register`来完成。 + +```java +// 该事件已在模组事件总线上被监听 +private void clientSetup(FMLClientSetupEvent event) { + event.enqueueWork( + // 假设:RegistryObject> MY_MENU + // 假设MyContainerScreen,其接受三个参数 + () -> MenuScreens.register(MY_MENU.get(), MyContainerScreen::new) + ); +} +``` + +!!! 警告 + `MenuScreens#register`不是线程安全的,因此它需要在并行调度事件提供的`#enqueueWork`内部调用。 + +[menus]: ./menus.md +[network]: ../networking/index.md +[screen]: #the-screen-subtype +[argb]: https://en.wikipedia.org/wiki/RGBA_color_model#ARGB32 +[component]: ../concepts/internationalization.md#translatablecontents +[keymapping]: ../misc/keymappings.md#inside-a-gui +[modbus]: ../concepts/events.md#mod-event-bus diff --git a/docs/translation/zh_CN/index.md b/docs/translation/zh_CN/index.md new file mode 100644 index 000000000..38c4800ff --- /dev/null +++ b/docs/translation/zh_CN/index.md @@ -0,0 +1,114 @@ +NeoForged 文档 中文翻译 +============================== + +:::info +欢迎访问[NeoForged文档中文翻译官方仓库][translation-repo],对我们的翻译内容提出意见或建议。 +::: + +# 前言 + +:::caution +请注意,由于NeoForged处于创始阶段,本文档可能未紧跟最新版本。 +::: + +这里是Minecraft模组API——[NeoForged]的官方文档。 + +该文档 _仅_ 针对Forge编纂,**而不是一个Java教程**。 + +如果你愿意对文档做出贡献,请阅读[向文档做出贡献][contributing]。 + +# 目录 + +- [主页](./index.md) +- [向文档做出贡献](./contributing.md) +- 入门 + - [概述](./gettingstarted/index.md) + - [模组文件](./gettingstarted/modfiles.md) + - [规划你的模组结构](./gettingstarted/structuring.md) + - [版本号](./gettingstarted/versioning.md) +- 核心概念 + - [注册表](./concepts/registries.md) + - [端位(Sides)](./concepts/sides.md) + - [事件](./concepts/events.md) + - [模组生命周期](./concepts/lifecycle.md) + - [资源](./concepts/resources.md) + - [国际化与本地化](./concepts/internationalization.md) +- 方块 + - [概述](./blocks/index.md) + - [方块状态](./blocks/states.md) +- 物品 + - [概述](./items/index.md) + - [BlockEntityWithoutLevelRenderer](./items/bewlr.md) +- 网络 + - [概述](./networking/index.md) + - [SimpleImpl](./networking/simpleimpl.md) + - [实体的同步](./networking/entities.md) +- 方块实体 + - [概述](./blockentities/index.md) + - [BlockEntityRenderer](./blockentities/ber.md) +- 游戏特效 + - [粒子效果](./gameeffects/particles.md) + - [音效](./gameeffects/sounds.md) +- 数据储存 + - [Capabilities](./datastorage/capabilities.md) + - [Saved Data](./datastorage/saveddata.md) + - [编解码器(Codecs)](./datastorage/codecs.md) +- 图形用户界面 + - [菜单(Menus)](./gui/menus.md) + - [屏幕(Screens)](./gui/screens.md) +- 渲染 + - 模型扩展 + - [概述](./rendering/modelextensions/index.md) + - [根变换](./rendering/modelextensions/transforms.md) + - [渲染类型](./rendering/modelextensions/rendertypes.md) + - [部分可见度](./rendering/modelextensions/visibility.md) + - [面数据](./rendering/modelextensions/facedata.md) + - 模型加载器 + - [概述](./rendering/modelloaders/index.md) + - [烘焙模型](./rendering/modelloaders/bakedmodel.md) + - [变换](./rendering/modelloaders/transform.md) + - [物品重载](./rendering/modelloaders/itemoverrides.md) +- 资源 + - 客户端资源(Assets) + - [概述](./resources/client/index.md) + - 配方 + - [概述](./resources/server/recipes/index.md) + - [自定义配方](./resources/server/recipes/custom.md) + - [原料](./resources/server/recipes/ingredients.md) + - [非数据包配方](./resources/server/recipes/incode.md) + - [战利品表](./resources/server/loottables.md) + - [全局战利品修改器](./resources/server/glm.md) + - [标签](./resources/server/tags.md) + - [进度](./resources/server/advancements.md) + - [条件性加载数据](./resources/server/conditional.md) +- 数据生成 + - [概述](./datagen/index.md) + - 客户端资源(Assets) + - [模型提供者](./datagen/client/modelproviders.md) + - [语言提供者](./datagen/client/localization.md) + - [音效提供者](./datagen/client/sounds.md) + - 服务端数据(Data) + - [配方提供者](./datagen/server/recipes.md) + - [战利品表提供者](./datagen/server/loottables.md) + - [标签提供者](./datagen/server/tags.md) + - [进度提供者](./datagen/server/advancements.md) + - [全局战利品修改器提供者](./datagen/server/glm.md) + - [数据包注册表对象提供者](./datagen/server/datapackregistries.md) +- 杂项功能 + - [配置](./misc/config.md) + - [键盘布局](./misc/keymappings.md) + - [游戏测试](./misc/gametest.md) + - [Forge更新检查器](./misc/updatechecker.md) + - [调试分析器](./misc/debugprofiler.md) +- 进阶主题 + - [访问转换器](./advanced/accesstransformers.md) +- 向Forge做出贡献 + - [概述](./forgedev/index.md) + - [Pull Request准则](./forgedev/prguidelines.md) +- 旧版本 + - [概述](./legacy/index.md) + - [移植到当前版本](./legacy/porting.md) + +[translation-repo]: https://github.com/srcres258/neo-doc +[NeoForged]: https://neoforged.net +[contributing]: ./contributing.md diff --git a/docs/translation/zh_CN/items/bewlr.md b/docs/translation/zh_CN/items/bewlr.md new file mode 100644 index 000000000..ef7d5c23d --- /dev/null +++ b/docs/translation/zh_CN/items/bewlr.md @@ -0,0 +1,34 @@ +BlockEntityWithoutLevelRenderer +=============================== +`BlockEntityWithoutLevelRenderer`是一种处理物品的动态渲染的方法。这个系统比旧的`ItemStack`系统简单得多,旧的`ItemStack`系统需要`BlockEntity`,并且不允许访问`ItemStack`。 + +使用BlockEntityWithoutLevelRenderer +----------------------------------- + +BlockEntityWithoutLevelRenderer允许你使用`public void renderByItem(ItemStack itemStack, ItemDisplayContext ctx, PoseStack poseStack, MultiBufferSource bufferSource, int combinedLight, int combinedOverlay)`来渲染物品。 + +为了使用BEWLR,`Item`必须首先满足其模型的`BakedModel#isCustomRenderer`返回true。如果没有,它将使用默认的`ItemRenderer#getBlockEntityRenderer`。一旦返回true,将访问该Item的BEWLR进行渲染。 + +!!! 注意 + 如果`Block#getRenderShape`设置为`RenderShape#ENTITYBLOCK_ANIMATED`,`Block`也会使用BEWLR进行渲染。 + +若要设置物品的BEWLR,必须在`Item#initializeClient`中使用`IClientItemExtensions`的一个匿名实例。在该匿名实例中,应重写`IClientItemExtensions#getCustomRenderer`以返回你的BEWLR的实例: + +```java +// 在你的物品类中 +@Override +public void initializeClient(Consumer consumer) { + consumer.accept(new IClientItemExtensions() { + + @Override + public BlockEntityWithoutLevelRenderer getCustomRenderer() { + return myBEWLRInstance; + } + }); +} +``` + +!!! 重要 + 每个模组都应该只有一个自定义BEWLR的实例。 + +这就行了,使用BEWLR不需要额外的设置。 diff --git a/docs/translation/zh_CN/items/index.md b/docs/translation/zh_CN/items/index.md new file mode 100644 index 000000000..7b5813e63 --- /dev/null +++ b/docs/translation/zh_CN/items/index.md @@ -0,0 +1,73 @@ +物品 +==== + +与方块一样,物品也是大多数模组的关键组成部分。方块在构成了你身边的存档的同时,物品也存在于物品栏中。 + +创建一个物品 +----------- + +### 基础物品 + +对于不需要特殊功能的简单物品(比如木棍或糖),不必自定义一个类。你可以通过使用`Item$Properties`对象实例化`Item`类来创建一个物品。这个`Item$Properties`对象可以通过调用其构造函数生成并通过调用其方法进行自定义。例如: + +| 方法 | 描述 | +|:------------------:|:----------------------------------------------| +| `requiredFeatures` | 设置在所添加到的`CreativeModeTab`中查看此物品所需的`FeatureFlag`。 | +| `durability` | 设置该物品的最大耐久。如果超过`0`,两个物品属性“damaged”和“damage”会被添加。 | +| `stacksTo` | 设置最大物品栈大小。你不能拥有一件既有耐久又可堆叠的物品。 | +| `setNoRepair` | 使此物品无法修复,即使它是有耐久的。 | +| `craftRemainder` | 设置该物品的容器物品,即熔岩桶在使用后将空桶还给你的方式。 | + +上面的方法是可链接的,这意味着它们`return this`以便于串行调用它们。 + +### 进阶物品 + +如上所述设置物品属性的方式仅适用于简单物品。如果你想要更复杂的物品,你应该继承`Item`类并重写其方法。 + +## 创造模式物品栏 + +可以通过[模组事件总线][modbus]上的`BuildCreativeModeTabContentsEvent`将物品添加到`CreativeModeTab`。可以通过`#accept`添加物品,而无需任何其他配置。 + +```java +// 已在模组事件总线上注册 +// 假设我们有一个名为ITEM的RegistryObject和一个名为BLOCK的RegistryObject +@SubscribeEvent +public void buildContents(BuildCreativeModeTabContentsEvent event) { + // 添加到ingredients创造模式物品栏 + if (event.getTabKey() == CreativeModeTabs.INGREDIENTS) { + event.accept(ITEM); + event.accept(BLOCK); // 接受一个ItemLike,假设方块已注册其物品 + } +} +``` + +你还可以通过`FeatureFlagSet`中的`FeatureFlag`或一个用于确定玩家是否有权查看管理员创造模式物品栏的boolean值来启用或禁用物品。 + +### 自定义创造模式物品栏 + +自定义`CreativeModeTab`必须[已被注册][registering]。生成器可以通过`CreativeModeTab#builder`创建。选项卡可以设置标题、图标、默认物品和许多其他属性。此外,Forge还提供了额外的方法来定制标签的图像、标签和插槽颜色,以及选项卡的排序位置等。 + +```java +// 假设我们有一个名为REGISTRAR的DeferredRegister +// 假设我们有一个名为ITEM的RegistryObject和一个名为BLOCK的RegistryObject +public static final RegistryObject EXAMPLE_TAB = REGISTRAR.register("example", () -> CreativeModeTab.builder() + // 设置所要展示的页的名称 + .title(Component.translatable("item_group." + MOD_ID + ".example")) + // 设置页图标 + .icon(() -> new ItemStack(ITEM.get())) + // 为物品栏页添加默认物品 + .displayItems((params, output) -> { + output.accept(ITEM.get()); + output.accept(BLOCK.get()); + }) + .build() +); +``` + +注册一个物品 +----------- + +物品必须经过[注册][registering]后才能发挥作用。 + +[modbus]: ../concepts/events.md#mod-event-bus +[registering]: ../concepts/registries.md#methods-for-registering diff --git a/docs/translation/zh_CN/legacy/index.md b/docs/translation/zh_CN/legacy/index.md new file mode 100644 index 000000000..9a53959b7 --- /dev/null +++ b/docs/translation/zh_CN/legacy/index.md @@ -0,0 +1,29 @@ +旧版本的文档 +=========== + +Forge已经存在多年了,你仍然可以轻松访问Minecraft 1.1版本的Forge版本。每个版本之间都有显著的差异,支持这么多不同的版本是不可能的。因此,Forge使用了一个LTS系统,其中以前的主要Minecraft版本被视为“LTS”(长期支持)。只有最新版本和任何当前的LTS版本才会有易于访问的文档,并包含在侧边栏的版本下拉列表中。然而,一些旧版本曾经是LTS,或者在某个时候是最新版本,并编写了文档。可以在这里找到带有这些版本文档的旧网站链接。 + +!!! 重要 + 这些旧文档网站仅供参考。不要在Forge discord或Forge论坛上寻求旧版本的帮助。**当你使用旧版本时,将不会获得支持。** + +### 以前已有文档的版本列表 + +不幸的是,并非所有版本都使用了相当长的时间,那些版本的文档可能不完整。无论何时发布新版本,都会随着时间的推移复制和调整上一版本的文档,以包含新的和更新的信息。当某个版本长期不受支持时,信息永远不会更新。准确率百分比表示本应更新的信息实际更新了多少。 + +| 版本 | 准确率 | 链接 | +|:-------------:|:----------:|:------------------------------------------| +| 1.12.x | 100% | https://docs.minecraftforge.net/en/1.12.x/ | +| 1.13.x | 10% | https://docs.minecraftforge.net/en/1.13.x/ | +| 1.14.x | 10% | https://docs.minecraftforge.net/en/1.14.x/ | +| 1.15.x | 85% | https://docs.minecraftforge.net/en/1.15.x/ | +| 1.16.x | 85% | https://docs.minecraftforge.net/en/1.16.x/ | +| 1.17.x | 85% | https://docs.minecraftforge.net/en/1.17.x/ | +| 1.18.x | 90% | https://docs.minecraftforge.net/en/1.18.x/ | +| 1.19.2 | 90% | https://docs.minecraftforge.net/en/1.19.2/ | +| 1.19.x | 90% | https://docs.minecraftforge.net/en/1.19.x/ | + +### RetroGradle + +**RetroGradle**是一项档案计划,旨在更新旧版ForgeGradle 1.x至2.3工具链及其Minecraft版本,以使用现代ForgeGradle 4.x及以上工具链。目标是通过将Minecraft Forge的所有过去发布的版本移动到可验证的工作和现代工具链来保留它们,该工具链是数据驱动的,而不是针对特定版本的工作流进行硬编码。 + +如果任何开发人员希望为这项档案工作做出贡献,请访问The Forge Project discord服务器,并询问指定频道的方向。请注意,此计划仅旨在为社区的利益 _保留_ 这些旧版本, _而**不是**支持为这些不受支持的旧版本开发模组。_ 不支持使用或开发不受支持的版本。 diff --git a/docs/translation/zh_CN/legacy/porting.md b/docs/translation/zh_CN/legacy/porting.md new file mode 100644 index 000000000..5ff5796b5 --- /dev/null +++ b/docs/translation/zh_CN/legacy/porting.md @@ -0,0 +1,22 @@ +移植到Minecraft 1.20 +==================== + +在这里,你可以找到如何从旧版本移植到当前版本的入门资料列表。有些版本被集中在一起,因为那个特定的版本从未有过太多的用途。 + +| 从 -> 到 | 入门资料 | +|:-----------------:|:----------------------------------------| +| 1.12 -> 1.13/1.14 | [Primer by williewillus][112to114] | +| 1.14 -> 1.15 | [Primer by williewillus][114to115] | +| 1.15 -> 1.16 | [Primer by 50ap5ud5][115to116] | +| 1.16 -> 1.17 | [Primer by 50ap5ud5][116to117] | +| 1.19.2 -> 1.19.3 | [Primer by ChampionAsh5357][1192to1193] | +| 1.19.3 -> 1.19.4 | [Primer by ChampionAsh5357][1193to1194] | +| 1.19.4 -> 1.20 | [Primer by ChampionAsh5357][1194to120] | + +[112to114]: https://gist.github.com/williewillus/353c872bcf1a6ace9921189f6100d09a +[114to115]: https://gist.github.com/williewillus/30d7e3f775fe93c503bddf054ef3f93e +[115to116]: https://gist.github.com/50ap5ud5/f4e70f0e8faeddcfde6b4b1df70f83b8 +[116to117]: https://gist.github.com/50ap5ud5/beebcf056cbdd3c922cc8993689428f4 +[1192to1193]: https://gist.github.com/ChampionAsh5357/c21724bafbc630da2ed8899fe0c1d226 +[1193to1194]: https://gist.github.com/ChampionAsh5357/163a75e87599d19ee6b4b879821953e8 +[1194to120]: https://gist.github.com/ChampionAsh5357/cf818acc53ffea6f4387fe28c2977d56 diff --git a/docs/translation/zh_CN/misc/config.md b/docs/translation/zh_CN/misc/config.md new file mode 100644 index 000000000..514905531 --- /dev/null +++ b/docs/translation/zh_CN/misc/config.md @@ -0,0 +1,135 @@ +配置 +==== + +配置定义了可以应用于模组实例的设置和Consumer偏好。Forge使用一个采用[TOML][toml]文件的配置系统,并使用[NightConfig][nightconfig]进行读取。 + +创建一个配置 +----------- + +可以使用`IConfigSpec`的子类型创建配置。Forge通过`ForgeConfigSpec`实现该类型,并通过`ForgeConfigSpec$Builder`实现其构造。生成器可以通过`Builder#push`创建一个部分,通过`Builder#pop`留下一个部分以将配置值分隔为多个部分。之后,可以使用以下两种方法之一构建配置: + + 方法 | 描述 + :--- | :--- +`build` | 创建`ForgeConfigSpec`. +`configure` | 创建一对包含配置值和`ForgeConfigSpec`的类。 + +!!! 注意 + `ForgeConfigSpec$Builder#configure`通常与`static`块和一个类一起使用,该类将`ForgeConfigSpec$Builder`作为其构造函数的一部分,用于附加和保存值: + + ```java + // 在某个配置类中 + ExampleConfig(ForgeConfigSpec.Builder builder) { + // 在此处在final字段中定义值 + } + + // 在该构造函数可被访问的某处 + static { + Pair pair = new ForgeConfigSpec.Builder() + .configure(ExampleConfig::new); + // 在某个常量字段中存储成对的值 + } + ``` + +可以为每个配置值提供额外的上下文,以提供额外的行为。必须先定义上下文,然后才能完全生成配置值: + +方法 | 描述 +:--- | :--- +`comment` | 提供配置值的作用说明。可以为多行注释提供多个字符串。 +`translation` | 为配置值的名称提供翻译键。 +`worldRestart` | 必须重新启动世界,才能更改配置值。 + +### ConfigValue + +配置值可以使用所提供的上下文(如果已定义)使用任何`#define`方法构建。 + +所有配置值方法都至少接受两个组件: + +* 表示变量名称的路径:一个被`.`分隔的字符串,表示配置值所在的部分 +* 不存在有效配置时的默认值 + +特定于`ConfigValue`的方法包含两个附加组件: + +* 用于确保反序列化对象有效的验证器 +* 表示配置值的数据类型的类 + +```java +// 对于某个ForgeConfigSpec$Builder生成器 +ConfigValue value = builder.comment("Comment") + .define("config_value_name", defaultValue); +``` + +可以使用`ConfigValue#get`获取值本身。这些值会被额外缓存,以防止从文件中进行多次读取。 + +#### 附加的配置值类型 + +* **范围值** + * 描述: 值必须在所定义的范围之间 + * 类型: `Comparable` + * 方法名称: `#defineInRange` + * 附加组件: + * 配置值可能的最小值和最大值 + * 表示配置值的数据类型的类 + +!!! 注意 + `DoubleValue`、`IntValue`和`LongValue`是将类型分别指定为`Double`、`Integer`和`Long`的范围值。 + +* **白名单值** + * 描述: 值必须在所提供的集合中 + * 类型: `T` + * 方法名称: `#defineInList` + * 附加组件: + * 配置所允许的值的集合 + +* **列表值** + * 描述: 值是一个条目列表 + * 类型: `List` + * 方法名称: `#defineList`,`#defineListAllowEmpty`(如果列表可为空) + * 附加组件: + * 用于确保列表中反序列化元素有效的验证器 + +* **枚举值** + * 描述: 在所提供的集合中的一个枚举值 + * 类型: `Enum` + * 方法名称: `#defineEnum` + * 附加组件: + * A getter to convert a string or integer into an enum + * A collection of the allowed values the configuration can be + +* **布尔值** + * 描述: A `boolean` value + * 类型: `Boolean` + * 方法名称: `#define` + +注册一个配置 +----------- + +一旦构建了`ForgeConfigSpec`,就必须对其进行注册,以允许Forge根据需要加载、跟踪和同步配置设置。配置应通过`ModLoadingContext#registerConfig`在模组构造函数中注册。配置可以注册为表示配置所属侧的给定类型`ForgeConfigSpec`,以及配置的特定文件名(可选)。 + +```java +// 在具有一个ForgeConfigSpec CONFIG的模组构造函数中 +ModLoadingContext.get().registerConfig(Type.COMMON, CONFIG); +``` + +以下是可用的配置类型的列表: + +类型 | 被加载 | 同步到客户端 | 客户端位置 | 服务端位置 | 默认文件后缀 +:---: | :---: | :---: | :---: | :---: | :--- +CLIENT | 仅在客户端 | 否 | `.minecraft/config` | N/A | `-client` +COMMON | 在两端 | 否 | `.minecraft/config` | `/config` | `-common` +SERVER | 仅在服务端 | 是 | `.minecraft/saves//serverconfig` | `/world/serverconfig` | `-server` + +!!! 提示 + Forge在相应的代码库中用文档详述了[配置类型][type]。 + +配置事件 +-------- + +每当加载或重新加载配置时发生的操作可以使用`ModConfigEvent$Loading`和`ModConfigEvent$Reloading`事件来完成。事件必须[注册][events]到模组事件总线。 + +!!! 警告 + 这些事件对于模组的所有配置都被调用;所提供的`ModConfig`对象应被用于表示正在加载或重新加载哪个配置。 + +[toml]: https://toml.io/ +[nightconfig]: https://github.com/TheElectronWill/night-config +[type]: https://github.com/MinecraftForge/MinecraftForge/blob/c3e0b071a268b02537f9d79ef8e7cd9b100db416/fmlcore/src/main/java/net/minecraftforge/fml/config/ModConfig.java#L108-L136 +[events]: ../concepts/events.md#creating-an-event-handler diff --git a/docs/translation/zh_CN/misc/debugprofiler.md b/docs/translation/zh_CN/misc/debugprofiler.md new file mode 100644 index 000000000..bcfb23bbe --- /dev/null +++ b/docs/translation/zh_CN/misc/debugprofiler.md @@ -0,0 +1,46 @@ +# 调试分析器 + +Minecraft提供了一个调试分析器,它提供系统数据、当前游戏设置、JVM数据、存档数据和tick信息,以查找耗时的代码。考虑到像`TickEvent`和计时`BlockEntities`这样的事情,这对想要找到滞后源的模组开发者和服务器所有者来说非常有用。 + +## 使用调试分析器 + +调试分析器使用起来非常简单。它需要调试绑定键`F3 + L`来启动分析器。10秒后,它将自动停止;但是,可以通过再次按绑定键提前停止。 + +!!! 注意 + 当然,你只能分析实际到达的代码路径。要分析的实体和方块实体必须存在于存档中才能显示在结果中。 + +停止调试器后,它将在运行目录中的`debug/profiling`子目录中创建一个新的zip。 +文件名的格式为日期和时间`yyyy-mm-dd_hh_mi_ss-WorldName-VersionNumber.zip` + +## 阅读一个分析结果 + +在每个端位的文件夹(`client`和`server`)中,你会发现一个包含结果数据的`profiling.txt`”文件。在顶部,它首先告诉它运行了多长时间(以毫秒为单位)以及在这段时间内运行了多少tick。 + +在下面,你将找到与以下片段类似的信息: +``` +[00] levels - 96.70%/96.70% +[01] | Level Name - 99.76%/96.47% +[02] | | tick - 99.31%/95.81% +[03] | | | entities - 47.72%/45.72% +[04] | | | | regular - 98.32%/44.95% +[04] | | | | blockEntities - 0.90%/0.41% +[05] | | | | | unspecified - 64.26%/0.26% +[05] | | | | | minecraft:furnace - 33.35%/0.14% +[05] | | | | | minecraft:chest - 2.39%/0.01% +``` +以下是每个部分的含义的小解释 + +| [02] | tick | 99.31% | 95.81% | +| :----------------------- | :---------------------- | :----------- | :----------- | +| 该部分的深度 | 该部分的名称 | 所花费的时间相对于其父项的百分比。对于层级0,它是一tick所用时间的百分比。对于层级1,它是其父层所用时间的百分比。 | 第二个百分比告诉整个tick所花的时间。 + +## 分析你自己的代码 + +调试分析器具有对`Entity`和`BlockEntity`的基本支持。如果你想分析其他内容,你可能需要手动创建你的部分,如下所示: +```java +ProfilerFiller#push(yourSectionName : String); +// 你想分析的代码 +ProfilerFiller#pop(); +``` +你可以从`Level`、`MinecraftServer`或`Minecraft`实例获取`ProfilerFiller`实例。 +现在,你只需要在结果文件中搜索你的部分的名称。 diff --git a/docs/translation/zh_CN/misc/gametest.md b/docs/translation/zh_CN/misc/gametest.md new file mode 100644 index 000000000..8de5aab5e --- /dev/null +++ b/docs/translation/zh_CN/misc/gametest.md @@ -0,0 +1,262 @@ +游戏测试 +======== + +游戏测试是运行游戏内单元测试的一种方式。该系统被设计为可扩展的,并可并行高效地运行大量不同的测试。测试对象交互和行为只是该框架众多应用程序中的一小部分。 + +创建一个游戏测试 +--------------- + +一个标准的游戏测试遵循以下三个基本步骤: + +1. 加载一个结构或模板,其中包含测试交互或行为的场景(scene)。 +1. 一种方法执行要在场景中执行的逻辑。 +1. 逻辑执行的方法。如果达到成功状态,则测试成功。否则,测试将失败,结果将存储在场景附近的讲台(lectern)内。 + +因此,要创建游戏测试,必须有一个现有的模板来保存场景的初始开始状态和一个提供执行逻辑的方法。 + +### 测试方法 + +游戏测试方法是一个`Consumer`引用,这意味着它接受一个`GameTestHelper`,但不返回任何内容。要识别游戏测试方法,它必须具有`@GameTest`注释: + +```java +public class ExampleGameTests { + @GameTest + public static void exampleTest(GameTestHelper helper) { + // 做一些事情 + } +} +``` + +`@GameTest`注释还包含配置游戏测试运行方式的成员。 + +```java +// 在某个类中 +@GameTest( + setupTicks = 20L, // 该测试花费20个tick来设置执行 + required = false // 失败将记录到日志,但不会影响批处理的执行 +) +public static void exampleConfiguredTest(GameTestHelper helper) { + // 做一些事情 +} +``` + +#### 相对定位 + +所有`GameTestHelper`方法都使用结构方块的当前位置将结构模板场景中的相对坐标转换为其绝对坐标。为了便于在相对定位和绝对定位之间进行转换,可以分别使用`GameTestHelper#absolutePos`和`GameTestHelper#relativePos`。 + +结构模板的相对位置可以在游戏中通过[test命令][test]加载结构,将玩家放置在所需位置,最后运行`/test pos`命令来获得。这将获取玩家相对于玩家200个方块内最近结构的坐标。该命令将相对位置导出为聊天中的可复制文本组件,用作最终的本地变量。 + +!!! 提示 + `/test pos`生成的局部变量可以通过将其附加到命令末尾来指定其引用名称: + + ```bash + /test pos # 导出'final BlockPos = new BlockPos(...);' + ``` + +#### 成功完成 + +游戏测试方法负责一件事:在有效完成时标记测试是否成功。如果在超时之前没有达到成功状态(如`GameTest#timeoutTicks`所定义),则测试自动失败。 + +`GameTestHelper`中有许多抽象方法,可用于定义成功状态;然而,有四个是非常重要的。 + +方法 | 描述 +:---: | :--- +`#succeed` | 测试被标记为成功。 +`#succeedIf` | 如果没有抛出`GameTestAssertException`,则会立即测试所提供的`Runnable`并成功。如果测试在该瞬时tick上没有成功,则将其标记为失败。 +`#succeedWhen` | 所提供的`Runnable`在超时之前每tick都会进行测试,如果对其中一个tick的检查没有引发`GameTestAssertException`,则会成功。 +`#succeedOnTickWhen` | 提供的`Runnable`在指定的tick上进行测试,如果没有抛出`GameTestAssertException`,则会成功。如果`Runnable`在任何其他tick上成功,则将其标记为失败。 + +!!! 重要 + 游戏测试每tick都会执行,直到测试被标记为成功。因此,在给定的tick上安排成功的方法必须小心,不要总是在之前的tick上失败。 + +#### 计划操作 + +并非所有操作都会在测试开始时发生。操作可以安排在特定的时间或间隔进行: + +方法 | 描述 +:---: | :--- +`#runAtTickTime` | 操作将在指定的tick上运行。 +`#runAfterDelay` | 操作将在当前tick后`x`tick时运行。 +`#onEachTick` | 操作在每个tick都会运行。 + +#### 断言 + +在游戏测试期间的任何时候,都可以进行断言以检查给定条件是否为真。`GameTestHelper`中有许多断言方法;然而,它简化为在不满足适当状态时抛出`GameTestAssertException`。 + +### 生成的测试方法 + +如果需要动态生成游戏测试方法,则可以创建测试方法生成器。这些方法不接受任何参数,并返回一个`TestFunction`的集合。要识别测试方法生成器,它必须具有`@GameTestGenerator`注释: + +```java +public class ExampleGameTests { + @GameTestGenerator + public static Collection exampleTests() { + // 返回一个TestFunction的集合 + } +} +``` + +#### TestFunction + +`TestFunction`是`@GameTest`注释和运行测试的方法所包含的包装信息。 + +!!! 提示 + 任何使用`@GameTest`注释的方法都会使用`GameTestRegistry#turnMethodIntoTestFunction`转换为`TestFunction`。该方法可以用作创建`TestFunction`的引用,而无需使用注释。 + +### 批量处理 + +游戏测试可以批量执行,而不是按注册顺序执行。可以通过提供相同的`GameTest#batch`字符串将测试添加到批次中。 + +批处理本身并没有提供任何有用的东西。但是,批处理可以用于在测试运行的当前存档上执行设置和拆卸(teardown)状态。这是通过用`@BeforeBatch`注释方法来完成的,用`@AfterBatch`来进行设置或拆卸。`#batch`方法必须与提供给游戏测试的字符串匹配。 + +批处理方法是`Consumer`引用,这意味着它们接受`ServerLevel`而不返回任何内容: + +```java +public class ExampleGameTests { + @BeforeBatch(batch = "firstBatch") + public static void beforeTest(ServerLevel level) { + // 进行设置(setup) + } + + @GameTest(batch = "firstBatch") + public static void exampleTest2(GameTestHelper helper) { + // 做一些事情 + } +} +``` + +注册一个游戏测试 +--------------- + +游戏测试必须注册后才能在游戏中运行。有两种方法:通过`@GameTestHolder`注释或`RegisterGameTestsEvent`。这两种注册方法仍然需要用`@GameTest`、`@GameTestGenerator`、`@BeforeBatch`或`@AfterBatch`对测试方法进行注释。 + +### GameTestHolder + +`@GameTestHolder`注释注册类型(类、接口、枚举或记录)中的任何测试方法。`@GameTestHolder`包含一个具有多种用途的单一方法。在该实例中,提供的`#value`必须是模组的mod id;否则,测试将不会在默认配置下运行。 + +```java +@GameTestHolder(MODID) +public class ExampleGameTests { + // ... +} +``` + +### RegisterGameTestsEvent + +`RegisterGameTestsEvent`也可以使用`#register`注册类或方法。事件监听器必须[添加][event]到模组事件总线。以这种方式注册的测试方法必须在每个用`@GameTest`注释的方法上向`GameTest#templateNamespace`提供其mod id。 + +```java +// 在某个类中 +public void registerTests(RegisterGameTestsEvent event) { + event.register(ExampleGameTests.class); +} + +// 在ExampleGameTests中 +@GameTest(templateNamespace = MODID) +public static void exampleTest3(GameTestHelper helper) { + // 进行设置(setup) +} +``` + +!!! 注意 + 提供给`GameTestHolder#value`和`GameTest#templateNamespace`的值可能与当前的mod id不同。需要更改[buildscript][namespaces]中的配置。 + +结构模板 +-------- + +游戏测试是在由结构或模板加载的场景中执行的。所有模板都定义了场景的尺寸以及将要加载的初始数据(方块和实体)。模板必须存储为`data//structures`中的`.nbt`文件。 + +!!! 提示 + 可以使用结构方块创建和保存结构模板。 + +模板的位置由以下几个因素指定: + +* 模板的命名空间是否被指定。 +* 类是否应被加到模板的名称之前。 +* 模板的名称是否被指定。 + +模板的命名空间由`GameTest#templateNamespace`确定,如果未指定则由`GameTestHolder#value`确定,如果两者都未指定则由`minecraft`确定。 + +如果将`@PrefixGameTestTemplate`应用于具有测试注释的类或方法并设置为`false`,则简单类名不会前置于模板的名称。否则,简单类名将变为小写并加上前缀,然后在模板名之前加上一个点。 + +模板的名称由`GameTest#template`决定。如果未指定,则使用方法的小写名称。 + +```java +// 所有结构的modid将为MODID +@GameTestHolder(MODID) +public class ExampleGameTests { + + // 类名已前置,模板名称未指定 + // 模板位置位于'modid:examplegametests.exampletest' + @GameTest + public static void exampleTest(GameTestHelper helper) { /*...*/ } + + // 类名未前置,模板名称未指定 + // 模板位置位于'modid:exampletest2' + @PrefixGameTestTemplate(false) + @GameTest + public static void exampleTest2(GameTestHelper helper) { /*...*/ } + + // 类名已前置,模板名称已指定 + // 模板位置位于'modid:examplegametests.test_template' + @GameTest(template = "test_template") + public static void exampleTest3(GameTestHelper helper) { /*...*/ } + + // 类名未前置,模板名称已指定 + // 模板位置位于'modid:test_template2' + @PrefixGameTestTemplate(false) + @GameTest(template = "test_template2") + public static void exampleTest4(GameTestHelper helper) { /*...*/ } +} +``` + +运行游戏测试 +----------- + +可以使用`/test`命令运行游戏测试。`test`命令具有高度可配置性;但是,只有少数几个对运行测试很重要: + +子命令 | 描述 +:---: | :--- +`run` | 运行指定的测试:`run ` +`runall` | 运行所有可用的测试。 +`runthis` | 运行离玩家15个方块内最近的测试。 +`runthese` | 运行离玩家200个方块内的测试。 +`runfailed` | 运行上一次运行中失败的所有测试。 + +!!! 注意 + 子命令跟在test命令后面:`/test `。 + +构建脚本(buildscript)配置 +-------------------------- + +游戏测试在构建脚本(`build.gradle`文件)中提供额外的配置设置,以运行并集成到不同的设置中。 + +### 启用其他命名空间 + +如果构建脚本是[按照推荐的方式进行设置][buildscript]的,那么只会启用当前mod id下的游戏测试。要使其他命名空间能够从中加载游戏测试,运行配置必须将属性`forge.enabledGameTestNamespaces`设置为一个字符串,指定用逗号分隔的每个命名空间。如果属性为空或未设置,则将加载所有命名空间。 + +```gradle +// 在某个运行配置里面 +property 'forge.enabledGameTestNamespaces', 'modid1,modid2,modid3' +``` + +!!! 警告 + 命名空间之间不能有空格;否则,将无法正确加载命名空间。 + +### 游戏测试服务端运行配置 + +游戏测试服务端是一种运行构建服务端的特殊配置。构建服务端返回所需的失败游戏测试数的退出代码。所有失败的测试都被记录到日志,无论是必需的还是可选的。此服务端可以使用`gradlew runGameTestServer`运行。 + +### 在其他运行配置中启用游戏测试 + +默认情况下,只有`client`、`server`和`gameTestServer`运行配置启用了游戏测试。如果另一个运行配置应该运行游戏测试,则`forge.enableGameTest`属性必须设置为`true`。 + +```gradle +// 在一个运行配置里面 +property 'forge.enableGameTest', 'true' +``` + +[test]: #running-game-tests +[namespaces]: #enabling-other-namespaces +[event]: ../concepts/events.md#creating-an-event-handler +[buildscript]: ../gettingstarted/index.md#simple-buildgradle-customizations diff --git a/docs/translation/zh_CN/misc/keymappings.md b/docs/translation/zh_CN/misc/keymappings.md new file mode 100644 index 000000000..9017aebb0 --- /dev/null +++ b/docs/translation/zh_CN/misc/keymappings.md @@ -0,0 +1,151 @@ +# 键盘布局 + +键盘布局(键映射)或键盘绑定定义了应与输入绑定的特定操作:鼠标单击、按键等。每当客户端可以进行输入时,都可以检查键盘布局定义的每个操作。此外,每个键盘布局都可以通过[控制选项菜单][controls]分配给任何输入。 + +## 注册一个`KeyMapping` + +`KeyMapping`可以通过仅在物理客户端上监听[**模组事件总线**][modbus]上的`RegisterKeyMappingsEvent`并调用`#register`来注册。 + +```java +// 在某个仅物理客户端的类中 + +// 键盘布局是延迟初始化的,因此在注册之前它不存在 +public static final Lazy EXAMPLE_MAPPING = Lazy.of(() -> /*...*/); + +// 事件仅在物理客户端上的模组事件总线上 +@SubscribeEvent +public void registerBindings(RegisterKeyMappingsEvent event) { + event.register(EXAMPLE_MAPPING.get()); +} +``` + +## 创建一个`KeyMapping` + +`KeyMapping`可以使用其构造函数创建。`KeyMapping`接受一个定义映射名称的[翻译键][tk],映射的默认输入,以及定义映射将放在[控制选项菜单][controls]中的类别的[翻译键][tk]。 + +!!! 提示 + 通过提供原版未提供的类别[翻译键][tk],可以将`KeyMapping`添加到自定义类别中。自定义类别转换键应包含mod id(例如`key.categories.examplemod.examplecategory`)。 + +### 默认输入 + +每个键映射都有一个与其关联的默认输入。这是通过`InputConstants$Key`提供的。每个输入由一个`InputConstants$Type`和一个整数组成,前者定义了提供输入的设备,后者定义了设备上输入的相关标识符。 + +原版提供三种类型的输入:`KEYSYM`,通过提供的`GLFW`键标记定义键盘,`SCANCODE`,通过平台特定扫描码定义键盘,以及`MOUSE`,定义鼠标。 + +!!! 注意 + 强烈建议键盘使用`KEYSYM`而不是`SCANCODE`,因为`GLFW`键令牌不与任何特定系统绑定。你可以在[GLFW文档][keyinput]上阅读更多内容。 + +整数取决于提供的类型。所有输入代码都在`GLFW`中定义:`KEYSYM`令牌以`GLFW_KEY_*`为前缀,而`MOUSE`代码以`GLFW_MOUSE_*`作为前缀。 + +```java +new KeyMapping( + "key.examplemod.example1", // 将使用该翻译键进行本地化 + InputConstants.Type.KEYSYM, // 在键盘上的默认映射 + GLFW.GLFW_KEY_P, // 默认键为P + "key.categories.misc" // 映射将在杂项(misc)类别中 +) +``` + +!!! 注意 + 如果键映射不应映射到默认值,则应将输入设置为`InputConstants#UNKNOWN`。原版构造函数将要求你通过`InputConstants$Key#getValue`提取输入代码,而Forge构造函数可以提供原始输入字段。 + +### `IKeyConflictContext` + +并非所有映射都用于每个上下文。有些映射仅在GUI中使用,而另一些映射仅在游戏中使用。为了避免在不同上下文中使用的同一键的映射相互冲突,可以分配`IKeyConflictContext`。 + +每个冲突上下文包含两种方法:`#isActive`,定义映射是否可以在当前游戏状态下使用;`#conflicts`,定义在相同或不同的冲突上下文中映射是否与键冲突。 + +目前,Forge通过`KeyConflictContext`定义了三个基本上下文:`UNIVERSAL`,这是默认的,意味着密钥可以在每个上下文中使用;`GUI`,这意味着映射只能在`Screen`打开时使用;`IN_GAME`,意味着映射只有在`Screen`未打开时才能使用。可以通过实现`IKeyConflictContext`来创建新的冲突上下文。 + +```java +new KeyMapping( + "key.examplemod.example2", + KeyConflictContext.GUI, // 映射只能在当一个屏幕打开时使用 + InputConstants.Type.MOUSE, // 在鼠标上的默认映射 + GLFW.GLFW_MOUSE_BUTTON_LEFT, // 在鼠标左键上的默认鼠标输入 + "key.categories.examplemod.examplecategory" // 映射将在新的示例类别中 +) +``` + +### `KeyModifier` + +如果修改键保持不变(例如`G`与`CTRL + G`),则修改器可能不希望映射具有相同的行为。为了解决这个问题,Forge在构造函数中添加了一个额外的参数来接受一个`KeyModifier`,它可以将control(`KeyModifier#CONTROL`)、shift(`KeyModifier#SHIFT`)或alt(`KeyModifier#ALT`)映射到任何输入。`KeyModifier#NONE`是默认值,不会应用任何修改器。 + +通过接纳修饰符键和相关输入,可以在[控制选项菜单][controls]中添加修改器。 + +```java +new KeyMapping( + "key.examplemod.example3", + KeyConflictContext.UNIVERSAL, + KeyModifier.SHIFT, // 默认映射要求shift被按下 + InputConstants.Type.KEYSYM, // 默认映射在键盘上 + GLFW.GLFW_KEY_G, // 默认键为G + "key.categories.misc" +) +``` + +## 检查一个`KeyMapping` + +可以检查`KeyMapping`以查看它是否已被单击。根据时间的不同,可以在条件中使用映射来应用关联的逻辑。 + +### 在游戏内 + +在游戏内,应通过在[**Forge事件总线**][forgebus]上监听`ClientTickEvent`并在while循环中检查`KeyMapping#consumeClick`来检查映射。`#consumeClick`仅当输入已执行但之前尚未处理的次数时才会返回`true`,因此不会无限拖延游戏。 + +```java +// 事件仅在物理客户端上的Forge事件总线上 +public void onClientTick(ClientTickEvent event) { + if (event.phase == TickEvent.Phase.END) { // 仅调用代码一次,因为tick事件在每个tick调用两次 + while (EXAMPLE_MAPPING.get().consumeClick()) { + // 在此处执行单击时的逻辑 + } + } +} +``` + +!!! 警告 + 不要将`InputEvent`用作`ClientTickEvent`的替代项。只有键盘和鼠标输入有单独的事件,所以它们不会处理任何额外的输入。 + +### Inside a GUI + +在GUI内,可以使用`IForgeKeyMapping#isActiveAndMatches`在其中一个`GuiEventListener`方法中检查映射。可以检查的最常见方法是`#keyPressed`和`#mouseClicked`。 + +`#keyPressed`接收`GLFW`键令牌、特定于平台的扫描代码和按下的修改器的位字段。通过使用`InputConstants#getKey`创建输入,可以根据映射检查键。修改器已经在映射方法本身中进行了检查。 + +```java +// 在某个Screen子类中 +@Override +public boolean keyPressed(int key, int scancode, int mods) { + if (EXAMPLE_MAPPING.get().isActiveAndMatches(InputConstants.getKey(key, scancode))) { + // 在此处执行按键时的逻辑 + return true; + } + return super.keyPressed(x, y, button); +} +``` + +!!! 注意 + 如果你不拥有要检查**键**的屏幕,你可以在[**Forge事件总线**][forgebus]上监听`ScreenEvent$KeyPressed`的`Pre`或`Post`事件。 + +`#mouseClicked`获取鼠标的x位置、y位置和单击的按钮。通过使用带有`MOUSE`输入的`InputConstants$Type#getOrCreate`创建输入,可以根据映射检查鼠标按钮。 + +```java +// 在某个Screen子类中 +@Override +public boolean mouseClicked(double x, double y, int button) { + if (EXAMPLE_MAPPING.get().isActiveAndMatches(InputConstants.TYPE.MOUSE.getOrCreate(button))) { + // 在此处执行鼠标单击时的逻辑 + return true; + } + return super.mouseClicked(x, y, button); +} +``` + +!!! 注意 + 如果你不拥有要检查**鼠标**的屏幕,你可以在[**Forge事件总线**][forgebus]上监听`ScreenEvent$MouseButtonPressed`的`Pre`或`Post`事件。 + +[modbus]: ../concepts/events.md#mod-event-bus +[controls]: https://minecraft.fandom.com/wiki/Options#Controls +[tk]: ../concepts/internationalization.md#translatablecontents +[keyinput]: https://www.glfw.org/docs/3.3/input_guide.html#input_key +[forgebus]: ../concepts/events.md#creating-an-event-handler diff --git a/docs/translation/zh_CN/misc/updatechecker.md b/docs/translation/zh_CN/misc/updatechecker.md new file mode 100644 index 000000000..2c731d359 --- /dev/null +++ b/docs/translation/zh_CN/misc/updatechecker.md @@ -0,0 +1,63 @@ +Forge更新检查器 +============== + +Forge提供了一个非常轻量级的可选择性加入的更新检查框架。如果任何模组有可用的更新,它将在主菜单和模组列表的'Mods'按钮上显示一个闪烁的图标,以及相应的更改日志。它*不会*自动下载更新。 + +入门 +---- + +你要做的第一件事是在`mods.toml`文件中指定`updateJSONURL`参数。此参数的值应该是指向更新JSON文件的有效URL。这个文件可以托管在你自己的网络服务器、GitHub或任何你想要的地方,只要你的模组的所有用户都能可靠地访问它。 + +更新JSON格式 +------------ + +JSON本身有一个相对简单的格式,如下所示: + +```js +{ + "homepage": "", + "": { + "": "", + // 列出给定Minecraft版本的所有模组版本,以及它们的更改日志 + // ... + }, + "promos": { + "-latest": "", + // 为给定的Minecraft版本声明你的模组的最新"bleeding-edge"版本 + "-recommended": "", + // 为给定的Minecraft版本声明你的模组的最新"stable"版本 + // ... + } +} +``` + +这是不言自明的,但需要注意: + +* `homepage`下的链接是当模组过时时将向用户显示的链接。 +* Forge使用内部算法来确定你的模组的一个版本字符串是否比另一个“新”。大多数版本控制方案应该是兼容的,但如果你担心方案是否受支持,请参阅`ComparableVersion`类。强烈建议遵守[Maven版本控制][mvnver]。 +* 可以使用`\n`将变更日志字符串分隔成多行。有些人更喜欢包含一个简略的变更日志,然后链接到一个提供完整变更列表的外部网站。 +* 手动输入数据可能很麻烦。你可以将`build.gradle`配置为在构建版本时自动更新此文件,因为Groovy具有本地JSON解析支持。这将留给读者练习。 + +- 这里可以找到一些例子,例如[nocubes][]、[Corail Tombstone][corail]和[Chisels & Bits 2][chisel]。 + +检索更新检查结果 +--------------- + +你可以使用`VersionChecker#getResult(IModInfo)`检索Forge更新检查器的结果。你可以通过`ModContainer#getModInfo`获取你的`IModInfo`。你可以在构造函数中使用`ModLoadingContext.get().getActiveContainer()`、`ModList.get().getModContainerById(<你的modId>)`或`ModList.get().getModContainerByObject(<你的模组实例>)`来获取`ModContainer`。你可以使用`ModList.get().getModContainerById()`获取任何其他模组的`ModContainer`。返回的对象有一个方法`#status`,表示版本检查的状态。 + +| 状态 | 描述 | +|----------------:|:------------| +| `FAILED` | 版本检查器无法连接到提供的URL。 | +| `UP_TO_DATE` | 当前版本等于推荐版本。 | +| `AHEAD` | 如果没有最新版本,则当前版本比推荐版本更新。 | +| `OUTDATED` | 有一个新的推荐版本或最新版本。 | +| `BETA_OUTDATED` | 有一个新的最新版本。 | +| `BETA` | 当前版本等于或高于最新版本。 | +| `PENDING` | 请求的结果尚未完成,因此你应该稍后再试。 | + +返回的对象还将具有`update.json`中指定的目标版本和任何变更日志行。 + +[mvnver]: ../gettingstarted/versioning.md +[nocubes]: https://cadiboo.github.io/projects/nocubes/update.json +[corail]: https://github.com/Corail31/tombstone_lite/blob/master/update.json +[chisel]: https://github.com/Aeltumn/Chisels-and-Bits-2/blob/master/update.json diff --git a/docs/translation/zh_CN/networking/entities.md b/docs/translation/zh_CN/networking/entities.md new file mode 100644 index 000000000..0bdab032f --- /dev/null +++ b/docs/translation/zh_CN/networking/entities.md @@ -0,0 +1,35 @@ +实体 +==== + +除了常规的网络消息之外,Forge还提供了各种其他系统来处理同步实体数据。 + +生成数据 +------- + +一般来说,由模组编写的实体的生成是由Forge单独处理的。 + +!!! 注意 + 这意味着简单地继承一个原版实体类可能不会继承它的所有行为。你可能需要自己实施某些原版行为。 + +你可以通过实现以下接口向Forge发送的生成数据包添加额外的数据。 + +### IEntityAdditionalSpawnData + +如果你的实体具有客户端所需的数据,但不会随时间变化,则可以使用此接口将其添加到实体生成数据包中。`#writeSpawnData`和`#readSpawnData`控制如何将数据编码到网络缓冲区/从网络缓冲区解码数据。 + +动态数据 +------- + +### 数据参数 + +这是用于将实体数据从服务端同步到客户端的主要原版系统。因此,可以参考一些原版的例子。 + +首先,对于要保持同步的数据,你需要一个`EntityDataAccessor`。这应该存储为你的实体类中的`static final`字段,通过调用`SynchedEntityData#defineId`并传递实体类和该类型数据的序列化器来获得。可用的序列化器实现可以在`EntityDataSerializers`类中的静态常量找到。 + +!!! 警告 + 你应该 __只__ _在相应实体的类_ 中为自己的实体创建数据参数。 + 向并非你所控制的实体添加参数可能会导致用于通过网络发送数据的ID不同步,从而导致难以调试的崩溃。 + +然后,重写`Entity#defineSynchedData`并为每个数据参数调用`this.entityData.define(...)`,传递参数和要使用的初始值。请记住始终首先调用`super`方法! + +然后,你可以通过实体的`entityData`实例获取并设置这些值。所做的更改将自动同步到客户端。 diff --git a/docs/translation/zh_CN/networking/index.md b/docs/translation/zh_CN/networking/index.md new file mode 100644 index 000000000..1a2abdf49 --- /dev/null +++ b/docs/translation/zh_CN/networking/index.md @@ -0,0 +1,20 @@ +网络 +==== + +服务端与客户端之间的通信是成功实现模组的中流砥柱。 + +网络通信有两个主要目标: + +1. 确保客户端视图与服务端视图“同步” + - 坐标(X,Y,Z)处的花刚刚生长 +2. 为客户端提供一种方法,告诉服务端玩家发生了变化 + - 玩家按下了一个按键 + +实现这些目标的最常见方法是在客户端和服务端之间传递消息。这些消息通常是结构化的,包含特定排列的数据,以便于发送和接收。 + +Forge提供了多种技术来促进通信,这些技术大多建立在[netty][]之上。 + +对于一个新模组来说,最简单的当是[SimpleImpl][channel],在这里,网络系统的大部分复杂性都被抽象掉了。它使用消息和处理器样式的系统。 + +[netty]: https://netty.io "Netty Website" +[channel]: ./simpleimpl.md "SimpleImpl in Detail" diff --git a/docs/translation/zh_CN/networking/simpleimpl.md b/docs/translation/zh_CN/networking/simpleimpl.md new file mode 100644 index 000000000..d8cb0b480 --- /dev/null +++ b/docs/translation/zh_CN/networking/simpleimpl.md @@ -0,0 +1,118 @@ +SimpleImpl +========== + +SimpleImpl是围绕`SimpleChannel`类的数据包系统的名称。使用此系统是迄今为止在客户端和服务端之间发送自定义数据的最简单方法。 + +快速入门 +-------- + +首先,你需要创建`SimpleChannel`对象。我们建议你在单独的类中执行此操作,可能类似于`ModidPacketHandler`。将`SimpleChannel`创建为此类中的静态字段,如下所示: + +```java +private static final String PROTOCOL_VERSION = "1"; +public static final SimpleChannel INSTANCE = NetworkRegistry.newSimpleChannel( + new ResourceLocation("mymodid", "main"), + () -> PROTOCOL_VERSION, + PROTOCOL_VERSION::equals, + PROTOCOL_VERSION::equals +); +``` + +第一个参数是通道的名称。第二个参数是返回当前网络协议版本的`Supplier`。第三个和第四个参数分别是`Predicate`,分别检查传入的连接协议版本是否与客户端或服务端网络兼容。在这里,我们只需直接与`PROTOCOL_VERSION`字段进行比较,这意味着客户端和服务端`PROTOCOL_VERSION`必须始终匹配,否则FML将拒绝登录。 + +版本检查器 +--------- + +如果你的模组不要求另一端拥有特定的网络通道,或者根本不要求对方是Forge实例,你应该注意正确定义你的版本兼容性检查器(`Predicate`参数),以处理版本检查器可以接收的其他“元版本”(在`NetworkRegistry`中定义)。这些是: + +* `ABSENT` - 如果该通道在另一个端点上丢失。请注意,在这种情况下,端点仍然是Forge端点,并且可能具有其他模组。 +* `ACCEPTVANILLA` - 如果端点是原版(或非Forge)端点(如Fabric——译者注)。 + +对两者返回`false`意味着该通道必须存在于另一端上。如果你只是复制上面的代码,这就是它的作用。请注意,在列表ping兼容性检查期间也会使用这些值,该检查负责在多人服务器选择屏幕中显示绿色复选框/红叉。 + +注册数据包 +--------- + +接下来,我们必须声明要发送和接收的消息类型。这是使用`INSTANCE#registerMessage`完成的,它接受5个参数: + +- 第一个参数是数据包的鉴别器。这是数据包的每个通道的唯一ID。我们建议你使用本地变量来保存ID,然后使用`id++`调用registerMessage。这将保证100%的唯一ID。 +- 第二个参数是实际的数据包类`MSG`。 +- 第三个参数是`BiConsumer`,负责将消息编码到所提供的`FriendlyByteBuf`中。 +- 第四个参数是`Function`,负责从所提供的`FriendlyByteBuf`中解码消息。 +- 最后一个参数是负责处理消息本身的`BiConsumer>`。 + +最后三个参数可以是Java中静态方法或实例方法的方法引用。请记住,实例方法`MSG#encode(FriendlyByteBuf)`仍然满足`BiConsumer`;`MSG`只不过成为隐含的第一个自变量。 + +处理数据包 +--------- + +在数据包处理器中,有几件事需要强调。数据包处理器同时具有对其可用消息对象和网络上下文。该上下文允许访问发送数据包的玩家(如果在服务端上),并允许一种方式将线程安全工作排入队列。 + +```java +public static void handle(MyMessage msg, Supplier ctx) { + ctx.get().enqueueWork(() -> { + // 要求线程安全的工作(大多数工作) + ServerPlayer sender = ctx.get().getSender(); // 发送该数据包的客户端 + // 处理一些事情 + }); + ctx.get().setPacketHandled(true); +} +``` + +从服务端发送到客户端的数据包应在另一个类中进行处理,并通过`DistExecutor#unsafeRunWhenOn`进行包装。 + +```java +// 在Packet类中 +public static void handle(MyClientMessage msg, Supplier ctx) { + ctx.get().enqueueWork(() -> + // 确保其仅在物理客户端上执行 + DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> ClientPacketHandlerClass.handlePacket(msg, ctx)) + ); + ctx.get().setPacketHandled(true); +} + +// 在ClientPacketHandlerClass中 +public static void handlePacket(MyClientMessage msg, Supplier ctx) { + // 处理一些事情 +} +``` + +请注意`#setPacketHandled`的存在,它用于告诉网络系统该数据包已成功完成处理。 + + +!!! 警告 + 从Minecraft 1.8开始,默认情况下在网络线程上处理数据包。 + + 这意味着你的处理器 _不_ 能直接与大多数游戏对象交互。Forge提供了一种方便的方法,可以通过提供的`NetworkEvent$Context`在主线程上执行代码。只需调用`NetworkEvent$Context#enqueueWork(Runnable)`,它将在下一次有机会时调用主线程上的给定`Runnable`。 + +!!! 警告 + 在服务端上处理数据包时要采取防御措施。客户端可能试图通过发送意外数据来对数据包处理过程施压。 + + 一个常见的问题是易受**任意区块生成**的攻击。当服务端信任客户端发送的方块位置来访问方块和块方实体时,通常会发生这种情况。当访问存档中的未加载区域中的方块和方块实体时,服务端将会要么生成要么从磁盘加载该区域,然后立即将其写入磁盘。利用这一点,可以在不留下痕迹的情况下对服务端的性能和存储空间造成**灾难性破坏**。 + + 为了避免这个问题,一个普遍的经验法则是,仅访问`Level#hasChunkAt`为true的方块和方块实体。 + + +发送数据包 +--------- + +### 向服务端发送 + +只有一种方法可以将数据包发送到服务端。这是因为客户端一次只能连接到一个服务端。要做到这一点,我们必须再次使用前面定义的`SimpleChannel`。只需调用`INSTANCE.sendToServer(new MyMessage())`。消息将被发送到对应其类型的处理器(如果存在)。 + +### 向客户端发送 + +数据包可以使用`SimpleChannel`直接发送到客户端:`HANDLER.sendTo(new MyClientMessage(), serverPlayer.connection.getConnection(), NetworkDirection.PLAY_TO_CLIENT)`。但是,这可能很不方便。Forge有一些可以使用的便利功能: + +```java +// 向一位玩家发送 +INSTANCE.send(PacketDistributor.PLAYER.with(serverPlayer), new MyMessage()); + +// 向正在追踪该存档某个区块的所有玩家发送 +INSTANCE.send(PacketDistributor.TRACKING_CHUNK.with(levelChunk), new MyMessage()); + +// 向所有已连接的玩家发送 +INSTANCE.send(PacketDistributor.ALL.noArg(), new MyMessage()); +``` + +还有其他类型的`PacketDistributor`可用;有关更多详细信息,请查看`PacketDistributor`类的文档。 diff --git a/docs/translation/zh_CN/rendering/modelextensions/ambientocclusion_annotated.png b/docs/translation/zh_CN/rendering/modelextensions/ambientocclusion_annotated.png new file mode 100644 index 0000000000000000000000000000000000000000..17009b076dcc793c929a9c0b62c1ecf42a40aa05 GIT binary patch literal 98157 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:special_cutout`和`:special_translucent`。 + +[mipmapping]: https://en.wikipedia.org/wiki/Mipmap \ No newline at end of file diff --git a/docs/translation/zh_CN/rendering/modelextensions/transforms.md b/docs/translation/zh_CN/rendering/modelextensions/transforms.md new file mode 100644 index 000000000..eb0f2eccf --- /dev/null +++ b/docs/translation/zh_CN/rendering/modelextensions/transforms.md @@ -0,0 +1,76 @@ +根变换 +====== + +在模型JSON的顶层添加`transform`条目向加载器建议,在方块模型的情况下,应在[方块状态][blockstate]文件中的旋转之前对所有几何体应用变换,在物品模型的情况中,应在[显示变换][displaytransform]之前对其应用变换。转换可通过`IUnbakedGeometry#bake()`中的`IGeometryBakingContext#getRootTransform()`获得。 + +自定义模型加载器可能会完全忽略此字段。 + +根变换可以用两种格式指定: + +1. 一个JSON对象,包含一个奇异的`matrix`条目,该条目包含一个嵌套JSON数组形式的原始转换矩阵,省略了最后一行(3*4矩阵,行主序)。矩阵是按平移、左旋转、缩放、右旋转和变换原点的顺序组成的。结构示例: + ```js + "transform": { + "matrix": [ + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ], + [ 0, 0, 0, 0 ] + ] + } + ``` +2. 一个JSON对象,包含以下可选项的任意组合: + * `origin`:用于旋转和缩放的原点 + * `translation`:相对平移 + * `rotation`或`left_rotation`:在缩放之前围绕要应用的平移原点旋转 + * `scale`:相对于平移原点的比例 + * `right_rotation`或`post_rotation`:在缩放之后要应用的围绕平移原点的旋转 + +元素的指定 +--------- + +如果转换被指定为选项4中提到的条目的组合,则这些条目将按照`translation`、`left_rotation`、`scale`、`right_rotation`的顺序应用。 +转换将移动到指定的原点,作为最后一步。 + +```js +{ + "transform": { + "origin": "center", + "translation": [ 0, 0.5, 0 ], + "rotation": { "y": 45 } + }, + // ... +} +``` + +这些元素的定义应为如下: + +### 原点 + +原点可以指定为表示三维矢量的3个浮点数的数组:`[ x, y, z ]`,也可以指定为三个默认值之一: + +* `"corner"` (0, 0, 0) +* `"center"` (.5, .5, .5) +* `"opposing-corner"` (1, 1, 1) + +如果未指定原点,则其默认为`"opposing-corner"`。 + +### 平移 + +平移必须指定为表示三维矢量的3个浮点数的数组:`[ x, y, z ]`,如果不存在,则默认为(0, 0, 0)。 + +### 左旋转和右旋转 + +可以通过以下四种方式中的任何一种指定旋转: + +* 具有单轴=>旋转度映射的单个JSON对象:`{ "x": 90 }` +* 具有上述格式的任意数量的JSON对象的数组(按指定顺序应用):`[ { "x": 90 }, { "y": 45 }, { "x": -22.5 } ]` +* 由3个浮点数组成的数组,指定围绕每个轴的旋转(以度为单位):`[ 90, 180, 45 ]` +* 直接指定四元数的4个浮点数的数组:`[ 0.38268346, 0, 0, 0.9238795 ]`(示例等于绕X轴45度) + +如果未指定相应的旋转,则默认为无旋转。 + +### 比例 + +比例必须指定为表示三维矢量的3个浮点数的数组:`[ x, y, z ]`,如果不存在,则默认为(1, 1, 1)。 + +[blockstate]: https://minecraft.fandom.com/wiki/Tutorials/Models#Block_states +[displaytransform]: ../modelloaders/transform.md \ No newline at end of file diff --git a/docs/translation/zh_CN/rendering/modelextensions/visibility.md b/docs/translation/zh_CN/rendering/modelextensions/visibility.md new file mode 100644 index 000000000..89c1b8b0d --- /dev/null +++ b/docs/translation/zh_CN/rendering/modelextensions/visibility.md @@ -0,0 +1,52 @@ +部分可见度 +========= + +在模型JSON的顶层添加`visibility`条目可以控制模型不同部分的可见性,以决定是否应将它们烘焙到最终的[`BakedModel`][bakedmodel]中。“零件”的定义取决于加载此模型的模型加载器,自定义模型加载器可以完全忽略此条目。在Forge提供的模型加载器中,只有[复合模型加载器][composite]和[OBJ模型加载器][obj]使用了此功能。可见性条目被指定为`"part name": boolean`条目。 + +具有两个部分的复合模型的示例,其中第二个部分不会烘焙到最终模型中,并且两个子模型覆盖此可见性,分别只显示第一个部分和两个部分: +```js +// mycompositemodel.json +{ + "loader": "forge:composite", + "children": { + "part_one": { + "parent": "mymod:mypartmodel_one" + }, + "part_two": { + "parent": "mymod:mypartmodel_two" + } + }, + "visibility": { + "part_two": false + } +} + +// mycompositechild_one.json +{ + "parent": "mymod:mycompositemodel", + "visibility": { + "part_one": false, + "part_two": true + } +} + +// mycompositechild_two.json +{ + "parent": "mymod:mycompositemodel", + "visibility": { + "part_two": true + } +} +``` + +给定部分的可见性是通过检查模型是否指定了该部分的可见性来确定的,如果不存在,则递归地检查模型的父级,直到找到条目或没有其他父级要检查,在这种情况下,它默认为true。 + +这允许进行以下设置,其中多个模型使用单个复合模型的不同部分: + +1. 复合模型指定多个组件 +2. 多个模型将此复合模型指定为其父模型 +3. 这些子模型分别指定部分的不同可见性 + +[bakedmodel]: ../modelloaders/bakedmodel.md +[composite]: ../modelloaders/index.md/#composite-models +[obj]: ../modelloaders/index.md/#wavefront-obj-models \ No newline at end of file diff --git a/docs/translation/zh_CN/rendering/modelloaders/bakedmodel.md b/docs/translation/zh_CN/rendering/modelloaders/bakedmodel.md new file mode 100644 index 000000000..ce5455e5e --- /dev/null +++ b/docs/translation/zh_CN/rendering/modelloaders/bakedmodel.md @@ -0,0 +1,57 @@ +烘焙模型(`BakedModel`) +======================= + +`BakedModel`是对普通模型加载器调用`UnbakedModel#bake`或对自定义模型加载器调用`IUnbakedGeometry#bake`的结果。与`UnbakedModel`或`IUnbakedGeometry`不同,`BakedModel`纯粹代表一种没有任何物品或方块概念的形状,而不是抽象的。它表示已经优化并简化为可以(几乎)进入GPU的几何体。它还可以处理物品或方块的状态以更改模型。 + +在大多数情况下,实际上没有必要手动实现此接口。相反,可以使用现有的实现之一。 + +### `getOverrides` + +返回要用于此模型的[`ItemOverrides`][overrides]。仅当此模型被渲染为物品时才使用此选项。 + +### `useAmbientOcclusion` + +如果模型在存档中渲染为方块,则有问题的方块不会发出任何光,并且环境光遮挡处于启用状态。这将导致使用[环境光遮挡](ambocc)来渲染模型。 + +### `isGui3d` + +如果模型被渲染为物品栏中的物品,在地面上被渲染为实体,在物品框架上,等等,这会使模型看起来“扁平”。在GUI中,这也会禁用照明。 + +### `isCustomRenderer` + +!!! 重要 + 除非你知道自己在做什么,否则只需`return false`然后继续其他事项。 + +将其渲染为物品时,返回`true`将导致模型不被渲染,转而回到`BlockEntityWithoutLevelRenderer#renderByItem`。对于某些原版物品,如箱子和旗帜,此方法被硬编码为将数据从物品复制到`BlockEntity`中,然后使用`BlockEntityRenderer`来渲染BE以代替物品。对于所有其他物品,它将使用由`IClientItemExtensions#getCustomRenderer`提供的`BlockEntityWithoutLevelRenderer`实例。有关详细信息,请参阅[BlockEntityWithoutLevelRenderer][bewlr]页。 + +### `getParticleIcon` + +粒子应使用的任何纹理。对于方块,它将在实体掉落在其上或其被破坏时显示。对于物品,它将在报废或被吃掉时显示。 + +!!! 重要 + 由于模型数据可能会对特定模型的渲染方式产生影响,因此不推荐使用不带参数的原版方法,而推荐使用`#getParticleIcon(ModelData)`。 + +### `getTransforms` + +此方法被废弃,推荐实现`#applyTransform`。如果实现了`#applyTransform`,则该默认实现是足够的。参见[变换][transform]。 + +### `applyTransform` + +参见[变换][transform]。 + +### `getQuads` + +这是`BakedModel`的主要方法。它返回一个`BakedQuad`的列表:包含将用于渲染模型的低级顶点数据的对象。如果模型被呈现为方块,那么传入的`BlockState`是非空的。如果模型被呈现为物品,则从`#getOverrides`返回的`ItemOverrides`负责处理物品的状态,并且`BlockState`参数将为`null`。 + +传入的`Direction`用于面剔除。如果正在渲染的另一个方块的给定边上的块是不透明的,则不会渲染与该边关联的面。如果该参数为`null`,则返回与边不关联的所有面(其永远不会被剔除)。 + +`rand`参数是Random的一个实例。 + +它还接受一个非null的`ModelData`实例。这可用于在通过`ModelProperty`渲染特定模型时定义额外数据。例如,一个这样的属性是`CompositeModel$Data`,用于使用`forge:composite`模型加载器存储模型的任何附加子模型数据。 + +请注意,此方法经常被调用:对于*一个存档中的每个方块*,非剔除面和支持的方块渲染层的每个组合(任何位置从0到28次)调用一次。这给方法应该尽可能快,并且可能需要大量缓存。 + +[overrides]: ./itemoverrides.md +[ambocc]: https://en.wikipedia.org/wiki/Ambient_occlusion +[bewlr]: ../../items/bewlr.md +[transform]: ./transform.md diff --git a/docs/translation/zh_CN/rendering/modelloaders/index.md b/docs/translation/zh_CN/rendering/modelloaders/index.md new file mode 100644 index 000000000..a21060022 --- /dev/null +++ b/docs/translation/zh_CN/rendering/modelloaders/index.md @@ -0,0 +1,26 @@ +自定义模型加载器 +=============== + +“模型”只是一种形状。它可以是一个简单的立方体,可以是几个立方体,也可以是截角二十面体,或者介于两者之间的任何东西。你将看到的大多数模型都是普通的JSON格式。其他格式的模型在运行时由`IGeometryLoader`加载到`IUnbakedGeometry`中。Forge为WaveFront OBJ文件、bucket、复合模型、不同渲染层中的模型提供了默认实现,并重新实现了原版的`builtin/generated`物品模型。大多数事情都不关心加载了什么模型或模型的格式,因为它们最终都由代码中的`BakedModel`表示。 + +!!! 警告 + 通过模型JSON中的顶级`loader`条目指定自定义模型加载程序将导致`elements`条目被忽略,除非它被自定义加载程序使用。所有其他普通条目仍将被加载并在未烘焙的`BlockModel`表示中可用,并且可能在自定义加载程序之外被使用。 + +WaveFront OBJ模型 +----------------- + +Forge为`.obj`文件格式添加了一个加载程序。要使用这些模型,JSON必须引用`forge:obj`加载程序。此加载程序接受位于已注册命名空间中且路径以`.obj`结尾的任何模型位置。`.mtl`文件应放置在与要自动使用的`.obj`具有相同名称的相同位置。`.mtl`文件可能需要手动编辑才能更改指向JSON中定义的纹理的路径。此外,纹理的V轴可以根据创建模型的外部程序翻转(即,V=0可能是底部边缘,而不是顶部边缘)。这可以在建模程序本身中纠正,也可以在模型JSON中这样做: + +```js +{ + // 在与'model'声明相同的级别上添加以下行 + "loader": "forge:obj", + "flip_v": true, + "model": "examplemod:models/block/model.obj", + "textures": { + // 可在.mtl中用#texture0引用 + "texture0": "minecraft:block/dirt", + "particle": "minecraft:block/dirt" + } +} +``` diff --git a/docs/translation/zh_CN/rendering/modelloaders/itemoverrides.md b/docs/translation/zh_CN/rendering/modelloaders/itemoverrides.md new file mode 100644 index 000000000..594ee0d67 --- /dev/null +++ b/docs/translation/zh_CN/rendering/modelloaders/itemoverrides.md @@ -0,0 +1,49 @@ +物品重载(`ItemOverrides`) +========================== + +`ItemOverrides`为[`BakedModel`][baked]提供了一种处理`ItemStack`状态并返回新`BakedModel`的方法;此后,返回的模型将替换旧模型。`ItemOverrides`表示任意函数`(BakedModel, ItemStack, ClientLevel, LivingEntity, int)` → `BakedModel`,使其适用于动态模型。在原版中,它用于实现物品属性重写。 + +### `ItemOverrides()` + +给定`ItemOverride`的列表,该构造函数将复制并烘焙该列表。可以使用`#getOverrides`访问烘焙后的覆盖。 + +### `resolve` + +这需要一个`BakedModel`、`ItemStack`、`ClientLevel`、`LivingEntity`和`int`来生成另一个用于渲染的`BakedModel`。这是模型可以处理其物品状态的地方。 + +这不应该改变存档。 + +### `getOverrides` + +返回一个不可变列表,该列表包含此`ItemOverrides`使用的所有[`BakedOverride`][override]。如果不适用,则返回空列表。 + +## `BakedOverride` + +这个类表示一个原版的物品覆盖,它为一个物品和一个模型的属性保存了几个`ItemOverrides$PropertyMatcher`,以备满足这些匹配器时使用。它们是原版物品JSON模型的`overrides`数组中的对象: + +```js +{ + // 在一个原版JSON物品模型内 + "overrides": [ + { + // 这是一个ItemOverride + "predicate": { + // 这是Map,包含属性的名称以及它们的最小值 + "example1:prop": 0.5 + }, + // 这是该覆盖的'location'或目标模型,如果上面的predicate匹配,则使用它 + "model": "example1:item/model" + }, + { + // 这是另一个ItemOverride + "predicate": { + "example2:prop": 1 + }, + "model": "example2:item/model" + } + ] +} +``` + +[baked]: ./bakedmodel.md +[override]: #bakedoverride diff --git a/docs/translation/zh_CN/rendering/modelloaders/transform.md b/docs/translation/zh_CN/rendering/modelloaders/transform.md new file mode 100644 index 000000000..de2c75820 --- /dev/null +++ b/docs/translation/zh_CN/rendering/modelloaders/transform.md @@ -0,0 +1,37 @@ +变换 +==== + +当[`BakedModel`][bakedmodel]被渲染为物品时,它可以根据在哪个变换中渲染它来应用特殊处理。“变换”是指在什么上下文中渲染模型。可能的转换在代码中由`ItemDisplayContext`枚举表示。有两种处理转换的系统:不推荐使用的原版系统,由`BakedModel#getTransforms`、`ItemTransforms`和`ItemTransform`构成;Forge系统,由方法`IForgeBakedModel#applyTransform`实现。原版代码被进行了修补,以便尽可能使用`applyTransform`而不是原版系统。 + +`ItemDisplayContext` +-------------------- + +`NONE` - 默认情况下,当未设置上下文时,用于显示实体;当`Block`的`RenderShape`设置为`#ENTITYBLOCK_ANIMATED`时,被Forge使用。 + +`THIRD_PERSON_LEFT_HAND`/`THIRD_PERSON_RIGHT_HAND`/`FIRST_PERSON_LEFT_HAND`/`FIRST_PERSON_RIGHT_HAND` - 第一人称值表示玩家何时将物品握在自己手中。第三人称值表示当另一个玩家拿着物品,而客户端用第三人称看着它们时。手的含义是不言自明的。 + +`HEAD` - 表示当任何玩家在头盔槽中佩戴该物品时(例如南瓜)。 + +`GUI` - 表示当该物品被在一个`Screen`中渲染时。 + +`GROUND` - 表示该物品在存档中作为一个`ItemEntity`被渲染时。 + +`FIXED` - 用于物品展示框。 + +原版的方式 +--------- + +原版处理转换的方式是通过`BakedModel#getTransforms`。此方法返回一个`ItemTransforms`,这是一个简单的对象,包含各种作为`public final`的`ItemTransform`字段。`ItemTransform`表示要应用于模型的旋转、平移和比例。`ItemTransforms`是这些的容器,除了`NONE`之外,每个`ItemDisplayContext`都有一个容器。在原版实现中,为`NONE`调用`#getTransform`会产生默认转换`ItemTransform#NO_TRANSFORM`。 + +Forge废弃了使用处理转换的整个原版系统,`BakedModel`的大多数实现应该简单地从`BakedModel#getTransforms`中`return ItemTransforms#NO_TRANSFORMS`(这是默认实现)。相反,他们应该实现`#applyTransform`。 + +Forge的方式 +----------- + +Forge处理转换的方法是`#applyTransform`,这是一种修补到`BakedModel`中的方法。它取代了`#getTransforms`方法。 + +#### `BakedModel#applyTransform` + +给定一个`ItemDisplayContext`、`PoseStack`和一个布尔值来确定是否对左手应用变换,此方法将生成一个要渲染的`BakedModel`。因为返回的`BakedModel`可以是一个全新的模型,所以这种方法比原版方法(例如,一张手里看起来很平但在地上皱巴巴的纸)更灵活。 + +[bakedmodel]: ./bakedmodel.md diff --git a/docs/translation/zh_CN/resources/client/index.md b/docs/translation/zh_CN/resources/client/index.md new file mode 100644 index 000000000..f29a9f4e3 --- /dev/null +++ b/docs/translation/zh_CN/resources/client/index.md @@ -0,0 +1,15 @@ +资源包 +====== + +[资源包][respack]允许通过`assets`目录自定义客户端资源。这包括纹理、模型、声音、本地化和其他。你的模组(以及Forge本身)也可以有资源包。因此,任何用户都可以修改该目录中定义的所有纹理、模型和其他资源。 + +### 创建一个资源包 +资源包存储在项目的资源中。`assets`目录包含该包的内容,而该包本身则由`assets`文件夹旁边的`pack.mcmeta`定义。 +你的模组可以有多个资源域,因为你可以添加或修改现有的资源包,比如原版的、Forge的或其他模组的。 +然后,你可以按照[在Minecraft Wiki][createrespack]中找到的步骤创建任何资源包。 + +附加阅读:[资源位置][resourcelocation] + +[respack]: https://minecraft.fandom.com/wiki/Resource_Pack +[createrespack]: https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack +[resourcelocation]: ../../concepts/resources.md#ResourceLocation diff --git a/docs/translation/zh_CN/resources/client/models/index.md b/docs/translation/zh_CN/resources/client/models/index.md new file mode 100644 index 000000000..9cb958697 --- /dev/null +++ b/docs/translation/zh_CN/resources/client/models/index.md @@ -0,0 +1,24 @@ +模型 +==== + +[模型系统][models]是Minecraft赋予方块和物品形状的方式。通过模型系统,方块和物品被映射到它们的模型,这些模型定义了它们的外观。模型系统的主要目标之一是不仅允许纹理,还允许资源包更改方块/物品的整个形状。事实上,任何添加物品或方块的模组也包含用于其方块和物品的迷你资源包。 + +模型文件 +------- + +模型和纹理通过[`ResourceLocation`][resloc]链接,但使用`ModelResourceLocation`存储在`ModelManager`中。模型通过方块或物品的注册表名称在不同位置引用,具体取决于它们是引用[方块状态][statemodel]还是[物品模型][itemmodels]。方块将使其`ModelResourceLocation`代表其注册表名称及其当前[`BlockState`][state]的字符串化版本,而物品将使用其注册表名称后跟`inventory`。 + +!!! 注意 + JSON模型只支持长方体元素;没有办法表达三角楔或类似的东西。要有更复杂的模型,必须使用另一种格式。 + +### 纹理 + +纹理和模型一样,包含在资源包中,并被称为`ResourceLocation`。在《我的世界》中,[UV坐标][UV] (0,0)表示**左上角**。UV*总是*从0到16。如果纹理较大或较小,则会缩放坐标以进行拟合。纹理也应该是正方形的,纹理的边长应该是2的幂,否则会破坏mipmapping(例如1x1、2x2、8x8、16x16和128x128是好的。不建议使用5x5和30x30,因为它们不是2的幂。5x10和4x8会完全断裂,因为它们不是正方形的。)。只有当纹理是[动画化的][animated]时,纹理才应该不是正方形。 + +[models]: https://minecraft.fandom.com/wiki/Tutorials/Models#File_path +[resloc]: ../../../concepts/resources.md#resourcelocation +[statemodel]: https://minecraft.fandom.com/wiki/Tutorials/Models#Block_states +[itemmodels]: https://minecraft.fandom.com/wiki/Tutorials/Models#Item_models +[state]: ../../../blocks/states.md +[uv]: https://en.wikipedia.org/wiki/UV_mapping +[animated]: https://minecraft.fandom.com/wiki/Resource_Pack?so=search#Animation diff --git a/docs/translation/zh_CN/resources/client/models/itemproperties.md b/docs/translation/zh_CN/resources/client/models/itemproperties.md new file mode 100644 index 000000000..f9c1b250d --- /dev/null +++ b/docs/translation/zh_CN/resources/client/models/itemproperties.md @@ -0,0 +1,62 @@ +物品属性 +======== + +物品属性是将物品的“属性”公开给模型系统的一种方式。一个例子是弓,其中最重要的特性是弓被拉了多远。然后,这些信息用于选择弓的模型,创建拉动弓的动画。 + +物品属性为其注册的每个`ItemStack`分配一个特定的`float`值,原版物品模型定义可以使用这些值来定义“覆盖”,其中物品默认为某个模型,但如果覆盖匹配,则覆盖该模型并使用另一个模型。它们之所以有用,主要是因为它们是连续的。例如,弓使用物品属性来定义其拉动动画。物品模型由'float'数字谓词决定,它不受限制,但通常在`0.0F`和`1.0F`之间。这允许资源包为拉弓动画添加他们想要的任意多个模型,而不是在动画中为他们的模型设置四个“槽”。指南针和时钟也是如此。 + +向物品添加属性 +------------- + +`ItemProperties#register`用于向某个物品添加属性。`Item`参数是要附加属性的物品(例如`ExampleItems#APPLE`)。`ResourceLocation`参数是所要赋予属性的名称(例如`new ResourceLocation("pull")`)。`ItemPropertyFunction`是一个函数接口,它接受`ItemStack`、它所在的`ClientLevel`(可以为null)、持有它的`LivingEntity`(可以是null)和包含持有实体的id的`int`(可能是`0`),返回属性的`float`值。对于修改后的物品属性,建议将模组的mod id用作命名空间(例如`examplemod:property`,而不仅仅是`property`,因为这实际上意味着`minecraft:property`)。这些操作应在`FMLClientSetupEvent`中完成。 +还有另一个方法`ItemProperties#registerGeneric`用于向所有物品添加属性,并且它不将`Item`作为其参数,因为所有物品都将应用此属性。 + +!!! 重要 + 使用`FMLClientSetupEvent#enqueueWork`执行这些任务,因为`ItemProperties`中的数据结构不是线程安全的。 + +!!! 注意 + Mojang反对使用`ItemPropertyFunction`而推荐使用`ClampedItemPropertyFunction`子接口,该子接口将结果夹在`0`和`1`之间。 + +覆盖的使用 +--------- + +覆盖的格式可以在[wiki][format]上看到,一个很好的例子可以在`model/item/bow.json`中找到。为了参考,这里是一个具有`examplemod:power`属性的物品的假设例子。如果值不匹配,则默认为当前模型,但如果有多个匹配,则会选择列表中的最后一个匹配。 + +!!! 重要 + predicate适用于*大于或等于*给定值的所有值。 + +```js +{ + "parent": "item/generated", + "textures": { + // Default + "layer0": "examplemod:items/example_partial" + }, + "overrides": [ + { + // power >= .75 + "predicate": { + "examplemod:power": 0.75 + }, + "model": "examplemod:item/example_powered" + } + ] +} +``` + +下面是支持代码中的一个假设片段。与旧版本(低于1.16.x)不同,这只需要在客户端完成,因为服务端上不存在`ItemProperties`。 + +```java +private void setup(final FMLClientSetupEvent event) +{ + event.enqueueWork(() -> + { + ItemProperties.register(ExampleItems.APPLE, + new ResourceLocation(ExampleMod.MODID, "pulling"), (stack, level, living, id) -> { + return living != null && living.isUsingItem() && living.getUseItem() == stack ? 1.0F : 0.0F; + }); + }); +} +``` + +[format]: https://minecraft.fandom.com/wiki/Tutorials/Models#Item_models diff --git a/docs/translation/zh_CN/resources/client/models/tinting.md b/docs/translation/zh_CN/resources/client/models/tinting.md new file mode 100644 index 000000000..7dc313bff --- /dev/null +++ b/docs/translation/zh_CN/resources/client/models/tinting.md @@ -0,0 +1,32 @@ +纹理色调 +======== + +原版中的许多方块和物品会根据它们的位置或特性(如草)改变其纹理颜色。模型支持在面上指定“色调索引”,这是可以由`BlockColor`和`ItemColor`处理的整数。有关如何在原版模型中定义色调索引的信息,请参阅[wiki][]。 + +### `BlockColor`/`ItemColor` + +这两个都是单方法接口。`BlockColor`接受一个`BlockState`、一个(可为空的)`BlockAndTintGetter`和一个(可为空的)`BlockPos`。`ItemColor`接受一个`ItemStack`。它们都采用一个`int`参数`tintIndex`,它是正在着色的面的色调索引。 它们都返回一个`int`,一个颜色乘数。这个`int`被视为4个无符号字节,即alpha、red、green和blue,按照从最高有效字节到最低有效字节的顺序。对于着色面上的每个像素,每个颜色通道的值是`(int)((float) base * multiplier / 255.0)`,其中`base`是通道的原始值,`multiplier`是颜色乘数的关联字节。 请注意,方块不使用Alpha通道。例如,未着色的草纹理看起来是白色和灰色的。草的`BlockColor`和`ItemColor`返回颜色乘数,red和blue分量较低,但alpha和green分量较高(至少在温暖的生物群系中),因此当执行乘法时,绿色会被带出,红色/蓝色会减少。 + +如果物品继承自`builtin/generated`模型,则每个层(“layer0”、“layer1”等)都有与其层索引相对应的色调索引。 + +### 创建颜色处理器 + +`BlockColor`需要注册到游戏的`BlockColors`实例中。`BlockColors`可以通过`RegisterColorHandlersEvent$Block`获取,`BlockColor`可以通过`#register`注册。请注意,这不会导致给定方块的`BlockItem`被着色。`BlockItem`是物品,需要使用`ItemColor`进行着色。 + +```java +@SubscribeEvent +public void registerBlockColors(RegisterColorHandlersEvent.Block event){ + event.register(myBlockColor, coloredBlock1, coloredBlock2, ...); +} +``` + +`ItemColor`需要注册到游戏的`ItemColors`实例中。`ItemColors`可以通过`RegisterColorHandlersEvent$Item`获取,`ItemColor`可以通过`#register`注册。此方法也被重载为接受`Block`,它只是将物品`Block#asItem`的颜色处理器注册为物品(即方块的`BlockItem`)。 + +```java +@SubscribeEvent +public void registerItemColors(RegisterColorHandlersEvent.Item event){ + event.register(myItemColor, coloredItem1, coloredItem2, ...); +} +``` + +[wiki]: https://minecraft.fandom.com/wiki/Tutorials/Models#Block_models diff --git a/docs/translation/zh_CN/resources/server/advancements.md b/docs/translation/zh_CN/resources/server/advancements.md new file mode 100644 index 000000000..25d9dd84a --- /dev/null +++ b/docs/translation/zh_CN/resources/server/advancements.md @@ -0,0 +1,165 @@ +进度 +==== + +进度是玩家可以实现的任务,可以推进游戏的进度。进度可以基于玩家可能直接参与的任何动作来触发。 + +原版中的所有进度实现都是通过JSON进行数据驱动的。这意味着模组不需要创建新的进度,只需要[数据包][datapack]。关于如何创建这些进度并将其放入模组的`resources`中的完整列表可以在[Minecraft Wiki][wiki]上找到。此外,进度可以[有条件加载和或保持默认][conditional],这取决于存在的信息(模组被加载、物品的存在等)。 + +进度标准 +-------- + +若要解锁一个进度,必须满足指定的标准。通过执行某个动作时执行的触发器来跟踪标准:杀死实体、更改物品栏、给动物喂食等。任何时候将进度加载到游戏中,定义的标准都会被读取并添加为触发器的监听器。然后调用一个触发器函数(通常称为`#trigger`),该函数检查所有监听器当前状态是否满足进度标准的条件。只有在通过完成所有条件获得进度后,才会删除进度的标准监听器。 + +需求被定义为包含字符串数组的一个数组,该数组表示在进度中指定的标准的名称。一旦满足一个字符串数组的条件,就完成了进度: + +```js +// 在某个进度JSON中 + +// 所定义的要满足的标准的列表 +"criteria": { + "example_criterion1": { /*...*/ }, + "example_criterion2": { /*...*/ }, + "example_criterion3": { /*...*/ }, + "example_criterion4": { /*...*/ } +}, + +// 该进度只能解锁一次 +// - 标准1和2均被满足 +// 或 +// - 标准3和4均被满足 +"requirements": [ + [ + "example_criterion1", + "example_criterion2" + ], + [ + "example_criterion3", + "example_criterion4" + ] +] +``` + +原版定义的标准触发器列表可以在`CriteriaTriggers`中找到。此外,JSON格式是在[Minecraft Wiki][triggers]上定义的。 + +### 自定义标准触发器 + +可以通过为已创建的`AbstractCriterionTriggerInstance`子类实现`SimpleCriterionTrigger`来创建自定义条件触发器。 + +### AbstractCriterionTriggerInstance子类 + +`AbstractCriterionTriggerInstance`表示在`criteria`对象中定义的单个标准。触发器实例负责保存定义的条件,返回输入是否与条件匹配,并将实例写入JSON用于数据生成。 + +条件通常通过构造函数传递。`AbstractCriterionTriggerInstance`父级构造函数要求实例将触发器的注册表名和玩家必须满足的条件定义为`ContextAwarePredicate`。触发器的注册表名称应该直接提供给父级,而玩家的条件应该是构造函数参数。 + +```java +// 其中ID是该触发器的注册表名称 +public ExampleTriggerInstance(ContextAwarePredicate player, ItemPredicate item) { + super(ID, player); + // 存储必须满足的物品条件 +} +``` + +!!! 注意 + 通常,触发器实例有一个静态构造函数,允许轻松创建这些实例以生成数据。这些静态工厂方法也可以静态导入,而不是类本身。 + + ```java + public static ExampleTriggerInstance instance(ContextAwarePredicate player, ItemPredicate item) { + return new ExampleTriggerInstance(player, item); + } + ``` + +此外,应该重写`#serializeToJson`方法。该方法应该将实例的条件添加到其他JSON数据中。 + +```java +@Override +public JsonObject serializeToJson(SerializationContext context) { + JsonObject obj = super.serializeToJson(context); + // 将条件写入json中 + return obj; +} +``` + +最后,应该添加一个方法,该方法接受当前数据状态并返回用户是否满足必要条件。玩家的条件已经通过`SimpleCriterionTrigger#trigger(ServerPlayer, Predicate)`进行了检查。大多数触发器实例称这个方法为`#matches`。 + +```java +// 此方法对于每个实例都是唯一的,因此不会被重写 +public boolean matches(ItemStack stack) { + // 由于ItemPredicate与一个物品栈匹配,因此一个物品栈是输入 + return this.item.matches(stack); +} +``` + +### SimpleCriterionTrigger + +`SimpleCriterionTrigger`子类,其中`T`是触发器实例的类型,负责指定触发器的注册表名、创建触发器实例以及检查触发器实例和在成功时运行附加监听器的方法。 + +触发器的注册表名称被提供给`#getId`。这应该与提供给触发器实例的注册表名称相匹配。 + +触发器实例是通过`#createInstance`创建的。此方法从JSON中读取一个标准。 + +```java +@Override +public ExampleTriggerInstance createInstance(JsonObject json, ContextAwarePredicate player, DeserializationContext context) { + // 从JSON中读取条件:item + return new ExampleTriggerInstance(player, item); +} +``` + +最后,定义了一个方法来检查所有触发器实例,并在满足它们的条件时运行监听器。此方法接受`ServerPlayer`和`AbstractCriterionTriggerInstance`子类中匹配的方法定义的任何其他数据。此方法应在内部调用`SimpleCriterionTrigger#trigger`以正确处理检查所有监听器。大多数触发器实例从称这个方法为`#trigger`。 + +```java +// 此方法对于每个触发器都是唯一的,因此不会被重写 +public void trigger(ServerPlayer player, ItemStack stack) { + this.trigger(player, + // AbstractCriterionTriggerInstance子类中的条件检查器方法 + triggerInstance -> triggerInstance.matches(stack) + ); +} +``` + +之后,应在`FMLCommonSetupEvent`期间使用`CriteriaTriggers#register`注册实例。 + +!!! 重要 + `CriteriaTriggers#register`必须通过`FMLCommonSetupEvent#enqueueWork`排入同步工作队列,因为该方法不是线程安全的。 + +### 触发器的调用 + +每当执行被检查的操作时,都应该调用`SimpleCriterionTrigger`子类定义的`#trigger`方法。 + +```java +// 在执行操作的某段代码中 +// 其中EXAMPLE_CRITERIA_TRIGGER是自定义标准触发器 +public void performExampleAction(ServerPlayer player, ItemStack stack) { + // 运行代码以执行操作 + EXAMPLE_CRITERIA_TRIGGER.trigger(player, stack); +} +``` + +进度奖励 +-------- + +当进度达成时,可以给予奖励。其可以是经验点数、战利品表、配方书的配方的组合,也可以是作为创造模式玩家执行的[函数][function]。 + +```js +// In some advancement JSON +"rewards": { + "experience": 10, + "loot": [ + "minecraft:example_loot_table", + "minecraft:example_loot_table2" + // ... + ], + "recipes": [ + "minecraft:example_recipe", + "minecraft:example_recipe2" + // ... + ], + "function": "minecraft:example_function" +} +``` + +[datapack]: https://minecraft.fandom.com/wiki/Data_pack +[wiki]: https://minecraft.fandom.com/wiki/Advancement/JSON_format +[conditional]: ./conditional.md#implementations +[function]: https://minecraft.fandom.com/wiki/Function_(Java_Edition) +[triggers]: https://minecraft.fandom.com/wiki/Advancement/JSON_format#List_of_triggers diff --git a/docs/translation/zh_CN/resources/server/conditional.md b/docs/translation/zh_CN/resources/server/conditional.md new file mode 100644 index 000000000..04edb6591 --- /dev/null +++ b/docs/translation/zh_CN/resources/server/conditional.md @@ -0,0 +1,179 @@ +条件性加载数据 +============= + +有时,模组开发者可能希望包括一些使用来自另一个模组的信息的数据驱动的对象,而不必明确地使该模组成为依赖项。其他情况可能是,当某些对象存在时,将其与其他模组编写的条目交换。这可以通过条件子系统来完成。 + +实现 +---- + +目前,条件加载已针对配方和进度实现。对于任何有条件的配方或进度,都会加载一个条件到数据对的列表。如果为列表中的某个数据指定的条件为true,则返回该数据。否则,将丢弃该数据。 + +```js +{ + // 需要为配方指定类型,因为它们可以具有自定义序列化器 + // 进度不需要这种类型 + "type": "forge:conditional", + + "recipes": [ // 或'advancements'(对于进度) + { + // 要检查的条件 + "conditions": [ + // 该列表中的条件用逻辑和(AND)相连 + { + // 条件1 + }, + { + // 条件2 + } + ], + "recipe": { // 或'advancement'(对于进度) + // 如果所有条件都成功,则使用的配方 + } + }, + { + // 如果上一个条件失败,则接下来要检查的条件 + }, + ] +} +``` + +通过`ConditionalRecipe$Builder`和`ConditionalAdvancement$Builder`,条件加载的数据还具有用于[数据生成][datagen]的包装。 + +条件 +---- + +条件是通过将`type`设置为[`IConditionSerializer#getID`][serializer]指定的条件名称来指定的。 + +### True和False + +布尔条件不包含任何数据,并返回条件的期望值。它们用`forge:true`和`forge:false`来表示。 + +```js +// 对于某个条件 +{ + // 将始终返回true(或为'forge:false'时始终返回false) + "type": "forge:true" +} +``` + +### Not、And和Or + +布尔运算符条件由正在操作的条件组成,并应用以下逻辑。它们用`forge:not`、`forge:and`和`forge:or`表示。 + + +```js +// 对于某个条件 +{ + // 反转存储条件的结果 + "type": "forge:not", + "value": { + // 一个条件 + } +} +``` + +```js +// 对于某个条件 +{ + // 将存储条件用逻辑和(AND)相连(或为'forge:or'时将存储条件用逻辑或(OR)相连) + "type": "forge:and", + "values": [ + { + // 第一个条件 + }, + { + // 第二个要用逻辑和(AND)连接的条件(或为'forge:or'时用逻辑或(OR)连接) + } + ] +} +``` + +### 模组被加载 + +只要在当前应用程序中加载了具有给定id的指定模组,`ModLoadedCondition`就会返回true。其由`forge:mod_loaded`表示。 + +```js +// 对于某个条件 +{ + "type": "forge:mod_loaded", + // 如果'examplemod'已被加载,则返回true + "modid": "examplemod" +} +``` + +### 物品存在 + +只要给定物品已在当前应用程序中注册,`ItemExistsCondition`就会返回true。其由`forge:item_exists`表示。 + +```js +// 对于某个条件 +{ + "type": "forge:item_exists", + // 如果'examplemod:example_item'已被注册,则返回true + "item": "examplemod:example_item" +} +``` + +### 标签为空 + +只要给定的物品标签中没有物品,`TagEmptyCondition`就会返回true。其由`forge:tag_empty`表示。 + +```js +// 对于某个条件 +{ + "type": "forge:tag_empty", + // 如果'examplemod:example_tag'是一个没有条目的物品标签,则返回true + "tag": "examplemod:example_tag" +} +``` + +创建自定义条件 +------------- + +可以通过实现`ICondition`及与其关联的`IConditionSerializer`来创建自定义条件。 + +### ICondition + +任何条件只需要实现两种方法: + +方法 | 描述 +:---: | :--- +getID | 该条件的注册表名称。必须等效于[`IConditionSerializer#getID`][serializer]。仅用于[数据生成][datagen]。 +test | 当条件满足时返回true。 + +!!! 注意 + 每个`#test`都可以访问一些代表游戏状态的`IContext`。目前,从注册表中只能获取标签。 + +### IConditionSerializer + +序列化器需要实现三种方法: + +方法 | 描述 +:---: | :--- +getID | 该条件的注册表名称。必须等效于[`ICondition#getID`][condition]。 +read | 从JSON中读取条件数据。 +write | 将给定的条件数据写入JSON。 + +!!! 注意 + 条件序列化器不负责写入或读取序列化器的类型,类似于Minecraft中的其他序列化器实现。 + +之后,应声明一个静态实例来保存初始化的序列化器,然后在`RecipeSerializer`的`RegisterEvent`期间或在`FMLCommonSetupEvent`期间使用`CraftingHelper#register`进行注册。 + +```java +// 在某个序列化器类中 +public static final ExampleConditionSerializer INSTANCE = new ExampleConditionSerializer(); + +// 在某个处理器类中 +public void registerSerializers(RegisterEvent event) { + event.register(ForgeRegistries.Keys.RECIPE_SERIALIZERS, + helper -> CraftingHelper.register(INSTANCE) + ); +} +``` + +!!! 重要 + 如果使用`FMLCommonSetupEvent`注册条件序列化器,则必须通过 `FMLCommonSetupEvent#enqueueWork`将其排入同步工作队列,因为`CraftingHelper#register`不是线程安全的。 + +[datagen]: ../../datagen/server/recipes.md +[serializer]: #iconditionserializer +[condition]: #icondition diff --git a/docs/translation/zh_CN/resources/server/glm.md b/docs/translation/zh_CN/resources/server/glm.md new file mode 100644 index 000000000..d4f510c65 --- /dev/null +++ b/docs/translation/zh_CN/resources/server/glm.md @@ -0,0 +1,145 @@ +全局战利品修改器 +=============== + +全局战利品修改器是一种数据驱动的方法,可以处理收割掉落的修改,而无需覆盖数十到数百个原版战利品表,也无需处理需要与另一个模组的战利品表交互的效果,而不知道可能加载了什么模组。全局战利品修改器也是堆叠的,而不是后来者为王,类似于标签。 + +注册一个全局战利品修改器 +---------------------- + +你将需要4件事物: + +1. 创建一个`global_loot_modifiers.json`。 + * 这将告诉Forge你的修改器以及类似于[tags][标签]的工作。 +2. 代表修改器的序列化json。 + * 这将包含有关你修改的所有数据,并允许数据包调整你的效果。 +3. 一个继承自`IGlobalLootModifier`的类。 + * 使修改器工作的操作代码。大多数模组开发者都可以继承`LootModifier`,因为它提供了基本功能。 +4. 最后,使用编解码器对操作类进行编码和解码。 + * 其应像任何其他`IForgeRegistryEntry`一样被[注册][registered]。 + +`global_loot_modifiers.json`文件 +-------------------------------- + +`global_loot_modifiers.json`表示要加载到游戏中的所有战利品修改器。此文件**必须**放在`data/forge/loot_modifiers/global_loot_modifiers.json`。 + +!!! 重要 + `global_loot_modifiers.json`只能在`forge`命名空间中被读取。如果该文件位于模组的命名空间下,则会被忽略。 + +`entries`是将要加载的修改器的*有序列表*。指定的[ResourceLocation][resloc]指向其在`data//loot_modifiers/.json`中的关联条目。这主要与数据包生成器有关,用于解决独立模组的修改器之间的冲突。 + +`replace`,当`true`时,会将行为从向全局列表添加战利品修改器更改为完全替换全局列表条目。为了与其他模组实现兼容,模组开发者将希望使用`false`。数据包作者可能希望用`true`以指定其覆盖。 + +```js +{ + "replace": false, // 必须存在 + "entries": [ + // 代表'data/examplemod/loot_modifiers/example_glm.json'中的一个战利品修改器 + "examplemod:example_glm", + "examplemod:example_glm2" + // ... + ] +} +``` + +序列化JSON +---------- + +该文件包含与修改器相关的所有潜在变量,包括修改任何战利品之前必须满足的条件。尽可能避免硬编码值,以便数据包作者可以根据需要调整平衡。 + +`type`表示用于读取关联JSON文件的[编解码器][codec]的注册表名称。这必须始终存在。 + +`conditions`应该表示该修改器要激活的战利品表条件。条件应该避免被硬编码,以允许数据包作者尽可能灵活地调整标准。这也必须始终存在。 + +!!! 重要 + 尽管`conditions`应该表示修改器激活所需的内容,但只有在使用捆绑的Forge类时才会出现这种情况。如果使用`LootModifier`作为子类,则所有条件都将用**逻辑与(AND)**相连,并检查是否应应用修改器。 + +还可以指定由序列化器读取并由修改器定义的任何附加属性。 + +```js +// 在data/examplemod/loot_modifiers/example_glm.json内 +{ + "type": "examplemod:example_loot_modifier", + "conditions": [ + // 普通的战利品表条件 + // ... + ], + "prop1": "val1", + "prop2": 10, + "prop3": "minecraft:dirt" +} +``` + +`IGlobalLootModifier` +--------------------- + +要提供全局战利品修改器指定的功能,必须指定一个`IGlobalLootModifier`实现。这些是每次序列化器解码JSON中的信息并将其提供给该对象时生成的实例。 + +为了创建新的修改器,需要定义两种方法:`#apply`和`#codec`。`#apply`获取将与上下文信息一起生成的当前战利品,例如当前等级或额外定义的参数。它返回要生成的掉落物列表。 + +!!! 注意 + 从任何一个修改器返回的掉落物列表都会按照它们注册的顺序输入到其他修改器中。因此,修改后的战利品可以被另一个战利品修改器修改。 + +`#codec`返回已注册的[编解码器][codec],用于将修改器编码到JSON或从JSON解码修改器。 + +### `LootModifier`子类 + +`LootModifier`是`IGlobalLootModifier`的一个抽象实现,用于提供大多数模组开发者可以轻松扩展和实现的基本功能。其通过定义`#apply`方法来检查条件,以确定是否修改生成的战利品,从而扩展了现有接口。 + +在子类实现中有两件事需要注意:构造函数必须接受`LootItemCondition`的一个数组和`#doApply`方法。 + +`LootItemCondition`的数组定义了在修改战利品之前必须为true的条件列表。所提供的条件是用**逻辑和(AND)**连在一起的,这意味着所有条件都必须为true。 + +`#doApply`方法的工作原理与`#apply`方法相同,只是它只在所有条件都返回true时执行。 + +```java +public class ExampleModifier extends LootModifier { + + public ExampleModifier(LootItemCondition[] conditionsIn, String prop1, int prop2, Item prop3) { + super(conditionsIn); + // 存储其余参数 + } + + @NotNull + @Override + protected ObjectArrayList doApply(ObjectArrayList generatedLoot, LootContext context) { + // 修改战利品并返回新的掉落物 + } + + @Override + public Codec codec() { + // 返回用于编码和解码此修改器的编解码器 + } +} +``` + +战利品修改器的编解码器 +-------------------- + +JSON和`IGlobalLootModifier`实例之间的桥梁是[`Codec`][codecdef],其中`T`表示要使用的`IGlobalLootModifier`的具体类型。 + +为了方便起见,通过`LootModifier#codecStart`为类似记录的编解码器提供了一个战利品条件编解码器。这用于相关战利品修改器的[数据生成][datagen]。 + +```java +// 对于某个DeferredRegister> REGISTRAR +public static final RegistryObject> = REGISTRAR.register("example_codec", () -> + RecordCodecBuilder.create( + inst -> LootModifier.codecStart(inst).and( + inst.group( + Codec.STRING.fieldOf("prop1").forGetter(m -> m.prop1), + Codec.INT.fieldOf("prop2").forGetter(m -> m.prop2), + ForgeRegistries.ITEMS.getCodec().fieldOf("prop3").forGetter(m -> m.prop3) + ) + ).apply(inst, ExampleModifier::new) + ) +); +``` + +[示例][examples]可以在Forge Git存储库中找到,包括精准采集和熔炼效果。 + +[tags]: ./tags.md +[resloc]: ../../concepts/resources.md#ResourceLocation +[codec]: #the-loot-modifier-codec +[registered]: ../../concepts/registries.md#methods-for-registering +[codecdef]: ../../datastorage/codecs.md +[datagen]: ../../datagen/server/glm.md +[examples]: https://github.com/MinecraftForge/MinecraftForge/blob/1.20.x/src/test/java/net/minecraftforge/debug/gameplay/loot/GlobalLootModifiersTest.java diff --git a/docs/translation/zh_CN/resources/server/index.md b/docs/translation/zh_CN/resources/server/index.md new file mode 100644 index 000000000..d8b755b90 --- /dev/null +++ b/docs/translation/zh_CN/resources/server/index.md @@ -0,0 +1,14 @@ +数据包 +====== +在1.13中,Mojang在游戏基底中添加了[数据包][datapack]。它们允许通过`data`目录修改逻辑服务端的文件。这包括进度、战利品表(loot_tables)、结构、配方、标签等。Forge和你的模组也可以有数据包。因此,任何用户都可以修改该目录中定义的所有配方、战利品表和其他数据。 + +### 创建一个数据包 +数据包存储在项目资源的`data`目录中。 +你的模组可以有多个数据域,因为你可以添加或修改现有的数据包,比如原版的、Forge的或其他模组的。 +然后,你可以按照[此处][createdatapack]的步骤创建任何数据包。 + +附加阅读:[资源位置][resourcelocation] + +[datapack]: https://minecraft.fandom.com/wiki/Data_pack +[createdatapack]: https://minecraft.fandom.com/wiki/Tutorials/Creating_a_data_pack +[resourcelocation]: ../../concepts/resources.md#ResourceLocation diff --git a/docs/translation/zh_CN/resources/server/loottables.md b/docs/translation/zh_CN/resources/server/loottables.md new file mode 100644 index 000000000..2a6a19616 --- /dev/null +++ b/docs/translation/zh_CN/resources/server/loottables.md @@ -0,0 +1,110 @@ +战利品表 +======= + +战利品表是逻辑文件,它规定了当发生各种操作或场景时应该发生什么。尽管原版系统纯粹处理物品生成,但该系统可以扩展为执行任意数量的预定义操作。 + +由数据驱动的表 +------------- + +原版中的大多数战利品表都是通过JSON进行数据驱动的。这意味着模组不需要创建新的战利品表,只需要[数据包][datapack]。关于如何在模组的`resources`文件夹中创建和放置这些战利品表的完整列表可以在[Minecraft Wiki][wiki]上找到。 + +使用战利品表 +----------- + +战利品表由其指向`data//loot_tables/.json`的`ResourceLocation`引用。与引用相关联的`LootTable`可以使用`LootDataResolver#getLootTable`获得,其中`LootDataResolver`可以通过`MinecraftServer#getLootData`获得。 + +战利品表总是使用给定的参数生成的。`LootParams`包含表的生成存档、特定的随机化器和种子(如果需要)、更好生成的运气、定义场景上下文的`LootContextParam`以及激活时应出现的任何动态信息。可以使用`LootParams$Builder`生成器的构造函数创建`LootParams`,并通过传递`LootContextParamSet`通过`LootParams$Builder#create`构建`LootParams`。 + +战利品表也可能有一些上下文。`LootContext`接受已构建的`LootParams`,并可以设置一些随机种子实例。上下文是通过生成器`LootContext$Builder`创建的,并使用`LootContext$Builder#create`通过传递表示要使用的随机实例的可为null的`ResourceLocation`来构建。 + +`LootTable`可用于使用以下可用方法之一生成`ItemStack`,其可能接受一个`LootParams`或一个`LootContext`: + +方法 | 描述 +:---: | :--- +`getRandomItemsRaw` | 消耗由战利品表生成的物品。 +`getRandomItems` | 返回由战利品表生成的物品。 +`fill` | 用已生成的战利品表填充容器。 + +!!! 注意 + 战利品表是为生成物品而构建的,因此这些方法需要对`ItemStack`进行一些处理。 + +附加特性 +------- + +Forge为战利品表提供了一些额外的行为,以更好地控制系统。 + +### `LootTableLoadEvent` + +`LootTableLoadEvent`是在Forge事件总线上触发的[事件][event],每当加载战利品表时就会触发。如果事件被取消,则会加载一个空的战利品表。 + +!!! 重要 + **不要**通过此事件修改战利品表的掉落。这些修改应该使用[全局战利品修改器][glm]来完成。 + +### 战利品池名称 + +Loot pools can be named using the `name` key. Any non-named loot pool will be the hash code of the pool prefixed by `custom#`. +可以使用`name`键对战利品池进行命名。任何未命名的战利品池都将是以`custom#`为前缀的池的哈希代码。 + +```js +// 对于某个战利品池 +{ + "name": "example_pool", // 战利品池将被命名为'example_pool' + "rolls": { + // ... + }, + "entries": { + // ... + } +} +``` + +### 抢夺修改器 + +战利品表现在除了受到抢夺附魔的影响外,还受到Forge事件总线上的`LootingLevelEvent`的影响。 + +### 附加的上下文参数 + +Forge扩展了某些参数集,以解决可能适用的缺失上下文。`LootContextParamSets#CHEST`现在允许使用`LootContextParams#KILLER_ENTITY`,因为箱子矿车是可以被破坏(或“杀死”)的实体。`LootContextParamSets#FISHING`还允许`LootContextParams#KILLER_ENTITY`,因为鱼钩也是一个实体,当玩家取回它时会收回(或“杀死”)。 + +### 熔炼时的多个物品 + +当使用`SmeltItemFunction`时,熔炼配方现在将返回结果中的实际物品数,而不是单个熔炼物品(例如,如果熔炼配方返回3个物品,并且有3次掉落,则结果将是9个熔炼物品,而不是3个)。 + +### 战利品表Id条件 + +Forge添加了一个额外的`LootItemCondition`,允许为特定的表生成某些物品。这通常用于[全局战利品修改器][glm]。 + +```js +// 在某个战利品池或池条目中 +{ + "conditions": [ + { + "condition": "forge:loot_table_id", + // 当该战利品表对于泥土时将适用 + "loot_table_id": "minecraft:blocks/dirt" + } + ] +} +``` + +### “工具能否执行操作”条件 + +Forge添加了一个额外的`LootItemCondition`,用于检查给定的`LootContextParams#TOOL`是否可以执行指定的`ToolAction`。 + +```js +// 在某个战利品池或池条目中 +{ + "conditions": [ + { + "condition": "forge:can_tool_perform_action", + // 当该工具可以像斧一样剥下原木时将适用 + "action": "axe_strip" + } + ] +} +``` + +[datapack]: https://minecraft.fandom.com/wiki/Data_pack +[wiki]: https://minecraft.fandom.com/wiki/Loot_table +[event]: ../../concepts/events.md#creating-an-event-handler +[glm]: ./glm.md diff --git a/docs/translation/zh_CN/resources/server/recipes/custom.md b/docs/translation/zh_CN/resources/server/recipes/custom.md new file mode 100644 index 000000000..1a69abc21 --- /dev/null +++ b/docs/translation/zh_CN/resources/server/recipes/custom.md @@ -0,0 +1,128 @@ +自定义配方 +========= + +每个配方定义都由三个组件组成:`Recipe`实现,它保存数据并用所提供的输入处理执行逻辑,`RecipeType`表示配方将用于的类别或上下文,以及`RecipeSerializer`,它处理配方数据的解码和网络通信。如何选择使用配方取决于实施者。 + +配方 +---- + +`Recipe`接口描述配方数据和执行逻辑。这包括匹配输入并提供相关联的结果。由于配方子系统默认执行物品转换,因此输入是通过`Container`子类型提供的。 + +!!! 重要 + 传递到配方中的`Container`应被视为其内容是不可变的。任何可变操作都应该通过`ItemStack#copy`对输入的一份副本执行。 + +为了能够从管理器获得配方实例,`#matches`必须返回true。此方法根据提供的容器进行检查,以查看相关联的输入是否有效。`Ingredient`可以通过调用`Ingredient#test`进行验证。 + +如果已经选择了配方,则使用`#assemble`构建配方,该`#assemble`可以使用来自输入的数据来创建结果。 + +!!! 提示 + `#assemble`应始终生成唯一的”`ItemStack`。如果不确定`#assemble`是否执行此操作,请在返回之前对结果调用`ItemStack#copy`。 + +大多数其他方法纯粹是为了与配方相结合。 + +```java +public record ExampleRecipe(Ingredient input, int data, ItemStack output) implements Recipe { + // 在此处实现方法 +} +``` + +!!! 注意 + 虽然在上面的示例中使用了一个记录,但在你自己的实现中不需要这样做。 + +RecipeType +---------- + +`RecipeType`负责定义配方将在其中使用的类别或上下文。例如,如果一个配方要在熔炉中熔炼,它的类型将是`RecipeType#BLASTING`。在高炉中进行熔炼的类型为`RecipeType#BLASTING`。 + +如果现有类型中没有一个与配方将在其中使用的上下文匹配,则必须[注册][forge]一个新的`RecipeType`。 + +`RecipeType`实例必须由新配方子类型中的`Recipe#getType`返回。 + +```java +// 对于某个RegistryObject EXAMPLE_TYPE +// 在ExampleRecipe中 +@Override +public RecipeType getType() { + return EXAMPLE_TYPE.get(); +} +``` + +RecipeSerializer +---------------- + +`RecipeSerializer`负责解码JSON,并通过网络为关联的`Recipe`子类型进行通信。序列化器解码的每个配方都保存为`RecipeManager`中的唯一实例。`RecipeSerializer`必须[已被注册][forge]。 + +`RecipeSerializer`只需要实现三个方法: + + 方法 | 描述 + :---: | :--- +fromJson | 将JSON解码为`Recipe`子类型。 +toNetwork | 将`Recipe`编码到缓冲区以发送到客户端。配方标识符无需编码。 +fromNetwork | 从服务端发送的缓冲区中解码`Recipe`。配方标识符不需要解码。 + +然后,新配方子类型中的`Recipe#getSerializer`必须返回该`RecipeSerializer`实例。 + +```java +// 对于某个RegistryObject EXAMPLE_SERIALIZER +// 在ExampleRecipe中 +@Override +public RecipeSerializer getSerializer() { + return EXAMPLE_SERIALIZER.get(); +} +``` + +!!! 提示 + 有一些有用的方法可以让配方的读写数据变得更容易。`Ingredient`可以使用`#fromJson`、`#toNetwork`和`#fromNetwork`,而`ItemStack`可以使用`CraftingHelper#getItemStack`、`FriendlyByteBuf#writeItem`和`FriendlyByteBuf#readItem`。 + +构建JSON +-------- + +自定义配方JSON与其他[配方][json]存储在同一个位置。指定的`type`应表示**配方序列化器**的注册表名称。任何附加数据都是由序列化器在解码期间指定的。 + +```js +{ + // 自定义序列化器的注册表名称 + "type": "examplemod:example_serializer", + "input": { + // 某些原料输入 + }, + "data": 0, // 配方所需的一些数据 + "output": { + // 某些物品栈输出 + } +} +``` + +非物品逻辑 +--------- + +如果物品未用作配方输入或结果的一部分,则[`RecipeManager`][manager]中提供的常规方法将无效。相反,应将用于测试配方有效性和/或提供结果的附加方法添加到自定义`Recipe`实例中。从那里,特定`RecipeType`的所有配方都可以通过`RecipeManager#getAllRecipesFor`获得,然后使用新实现的方法进行检查和/或提供结果。 + +```java +// 在某个Recipe子实现ExampleRecipe中 + +// 检查该位置的方块,看它是否与存储的数据匹配 +boolean matches(Level level, BlockPos pos); + +// 创建要将指定位置的方块设置为的方块状态 +BlockState assemble(RegistryAccess access); + +// 在某个管理器类中 +public Optional getRecipeFor(Level level, BlockPos pos) { + return level.getRecipeManager() + .getAllRecipesFor(exampleRecipeType) // 获取所有配方 + .stream() // 在所有配方中查阅类型 + .filter(recipe -> recipe.matches(level, pos)) // 检查该配方输入是否合法 + .findFirst(); // 查找与输入匹配的第一个配方 +} +``` + +数据生成 +------- + +所有自定义配方,无论输入或输出数据如何,都可以使用`RecipeProvider`创建到用于[数据生成][datagen]的`FinishedRecipe`中。 + +[forge]: ../../../concepts/registries.md#methods-for-registering +[json]: https://minecraft.fandom.com/wiki/Recipe#JSON_format +[manager]: ./index.md#recipe-manager +[datagen]: ../../../datagen/server/recipes.md#custom-recipe-serializers diff --git a/docs/translation/zh_CN/resources/server/recipes/incode.md b/docs/translation/zh_CN/resources/server/recipes/incode.md new file mode 100644 index 000000000..4778347c6 --- /dev/null +++ b/docs/translation/zh_CN/resources/server/recipes/incode.md @@ -0,0 +1,62 @@ +非数据包配方 +=========== + +并不是所有的配方都足够简单或迁移到使用数据驱动的配方。一些子系统仍然需要在代码库中进行修补,以提供对添加新配方的支持。 + +酿造配方 +------- + +酿造是代码中为数不多的仍然存在的配方之一。酿造配方是作为`PotionBrewing`中的引导程序的一部分添加的,用于容器、容器配方和药水混合物。为了扩展现有系统,Forge允许通过在`FMLCommonSetupEvent`中调用`BrewingRecipeRegistry#addRecipe`来添加酿造配方。 + +!!! 警告 + `BrewingRecipeRegistry#addRecipe`必须在同步工作队列中通过`#enqueueWork`调用,因为该方法不是线程安全的。 + +默认实现接受标准实现的输入成分、催化剂成分和物品栈输出。此外,还可以提供一个`IBrewingRecipe`实例来执行转换。 + +### IBrewingRecipe + +`IBrewingRecipe`是一个伪[`Recipe`][recipe]接口,用于检查输入和催化剂是否有效,并在有效时提供相关输出。它分别通过`#isInput`、`#isIngredient`和`#getOutput`提供。输出方法可以访问输入和催化剂物品栈来构建结果。 + +!!! 重要 + 在`ItemStack`或`CompoundTag`之间复制数据时,请确保使用它们各自的`#copy`方法来创建唯一的实例。 + +没有类似原版的包装来添加额外的药水容器或药水混合物。需要添加一个新的`IBrewingRecipe`实现来复制此行为。 + +铁砧配方 +------- + +铁砧负责接收损坏的输入,并给定一些材料或类似的输入,消除输入结果上的一些损坏。因此,它的系统不是简易地被数据驱动。然而,当用户具有所需的经验等级时,由于铁砧配方是具有一定数量的材料等于一定输出的输入,因此可以通过`AnvilUpdateEvent`对其进行修改以创建伪配方系统。这接受了输入和材料,并允许模组开发者指定输出、经验等级成本和用于输出的材料数量。该事件还可以通过[取消][cancel]来阻止任何输出。 + +```java +// 检查左边和右边的物品是否正确 +// 当正确时,设置输出,经验等级消耗,以及材料数量 +public void updateAnvil(AnvilUpdateEvent event) { + if (event.getLeft().is(...) && event.getRight().is(...)) { + event.setOutput(...); + event.setCost(...); + event.setMaterialCost(...); + } +} +``` + +该更新事件必须被[绑定][attached]到Forge事件总线。 + +织布机配方 +--------- + +织布机负责将染料和图案(从织布机或物品上)应用到旗帜上。虽然旗帜和染料必须分别为`BannerItem`或`DyeItem`,但可以在织布机中创建和应用自定义图案。旗帜图案可以通过[注册][registering]一个`BannerPattern`来创建。 + +!!! 重要 + `minecraft:no_item_required`标签中的`BannerPattern`在织布机中作为一个选项出现。不在此标签中的图案必须有一个附带的`BannerPatternItem`才能与关联的标签一起使用。 + +```java +private static final DeferredRegister REGISTER = DeferredRegister.create(Registries.BANNER_PATTERN, "examplemod"); + +// 接受要通过网络发送的图案名称 +public static final BannerPattern EXAMPLE_PATTERN = REGISTER.register("example_pattern", () -> new BannerPattern("examplemod:ep")); +``` + +[recipe]: ./custom.md#recipe +[cancel]: ../../../concepts/events.md#canceling +[attached]: ../../../concepts/events.md#creating-an-event-handler +[registering]: ../../../concepts/registries.md#registries-that-arent-forge-registries diff --git a/docs/translation/zh_CN/resources/server/recipes/index.md b/docs/translation/zh_CN/resources/server/recipes/index.md new file mode 100644 index 000000000..4c8c4f5c5 --- /dev/null +++ b/docs/translation/zh_CN/resources/server/recipes/index.md @@ -0,0 +1,102 @@ +配方 +==== + +配方是一种将一定数量的对象转换为Minecraft世界中其他对象的方法。尽管原版系统纯粹处理物品转换,但整个系统可以扩展为使用程序员创建的任何对象。 + +由数据驱动的配方 +----------------- + +原版中的大多数配方实现都是通过JSON进行数据驱动的。这意味着创建新配方不需要模组,只需要[数据包][datapack]。关于如何创建这些配方并将其放入模组的`resources`文件夹的完整列表可以在[Minecraft Wiki][wiki]上找到。 + +可以在配方书中获得配方,作为完成[进度][advancement]的奖励。配方进度总是以`minecraft:recipes/root`为其父项,以免出现在进度屏幕上。获得配方进度的默认标准是检查用户是否已通过一次使用解锁配方或通过某个如`/recipe`的命令接收配方: + +```js +// 在某个配方进度json中 +"has_the_recipe": { // 条件标签 + // 如果examplemod:example_recipe被使用,则成功 + "trigger": "minecraft:recipe_unlocked", + "conditions": { + "recipe": "examplemod:example_recipe" + } +} +//... +"requirements": [ + [ + "has_the_recipe" + // ... 解锁配方所需的其他用逻辑或相连的条件标签 + ] +] +``` + +由数据驱动的配方及其解锁的进度可以通过`RecipeProvider`[生成][datagen]。 + +配方管理器 +----------- + +配方是通过`RecipeManager`加载和存储的。任何与获取可用配方相关的操作都由该管理器负责。有两种重要的方法需要了解: + + 方法 | 描述 + :---: | :--- +`getRecipeFor` | 获取与当前输入匹配的第一个配方。 +`getRecipesFor` | 获取与当前输入匹配的所有配方。 + +每个方法都接受一个`RecipeType`,表示使用配方的方法(合成、烧炼等),一个保存输入配置的`Container`,以及与容器一起传递给`Recipe#matches`的当前存档。 + +!!! 重要 + Forge提供了`RecipeWrapper`实用类,该类扩展了`Container`,用于包装`IItemHandler`,并将其传递给需要`Container`参数的方法。 + + ```java + // 在具有IItemHandlerModifiable处理器的某给方法中 + recipeManger.getRecipeFor(RecipeType.CRAFTING, new RecipeWrapper(handler), level); + ``` + +附加特性 +-------- + +Forge为配方纲要及其实现提供了一些额外的行为,以更好地控制系统。 + +### 配方的ItemStack结果 + +除了`minecraft:stonecutting`配方外,所有原版配方序列化器都会扩展`result`标签,以将完整的`ItemStack`作为`JsonObject`,而不是在某些情况下仅仅是物品名称和数量。 + +```js +// 在某个配方JSON中 +"result": { + // 要作为结果提供的注册表物品的名称 + "item": "examplemod:example_item", + // 要返回的物品数量 + "count": 4, + // 物品栈的标签数据,也可以是一个字符串 + "nbt": { + // 在此处添加标签数据 + } +} +``` + +!!! 注意 + `nbt`标签也可以是一个字符串,其中包含无法正确表示为JSON对象(如`IntArrayTag`)的数据的字符串化NBT(或SNBT)。 + +### 条件性配方 + +配方及其所解锁的进度可以[有条件地加载和保持默认][conditional],具体取决于存在的信息(模组的被加载、物品的存在等)。 + +### 更大的合成网格 + +默认情况下,原版声明合成网格的最大宽度和高度为3x3正方形。这可以通过在`FMLCommonSetupEvent`中使用新的宽度和高度调用`ShapedRecipe#setCraftingSize`来扩展。 + +!!! 警告 + `ShapedRecipe#setCraftingSize`**不**是线程安全的。因此,它应该通过`FMLCommonSetupEvent#enqueueWork`排入同步工作队列。 + +配方中较大的合成网格可以是[数据生成的][datagen]。 + +### 原料类型 + +一些额外的[原料类型][ingredients]被添加,以允许配方具有检查标签数据或将多种原料组合到单个输入检查器中的输入。 + +[datapack]: https://minecraft.fandom.com/wiki/Data_pack +[wiki]: https://minecraft.fandom.com/wiki/Recipe +[advancement]: ../advancements.md +[datagen]: ../../../datagen/server/recipes.md +[cap]: ../../../datastorage/capabilities.md +[conditional]: ../conditional.md#implementations +[ingredients]: ./ingredients.md#forge-types diff --git a/docs/translation/zh_CN/resources/server/recipes/ingredients.md b/docs/translation/zh_CN/resources/server/recipes/ingredients.md new file mode 100644 index 000000000..6ca6ab75a --- /dev/null +++ b/docs/translation/zh_CN/resources/server/recipes/ingredients.md @@ -0,0 +1,175 @@ +原料 +==== + +`Ingredient`是基于物品的输入的predicate处理器,用于检查某个`ItemStack`是否满足成为配方中有效输入的条件。所有接受输入的[原版配方][recipes]都使用`Ingredient`或`Ingredient`的列表,然后将其合并为单一的`Ingredient`。 + +自定义原料 +--------- + +自定义原料可以通过将`type`设置为[原料的序列化器][serializer]的名称来指定,[复合原料][compound]除外。当没有指定类型时,`type`默认为原版原料`minecraft:item`。自定义原料也可以很容易地用于[数据生成][datagen]。 + +### Forge类型 + +Forge提供了一些额外的`Ingredient`类型供程序员实现。 + +#### CompoundIngredient + +尽管它们在功能上是相同的,但复合原料取代了在配方中实现原料列表的方式。它们作为一个逻辑或(OR)集合工作,其中传入的物品栈必须至少在一个提供的原料中。进行此更改是为了允许自定义原料在列表中正确工作。因此,**无需指定类型**。 + +```js +// 对于某个输入 +[ + // 这些原料中必须至少有一种必须匹配才能成功 + { + // 原料 + }, + { + // 自定义原料 + "type": "examplemod:example_ingredient" + } +] +``` + +#### StrictNBTIngredient + +`StrictNBTIngredient`比较`ItemStack`上的物品、耐久和共享标签(由`IForgeItem#getShareTag`定义),以保证确切的等效性。这可以通过将`type`指定为`forge:nbt`来使用。 + +```js +// 对于某个输入 +{ + "type": "forge:nbt", + "item": "examplemod:example_item", + "nbt": { + // 添加nbt数据(必须与物品栈上的数据完全匹配) + } +} +``` + +### PartialNBTIngredient + +`PartialNBTIngredient`是[`StrictNBTIngredient`][nbt]的宽松版本,因为它们与共享标签中指定的单个或一组物品以及仅键(由`IForgeItem#getShareTag`定义)进行比较。这可以通过将`type`指定为`forge:partial_nbt`来使用。 + +```js +// 对于某个输入 +{ + "type": "forge:partial_nbt", + + // 'item'或'items'必须被指定 + // 如果都指定了,那么只有'item'会被读取 + "item": "examplemod:example_item", + "items": [ + "examplemod:example_item", + "examplemod:example_item2" + // ... + ], + + "nbt": { + // 仅检查'key1'和'key2'的等效性 + // 不会检查物品栈中的所有其他键 + "key1": "data1", + "key2": { + // 数据2 + } + } +} +``` + +### IntersectionIngredient + +`IntersectionIngredient`工作为一个逻辑和(AND)集合,其中传入的物品必须与所有提供的原料匹配。必须至少提供两种原料。这可以通过将`type`指定为`forge:intersection`来使用。 + +```js +// 对于某个输入 +{ + "type": "forge:intersection", + + // 所有这些原料都必须返回true才能成功 + "children": [ + { + // 原料1 + }, + { + // 原料2 + } + // ... + ] +} +``` + +### DifferenceIngredient + +`DifferenceIngredient`工作为一个减法(SUB)集合,其中传入的物品栈必须与第一个原料匹配,但不能与第二个原料匹配。这可以通过将`type`指定为`forge:difference`来使用。 + +```js +// 对于某个输入 +{ + "type": "forge:difference", + "base": { + // 该物品栈所存在的原料 + }, + "subtracted": { + // 该物品栈所不存在的原料 + } +} +``` + +创建自定义原料 +------------- + +可以通过为创建的`Ingredient`子类实现`IIngredientSerializer`来创建自定义原料。 + +!!! 提示 + 自定义原料应该是`AbstractIngredient`的子类,因为它提供了一些有用的抽象以便于实现。 + +### 原料的子类 + +对于每个原料子类,有三种重要的方法需要实现: + + 方法 | 描述 + :---: | :--- +getSerializer | 返回用于读取和写入原料的[serializer]。 +test | 如果输入对此原料有效,则返回true。 +isSimple | 如果原料与物品栈的标签匹配,则返回false。`AbstractIngredient`的子类需要定义此行为,而`Ingredient`子类默认返回true。 + +所有其他定义的方法都留给读者练习,以便根据原料子类的需要使用。 + +### IIngredientSerializer + +`IIngredientSerializer`子类型必须实现三种方法: + + 方法 | 描述 + :---: | :--- +parse (JSON) | 将`JsonObject`转换为`Ingredient`。 +parse (Network) | 返回用于解码`Ingredient`的网络缓冲区。 +write | 将一个`Ingredient`写入网络缓冲区。 + +此外,`Ingredient`子类应实现`Ingredient#toJson`,以便与[数据生成][datagen]一起使用。`AbstractIngredient`的子类使`#toJson`成为一个需要实现该方法的抽象方法。 + +之后,应声明一个静态实例来保存初始化的序列化器,然后在`RecipeSerializer`的`RegisterEvent`期间或在`FMLCommonSetupEvent`期间使用`CraftingHelper#register`进行注册。`Ingredient`子类在`Ingredient#getSerializer`中返回序列化器的静态实例。 + +```java +// 在某个序列化器类中 +public static final ExampleIngredientSerializer INSTANCE = new ExampleIngredientSerializer(); + +// 在某个处理器类中 +public void registerSerializers(RegisterEvent event) { + event.register(ForgeRegistries.Keys.RECIPE_SERIALIZERS, + helper -> CraftingHelper.register(registryName, INSTANCE) + ); +} + +// 在某个原料类中 +@Override +public IIngredientSerializer getSerializer() { + return INSTANCE; +} +``` + +!!! 提示 + 如果使用`FMLCommonSetupEvent`注册原料序列化器,则必须通过`FMLCommonSetupEvent#enqueueWork`将其排入同步工作队列,因为`CraftingHelper#register`不是线程安全的。 + +[recipes]: https://minecraft.fandom.com/wiki/Recipe#List_of_recipe_types +[nbt]: #strictnbtingredient +[serializer]: #iingredientserializer +[compound]: #compoundingredient +[datagen]: ../../../datagen/server/recipes.md diff --git a/docs/translation/zh_CN/resources/server/tags.md b/docs/translation/zh_CN/resources/server/tags.md new file mode 100644 index 000000000..a6f21ed0f --- /dev/null +++ b/docs/translation/zh_CN/resources/server/tags.md @@ -0,0 +1,120 @@ +标签 +==== + +标签是游戏中用于将相关事物分组在一起并提供快速成员身份检查的通用对象集。 + +声明你自己的组别 +--------------- +标签在你的模组的[数据包][datapack]中声明。例如,给定标识符为`modid:foo/tagname`的`TagKey`将引用位于`/data//tags/blocks/foo/tagname.json`的标签。`Block`、`Item`、`EntityType`、`Fluid`以及`GameEvent`的标签,将使用复数形式作为其文件夹位置,而所有其他注册表使用单数形式(`EntityType`使用文件夹`entity_types`,而`Potion`将使用文件夹`potion`)。 +类似地,你可以通过声明自己的JSON来附加或覆盖在其他域(如原版)中声明的标签。 +例如,要将你自己的模组的树苗添加到原版树苗标签中,你可以在`/data/minecraft/tags/blocks/saplings.json`中指定它,如果`replace`选项为false,原版将在重载时将所有内容合并到一个标签中。 +如果`replace`为true,那么指定`replace`的json之前的所有条目都将被删除。 +列出的不存在的值将导致标签出错,除非使用`id`字符串和设置为false的`required`布尔值列出该值,如以下示例所示: + +```js +{ + "replace": false, + "values": [ + "minecraft:gold_ingot", + "mymod:my_ingot", + { + "id": "othermod:ingot_other", + "required": false + } + ] +} +``` + +有关基本语法的描述,请参阅[原版wiki][tags]。 + +原版语法上还有一个Forge扩展。 +你可以声明一个与`values`数组格式相同的`remove`数组。此处列出的任何值都将从标签中删除。这相当于原版`replace`选项的细粒度版本。 + + +在代码中使用标签 +--------------- +登录和重新加载时,所有注册表的标签都会自动从服务器发送到任何远程客户端。`Block`、`Item`、`EntityType`、`Fluid`和`GameEvent`都被特殊地包装,因为它们具有`Holder`,允许通过对象本身访问可用标签。 + +!!! 注意 + 在未来版本的Minecraft中,侵入性的`Holder`可能会被移除。如果被移除了,则可以使用以下方法来查询关联的`Holder`。 + +### ITagManager + +Forge封装的注册表提供了一个额外的帮助,用于通过`ITagManager`创建和管理标签,该标签可以通过`IForgeRegistry#tags`获得。可以使用`#createTagKey`或`#createOptionalTagKey`创建标签。标签或注册表对象也可以分别使用`#getTag`或`#getReverseTag` 检查。 + +#### 自定义注册表 + +自定义注册表可以在分别通过`#createTagKey`或`#createOptionalTagKey`构造其`DeferredRegister`时创建标签。然后,可以使用通过调用`DeferredRegister#makeRegistry`获得的`IForgeRegistry`来检查它们的标签或注册表对象。 + +### 引用标签 + +创建标签包装有四种方法: + +方法 | 对于 +:---: | :--- +`*Tags#create` | `BannerPattern`、`Biome`、`Block`、`CatVariant`、`DamageType`、`EntityType`、`FlatLevelGeneratorPreset`、`Fluid`、`GameEvent`、`Instrument`、`Item`、`PaintingVariant`、`PoiType`、`Structure`以及`WorldPreset`,其中`*`代表这些类型之一。 +`ITagManager#createTagKey` | 由Forge包装的原版注册表,可从`ForgeRegistries`取得。 +`DeferredRegister#createTagKey` | 自定义的Forge注册表。 +`TagKey#create` | 无Forge包装的原版注册表,可从`Registry`取得。 + +注册表对象可以通过其`Holder`或通过`ITag`/`IReverseTag`分别检查其标签或注册表对象是否为原版或Forge注册表对象。 + +原版注册表对象可以使用`Registry#getHolder`或`Registry#getHolderOrThrow`获取其关联的持有者,然后使用`Holder#is`比较注册表对象是否有标签。 + +Forge注册表对象可以使用`ITagManager#getTag`或`ITagManager#getReverseTag`获取其标签定义,然后分别使用`ITag#contains`或`IReverseTag#containsTag`比较注册表对象是否有标签。 + +持有标签的注册表对象在其注册表对象或状态感知类中包含一个名为`#is`的方法,以检查该对象是否属于某个标签。 + +举一个例子: +```java +public static final TagKey myItemTag = ItemTags.create(new ResourceLocation("mymod", "myitemgroup")); + +public static final TagKey myPotionTag = ForgeRegistries.POTIONS.tags().createTagKey(new ResourceLocation("mymod", "mypotiongroup")); + +public static final TagKey myVillagerTypeTag = TagKey.create(Registries.VILLAGER_TYPE, new ResourceLocation("mymod", "myvillagertypegroup")); + +// 在某个方法中: + +ItemStack stack = /*...*/; +boolean isInItemGroup = stack.is(myItemTag); + +Potion potion = /*...*/; +boolean isInPotionGroup = ForgeRegistries.POTIONS.tags().getTag(myPotionTag).contains(potion); + +ResourceKey villagerTypeKey = /*...*/; +boolean isInVillagerTypeGroup = BuiltInRegistries.VILLAGER_TYPE.getHolder(villagerTypeKey).map(holder -> holder.is(myVillagerTypeTag)).orElse(false); +``` + +惯例 +---- + +有几个惯例将有助于促进该生态系统中的兼容性: + +* 如果有适合你的方块或物品的原版标签,请将其添加到该标签中。请参阅[原版标签列表][taglist]。 +* 如果有一个Forge标签适合你的方块或物品,请将其添加到该标签中。Forge声明的标签列表可以在[GitHub][forgetags]上看到。 +* 如果有一组你认为应该由社区共享的东西,请使用`forge`命名空间,而不是你的mod id。 +* 标签命名约定应遵循原版约定。特别是,物品和方块组别是复数而不是单数(例如`minecraft:logs`、`minecraft:saplings`)。 +* 物品标签应根据其类型分类到子目录中(例如`forge:ingots/iron`、`forge:nuggets/brass`等)。 + + +从OreDictionary迁移 +------------------- + +* 对于配方,标签可以直接以原版配方格式使用(见下文)。 +* 有关代码中的匹配物品,请参阅上面的章节。 +* 如果你要声明一种新类型的物品组别,请遵循以下几个命名约定: + * 使用`domain:type/material`。当名称是所有模组开发者都应该采用的通用名称时,请使用`forge`域。 + * 例如,铜锭应在`forge:ingots/brass`标签下注册,钴粒应在`forge:nuggets/cobalt`标签下注册。 + + +在配方和进度中使用标签 +-------------------- + +原版直接支持标签。有关用法的详细信息,请参阅[配方][recipes]和[进度][advancements]的原版wiki页面。 + +[datapack]: ./index.md +[tags]: https://minecraft.fandom.com/wiki/Tag#JSON_format +[taglist]: https://minecraft.fandom.com/wiki/Tag#List_of_tags +[forgetags]: https://github.com/MinecraftForge/MinecraftForge/tree/1.19.x/src/generated/resources/data/forge/tags +[recipes]: https://minecraft.fandom.com/wiki/Recipe#JSON_format +[advancements]: https://minecraft.fandom.com/wiki/Advancement