译者序
fabric是asie(foamfix作者)和modmuss50(techreborn作者)共同搞出的一个mod加载器(和forge相同定位),具有轻量事少的特性,以下是它相对于forge的优势:
安装开发环境
预准备
- JDK8或更高版本 https://adoptopenjdk.net/
- 一个IDE,比如 Intellij IDEA
步骤
- 从 fabric-example-mod (如果你想用kotlin的话,从kotlin模板下载) ,许可证和readme不必下载,因为它跟你的mod没什么关系。
- 编辑gradle.properties:
- 确保archives_base_name和maven_group已经设置为你喜欢的值。
- 确保已经更新了MC,反混淆和fabric loader(译注:fabric loader和FML是一个定位) - 你可以从这个网站根据你的版本,查询到所有你需要的配置。
- 在build.gradle文件里添加所有你需要的依赖项。
- 在IDE中打开build.gradle(译注:idea是“认识”build.gradle文件的,你打开的时候,它会问你是否打开gradle project,点是它就会自动给你导入,但是不会生成源码),以导入工程。
- 在IDE中执行genSources(见右侧gradle-tasks)任务。如果你的IDE没有gradle集成,在命令行中执行./gradlew genSources命令(这时源码才会生成)。
- 如果希望有IDE内运行配置,需要执行如下命令:
- 对IntelliJ IDEA: ./gradlew idea.
- 对Eclipse: ./gradlew eclipse.
- 对VSCode:详见这里。
- 好好开始吧!
准备开始
建议
- 虽然Fabric API(译注:相当于除了FML以外的forge本体)并不一定要用,但其初衷是提供统一兼容性,和原版没有的钩子,因此也强烈推荐带上它!
- 因为fabric还在开发早期,偶尔fabricloom会突然出问题,需要手动清除一下gradle缓存(位于.gradle/caches/fabric-loom),问题确定后我们会公布出来。
- 不要害怕提问!风里雨里,我们等你,助你美梦成真。
一些问题
我的世界没有声音?
- 在Java启动器的JVM参数指定区域输入
- 在服务器启动指令的 -jar前面加这个参数
- 直接调用 launcher.jar时在-jar之前加这个参数
- <?xml version="1.0" encoding="UTF-8"?>
- <Configuration status="WARN" packages="com.mojang.util">
- <Appenders>
- <Console name="SysOut" target="SYSTEM_OUT">
- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n" />
- </Console>
- <Queue name="ServerGuiConsole">
- <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n" />
- </Queue>
- <RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n" />
- <Policies>
- <TimeBasedTriggeringPolicy />
- <OnStartupTriggeringPolicy />
- </Policies>
- <DefaultRolloverStrategy max="1000"/>
- </RollingRandomAccessFile>
- </Appenders>
- <Loggers>
- <Root level="debug">
- <filters>
- <MarkerFilter marker="NETWORK_PACKETS" onMatch="ACCEPT" onMismatch="DENY" />
- </filters>
- <AppenderRef ref="SysOut"/>
- <AppenderRef ref="File"/>
- <AppenderRef ref="ServerGuiConsole"/>
- </Root>
- </Loggers>
- </Configuration>
- <?xml version="1.0" encoding="UTF-8"?>
- <Configuration status="WARN">
- <Appenders>
- <Console name="SysOut" target="SYSTEM_OUT">
- <XMLLayout />
- </Console>
- <RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n" />
- <Policies>
- <TimeBasedTriggeringPolicy />
- <OnStartupTriggeringPolicy />
- </Policies>
- <DefaultRolloverStrategy max="1000"/>
- </RollingRandomAccessFile>
- </Appenders>
- <Loggers>
- <Root level="debug">
- <filters>
- <MarkerFilter marker="NETWORK_PACKETS" onMatch="ACCEPT" onMismatch="DENY" />
- </filters>
- <AppenderRef ref="SysOut"/>
- <AppenderRef ref="File"/>
- </Root>
- </Loggers>
- </Configuration>
- <?xml version="1.0" encoding="UTF-8"?>
- <Configuration status="WARN" packages="com.mojang.util">
- <Appenders>
- <Console name="SysOut" target="SYSTEM_OUT">
- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n" />
- </Console>
- <Queue name="ServerGuiConsole">
- <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n" />
- </Queue>
- <RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n" />
- <Policies>
- <TimeBasedTriggeringPolicy />
- <OnStartupTriggeringPolicy />
- </Policies>
- <DefaultRolloverStrategy max="1000"/>
- </RollingRandomAccessFile>
- </Appenders>
- <Loggers>
- <Root level="debug">
- <AppenderRef ref="SysOut"/>
- <AppenderRef ref="File"/>
- <AppenderRef ref="ServerGuiConsole"/>
- </Root>
- </Loggers>
- </Configuration>
- <?xml version="1.0" encoding="UTF-8"?>
- <Configuration status="WARN">
- <Appenders>
- <Console name="SysOut" target="SYSTEM_OUT">
- <XMLLayout />
- </Console>
- <RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n" />
- <Policies>
- <TimeBasedTriggeringPolicy />
- <OnStartupTriggeringPolicy />
- </Policies>
- <DefaultRolloverStrategy max="1000"/>
- </RollingRandomAccessFile>
- </Appenders>
- <Loggers>
- <Root level="debug">
- <AppenderRef ref="SysOut"/>
- <AppenderRef ref="File"/>
- </Root>
- </Loggers>
- </Configuration>
- <?xml version="1.0" encoding="UTF-8"?>
- <Configuration status="WARN" packages="net.minecraft,com.mojang">
- <Appenders>
- <Console name="WINDOWS_COMPAT" target="SYSTEM_OUT"></Console>
- <Queue name="TerminalConsole">
- <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n" />
- </Queue>
- <RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
- <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n" />
- <Policies>
- <TimeBasedTriggeringPolicy />
- <OnStartupTriggeringPolicy />
- </Policies>
- <DefaultRolloverStrategy max="1000"/>
- </RollingRandomAccessFile>
- </Appenders>
- <Loggers>
- <Root level="debug">
- <AppenderRef ref="WINDOWS_COMPAT"/>
- <AppenderRef ref="File"/>
- <AppenderRef ref="TerminalConsole"/>
- </Root>
- </Loggers>
- </Configuration>
- <?xml version="1.0" encoding="UTF-8"?>
- <Configuration status="WARN" packages="com.mojang">
- <Appenders>
- <Console name="SysOut" target="SYSTEM_OUT">
- <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n" />
- </Console>
- <Queue name="DevelopmentConsole">
- <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n" />
- </Queue>
- <Async name="Async">
- <AppenderRef ref="SysOut"/>
- <AppenderRef ref="DevelopmentConsole"/>
- </Async>
- </Appenders>
- <Loggers>
- <Root level="debug">
- <AppenderRef ref="Async"/>
- </Root>
- </Loggers>
- </Configuration>
fabric限定的改动
- afterEvaluate { ... } - 审核(Evaluate)之后,fabricloom的remapJar就会启动,这样remapJar.output就只能读取。
- mainArtifact(remapJar) (或者loom0.2.4之前的 mainArtifact(remapJar.output)) - 提交到curseforge的mod实例应为remapJar任务的输出, 也就是重新混淆的(可用于玩家安装的)mod jar文件。
- uploadTask.dependsOn(remapJar) - 确保curseforge只在remapJar重新混淆任务完成后,才进行上传任务。
- forgeGradleIntegration = false - 因为你用的不是forgegradle,所以这个联动被设定为false。
Mod ID(mod标识符)
Tags
Maven组名和包名
物理端
逻辑端
- net.minecraft.world.World
- net.minecraft.entity.Entity
- net.minecraft.block.entity.BlockEntity</font>
我们需要再深入些!
物理客户端
物理服务端
逻辑客户端
逻辑服务端
通信
逻辑服务端的杂项
- 游戏运行时只有一个逻辑服务端存在
- 世界和实体总在计算游戏逻辑(换句话说,world对象的isClient字段被写死成false)
- 远程控制,资源包发送,提供图标
结论
逻辑客户端 | 逻辑服务端 | |
物理客户端 | 单例 始终存在 | 存档加载时存在 |
逻辑服务端 | 没有 | 始终存在 |
名字和链接 | 描述 | 维护者 |
AutoConfig | 基于注解的配置API | sargunv |
Cardinal Components API | 为各种游戏元素(方块,实体,世界等)动态附加数据 | NerdHubMC |
Cardinal Energy | 能量API | Abused Master |
ClothConfig | 客户端配置界面构建 | Danielshe |
Cotton Client Commands | 客户端命令API | Juuz |
CottonEnergy | 能量API | Cotton |
Fiber | 配置API | Daemonic Labs |
LibBlockAttributes | 实体属性,物品管理,流体管理 | AlexIIL |
LibGui | GUI工具箱 | Cotton |
Mesh | 全方位的类库,自动注册,合成生成,多方块结构(开发中) | UpcraftLP |
Reach Entity Attributes | 调整生物手长和攻击范围的属性 | JamiesWhiteShirt |
Satin | 后处理着色器(光影)的简单包装 | Pyrofab |
重载修改过的类
重载材质
重载合成和战利品表
你的物品现在有个奇怪的名字,比如item.tutorial.my_item对吧?那是因为你的物品名还没有本地化。
新建语言文件
添加翻译
- { "item.tutorial.my_item": "My Item", "item.tutorial.my_awesome.item": "My Awesome Item", [...]}
前一个字符串(比如上面的"item.tutorial.my_item")可以是任何可以翻译的东西(不管是物品名还是TranslatableText),如果你在按照这个教程做,记得把modid重命名为“tutorial”,或者任何你喜欢的modid。
使用自定义可翻译文本
- @Override
- public void appendTooltip(ItemStack itemStack, World world, List<Text> tooltip, TooltipContext tooltipContext) {
- tooltip.add(new TranslatableText("item.tutorial.fabric_item.tooltip"));
- }
- {
- "item.tutorial.fabric_item.tooltip": "My Tooltip"
- }
为TranslatableText添加动态值
- {
- "item.tutorial.fabric_item.tooltip": "My Tooltip in day %d, and month %d"
- }
- int currentDay = 4;
- int currentMonth = 7;
- tooltip.add(new TranslatableText("item.tutorial.fabric_item.tooltip", currentDay, currentMonth));
换行
- {
- "item.tutorial.fabric_item.tooltip_1": "Line 1 of my tooltip"
- "item.tutorial.fabric_item.tooltip_2": "Line 2 of my tooltip"
- }
然后各自添加TranslatableText的每一部分:
- tooltip.add(new TranslatableText("item.tutorial.fabric_item.tooltip_1"));
- tooltip.add(new TranslatableText("item.tutorial.fabric_item.tooltip_2"));
- Line 1 of my tooltip
- Line 2 of my tooltip
翻译条目格式
游戏元素类型 | 格式 | 实例 |
方块 | block.<modid>.<registry-id> | "block.tutorial.example_block": "Example Block" |
物品 | item.<modid>.<registry-id> | "item.tutorial.my_item": "My Item" |
创造标签栏 | itemGroup.<modid>.<registry-id> | "itemGroup.tutorial.my_group": "My Group" |
流体 | fluid.<modid>.<registry-id> | |
声音事件 | sound_event.<modid>.<registry-id> | |
效果 | mob_effect.<modid>.<registry-id> | |
附魔 | enchantment.<modid>.<registry-id> | |
实体类型 | entity_type.<modid>.<registry-id> | |
药水 | potion.<modid>.<registry-id> | |
群系 | biome.<modid>.<registry-id> |
添加基础的合成(其实跟原版合成一样)
- {
- "type": "minecraft:crafting_shaped",
- "pattern": [
- "WWW",
- "WR ",
- "WWW"
- ],
- "key": {
- "W": {
- "tag": "minecraft:logs"
- },
- "R": {
- "item": "minecraft:redstone"
- }
- },
- "result": {
- "item": "tutorial:fabric_item",
- "count": 4
- }
- }
- type: 代表一个有序工作台合成。
- result: 这个合成的产物是4个tutorial:fabric_item物品,count字段则是可选的,如果没有指定count,默认则是1.
- pattern: 一个“模板”代表了合成表的形式,每个字母代表了一个物品,空白区域代表那个格子没有物品,至于字母代表的物品是什么,在key中定义。
- key:pattern中的字母代表的物品,在这里W代表了任何具有minecraft:logs标签的物品(所有的木头),R则代表了红石这个物品,如果想了解更多标签的事情,请看这里。
4个fabric_item的合成 | ||
任何原木 | 任何原木 | 任何原木 |
任何原木 | 红石 | 空 |
任何原木 | 任何原木 | 任何原木 |
yarn是fabricloom的默认反混淆,靠接受社区贡献推陈出新。fabricloom的反混淆表由build.gradle的mappings依赖配置项指定,也可以通过更新依赖项而更新。MC本身和带有mod-argument的配置项(例如modCompile,相当于gradle自带的compile项加了个mod)的其他依赖,在运行时会被重混淆。还没有在yarn中反混淆的类,方法和字段名会被中间名(像class_1234, method_1234 ,field_1234)这种替代。
- dependencies { [...] mappings "net.fabricmc:yarn:${project.yarn_mappings}"}
- jar任务输出的“dev”jar文件没有被重混淆,所以没用,不能用做mod,只能以相符的反混淆表在开发环境工作,实际场合应该使用remapJar来构建mod本身,以及使用modCompile之类来引入依赖。
- yarn反混淆名只用于开发环境。在开发环境之外,就只有中间名存在,也就是说你(反编译mod)看到的和你写的代码并不一样,很显然loom会帮你进行yarn和中间名的转换,但是如果用到反射,就要谨慎。
重混淆
混淆与反混淆
Intermediary(中间名)
更新yarn反混淆表
需要
- Fabric-Loom 0.2.2或更高版本
- Java代码(kotlin和scala还没有)
引导
- 写出你想用的yarn版本,比如“net.fabricmc:yarn:1.14.1 Pre-Release 2+build.2”。
- 确定这个版本的yarn表已经在本地创建。目前唯一的办法是在build.gradle文件里将”minecraft“和”mappings“字段改成新版本,然后执行任何gradle命令(反正都会崩溃,在此之前yarn表会下载下来),然后再把字段改回去。(译注:这段话把我搞得云里雾里,因为在idea中,启用了gradle的auto import后,idea就会自动为你下载yarn表,或者自动执行你的其他改动)
- 运行这段魔法一样的命令: gradle migrateMappings -PtargetMappingsArtifact="net.fabricmc:yarn:1.14.1 Pre-Release 2+build.2" -PinputDir=src/main/java -PoutputDir=remappedSrc,在这里:
- “targetMappingsArtifact” 指代目标反混淆表的版本,重要的是,当运行此命令时,build.gradle必须是你现在的(而不是你要换的)yarn版本!
- “inputDir” 是输入路径,包含Java源代码。
- “outputDir” 是输出路径,如果没有会重建。
- 如果一切正常,将修改完毕的代码复制到输入路径。
- public class ExampleMod implements ModInitializer
- {
- // 创建新物品的实例
- public static final Item FABRIC_ITEM = new Item(new Item.Settings().group(ItemGroup.MISC));
- [...]
- }
- public class ExampleMod implements ModInitializer
- {
- // 创建新item的实例
- public static final Item FABRIC_ITEM = new Item(new Item.Settings().group(ItemGroup.MISC));
-
- @Override
- public void onInitialize()
- {
- Registry.register(Registry.ITEM, new Identifier("tutorial", "fabric_item"), FABRIC_ITEM);
- }
- }
- {
- “parent” : “item / generated” ,
- “textures” : {
- “layer0” : “tutorial:item / fabric_item”
- }
- }
- public class FabricItem extends Item
- {
- public FabricItem(Settings settings)
- {
- super(settings);
- }
- }
- public class FabricItem extends Item
- {
- public FabricItem(Settings settings)
- {
- super(settings);
- }
-
- @Override
- public TypedActionResult<ItemStack> use(World world, PlayerEntity playerEntity, Hand hand)
- {
- playerEntity.playSound(SoundEvents.BLOCK_WOOL_BREAK, 1.0F, 1.0F);
- return new TypedActionResult<>(ActionResult.SUCCESS, playerEntity.getStackInHand(hand));
- }
- }
- public class ExampleMod implements ModInitializer
- {
- // 创建新item的实例
- public static final FabricItem FABRIC_ITEM = new FabricItem(new Item.Settings().group(ItemGroup.MISC));
- [...]
- }
- public class ExampleMod implements ModInitializer
- {
- // 创建新item的实例,其中最大堆叠数是16
- public static final FabricItem FABRIC_ITEM = new FabricItem(new Item.Settings().
- group(ItemGroup.MISC).maxCount(16));
- [...]
- }
- public class ExampleMod implements ModInitializer
- {
- // ...
- public static final ItemGroup ITEM_GROUP = FabricItemGroupBuilder.build(
- new Identifier("tutorial", "general"),
- () -> new ItemStack(Blocks.COBBLESTONE));
-
- public static final ItemGroup OTHER_GROUP = FabricItemGroupBuilder.create(
- new Identifier("tutorial", "other"))
- .icon(() -> new ItemStack(Items.BOWL))
- .build();
- // ...
- }
- public static final Item YOUR_ITEM = new Item(new Item.Settings().itemGroup(ExampleMod.ITEM_GROUP));
- public class ExampleMod implements ModInitializer
- {
- // ...
- public static final ItemGroup ITEM_GROUP = FabricItemGroupBuilder.build(
- new Identifier("tutorial", "general"),
- () -> new ItemStack(Blocks.COBBLESTONE));
-
- public static final ItemGroup OTHER_GROUP = FabricItemGroupBuilder.create(
- new Identifier("tutorial", "other"))
- .icon(() -> new ItemStack(Items.BOWL))
- .appendItems(stacks ->
- {
- stacks.add(new ItemStack(Blocks.BONE_BLOCK));
- stacks.add(new ItemStack(Items.APPLE));
- stacks.add(PotionUtil.setPotion(new ItemStack(Items.POTION), Potions.WATER));
- stacks.add(ItemStack.EMPTY);
- stacks.add(new ItemStack(Items.IRON_SHOVEL));
- })
- .build();
- // ...
- }
- @Override
- public void appendTooltip(ItemStack itemStack, World world, List<Text> tooltip, TooltipContext tooltipContext) {
- tooltip.add(new TranslatableText("item.tutorial.fabric_item.tooltip"));
- }
- public class ExampleMod implements ModInitializer
- {
- // an instance of our new block
- public static final Block EXAMPLE_BLOCK = new Block(FabricBlockSettings.of(Material.METAL).build());
- [...]
- }
- public class ExampleMod implements ModInitializer
- {
- // block creation
- […]
-
- @Override
- public void onInitialize()
- {
- Registry.register(Registry.BLOCK, new Identifier("tutorial", "example_block"), EXAMPLE_BLOCK);
- }
- }
- public class ExampleMod implements ModInitializer
- {
- // block creation
- […]
-
- @Override
- public void onInitialize()
- {
- // block registration
- [...]
-
- Registry.register(Registry.ITEM, new Identifier("tutorial", "example_block"), new BlockItem(EXAMPLE_BLOCK, new Item.Settings().group(ItemGroup.MISC)));
- }
- }
- Blockstate: src/main/resources/assets/tutorial/blockstates/example_block.json
- Block Model: src/main/resources/assets/tutorial/models/block/example_block.json
- Item Model: src/main/resources/assets/tutorial/models/item/example_block.json
- Block Texture: src/main/resources/assets/tutorial/textures/block/example_block.png
方块状态文件决定了方块在不同状态下(blockstate)的模型,因为我们的方块只有一个状态,文件就会像这样简单:
- {
- "variants": {
- "": { "model": "tutorial:block/example_block" }
- }
- }
方块模型文件定义了形状和材质,在这里使用cube_all,让方块的所有面简单地使用同一个材质。
- {
- "parent": "block/cube_all",
- "textures": {
- "all": "tutorial:block/example_block"
- }
- }
大多数情况下,方块和手里拿着的方块看上去一样,这样从方块模型文件派生出一个物品模型文件就可以了:
- {
- "parent": "tutorial:block/example_block"
- }
(为你的方块画一个texture.png)现在打开MC,方块已经有材质了!
添加战利品表
- "type": "minecraft:block",
- "pools": [
- {
- "rolls": 1,
- "entries": [
- {
- "type": "minecraft:item",
- "name": "tutorial:example_block"
- }
- ],
- "conditions": [
- {
- "condition": "minecraft:survives_explosion"
- }
- ]
- }
- ]
- }
创建方块类
- public class ExampleBlock extends Block
- {
- public ExampleBlock(Settings settings)
- {
- super(settings);
- }
- }
- @Environment(EnvType.CLIENT)
- public BlockRenderLayer getRenderLayer() {
- return BlockRenderLayer.TRANSLUCENT;
- }
- public class ExampleMod implements ModInitializer
- {
- // 新物品的实例
- public static final ExampleBlock EXAMPLE_BLOCK = new ExampleBlock(Block.Settings.of(Material.STONE));
- [...]
- }
- public class MyBlock extends Block {
- public static final BooleanProperty MyBlockIsHard = BooleanProperty.of("is_hard");
- }
- public class MyBlock extends Block {
- [...]
- @Override
- protected void appendProperties(StateFactory.Builder<Block, BlockState> stateFactory) {
- stateFactory.add(MyBlockIsHard);
- }
- }
- public class MyBlock extends Block {
- [...]
- public MyBlock(Settings settings) {
- super(settings);
- setDefaultState(getStateFactory().getDefaultState().with(MyBlockIsHard, false));
- }
- }
- public class MyBlock extends Block {
- [...]
- @Override
- public boolean activate(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult blockHitResult) {
- world.setBlockState(pos, MyBlocks.MY_BLOCK_INSTANCE.getDefaultState().with(MyBlockIsHard, true));
- return true;
- }
- }
- public class MyBlock extends Block {
- [...]
- @Override
- public float getHardness(BlockState blockState, BlockView blockView, BlockPos pos) {
- boolean isHard = blockState.get(MyBlockIsHard);
- if(isHard) return 2.0f;
- else return 0.5f;
- }
- }
关于性能
新建方块实体
- public class DemoBlockEntity extends BlockEntity {
- public DemoBlockEntity() {
- super(ExampleMod.DEMO_BLOCK_ENTITY);
- }
- }
注册方块实体
- public static BlockEntityType<DemoBlockEntity> DEMO_BLOCK_ENTITY;
-
- @Override
- public void onInitialize() {
- DEMO_BLOCK_ENTITY = Registry.register(Registry.BLOCK_ENTITY, "modid:demo", BlockEntityType.Builder.create(DemoBlockEntity::new, DEMO_BLOCK).build(null));
- }
BlockEntityType创建和注册之后,就可以在你的Block类中实现BlockEntityProvider了:
- @Override
- public BlockEntity createBlockEntity(BlockView blockView) {
- return new DemoBlockEntity();
- }
序列化数据
- public class DemoBlockEntity extends BlockEntity {
-
- // Store the current value of the number
- private int number = 7;
-
- public DemoBlockEntity() {
- super(ExampleMod.DEMO_BLOCK_ENTITY);
- }
-
- // Serialize the BlockEntity
- public CompoundTag toTag(CompoundTag tag) {
- super.toTag(tag);
- // Save the current value of the number to the tag
- tag.putInt("number", number);
- return tag;
- }
- }
为了获取数据,需要重写fromTag(),这个方法是toTag()的逆过程,从已经保存的NBT中获取你需要的数据而不是保存过程本身。和toTag()一样,你必须调取super.fromTag(),使用和先前一样的键(number),来获取你保存的数据,见下:
- // Deserialize the BlockEntity
- public void fromTag(CompoundTag tag) {
- super.fromTag(tag);
- number = tag.getInt("number");
- }
实现Inventory接口
- /**
- * A simple {@code Inventory} implementation with only default methods + an item list getter.
- *
- * Originally by Juuz
- */
- public interface ImplementedInventory extends Inventory {
- /**
- * 获取“箱子”里的所有物品。
- * 每次调用都应该返回相同实例。
- */
- DefaultedList<ItemStack> getItems();
- // Creation
- /**
- * 从物品清单新建一个Invnetory
- */
- static ImplementedInventory of(DefaultedList<ItemStack> items) {
- return () -> items;
- }
- /**
- * 新建一个指定大小的Inventory
- */
- static ImplementedInventory ofSize(int size) {
- return of(DefaultedList.ofSize(size, ItemStack.EMPTY));
- }
- // Inventory
- /**
- * 返回Inventory大小
- */
- @Override
- default int getInvSize() {
- return getItems().size();
- }
- /**
- * @如果Inventory是空的,返回true,否则返回false
- */
- @Override
- default boolean isInvEmpty() {
- for (int i = 0; i < getInvSize(); i++) {
- ItemStack stack = getInvStack(i);
- if (!stack.isEmpty()) {
- return false;
- }
- }
- return true;
- }
- /**
- * 获取某个格子中的物品。
- */
- @Override
- default ItemStack getInvStack(int slot) {
- return getItems().get(slot);
- }
- /**
- * 从格子里取出“一堆”物品(ItemStack)
- * <p>(默认实现) 如果格子里的物品数量比你要取出的物品数量少,就取出所有物品
- */
- @Override
- default ItemStack takeInvStack(int slot, int count) {
- ItemStack result = Inventories.splitStack(getItems(), slot, count);
- if (!result.isEmpty()) {
- markDirty();
- }
- return result;
- }
- /**
- * 移除并返回当前格子里的ItemStack
- */
- @Override
- default ItemStack removeInvStack(int slot) {
- return Inventories.removeStack(getItems(), slot);
- }
- /**
- * 用你提供的ItemStack替换格子里的
- * <p>如果ItemStack堆叠数太大 ({@link Inventory#getInvMaxStackAmount()}),
- * 就会被限制到Inventory允许的堆叠数
- */
- @Override
- default void setInvStack(int slot, ItemStack stack) {
- getItems().set(slot, stack);
- if (stack.getCount() > getInvMaxStackAmount()) {
- stack.setCount(getInvMaxStackAmount());
- }
- }
- /**
- * 清除物品 {@linkplain #getItems() the item list}}.
- */
- @Override
- default void clear() {
- getItems().clear();
- }
- @Override
- default void markDirty() {
- // 如果你想在保存时做些什么就重写这个
- }
- @Override
- default boolean canPlayerUseInv(PlayerEntity player) {
- return true;
- }
- }
现在你的BlockEntity实现了ImplementedInventory接口(由于全是default实现,跟直接继承了抽象类一样),提供给它一个DefaultedList<ItemStack>的实例来存储物品,然后向里存储最多两种物品:
- public class DemoBlockEntity extends BlockEntity implements ImplementedInventory {
- private final DefaultedList<ItemStack> items = DefaultedList.ofSize(2, ItemStack.EMPTY);
-
- @Override
- public DefaultedList<ItemStack> getItems() {
- return items;
- }
- [...]
- }
还需要把这些内容物保存到nbt,然后从此加载,Inventories类(译注:相当于forge的ItemStackHelper)有相应的方法可以简单做到:
复制代码
- public class DemoBlockEntity extends BlockEntity implements ImplementedInventory {
- [...]
- @Override
- public void fromTag(CompoundTag tag) {
- super.fromTag(tag);
- Inventories.fromTag(tag,items);
- }
- @Override
- public CompoundTag toTag(CompoundTag tag) {
- Inventories.toTag(tag,items);
- return super.toTag(tag);
- }
- }
从Inventory插入和提取物品
- public class ExampleBlock extends Block implements BlockEntityProvider {
- [...]
- @Override
- public boolean activate(BlockState blockState, World world, BlockPos blockPos, PlayerEntity player, Hand hand, BlockHitResult blockHitResult) {
- if (world.isClient) return true;
- Inventory blockEntity = (Inventory) world.getBlockEntity(blockPos);
- if (!player.getStackInHand(hand).isEmpty()) {
- // Check what is the first open slot and put an item from the player's hand there
- if (blockEntity.getInvStack(0).isEmpty()) {
- // Put the stack the player is holding into the inventory
- blockEntity.setInvStack(0, player.getStackInHand(hand).copy());
- // Remove the stack from the player's hand
- player.getStackInHand(hand).setCount(0);
- } else if (blockEntity.getInvStack(1).isEmpty()) {
- blockEntity.setInvStack(1, player.getStackInHand(hand).copy());
- player.getStackInHand(hand).setCount(0);
- } else {
- // If the inventory is full we'll print it's contents
- System.out.println("The first slot holds "
- + blockEntity.getInvStack(0) + " and the second slot holds " + blockEntity.getInvStack(1));
- }
- }
- return true;
- }
- }
- public class ExampleBlock extends Block implements BlockEntityProvider {
- [...]
- @Override
- public boolean activate(BlockState blockState, World world, BlockPos blockPos, PlayerEntity player, Hand hand, BlockHitResult blockHitResult) {
- ...
- if (!player.getStackInHand(hand).isEmpty()) {
- ...
- } else {
- // If the player is not holding anything we'll get give him the items in the block entity one by one
-
- // Find the first slot that has an item and give it to the player
- if (!blockEntity.getInvStack(1).isEmpty()) {
- // Give the player the stack in the inventory
- player.inventory.offerOrDrop(world, blockEntity.getInvStack(1));
- // Remove the stack from the inventory
- blockEntity.removeInvStack(1);
- } else if (!blockEntity.getInvStack(0).isEmpty()) {
- player.inventory.offerOrDrop(world, blockEntity.getInvStack(0));
- blockEntity.removeInvStack(0);
- }
- }
- return true;
- }
- }
实现SidedInventory
如果你想基于(方块的)每一个面,有不同的交互逻辑(比如漏斗或者其他mod),需要实现SidedInventory接口,比方说你不想从方块上方插入物品,就可以这样做:
- public class DemoBlockEntity extends BlockEntity implements ImplementedInventory, SidedInventory {
- [...]
- @Override
- public int[] getInvAvailableSlots(Direction var1) {
- // Just return an array of all slots
- int[] result = new int[getItems().size()];
- for (int i = 0; i < result.length; i++) {
- result[i] = i;
- }
-
- return result;
- }
-
- @Override
- public boolean canInsertInvStack(int slot, ItemStack stack, Direction direction) {
- return direction != Direction.UP;
- }
-
- @Override
- public boolean canExtractInvStack(int slot, ItemStack stack, Direction direction) {
- return true;
- }
- }
根据群系改变方块颜色
- public class ExampleModClient implements ClientModInitializer {
- @Override
- public void onInitializeClient() {
- ColorProviderRegistry.BLOCK.register((block, pos, world, layer) -> {
- BlockColorProvider provider = ColorProviderRegistry.BLOCK.get(Blocks.GRASS);
- return provider == null ? -1 : provider.getColor(block, pos, world, layer);
- }, YOUR_BLOCK_INSTANCE);
- }
- }
- public class ExampleModClient implements ClientModInitializer {
- @Override
- public void onInitializeClient() {
- ColorProviderRegistry.ITEM.register((item, layer) -> {
- // These values are represented as temperature and humidity, and used as coordinates for the color map
- double temperature = 0.5D; // a double value between 0 and 1
- double humidity = 1.0D; // a double value between 0 and 1
- return GrassColorHandler.getColor(temperature, humidity);
- }, YOUR_ITEM_INSTANCE);
- }
- }
让方块变透明
- class MyBlock extends Block {
- @Override
- public BlockRenderLayer getRenderLayer() {
- return BlockRenderLayer.TRANSLUCENT;
- }
- [...]
- }
你可能还想让你的方块变成透明方块(译注:一般指不能放火把的方块),需要使用Material构造器,将blocksLight设置成false。
- class MyBlock extends Block {
- private static Material myMaterial = new Material(
- MaterialColor.AIR, //materialColor,
- false, //isLiquid,
- false, // isSolid,
- true, // blocksMovement,
- false,// blocksLight, <----- 这部分很重要,其他随意
- true,// !requiresTool,
- false, // burnable,
- false,// replaceable,
- PistonBehavior.NORMAL// 活塞行为
- );
-
- public MyBlock() {
- super(Settings.of(myMaterial);
- }
-
- [...]
- }
- @Override
- public BlockRenderType getRenderType(BlockState blockState) {
- return BlockRenderType.INVISIBLE;
- }
- @Override
- public VoxelShape getOutlineShape(BlockState blockState, BlockView blockView, BlockPos blockPos, EntityContext entityContext) {
- return VoxelShapes.cuboid(0,0,0,0,0,0);
- }
示例
- public class MyBlockEntityRenderer extends BlockEntityRenderer<DemoBlockEntity> {
- // A jukebox itemstack
- private static ItemStack stack = new ItemStack(Items.JUKEBOX, 1);
-
- @Override
- public void render(DemoBlockEntity blockEntity, double x, double y, double z, float partialTicks, int destroyStage) {
- }
- }
需要注册我们的BlockEntityRenderer,但是只针对客户端。在单人游戏里这不影响什么,因为服务端和客户端运行在一个进程上,但是在多人游戏里,客户端和服务端运行在不同的进程上,服务端可没有BlockEntityRenderer,那么就不会接受注册。要运行只针对客户端的初始化代码,需要设置一个client入点。
在主类旁边新建一个实现了ClientModInitializer的新类:
- public class ExampleModClient implements ClientModInitializer {
- @Override
- public void onInitializeClient() {
- // Here we will put client-only registration code
- }
- }
在fabric.mod.json文件中,把这个类设置成client入点(按需调整路径):
- "entrypoints": {
- [...]
- "client": [
- {
- "value": "tutorial.path.to.ExampleModClient"
- }
- ]
- }
在ClientModInitializer中注册BlockEntityRenderer:
- @Override
- public void onInitializeClient() {
- BlockEntityRendererRegistry.INSTANCE.register(DemoBlockEntity.class, new MyBlockEntityRenderer());
- }
需要重写render()方法,这个方法每帧都会被调用!在其中进行渲染,对于初学者来说,先调用GLStateManager.pushMatrix();在进行GL调用时这一步必不可少(译注:这是为了保证待渲染对象不会被长时间的变换“走形”),也就是下面要做的:
- public void render(DemoBlockEntity blockEntity, double x, double y, double z, float partialTicks, int destroyStage) {
- GlStateManager.pushMatrix();
- }
- 获取当前的世界时间,这个“实时”变化。
- 添加不完整刻(partial ticks),不完整刻是一个小数值,代表上一个完整刻(full tick)到现在过去的时间,如果不用它,动画会变得不平稳,因为一秒的tick数(正常是20)少于一秒的帧数(一般是60)。
- 除以8来减慢移动速度;
- 取其正弦值来生成一个-1~1之间的值,类似一个正弦波。
- 除以4来垂直压缩正弦波,这样物品的跳动幅度不会太大。
译注:GLStateManager.method可以用GL11,GL12等类中的等效方法代替(事实上前者调用了后者而已),例如GLStateManager.rotatef()可以用GL11.glRotatef()代替,不过好处只是少打了两个字。
- public void render(DemoBlockEntity blockEntity, double x, double y, double z, float partialTicks, int destroyStage) {
- [...]
- // 计算y值的偏移值
- double offset = Math.sin((blockEntity.getWorld().getTime() + partialTicks) / 8.0) / 4.0;
- // 平移物品
- GlStateManager.translated(x + 0.5, y + 1.25 + offset, z + 0.5);
- // 旋转物品,这里指以一个四元float数做转轴去旋转
- GlStateManager.rotatef((blockEntity.getWorld().getTime() + partialTicks) * 4, 0, 1, 0);
- }
最后,我们获取到MC的ItemRenderer,然后用其renderItem方法渲染这个唱片机,还需要传入ModelTransformation.Type.GROUND到renderItem方法中,因为我们想要一个类似于物品放在地上的效果,试一下这个枚举值看看会发生什么。在GL方法调用后,需要调用GlStateManager.popMatrix()方法:
- public void render(DemoBlockEntity blockEntity, double x, double y, double z, float partialTicks, int destroyStage) {
- [...]
- MinecraftClient.getInstance().getItemRenderer().renderItem(stack, ModelTransformation.Type.GROUND);
-
- // 必不可少
- GlStateManager.popMatrix();
- }
- @Override
- public void render(DemoBlockEntity blockEntity, double x, double y, double z, float partialTicks, int destroyStage) {
- [...]
-
- // 把这部分放在 "MinecraftClient.getInstance().getItemRenderer().renderItem(stack, ModelTransformation.Type.GROUND);"之上
- int light = blockEntity.getWorld().getLightmapIndex(blockEntity.getPos().up(), 0);
- GLX.glMultiTexCoord2f(GLX.GL_TEXTURE1, (float) (light & 0xFFFF), (float) ((light >> 16) & 0xFFFF));
-
- [...]
- }
现在,唱片机应该有正常的光照了。
添加实体是在添加完物品和方块后的下一个步骤。
要添加实体,需要3个类。
- Entity类,它用来提供AI(逻辑/思维方式)
- Renderer类,让实体与模型绑定(给实体提供模型)
- Model类,玩家所看到的模型
注册实体
与方块和物品不同,一般实体都会有一个专门的类。由于我们又做了个苦力怕,所以让实体类型继承CreeperEntity:(译注:fabric的很多反混淆名其实是forge的倒过来)
public class CookieCreeperEntity extends CreeperEntity {
[...]
}
IDE将会提示你,建立一个和父类相匹配的构造函数。
注册实体需要使用Registry.ENTITY_TYPE,可以使用EntityType.Builder或 FabricEntityTypeBuilder来获取所需的注册表实例(这里推荐使用后者)。
- public static final EntityType<CookieCreeperEntity> COOKIE_CREEPER =
- Registry.register(
- Registry.ENTITY_TYPE,
- new Identifier("wiki-entity", "cookie-creeper"),
- FabricEntityTypeBuilder.create(EntityCategory.AMBIENT, CookieCreeperEntity::new).size(EntityDimensions.fixed(1, 2)).build()
- );
使用size()方法来设置实体的碰撞箱。苦力怕宽为一格,高为二格,所以这里填(1,2)。
完成后进入游戏,你可以用/summon来查看你创建的实体。如果一切正常,那么就可以看到一个普通的苦力怕。
创建渲染器
我们创建的苦力怕自带一个模型,因为它继承了Creeper类。现在我们要把它的绿色迷彩色皮肤换成曲奇皮肤。
首先,创建一个有两个泛型参数<Entity,Model>的MobEntityRenderer类,因为我们现在用的是爬行者模型,所以我们需要给苦力怕模型一个泛型参数来告诉它,这不是苦力怕实体(译注:比如说,模型是一件衣服,但它不知道自己的主人是谁,给予了类型后就能让它明白,XXX不是它的主人,它不需要穿上这个模型)
- public class CookieCreeperRenderer extends MobEntityRenderer<CookieCreeperEntity, CreeperEntityModel<CookieCreeperEntity>> {
- [...]
- }
需要重写getTexture方法并添加构造函数。默认情况下,构造函数有3个参数(EntityRenderDispatcher,EntityModel,float),但我们可以删除后2个并自己创建它们:
- public CookieCreeperRenderer(EntityRenderDispatcher entityRenderDispatcher_1)
- {
- super(entityRenderDispatcher_1, new CreeperEntityModel<>(), 1);
- }
对于getTexture方法,需要返回模型的材质。如果为null,那么实体将不可见。这里有一个人人可用的曲奇苦力怕皮肤,点击此处下载。
默认的实体材质位置为:textures/entity/entity_name/entity.png 。示例如下:
- @Override
- protected Identifier getTexture(CookieCreeperEntity cookieCreeperEntity)
- {
- return new Identifier("wiki-entity:textures/entity/cookie_creeper/creeper.png");
- }
文件存储在resources/assets/wiki-entity/textures/entity/cookie_creeper/creeper.png中。
最后。需要把实体与渲染器连接。由于渲染只发生在客户端,因此你需要在ClientModInitializer中执行此类工作:
EntityRendererRegistry.INSTANCE.register(CookieCreeperEntity.class, (entityRenderDispatcher, context) -> new CookieCreeperRenderer(entityRenderDispatcher));
- public void render(DemoBlockEntity blockEntity, double x, double y, double z, float partialTicks, int destroyStage) {
- [...]
- MinecraftClient.getInstance().getItemRenderer().renderItem(stack, ModelTransformation.Type.GROUND);
-
- // Mandatory call after GL calls
- GlStateManager.popMatrix();
- }
这将把实体与渲染器连接。现在进入游戏,就能看见创建的实体了:
如果你想用自己的模型,可以创建一个继承EntityModel的新类,并在渲染器中提供模型。这非常复杂,将单独给一个教程。
向世界中添加矿石
- 挨个迭代注册的生物群系,将矿石添加到其中;
- 使用RegistryEntryAddedCallback(译注:XXXCallback即为fabric提供的事件,fabric有一些事件)确保你的矿石添加到任何mod添加的群系中。
将矿石添加到群系
- private void handleBiome(Biome biome) {
- if(biome.getCategory() != Biome.Category.NETHER && biome.getCategory() != Biome.Category.THEEND) {
- biome.addFeature(
- GenerationStep.Feature.UNDERGROUND_ORES,
- Biome.configureFeature(
- Feature.ORE,
- new OreFeatureConfig(
- OreFeatureConfig.Target.NATURAL_STONE,
- Blocks.NETHER_QUARTZ_ORE.getDefaultState(),
- 8 //矿脉的大小(有几个矿)
- ),
- Decorator.COUNT_RANGE,
- new RangeDecoratorConfig(
- 8, //Number of ms per chunk
- 0, //偏移量
- 0, //最低y轴高度
- 64 //最高y轴高度
- )));
- }
- }
迭代注册的生物群系
- @Override
- public void onInitialize() {
- //循环迭代所有已经注册的群系
- Registry.BIOME.forEach(this::handleBiome);
-
- //准备监听其他群系
- RegistryEntryAddedCallback.event(Registry.BIOME).register((i, identifier, biome) -> handleBiome(biome));
- }
然后就可以看到生成在主世界的石英矿:
这里我们关注结构注册和如何生成在世界中。IglooGenerator和IglooFeature是很好的1.14原版结构生成范例(Igloo:冰屋)。
建立一个特性(feature)
- shouldStartAt: 测试目的,返回true。
- getName: 结构的名称
- getRadius: 结构的半径,用于放置结构
- getSeedModifier:获取种子调整器
- public static class MyStructureStart extends StructureStart {
- public MyStructureStart (StructureFeature<?> structureFeature, int x, int y, Biome biome, MutableIntBoundingBox mutableIntBoundingBox, int z, long w) {
- super(structureFeature, x , y, biome, mutableIntBoundingBox, z, w);
- }
- @Override
- public void initialize(ChunkGenerator<?> chunkGenerator, StructureManager structureManager, int chunkX, int chunkZ, Biome biome) {
- DefaultFeatureConfig defaultFeatureConfig = chunkGenerator.getStructureConfig(biome, MyMainclass.myFeature);
- int x = chunkX * 16;
- int z = chunkZ * 16;
- BlockPos startingPos = new BlockPos(x, 0, z);
- Rotation rotation = Rotation.values()[this.random.nextInt(Rotation.values().length)];
- MyGenerator.addParts(structureManager, startingPos, rotation, this.children, this.random, defaultFeatureConfig);
- this.setBoundingBoxFromChildren();
- }
- }
世界尝试生成新结构时会调用这个类,是特性和生成器之间的分歧。对主类中变量的对象引用还不存在,但是结尾会声明一个。你也可以将其配置项设置成一个DefaultFeatureConfig,然后在getStructureStartFactory()方法中返回MyStructureStart::new来返回这个类的实例。以上是结构文件和直接从generate方法生成的部分,有两个方法处理这个:
- 如果你想,可以简单地重写generate()方法,然后使用setBlockState()把方块直接放在世界中,这样做是有效的,在1.13之前很流行。
- 使用结构文件和生成器,它们功能强大,非常推荐。
声明一个生成器
现在你看到了,需要一个生成器,姑且叫MyGenerator,在StructureStart.initialize()方法中引用,不需要重写什么,但是需要按照下面的做:
- 一个指向你的结构文件的Identifier,比方说“igloo/top”。
- 一些安放结构的方法——addPart是个好名字。
- public static void addParts(StructureManager structureManager, BlockPos blockPos, Rotation rotation,
- List<StructurePiece> list, Random random, DefaultFeatureConfig featureConfig)
- }
在addParts方法中,可以选择向你的生成过程添加结构分块(Piece),示例如下:
- list.add(new MyGenerator.Piece(structureManager, identifier, blockPos, rotation));
- public static class Piece extends SimpleStructurePiece {
- private Rotation rotation;
- private Identifier template;
-
- public Piece(StructureManager structureManager, Identifier identifier, BlockPos blockPos, Rotation rotation) {
- super(MyModClass.myStructurePieceType, 0);
- this.pos = blockPos;
- this.rotation = rotation;
- this.template = identifier;
- this.setStructureData(structureManager);
- }
-
- public Piece(StructureManager structureManager, CompoundTag compoundTag) {
- super(MyModClass.myStructurePieceType, compoundTag);
- this.identifier = new Identifier(compoundTag.getString("Template"));
- this.rotation = Rotation.valueOf(compoundTag.getString("Rot"));
- this.setStructureData(structureManager);
- }
-
- @Override
- protected void toNbt(CompoundTag compoundTag) {
- super.toNbt(compoundTag);
- compoundTag.putString("Template", this.template.toString());
- compoundTag.putString("Rot", this.rotation.name());
- }
-
- public void setStructureData(StructureManager structureManager) {
- Structure structure = structureManager.getStructureOrBlank(this.identifier);
- StructurePlacementData structurePlacementData = (new StructurePlacementData()).setRotation(this.rotation).setMirrored(Mirror.NONE).setPosition(pos).addProcessor(BlockIgnoreStructureProcessor.IGNORE_STRUCTURE_BLOCKS);
- this.setStructureData(structure, this.pos, structurePlacementData);
- }
-
- @Override
- protected void handleMetadata(String string, BlockPos blockPos, IWorld iWorld, Random random, MutableIntBoundingBox mutableIntBoundingBox) {
-
- }
-
- @Override
- public boolean generate(IWorld iWorld, Random random, MutableIntBoundingBox mutableIntBoundingBox, ChunkPos chunkPos) {
- }
- }
- @Override
- public boolean generate(IWorld iWorld, Random random, MutableIntBoundingBox mutableIntBoundingBox, ChunkPos chunkPos) {
- int yHeight = iWorld.getTop(Heightmap.Type.WORLD_SURFACE_WG, this.pos.getX() + 8, this.pos.getZ() + 8);
- this.pos = this.pos.add(0, yHeight - 1, 0);
- return super.generate(iWorld, random, mutableIntBoundingBox, chunkPos);
- }
注册特性
- StructurePieceType
- StructureFeature<DefaultFeatureConfig>
- StructureFeature<?>
- public static final StructurePieceType myStructurePieceType = Registry.register(Registry.STRUCTURE_PIECE, "my_piece", MyGenerator.Piece::new);
复制代码
- public static final StructureFeature<DefaultFeatureConfig> myFeature = Registry.register(Registry.FEATURE, "my_feature", new MyFeature());
- public static final StructureFeature<?> myStructure = Registry.register(Registry.STRUCTURE_FEATURE, "my_structure", myFeature);
- Feature.STRUCTURES.put("My Awesome Feature", myFeature);
- for(Biome biome : Registry.BIOME) {
- if(biome.getCategory() != Biome.Category.OCEAN && biome.getCategory() != Biome.Category.RIVER) {
- biome.addStructureFeature(myFeature, new DefaultFeatureConfig());
- biome.addFeature(GenerationStep.Feature.SURFACE_STRUCTURES, Biome.configureFeature(myFeature, new DefaultFeatureConfig(), Decorator.CHANCE_PASSTHROUGH, new ChanceDecoratorConfig(0)));
- }
- }
- 新建群系
- 注册群系
- 将群系添加到世界的气候区域
- 允许玩家生成在群系中
新建群系
- 群系的基本属性
- 群系会有什么特性(树,植物和结构)
- 群系会生成什么实体
- public class MyBiome extends Biome
- {
- public MyBiome()
- {
- super(new Biome.Settings().configureSurfaceBuilder(SurfaceBuilder.DEFAULT, SurfaceBuilder.GRASS_CONFIG).precipitation(Biome.Precipitation.RAIN).category(Biome.Category.PLAINS).depth(0.24F).scale(0.2F).temperature(0.6F).downfall(0.7F).waterColor(4159204).waterFogColor(329011).parent((String)null));
- }
- }
- public class MyBiome extends Biome
- {
- public MyBiome()
- {
- super(new Biome.Settings().configureSurfaceBuilder(SurfaceBuilder.DEFAULT, SurfaceBuilder.GRASS_CONFIG).precipitation(Biome.Precipitation.RAIN).category(Biome.Category.PLAINS).depth(0.24F).scale(0.2F).temperature(0.6F).downfall(0.7F).waterColor(4159204).waterFogColor(329011).parent((String)null));
-
- this.addStructureFeature(Feature.MINESHAFT, new MineshaftFeatureConfig(0.004D, MineshaftFeature.Type.NORMAL));
- this.addStructureFeature(Feature.STRONGHOLD, FeatureConfig.DEFAULT);
- this.addStructureFeature(Feature.VILLAGE, new VillageFeatureConfig("village/plains/town_centers", 6));
- DefaultBiomeFeatures.addLandCarvers(this);
- DefaultBiomeFeatures.addDefaultStructures(this);
- DefaultBiomeFeatures.addDefaultLakes(this);
- DefaultBiomeFeatures.addDungeons(this);
- DefaultBiomeFeatures.addExtraMountainTrees(this);
- DefaultBiomeFeatures.addDefaultFlowers(this);
- DefaultBiomeFeatures.addDefaultGrass(this);
- DefaultBiomeFeatures.addMineables(this);
- DefaultBiomeFeatures.addDefaultOres(this);
- DefaultBiomeFeatures.addDefaultDisks(this);
- DefaultBiomeFeatures.addDefaultVegetation(this);
- DefaultBiomeFeatures.addSprings(this);
- DefaultBiomeFeatures.addFrozenTopLayer(this);
- this.addSpawn(EntityCategory.CREATURE, new Biome.SpawnEntry(EntityType.SHEEP, 12, 4, 4));
- this.addSpawn(EntityCategory.CREATURE, new Biome.SpawnEntry(EntityType.PIG, 10, 4, 4));
- this.addSpawn(EntityCategory.CREATURE, new Biome.SpawnEntry(EntityType.CHICKEN, 10, 4, 4));
- this.addSpawn(EntityCategory.CREATURE, new Biome.SpawnEntry(EntityType.COW, 8, 4, 4));
- this.addSpawn(EntityCategory.AMBIENT, new Biome.SpawnEntry(EntityType.BAT, 10, 8, 8));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.SPIDER, 100, 4, 4));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.ZOMBIE, 95, 4, 4));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.ZOMBIE_VILLAGER, 5, 1, 1));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.SKELETON, 100, 4, 4));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.CREEPER, 100, 4, 4));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.SLIME, 100, 4, 4));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.ENDERMAN, 10, 1, 4));
- this.addSpawn(EntityCategory.MONSTER, new Biome.SpawnEntry(EntityType.WITCH, 5, 1, 1));
- }
- }
- public class TutorialBiomes
- {
- public static final Biome MY_BIOME = Registry.register(Registry.BIOME, new Identifier("tutorial", "my_biome"), new MyBiome());
- }
- {
- "biome.tutorial.my_biome": "My Biome"
- }
- public class ExampleMod implements ModInitializer
- {
- @Override
- public void onInitialize()
- {
- OverworldBiomes.addContinentalBiome(OverworldClimate.TEMPERATE, TutorialBiomes.MY_BIOME, 2D);
- OverworldBiomes.addContinentalBiome(OverworldClimate.COOL, TutorialBiomes.MY_BIOME, 2D);
- }
- }
- FabricBiomes.addSpawnBiome(TutorialBiomes.MY_BIOME);
其他的实用Biome方法
- 设置河流
- OverworldBiomes.setRiverBiome(TutorialBiomes.MY_BIOME, null);
- 添加群系变体
- OverworldBiomes.addBiomeVariant(Biomes.PLAINS, TutorialBiomes.MY_BIOME, 0.33);
- 添加山变体
- OverworldBiomes.addHillsBiome(TutorialBiomes.MY_BIOME, Biomes.MOUNTAINS, 1);
- 添加群系边缘
- OverworldBiomes.addEdgeBiome(TutorialBiomes.MY_BIOME, Biomes.FOREST, 1);
- 添加海岸和海滩
- OverworldBiomes.addShoreBiome(TutorialBiomes.MY_BIOME, Biomes.STONE_BEACH, 1);
DimensionType(维度类型)
ChunkGenerator(区块生成器)
ChunkGeneratorType(区块生成器类型)
Biome(生物群系)
BiomeSource(生物群系源)
SurfaceBuilder(表面构建器)
新建一个护甲材料类
- 名称(name),稍后会用做“护甲标签”。
- 耐久因子(durabilityMultiplier),基础数值乘以耐久因子即为最终耐久。
- 护甲值(armorValues),或者原版代码中的“保护点数(Protection Amounts)” ,这是个整型数组。
- 附魔能力(Enchantability),代表了护甲在附魔时得到高级附魔或者多个附魔的概率。
- 声音事件(equipSound),用在原版护甲的声音事件是 SoundEvents.ITEM.EQUIP.ARMOR.X, X是护甲的类型。
- 护甲韧性(toughness). 这是第二个保护值,遭受高伤害时护甲会更加坚韧,掉耐久少(译注:只有钻石护甲有这个参数)。
- 修复材料(repairIngredient),这是一个 Supplier<Ingredient>实例而不是物品(Item), 一会会讲。
- public enum CustomArmorMaterial implements ArmorMaterial {
- private final String name;
- private final int durabilityMultiplier;
- private final int[] armorValues;
- private final int enchantability;
- private final SoundEvent equipSound;
- private final float toughness;
- private final Lazy<Ingredient> repairIngredient;
-
- CustomArmorMaterial(String name, int durabilityMultiplier, int[] armorValueArr, int enchantability, SoundEvent soundEvent, float toughness, Supplier<Ingredient> repairIngredient) {
- this.name = name;
- this.durabilityMultiplier = durabilityMultiplier;
- this.armorValues = armorValueArr;
- this.enchantability = enchantability;
- this.equipSound = soundEvent;
- this.toughness = toughness;
- this.repairIngredient = new Lazy(repairIngredient); // 需要一个Lazy类型的变量,为以后做准备
- }
- }
- public enum CustomArmorMaterial implements ArmorMaterial {
- private static final int[] baseDurability = {13, 15, 16, 11};
- private final String name;
- private final int durabilityMultiplier;
- private final int[] armorValues;
- private final int enchantability;
- private final SoundEvent equipSound;
- private final float toughness;
- private final Lazy<Ingredient> repairIngredient;
-
- CustomArmorMaterial(String name, int durabilityMultiplier, int[] armorValueArr, int enchantability, SoundEvent soundEvent, float toughness, Supplier<Ingredient> repairIngredient) {
- this.name = name;
- this.durabilityMultiplier = durabilityMultiplier;
- this.armorValues = armorValueArr;
- this.enchantability = enchantability;
- this.equipSound = soundEvent;
- this.toughness = toughness;
- this.repairIngredient = new Lazy(repairIngredient);
- }
-
- public int getDurability(EquipmentSlot equipmentSlot_1) {
- return BASE_DURABILITY[equipmentSlot_1.getEntitySlotId()] * this.durabilityMultiplier;
- }
-
- public int getProtectionAmount(EquipmentSlot equipmentSlot_1) {
- return this.protectionAmounts[equipmentSlot_1.getEntitySlotId()];
- }
-
- public int getEnchantability() {
- return this.enchantability;
- }
-
- public SoundEvent getEquipSound() {
- return this.equipSound;
- }
-
- public Ingredient getRepairIngredient() {
- // We needed to make it a Lazy type so we can actually get the Ingredient from the Supplier.
- return this.repairIngredientSupplier.get();
- }
-
- @Environment(EnvType.CLIENT)
- public String getName() {
- return this.name;
- }
-
- public float getToughness() {
- return this.toughness;
- }
- }
- public enum CustomArmorMaterial implements ArmorMaterial {
- WOOL("wool", 5, new int[]{1,3,2,1}, 15, SoundEvents.BLOCK_WOOL_PLACE, 0.0F, () -> {
- return Ingredient.ofItems(Items.WHITE_WOOL);
- });
- [...]
- }
- public class ExampleMod implements ModInitializer {
- public static final Item WOOL_HELMET = new ArmorItem(CustomArmorMaterial.WOOL, EquipmentSlot.HEAD, (new Item.Settings().group(ItemGroup.COMBAT)));
- public static final Item WOOL_CHESTPLATE = new ArmorItem(CustomArmorMaterial.WOOL, EquipmentSlot.CHEST, (new Item.Settings().group(ItemGroup.COMBAT)));
- public static final Item WOOL_LEGGINGS = new ArmorItem(CustomArmorMaterial.WOOL, EquipmentSlot.LEGS, (new Item.Settings().group(ItemGroup.COMBAT)));
- public static final Item WOOL_BOOTS = new ArmorItem(CustomArmorMaterial.WOOL, EquipmentSlot.FEET, (new Item.Settings().group(ItemGroup.COMBAT)));
- }
- [...]
- public void onInitialize() {
- Registry.register(Registry.ITEM,new Identifier("tutorial","wool_helmet"), WOOL_HELMET);
- Registry.register(Registry.ITEM,new Identifier("tutorial","wool_chestplate"), WOOL_CHESTPLATE);
- Registry.register(Registry.ITEM,new Identifier("tutorial","wool_leggings"), WOOL_LEGGINGS);
- Registry.register(Registry.ITEM,new Identifier("tutorial","wool_boots"), WOOL_BOOTS);
- }
- {
- "pack":{
- "pack_format":4, //4是1.13以后的资源包号,1.12是3
- "description":"Tutorial Mod"
- }
- }
- 新建一个Enchantment或者已经存在的附魔类(例如DamageEnchantment)的子类
- 注册你的附魔(注:注册附魔时,对应的附魔书也会自动注册,附魔的翻译条目enchantment.modid.enchantname会作为书名而存在)
- 自定义附魔机制和功能
- 为附魔添加翻译条目
创建附魔类
复制代码
- public class FrostEnchantment extends Enchantment
- {
- public WrathEnchantment(Weight weight, EnchantmentTarget target, EquipmentSlot[] slots)
- {
- super(weight, target, slots);
- }
- }
- @Override
- public int getMinimumPower(int int_1)
- {
- return 1;
- }
- @Override
- public int getMaximumLevel()
- {
- return 3;
- }
- @Override
- public void onTargetDamaged(LivingEntity user, Entity target, int level)
- {
- if(target instanceof LivingEntity)
- {
- ((LivingEntity) target).addPotionEffect(new StatusEffectInstance(StatusEffects.SLOWNESS, 20 * 2 * level, level - 1));
- }
- super.onTargetDamaged(user, target, level);
- }
注册附魔
- private static Enchantment FROST;
-
- @Override
- public void onInitialize()
- {
- FROST = Registry.register(
- Registry.ENCHANTMENT,
- new Identifier("tutorial", "frost"),
- new FrostEnchantment(
- Enchantment.Weight.VERY_RARE,
- EnchantmentTarget.WEAPON,
- new EquipmentSlot[] {
- EquipmentSlot.MAINHAND
- }
- )
- );
- }
添加翻译和测试
- {
- "enchantment.tutorial.frost": "Frost"
- }
- 新建一个FabricKeyBinding对象
- 注册你的按键
- 对你的按键起反应
创建快捷键
- private static FabricKeyBinding keyBinding;
- keyBinding = FabricKeyBinding.Builder.create(
- new Identifier("tutorial", "spook"),
- InputUtil.Type.KEYSYM, //KEYSYM指键盘,MOUSE指鼠标
- GLFW.GLFW_KEY_R, //R键
- "Wiki Keybinds"
- ).build();
- private static final Map<String, Integer> categoryOrderMap = (Map)SystemUtil.consume(Maps.newHashMap(), (hashMap_1) -> {
- hashMap_1.put("key.categories.movement", 1);
- hashMap_1.put("key.categories.gameplay", 2);
- hashMap_1.put("key.categories.inventory", 3);
- hashMap_1.put("key.categories.creative", 4);
- hashMap_1.put("key.categories.multiplayer", 5);
- hashMap_1.put("key.categories.ui", 6);
- hashMap_1.put("key.categories.misc", 7);
- });
- KeyBindingRegistry.INSTANCE.register(keyBinding);
响应快捷键
- ClientTickCallback.EVENT.register(e ->
- {
- if(keyBinding.isPressed()) System.out.println("was pressed!");
- });
- 新建事件回调接口
- 从mixin触发事件
- 新建测试实现
新建回调接口
复制代码
- public interface SheepShearCallback</font>
- {
- Event<SheepShearCallback> EVENT = EventFactory.createArrayBacked(SheepShearCallback.class,
- (listeners) -> (player, sheep) -> {
- for (SheepShearCallback event : listeners) {
- ActionResult result = event.interact(player, sheep);
- if(result != ActionResult.PASS) {
- return result;
- }
- }
- return ActionResult.PASS;
- });
- ActionResult interact(PlayerEntity player, SheepEntity sheep);
- (listeners) -> (player, sheep) -> {for (SheepShearCallback event : listeners) {
- ActionResult result = event.interact(player, sheep);
- // ....
- if(result != ActionResult.PASS) {
- return result;
- }
- }
-
- return ActionResult.PASS;
- /**
- * 剪羊毛的回调。
- * 在羊被剪毛,物品掉落和破坏之前调用。
- * 返回:
- * - SUCCESS 会取消事件传递给下一个监听者,继续正常的剪毛行为。
- * - PASS 传递到下一个监听者,如果没有监听者则返回SUCCESS
- * - FAIL取消事件传递,不剪毛。
- /**
- @Mixin(SheepEntity.class)
- public class SheepShearMixin
- {
- @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/passive/SheepEntity;dropItems()V"),
- method = "interactMob", cancellable = true)
- private void onShear(final PlayerEntity player, final Hand hand, final
- CallbackInfoReturnable<Boolean> info) {
- ActionResult result = SheepShearCallback.EVENT.invoker().interact(player, (SheepEntity) (Object) this);
- if(result == ActionResult.FAIL) {
- info.cancel();
- }
- }
- }
用监听器测试事件
- SheepShearCallback.EVENT.register((player, sheep) ->
- {
- sheep.setSheared(true);
-
- // 在羊的位置新建一个钻石掉落物
- ItemStack stack = new ItemStack(Items.DIAMOND);
- ItemEntity itemEntity = new ItemEntity(player.world, sheep.x, sheep.y, sheep.z, stack);
- player.world.spawnEntity(itemEntity);
- return ActionResult.FAIL;
- });
监听战利品表
fabric api有一个战利品表加载时触发的事件,即LootTableLoadingCallback,可以在主类中为其注册一个事件监听者,在此之前先检查一下minecraft:blocks/coal_ore这个战利品表。
- private static final Identifier COAL_ORE_LOOT_TABLE_ID = new Identifier("minecraft", "blocks/coal_ore");
-
- // Actual code
-
- LootTableLoadingCallback.EVENT.register((resourceManager, lootManager, id, supplier, setter) -> {
- if (COAL_ORE_LOOT_TABLE_ID.equals(id)) {
- // Our code will go here
- }
- });
将物品添加到战利品表中
- LootTableLoadingCallback.EVENT.register((resourceManager, lootManager, id, supplier, setter) -> {
- if (COAL_ORE_LOOT_TABLE_ID.equals(id)) {
- FabricLootPoolBuilder poolBuilder = FabricLootPoolBuilder.builder()
- .withRolls(ConstantLootTableRange.create(1)); // 等同于战利品表json里的 "rolls": 1
-
- supplier.withPool(poolBuilder);
- }
- });
池里还没有物品,所以需要添加一个条目,如下:
- LootTableLoadingCallback.EVENT.register((resourceManager, lootManager, id, supplier, setter) -> {
- if (COAL_ORE_LOOT_TABLE_ID.equals(id)) {
- FabricLootPoolBuilder poolBuilder = FabricLootPoolBuilder.builder()
- .withRolls(ConstantLootTableRange.create(1))
- .withEntry(ItemEntry.builder(Items.EGG));
-
- supplier.withPool(poolBuilder);
- }
- });
注册命令
- CommandRegistry.INSTANCE.register(false, dispatcher -> TutorialCommands.register(dispatcher)); // All commands are registered in a single class that references every command.
-
- CommandRegistry.INSTANCE.register(false, dispatcher -> { // You can also just reference every single class also. There is also the alternative of just using CommandRegistry
- TutorialCommand.register(dispatcher);
- TutorialHelpCommand.register(dispatcher);
- });
-
- CommandRegistry.INSTANCE.register(true, dispatcher -> { // Or directly registering the command to the dispatcher.
- dispatcher.register(LiteralArgumentBuilder.literal("tutorial").executes(ctx -> execute(ctx)));
- });
一个基础的命令
- // 命令的基础,必须是字面量参数
- dispatcher.register(CommandManager.literal("foo")
- // 添加一个叫做bar的整型参数
- .then(CommandManager.argument("bar", integer())
- //当输入命令foo和参数bar并回车之后,命令就会执行
Brigadier分析
CommandContexts(命令上下文)
参数
- import static com.mojang.brigadier.arguments.StringArgumentType.getString; // getString(ctx, "string")
- import static com.mojang.brigadier.arguments.StringArgumentType.word; // word(), string(), greedyString()
- import static net.minecraft.server.command.CommandManager.literal; // literal("foo")
- import static net.minecraft.server.command.CommandManager.argument; // argument("bar", word())
- import static net.minecraft.server.command.CommandManager.*; // Import everything
Suggestion(建议)
- SUMMONABLE_ENTITIES
- AVAILIBLE_SOUNDS
- ALL_RECIPES
- ASK_SERVER
- public static SuggestionProvider<ServerCommandSource> suggestedStrings() {
- return (ctx, builder) -> getSuggestionsBuilder(builder, /*Access to a list here*/);
- }
-
- private static CompletableFuture<Suggestions> getSuggestionsBuilder(SuggestionsBuilder builder, List<String> list) {
- String remaining = builder.getRemaining().toLowerCase(Locale.ROOT);
-
- if(list.isEmpty()) { // If the list is empty then return no suggestions
- return Suggestions.empty(); // No suggestions
- }
-
- for (String str : list) { // Iterate through the supplied list
- if (str.toLowerCase(Locale.ROOT).startsWith(remaining)) {
- builder.suggest(str); // Add every single entry to suggestions list.
- }
- }
- return builder.buildFuture(); // Create the CompletableFuture containing all the suggestions
- }
- argument(argumentName, word())
- .suggests(CompletionProviders.suggestedStrings())
- .then(/*Rest of the command*/));
Require(需求)
- dispatcher.register(literal("foo")
- .requires(source -> source.hasPermissionLevel(4))
- .executes(ctx -> {
- ctx.getSource().sendFeedback(new LiteralText("You are an operator", false));
- return 1;
- });
异常
- dispatcher.register(CommandManager.literal("coinflip")
- .executes(ctx -> {
- Random random = new Random();
-
- if(random.nextBoolean()) { // If heads succeed.
- ctx.getSource().sendMessage(new TranslateableText("coin.flip.heads"))
- return Command.SINGLE_SUCCESS;
- }
- throw new SimpleCommandExceptionType(new TranslateableText("coin.flip.tails")).create(); // Oh no tails, you lose.
- }));
- DynamicCommandExceptionType used_name = new DynamicCommandExceptionType(name -> {
- return new LiteralText("The name: " + (String) name + " has been used");
- });
- public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
- LiteralCommandNode node = registerMain(dispatcher); // 注册主命令
- dispatcher.register(literal("tell")
- .redirect(node)); // 别名1, 重定向到主命令
- dispatcher.register(literal("w")
- .redirect(node)); // 别名2,重定向到主命令
- }
-
- public static LiteralCommandNode registerMain(CommandDispatcher<ServerCommandSource> dispatcher) {
- return dispatcher.register(literal("msg")
- .then(argument("targets", EntityArgumentType.players())
- .then(argument("message", MessageArgumentType.message())
- .executes(ctx -> {
- return execute(ctx.getSource(), getPlayers(ctx, "targets"), getMessage(ctx, "message"));
- }))));
- }
ServerCommandSource
- ServerCommandSource source = ctx.getSource();
- // 获取命令发起者,一直有效。
-
- Entity sender = source.getEntity();
- // 未检查,如果发起者是控制台的话则可能是null
-
- Entity sender2 = source.getEntityOrThrow();
- // 如果发起者不是实体则终止指令
- // 获取结果可能是玩家,向玩家发送反馈,告知发起者必须是实体
- // 该方法需要你的方法抛出一个CommandSyntaxException.
- // ServerCommandSource中可以返回一个CommandBlock(命令方块)实体,任何活的实体和玩家
- source.getPosition();
- //以Vec3(三维向量)的形式获取发起者的位置,可以定位实体和命令方块,如果是控制台则是这个世界的出生点
一些实例
- public static void register(CommandDispatcher<ServerCommandSource> dispatcher){
- dispatcher.register(literal("broadcast")
- .requires(source -> source.hasPermissionLevel(2)) // Must be a game master to use the command. Command will not show up in tab completion or execute to non op's or any op that is permission level 1.
- .then(argument("color", ColorArgumentType.color())
- .then(argument("message", greedyString())
- .executes(ctx -> broadcast(ctx.getSource(), getColor(ctx, "color"), getString(ctx, "message")))))); // You can deal with the arguments out here and pipe them into the command.
- }
-
- public static int broadcast(ServerCommandSource source, Formatting formatting, String message) {
- Text text = new LiteralText(message).formatting(formatting);
-
- source.getMinecraftServer().getPlayerManager().broadcastChatMessage(text, false);
- return Command.SINGLE_SUCCESS; // Success
- }
/giveMeDiamond首先是注册“giveMeDiamond”指令的基础代码,然后执行体告知CommandDispatcher运行哪个方法。
- public static LiteralCommandNode register(CommandDispatcher<ServerCommandSource> dispatcher) { // You can also return a LiteralCommandNode for use with possible redirects
- return dispatcher.register(literal("giveMeDiamond")
- .executes(ctx -> giveDiamond(ctx)));
- }
因为钻石只能给玩家,所以要检查CommandSource是不是玩家,可以用getPlayer()方法,同时检测,如果发起者不是玩家,就抛出错误。
- public static int giveDiamond(CommandContext<ServerCommandSource> ctx) throws CommandSyntaxException {
- ServerCommandSource source = ctx.getSource();
-
- PlayerEntity self = source.getPlayer(); // 如果不是玩家命令则结束
然后我们将其添加到玩家的库存(背包)中,检查一下这个Inventory对象是否为空:
- if(!player.inventory.insertStack(new ItemStack(Items.DIAMOND))){
- throw new SimpleCommandExceptionType(new TranslateableText("inventory.isfull")).create();
- }
- return 1;
- }
安条克手雷
- public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
- dispatcher.register(literal("antioch")
- .then(required("location", BlockPosArgumentType.blockPos()
- .executes(ctx -> antioch(ctx.getSource(), BlockPosArgument.getBlockPos(ctx, "location")))))
- .executes(ctx -> antioch(ctx.getSource(), null)));
- }
- public static int antioch(ServerCommandSource source, BlockPos blockPos) throws CommandSyntaxException {
-
- if(blockPos==null) {
- blockPos = LocationUtil.calculateCursorOrThrow(source, source.getRotation());
用命令找群系
- public class CommandLocateBiome {
- // 首先注册
- public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
- LiteralCommandNode<ServerCommandSource> basenode = dispatcher.register(literal("findBiome")
- .then(argument("biome_identifier", identifier()).suggests(BiomeCompletionProvider.BIOMES) // We use Biome suggestions for identifier argument
- .then(argument("distance", integer(0, 20000))
- .executes(ctx -> execute(ctx.getSource(), getIdentifier(ctx, "biome_identifier"), getInteger(ctx, "distance"))))
- .executes(ctx -> execute(ctx.getSource(), getIdentifier(ctx, "biome_identifier"), 1000))));
- // 注册重定向
- dispatcher.register(literal("biome")
- .redirect(basenode));
- }
- // 方法开始
- private static int execute(ServerCommandSource source, Identifier biomeId, int range) throws CommandSyntaxException {
- Biome biome = Registry.BIOME.get(biomeId);
-
- if(biome == null) {
我能给CommandSource发送什么样的反馈信息?
为什么我的IDE会报告命令的execute方法“needs to catch or throw a CommandSyntaxException”?
可以在运行时注册命令吗?
可以在运行时取消注册命令吗?
可以注册客户端的命令吗?
- dispatcher.register(literal("publish")
- private static Type instance;
-
- static { // 类初始化的静态选项,方便别的api调取
- instance = new Type();
- }
-
- public void onInitalize() { //如果在你的主类中
- instance = this;
- }
-
- public static Type getInstance() {
- return instance;
- }
播放已经存在的声音
- if (!world.isClient) {
- world.playSound(
- null, // 玩家,不知道所以置null,如果知道的话写上
- blockPos, // 声音的来源位置
- SoundEvents.BLOCK_ANVIL_LAND, // 在这里是铁砧掉落的声音
- SoundCategory.BLOCKS, // 指定哪个音量条控制声音
- 1f, // 音量因子, 1是正常, 0.5是半音量, 等等
- 1f // 音高因子, 1为正常, 0.5是半音高, 等等
- );
- }
通过重写Block.activate()方法,指定方块右击时的行为来播放声音:
- public class ExampleBlock extends Block {
- [...]
-
- @Override
- public boolean activate(BlockState blockState, World world, BlockPos blockPos, PlayerEntity placedBy, Hand hand, BlockHitResult blockHitResult) {
- if (!world.isClient) {
- world.playSound(null, blockPos, SoundEvents.BLOCK_ANVIL_LAND, SoundCategory.BLOCKS, 1f, 1f);
- }
- return false;
- }
- }
添加自定义声音
步骤1:添加.ogg声音文件
步骤2: 添加sounds.json文件,或者添加到已有的文件
- {
- "my_sound": {
- "sounds": [
- "tutorial:my_sound"
- ]
- }
- }
- {
- "my_sound": {
- "category": "my_sounds",
- "subtitle": "*punch*",
- "sounds": [
- "tutorial:my_sound"
- ]
- }
- }
步骤3:新建声音事件
- public class ExampleMod {
- [...]
- public static final Identifier MY_SOUND_ID = new Identifier("tutorial:my_sound")
- public static SoundEvent MY_SOUND_EVENT = new SoundEvent(MY_SOUND_ID);
- }
步骤4:注册事件
- @Override
- public void onInitialize(){
- [...]
- Registry.register(Registry.SOUND_EVENT, ExampleMod.MY_SOUND_ID, MY_SOUND_EVENT);
- }
- public class ExampleBlock extends Block {
- @Override
- public boolean activate(BlockState blockState, World world, BlockPos blockPos, PlayerEntity placedBy, Hand hand, BlockHitResult blockHitResult) {
- if (!world.isClient) {
- world.playSound(
- null,blockPos, ExampleMod.<b>MY_SOUND_EVENT</b>,SoundCategory.BLOCKS, 1f,1f);
- }
- return false;
- }
- }
- 调高游戏音量;
- 删除output文件夹。
- 由于fabric api是基于注入的,所以不建议给类打补丁(patch),让修改赤裸裸暴露在用户面前,同时如果你遇到了难以解决的问题,fabric的原版类扩展可能会有帮助,比如:
- Block.Settings → FabricBlockSettings
- EntityType.Builder → FabricEntityTypeBuilder
- 官方配置系统尚未完成,目前可以用Java的.properties或者json文件代替。
- 对于内置的资源包和数据包,请确保各自的路径是“assets/[mod id]”和“data/[mod id]”,idea用户可能会无意间建立一个“assets.[modid]”路径,这是无效的。
Mixins
- 要把类转换成它没有实现的接口,或者转换一个final类,或者将mixin转成目标类,可以用“(TargetClass) (Object) sourceClassObject”的把戏。
- 要调整构造函数,使用“<init>”(静态构造器则是“<cinit>”)作为mixin的方法名,注意构造器上的@Inject注解只适用于@At("RETURN"),官方不支持其他的注入方式!
- @Redirect和@ModifyConstant注解目前还不能嵌套(应用于同时修改同一个地方的多个mod),后续开发中可能改进——但是就现在,尽可能避免使用它们和@Overwrite注解(我们在讨论将钩子带到fabric api中,或者再细一点——将其放入一个jar in jar(指大jar里套的小jar))。
- 如果添加自定义的字段和方法,尤其是当它们和接口没有关联时——可以考虑给它们加“modid_”前缀,或者别的什么特别的字符串,比如“mymod_secretValue”代替“secretValue”,用来避免不同的mod添加了同名字段或方法之间的冲突!
网络
- 包总是在网络线程(network thread)被执行,但是MC大多数东西的访问都是线程不安全的。一般来说如果你不确定你在做什么,就需要在网络线程上解析所有包(读取出所有值),然后在服务端/客户端主线程上使用TaskQueue<T>这样的任务队列执行附加操作。
坑
- 避免使用任何java.awt包,这个(古老的)包在所有的系统上都有问题,几个用户说它往往会挂起MC。
模式改变
嵌套的jar
- fabric mod形式的依赖提供方案,允许fabric loader根据整合包的依赖设置匹配最好的依赖版本。
- 允许将类库转为fabric mod的解决方案,避免内嵌不干净(译注:指玩家装了一个前置,mod内部又带一个不同版本的前置)造成的冲突,还允许fabric mod开发者成为权威的版本来源。
- 在联立的大jar中,清晰地分开子项目/子模块,使之可以分离使用。
- (直接)用在非mod的Java类库上。
- 如果类库可以安全嵌入不同的包中,那么嵌套jar就不是最好的解决方案。注意fabric的modid只能存在一个,意思是潜在的版本冲突会阻止加载一个包——内嵌的类库没有这个问题。
不兼容
- 如果你的mod使用了plugin-loader,请抛弃它然后使用新的入点(entrypoint)机制。
fabric.mod.json
必要字段
- schemaVersion:内部机制需要,(目前)必须是1.
- id:定义modid,一个字母,数字,下划线组成的字串,长度在1-63之间。
- version:定义mod版本,建议遵守 Semantic Versioning 2.0.0 规范。
可选字段
- environment:定义了mod运行在哪里,只在客户端 (客户端mod), 只在服务端 (插件), 两边都有 (一般mod).。包含了三种标识符:
- *:两处都运行,默认
- client:只运行在客户端
- server:只运行在服务端
- entrypoints: 定义了mod的主类在哪里。
- mod有三种entrypoint(入点):
- main:首先运行,针对实现了ModInitializer的类。
- client:第二个运行,只在客户端,针对实现了ClientModInitializer的类。
- server: 第二个运行,只在服务端,针对实现了DedicatedServerModInitializer的类。
- 每个入点都可以加载任意数量的类。类(或者是方法,静态字段)可以用如下两个方法指定:
- 如果用的是Java,直接填入全引用名即可,例如:
复制代码- "main": [
- "net.fabricmc.example.ExampleMod",
- "net.fabricmc.example.ExampleMod::handle"
- ]
- "main": [
- 如果用的是别的和Java兼容的语言,还有fabric适配器的话,应该使用下面的语法:
复制代码- "main": [
- {
- "adapter": "kotlin",
- "value": "package.ClassName"
- }
- ]
- "main": [
- 如果用的是Java,直接填入全引用名即可,例如:
- mod有三种entrypoint(入点):
- jars: 一批放在你的jar文件里准备加载的小jar,在使用jar字段前,请先阅读“高级:从fabric loader 0.3迁移到0.4”部分。每个条目都是一个包含有“file”键的对象,是你的mod jar到嵌套的小jar之间的通路,比如:
- "jars": [
- {
- "file": "nested/vendor/dependency.jar"
- }
- ]
- languageAdapters:使用适配器字典,将你所用的语言适配到Java,如下:
复制代码- "languageAdapters": {
- "kotlin": "net.fabricmc.language.kotlin.KotlinAdapter"
- }
- "languageAdapters": {
- config: 指向你的mod jar内部的mixin文件位置。
- environment:和上面说的environment字段作用一样,如下: 复制代码
- "mixins": [
- "modid.mixins.json",
- {
- "config": "modid.client-mixins.json",
- "environment": "client"
- }
- ]
- "mixins": [
依赖关系
- depends 代表必要前置,没有这个mod,游戏就会崩溃。
- recommends 代表不必要的依赖,没有这个mod游戏会有警告。
- suggests 代表联动,作为一个元数据而存在。
- breaks 代表不兼容,它们和你的mod共存,则会导致崩溃。
- conflicts 代表冲突,这些mod和你的mod共存会引发一些bug,这时游戏会有警告。
- "depends": {
- "fabricloader": ">=0.4.0",
- "fabric": "*"
- },
- "suggests": {
- "flamingo": "*"
- }
- }
元数据
- name: 定义了对玩家友好的名称,如果没有提供,默认会和modid相同。
- description: 定义了mod的描述,如果没提供就是空的。
- contact: 定义了开发者的联系方式,具有如下字段:
- email:开发者的邮箱,需要是个有效邮箱地址。
- irc: 开发者的IRC频道,必须要是个有效链接,比如irc://irc.esper.net:6667/charset。端口可选,若不指定默认是6667.
- homepage:开发者的主页,必须是有效的HTTP/HTTPS地址。
- issues: 反馈页面,需要是有效地址。
- sources: 源代码库位置。可以根据使用的VCS写成特殊的URL。
- mod还会提供其他的字段,比如微博,推特,QQ之类的,都要是有效URL。(QQ不一定是)
- authors 作者名单。
- contributors 贡献者名单。
- license 许可证信息。
- icon 定义了mod的图标,一个方的png图片(MC资源包用的是128×128,不过不硬性要求,可以是更高清晰度) ,有两种形式提供:
- 某个png图片的路径。
- 可以查找到图片路径的字典。
自定义字段
可以在custom里面定义自己的字段,fabric loader会忽略它们,不过如果你的字段进入了标准规范,那么高度推荐命名空间化你的字段,防止冲突。
fabric渲染API
- 增强的方块模型渲染:同一个模型下应有直射光,漫射光和封闭环境光的控制,多个混合模式(固体,镂空,透明)。
- 动态方块模型:一些或者所有的方块模型都可以基于世界状态,在区块重建的过程中生成或者调整,无论有没有方块实体。
- 增强的物品模型渲染:物品模型应和方块模型有相似的选项,输出的模型也可以根据物品状态动态变化。
mod开发者
模型加载器开发者
fabric渲染api做来就是为了做任何模型加载器的后端,但是不指定也不实现任何模型格式,制作一个模型加载器,对fabric开发是很大的贡献!如果你想制作模型加载器,应该看完一整个API和下面的部分。
模型类库开发者
渲染器开发者
- 你要做什么?你的渲染器非常快吗?有尝试节约内存吗?可以带来新特效吗?
- 你的渲染器应该支持所有fabric api所支持的特性,你明白整个api怎么用了吗?
- 你想引入或者支持什么样的api扩展?
- 你知道怎么编写和调试mixin吗?
- 你的渲染器如何调整MC的渲染流水线?需要在哪打补丁?
- 如何避免过度内存分配?
- 如何保证线程安全?
准备开始!
获取渲染器实例
- RendererAccess.INSTANCE.getRenderer()
保留对渲染器实例的引用是安全的,这个实例可以保证非空,除非你这里的fabric不带渲染器或者默认的渲染器被禁用了(这并不正常),如果这时你的mod依赖了api中的特性,那么没有渲染器,你的mod就该崩了。
材质(Material,而不是Texture)
获取材质
命名材质
材质属性
混合模式(BlendMode)
- 允许多个渲染层的效果叠加在一个方块/模型上。
- 通过MaterialFinder.blendMode()设置
- 接受BlockRenderLayer枚举值
- 材质不一定在给定的通路(pass)中渲染——只是看上去如此(一些渲染器可能会合并通路,或者做其他的优化,不要给BlockRenderLayer通路写mixin)。
- 如果模式是空(即默认),地形渲染器会使用Block.getRenderLayer()的值,其他的渲染上下文会使用BlockRenderLayer.TRANSLUCENT作为默认。
漫射光(Diffuse)开启和关闭
- 控制漫射光的颜色调整。
- 默认开启。
- 用MaterialFinder.disableDiffuse()禁用漫射光。
- 在原版MC,这会导致方块的边缘和底部变得更暗,差别可见。
- 渲染器实现可能使用不同的漫射光模型,但是仍然遵守这个设置。
密闭环境光(Ambient Occlusion Shading)开启/关闭
- 控制密闭环境光的颜色调整。
- 默认打开。
- MaterialFinder.disableAo()以禁用。
- 在原版这会导致内部的角落更暗,创造视差。
- 渲染器实现可能会换密闭环境光照模型,但仍然遵守这个设置。
直射光渲染(Emissive Rendering)开启/关闭
- 启用之后,四边形会全亮度发光。
- 默认禁用。
- 通过MaterialFinder.emissive()启用。
- 不需要提供逐顶点光照图,也不建议你去提供。
- 除非被禁用,否则漫射光和密闭环境光也会起作用。
精灵(Sprite)深度
- 默认是1,渲染api目前也只支持1.
- 大于1的值以后会有,为扩展准备。
- 扩展可以用这个在四边形上放若干个精灵,制作表面特效
颜色索引(ColorIndex)开启/关闭
- 控制方块颜色索引应用于顶点颜色。
- 如果方块的colorindex != -1则会引用默认值(MutableQuadView.colorIndex()来设置)。
- 使用MaterialFinder.disableColorIndex()阻止颜色索引的应用。
- 一般只有在支持精灵深度>1的扩展上才有用,这种情况下允许方块颜色只应用于特定精灵图层。
mod(模组)
嵌套Jar
入点(EntryPoint)
- main:首先运行,针对实现了ModInitializer的类。
- client:第二个运行,只在客户端,针对实现了ClientModInitializer的类。
- server: 第二个运行,只在服务端,针对实现了DedicatedServerModInitializer的类。
- 类: net.fabricmc.example.ExampleMod 用类名来引用那个主类,这个类必须有一个公共无参构造器,必须实现所期望的类型(默认指ModInitializer),返回值是类的实例。
- 方法: net.fabricmc.example.ExampleMod::method 用方法名来引用方法,如果方法不是静态的,那么就会构造一个类的实例,来调用这个方法,所以说方法归属的那个类还是要有无参构造器。方法(入点)只能用于接口类型,且必须和接口中的抽象方法有一样的参数和返回值,返回值是个接口的代理实现,通过代理来实现抽象方法。
- 字段: net.fabricmc.example.ExampleMod::field 用字段名去引用字段,字段必须是公开和静态的。字段类型必须是期望的类型,返回值是字段的值。
Mixin
混淆与反混淆
类加载和修改
启动器(当然,不是HMCL那种)
依赖配置项
- minecraft: 决定了在开发环境中使用的MC版本。
- mappings: 决定了开发环境中使用的反混淆表版本。
- modCompile, modImplementation, modApi and modRuntime: compile, implementation, api和runtime 这几个gradle配置项的mod版变种,会根据当前的反混淆表版本重混淆mod,嵌套的jar可以选择提取和重混淆。
- include: 声明一个在remapJar的输出中,应该以jar-in-jar形式打包的依赖,此依赖配置项不可传递。对于非mod的依赖,loom会为其生成一个带有fabric.mod.json的mod jar文件,使用其名称作为modid,版本也是相同的。
默认的gradle任务
- cleanLoomBinaries: 针对当前配置版本的MC和反混淆表,从用户缓存删除合并后的Minecraft.jar,中间名反混淆的minecraft.jar和已经反混淆的minecraft.jar。
- cleanLoomMappings: 针对当前配置版本的MC和反混淆表,从用户缓存删除反混淆表, 中间名反混淆的minecraft.jar和已经反混淆的minecraft.jar,还有项目配置缓存。
- migrateMappings: 将当前的源码迁移到指定版本的反混淆表,参考“使用反混淆表和更新yarn反混淆”一章。
- remapJar: 提供重混淆的jar文件(真正的mod),也可以放在jar-in-jar文件中嵌套。
- genSources: 两个任务的集合体。
- genSourcesDecompile 用FernFlower反编译已经反混淆的MC.jar来生成一个源码(source)jar文件,还生成一个行数表。
- genSourcesRemapLineNumbers 使用生成的行数表生成一个“行对齐反混淆”的jar文件,重新对齐了二进制文件和源码jar文件的行数,行对齐的jar文件会取代一开始的反混淆jar。
- downloadAssets: 根据当前MC版本,下载资源索引和资源文件本身,存储到用户缓存中。
- genIdeaWorkspace: 需要idea和先运行genSources指令,在当前idea项目中安装MC的运行配置项,并创建run文件夹(如果不存在的话)。
- genEclipseRuns: 需要eclipse和先运行genSources指令,安装eclipse的运行配置项,并创建run文件夹(如果不存在的话)。
- vscode: 需要先运行genSources指令,用.vscode目录下的运行配置生成/重写vscode的launch.json文件,并创建run文件夹(如果不存在的话)。
- remapSourcesJar:只有在一个AbstractArchiveTask类型的sourcesJar任务存在时才存在。重新混淆sourcesJar输出的文件。
- runClient: 一个JavaExec任务,将fabric loader作为MC客户端执行。
- runServer:一个JavaExec任务,将fabric loader作为MC专用服务端(远程)执行。
默认配置(译注:真的不是build.gradle和settings.gradle里的东西又讲了一遍?)
- 应用如下插件:java, eclipse和idea.
- 添加如下maven库: Fabric自己:https://maven.fabricmc.net/, Mojang:https://libraries.minecraft.net/, Maven中心库和JCenter(译注:后面两个也可以用腾讯/阿里/华为等的国内maven源代替)
- 配置idea插件排除掉.gradle,.build,.idea和out文件夹,下载javadocs源码,产生输出目录。
- 用genIdeaWorkspace任务,完成idea的配置。
- 用genEclipseWorkspace任务,完成eclipse的配置。
- 如果.idea文件夹已经存在于项目(root project)下,下载资源(dowanloadAssets),在.idea/runConfigurations中安装运行配置。
- 使用annotationProcessor配置项,添加net.fabricmc:fabric-mixin-compile-extensions和其依赖。
- 使用Mixin annotation processor(mixin注解处理器),配置所有的免测试JavaCompile(Java编译)任务。
- 配置remapJar任务,输出一个jar文件(与jar任务输出的jar文件同名),然后给jar任务加上一个“dev”分类标签。
- 配置remapSourcesJar任务,处理sourcesJar任务输出的jar文件,如果后者存在的话。
- 添加remapJar任务和remapSourcesJar任务作为build任务的依赖。
- 配置remapJar任务和remapSourcesJar任务,将其输出作为存档的实例。
- 对每个MavenPublication (来自maven-publish插件):
- 以mod特化版的依赖配置,手动将依赖添加到POM文件中,让依赖配置项有一个maven作用域。
- 以mod特化版的依赖配置,手动将依赖添加到POM文件中,让依赖配置项有一个maven作用域。
配置项
- runDir (String): 默认的“运行” 。由运行配置和runServer和runClient任务定义的run文件夹。
- refmapName (String):默认的 “${projectName}-refmap.json” 定义了mixin引用表。
- loaderLaunchMethod (String): 默认String参数是空的,在运行配置里面定义这个参数,指定启动fabric loader的方法。Knot使用的launch()方法是默认的,如果设置成另一个值,loom会尝试读取fabric-installer.${method}.json获取运行配置,如果什么都没找到就退回fabric-installer.json,如果设置成“launchwrapper”且找不到fabric installer的定义,那么运行配置将默认使用launchwrapper来启动fabric loader。
- remapMod (boolean): 默认为true,如果设置成false,就会禁用remapJar, remapSourcesJar和jar任务的配置。
- autoGenIDERuns (boolean): 默认为true,如果是false,就会禁用资源的自动下载和intellij运行配置的生成(如果项目中已经存在.idea文件夹的话)。
- extractJars (boolean): 默认为false,如果是true,loom会递归提取和重混淆mod依赖带来的嵌套jar。
发布
- mavenJava(MavenPublication) {
- artifact(jar.archivePath) {
- builtBy remapJar
- }
- // artifact(sourcesJar) {
- // builtBy remapSourcesJar
- // }
- ...
- }
任务类型
- net.fabricmc.loom.task.RemapJarTask: 输入一个jar,输出一个重混淆之后的jar,应该被配置成依赖于生成输入jar的任务。remapJar任务不是AbstractArchiveTask,属性如下:
- input (Object): 默认参数是空的,该参数定义了要被重混淆的jar文件,用Project.file解决。
- output (Object): 默认参数是空的,定义了要输出的文件,用Project.file解决。
- addNestedDependencies (boolean):默认为false,如果是true,loom会使用include依赖配置项指定的依赖嵌套进jar文件中的META-INF/jars,还会在fabric.mod.json文件中声明它们。
- input (Object): 默认参数是空的,该参数定义了要被重混淆的jar文件,用Project.file解决。
- net.fabricmc.loom.task.RemapSourcesJarTask: 输入一个Java源代码jar,输出一个重混淆之后的java源码jar,应该被配置成依赖于生成输入jar的任务。该任务不是AbstractArchiveTask,属性如下:
- input (Object): 默认参数是空的,该参数定义了要被重混淆的源码jar文件,用Project.file解决。
- output (Object): 默认参数是空的,定义了要输出的源码jar文件,用Project.file解决。
- targetNamespace (String): 默认是“intermediary”(中间名) ,定义了要重混淆到的命名空间,只要参数值不是“named”,一律重混淆成中间名。
安装开发环境
- 根据当前配置的MC版本,从MC官方渠道下载客户端和服务端jar。
- 把客户端和服务端合并起来,产生一个带有@Environment和@EnvironmentInterface的合并后(merged)jar。
- 下载当前配置的反混淆表。
- 将合并jar文件用中间名表反混淆一下,生成一个中间名jar文件。
- 将合并jar文件用yarn表反混淆,会生成一个“反混淆的jar文件”。
- 可选:反编译反混淆之后的jar文件,产出一个源码jar和行对齐jar,然后行对齐jar替换反混淆的jar。
- 添加MC的依赖。
- 下载MC的资源。
- 处理和容纳mod特化的依赖(可选提取和重混淆嵌套的jar)。
缓存
- ${GRADLE_HOME}/caches/fabric-loom: 用户缓存文件夹,可以被所有loom项目共享,用来缓存MC的资源,jar,合并的jar,中间名jar和反混淆的jar。(译注:可以自己新建GRADLE_HOME这个用户变量,借此修改.gradle缓存文件夹的位置)
- .gradle/loom-cache: 项目持久缓存,可以被项目本身和子项目使用,用于缓存重混淆的mod,和include生成的嵌套mod jar。
- build/loom-cache: 项目构建缓存。
- **/build/loom-cache: 子项目构建缓存。