User Tools

Site Tools


zh_cn:tutorial:trees

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
Last revisionBoth sides next revision
zh_cn:tutorial:trees [2021/07/25 10:45] – [Creating a BlockStateProvider] breakicezh_cn:tutorial:trees [2022/08/18 03:40] – [添加树木 [1.17](高级)] solidblock
Line 1: Line 1:
 +FIXME 本文有一段时间没有更新了,可能对未来版本不起作用。请参考[[tutorial:trees|英文页面]]。
 +
 +===== 添加树木 [1.17](高级) =====
 +阅读本文之前,建议先学习如何创建一个特征地形。\\
 +参见 [[zh_cn:tutorial:features]]
 +
 +树木是在你的 mod 中拓展原版世界生成的一个好方法。\\
 +注意本话题较为高级,因此开始之前,最好要有关于修改世界生成的丰富经验。
 +
 +===== 创建简单的树木 =====
 +
 +==== 结构 ====
 +原版的树的结构分为不同的种类,方便你做出复杂并且漂亮的树。\\
 +概览如下:
 +
 +  - ''TrunkPlacer'':生成树干。
 +  - ''FoliagePlacer'':生成树叶。
 +  - ''SaplingGenerator'':根据周围环境,从树苗产生树的''ConfiguredFeature''
 +  - ''TreeDecorator'':可以用这个来为树生成额外的元素,如蜂箱、藤蔓(可选)。
 +  - ''BlockStateProvider'':根据周围环境返回方块。如果需要让树的一部分是A方块,另一部分是B方块,就可以使用这个函数。
 +
 +如果想让树木长得不那么像是原版的树木,可以选择创建自定义的实现。但是实际上原版的实现通常足够模组的开发了。
 +
 +==== 创建 ConfiguredFeature ====
 +不需要创建新的 ''Feature'' ,因为原版的 ''TreeFeature'' 是可以配置的。
 +
 +把这个添加到你的 ''ModInitializer'' 主体:
 +
 +<code java>
 +public static final ConfiguredFeature<?, ?> TREE_RICH = Feature.TREE
 +  // 使用builder配置特征地形
 +  .configure(new TreeFeatureConfig.Builder(
 +    new SimpleBlockStateProvider(Blocks.NETHERITE_BLOCK.getDefaultState()), // 树干方块提供器
 +    new StraightTrunkPlacer(8, 3, 0), // 放置竖直树干
 +    new SimpleBlockStateProvider(Blocks.DIAMOND_BLOCK.getDefaultState()), // 树叶方块提供器
 +    new SimpleBlockStateProvider(RICH_SAPLING.getDefaultState()), // 树苗提供器,用来决定树木可以生长在什么方块上
 +    new BlobFoliagePlacer(ConstantIntProvider.create(5), ConstantIntProvider.create(0), 3), // 生成水滴状的树叶(半径、相对于树干的偏移、高度)
 +    new TwoLayersFeatureSize(1, 0, 1) // 不同层的树木的宽度,用于查看树木在不卡到方块中可以有多高
 +  ).build())
 +  .spreadHorizontally()
 +  .applyChance(3); // 每个区块大约33%的概率生成(1/x)
 +</code>
 +
 +现在只需要像往常那样向游戏注册 ''ConfiguredFeature'' 然后用 FabricAPI 修改生物群系:
 +
 +<code java>
 +@Override
 +public void onInitialize() {
 +  RegistryKey<ConfiguredFeature<?, ?>> treeRich = RegistryKey.of(Registry.CONFIGURED_FEATURE_KEY, new Identifier("tutorial", "tree_rich"));
 +
 +  Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, treeRich.getValue(), TREE_RICH);
 +
 +  // 应该为树木使用 VEGETAL_DECORATION 生成步骤
 +  BiomeModifications.addFeature(BiomeSelectors.foundInOverworld(), GenerationStep.Feature.VEGETAL_DECORATION, treeRich);
 +}
 +</code>
 +
 +==== 创建树苗 ====
 +树苗是生长树木的一类特殊方块,需要 ''SaplingGenerator''
 +
 +=== 创建 SaplingGenerator ===
 +简单的生成器接收树木的 ''ConfiguredFeature'' 并将其返回,如下所示:
 +
 +<code java>
 +public class RichSaplingGenerator extends SaplingGenerator {
 +  private final ConfiguredFeature<TreeFeatureConfig, ?> feature;
 +
 +  public RichSaplingGenerator(ConfiguredFeature<?, ?> feature) {
 +    this.feature = (ConfiguredFeature<TreeFeatureConfig, ?>) feature;
 +  }
 +
 +  @Nullable
 +  @Override
 +  protected ConfiguredFeature<TreeFeatureConfig, ?> getTreeFeature(Random random, boolean bees) {
 +    return feature;
 +  }
 +}
 +</code>
 +
 +后面会展示高级的''SaplingGenerator''的例子。
 +
 +=== 创建 SaplingBlock ===
 +创建方块本身需要继承''SaplingBlock''类,而不是直接将其实例化,因为其构造器的访问权限是 protected 的。
 +
 +<code java>
 +public class RichSaplingBlock extends SaplingBlock {
 +  public RichSaplingBlock(SaplingGenerator generator, Settings settings) {
 +    super(generator, settings);
 +  }
 +}
 +</code>
 +
 +=== 注册SaplingBlock ===
 +要注册树苗,按照注册方块的以下步骤(参见[[zh_cn:tutorial:blocks]]),但传入带有 ''ConfiguredFeature'' 的生成器的实例。
 +
 +把这个放在用于你的树苗方块的类中:
 +
 +<code java>
 +public static final RICH_SAPLING = new RichSaplingBlock(new RichSaplingGenerator(TREE_RICH), FabricBlockSettings.copyOf(Blocks.OAK_SAPLING.getDefaultState()));
 +
 +public static void register() {
 +  Registry.register(Registry.BLOCK, new Identifier("tutorial", "rich_sapling"), RICH_SAPLING);
 +  Registry.register(Registry.ITEM, new Identifier("tutorial", "rich_sapling"), new BlockItem(RICH_SAPLING, ItemGroup.MISC));
 +}
 +
 +</code>
 +
 +===== 创建 TrunkPlacer =====
 +''TrunkPlacer'' 创建由 ''BlockStateProvider'' 提供的树干方块。
 +
 +==== 原版 TrunkPlacers ====
 +在创建 ''TrunkPlacer'' 之前,先看看可以从原版复用的 ''TrunkPlacer'' 避免做重复的工作:
 +
 +  * ''StraightTrunkPlacer''
 +  * ''ForkingTrunkPlacer''
 +  * ''GiantTrunkPlacer''
 +  * ''BendingTrunkPlacer''
 +
 +==== 创建TrunkPlacerType ====
 +往游戏注册 ''TrunkPlacer'' 需要 ''TrunkPlacerType''
 +
 +可惜 FabricAPI 目前没有用于创建和注册''TrunkPlacer''的API,所以我们需要使用mixins。
 +
 +我们准备创建一个调用器(invoker)(见[[https://github.com/2xsaiko/mixin-cheatsheet/blob/master/invoker.md]])来调用私有静态的 ''TrunkPlacerType.register'' 方法。
 +
 +以下是我们的 mixin ,不要忘记加到 mixin 配置中:
 +
 +<code java>
 +
 +@Mixin(TrunkPlacerType.class)
 +public interface TrunkPlacerTypeInvoker {
 +    @Invoker
 +    static <P extends TrunkPlacer> TrunkPlacerType<P> callRegister(String id, Codec<P> codec) {
 +        throw new IllegalStateException();
 +    }
 +}
 +</code>
 +
 +==== 创建 TrunkPlacer ====
 +''TrunkPlacer'' 包含:
 +
 +  * 用于序列化的编码解码器。编码解码器(codec)是其自己的话题(topic),这里我们只需要使用 ''fillTrunkPlacerFields'' 方法来生成。
 +  * 获取器(getter),返回 ''TrunkPlacerType''
 +  * ''generate'' 方法,该方法中放置树干并返回 ''TreeNode'' 列表,用于树叶放置器放置树木。
 +
 +''TrunkPlacer'' 将在世界中创建两个对角线形的树干:
 +
 +<code java>
 +public class RichTrunkPlacer extends TrunkPlacer {
 +    // 使用fillTrunkPlacerFields来创建编码解码器
 +    public static final Codec<RichTrunkPlacer> CODEC = RecordCodecBuilder.create(instance -> 
 +        fillTrunkPlacerFields(instance).apply(instance, RichTrunkPlacer::new));
 +
 +    public RichTrunkPlacer(int baseHeight, int firstRandomHeight, int secondRandomHeight) {
 +        super(baseHeight, firstRandomHeight, secondRandomHeight);
 +    }
 +
 +    @Override
 +    protected TrunkPlacerType<?> getType() {
 +        return Tutorial.RICH_TRUNK_PLACER;
 +    }
 +
 +    @Override
 +    public List<FoliagePlacer.TreeNode> generate(TestableWorld world, BiConsumer<BlockPos, BlockState> replacer, Random random, int height, BlockPos startPos, TreeFeatureConfig config) {
 +        // 将树干下的方块设为泥土
 +        this.setToDirt(world, replacer, random, startPos.down(), config);
 +        
 +        // 迭代到树干高度限制,并使用 TrunkPlacer 中的 getAndSetState 方法放置两个方块
 +        for (int i = 0; i < height; i++) {
 +            this.getAndSetState(world, replacer, random, startPos.up(i), config);
 +            this.getAndSetState(world, replacer, random, startPos.up(i).east().north(), config);
 +        }
 +
 +        // 创建两个树木节点——一个用于第一个树干,另一个用于第二个
 +        // 将树干中最高的方块设为中心坐标给 FoliagePlacer 使用
 +        return ImmutableList.of(new FoliagePlacer.TreeNode(startPos.up(height), 0, false),
 +                                new FoliagePlacer.TreeNode(startPos.east().north().up(height), 0, false));
 +    }
 +}
 +</code>
 +
 +==== 注册并使用TrunkPlacer ====
 +使用你的调用器,为你的 ''TrunkPlacer'' 创建并注册 ''TrunkPlacerType'' 的实例。把这个放到你的 ''ModInitializer'' 主体中:
 +
 +<code java>
 +public static final TrunkPlacerType<RichTrunkPlacer> RICH_TRUNK_PLACER = TrunkPlacerTypeInvoker.callRegister("rich_trunk_placer", RichTrunkPlacer.CODEC);
 +</code>
 +
 +现在将你的 ''StraightTrunkPlacer'' 替换为你刚创建的 ''RichTrunkPlacer'' 就好了:
 +<code java>
 +[...]
 +new RichTrunkPlacer(8, 3, 0),
 +[...]
 +</code>
 +
 +===== 创建FoliagePlacer =====
 +''FoliagePlacer'' 会从由 ''BlockStateProvider'' 提供的方块创建树叶,如果你提供钻石矿方块的话或许会变成钻石矿树叶。竟然会如此的闪耀!
 +
 +==== 原版FoliagePlacer ====
 +创建 ''FoliagePlacer'' 之前,看看可直接使用的原版 ''FoliagePlacer'' 以避免重复造轮子:
 +
 +  * ''BlobFoliagePlacer''
 +  * ''BushFoliagePlacer''
 +  * ''RandomSpreadFoliagePlacer''
 +
 +==== 创建 FoliagePlacerType ====
 +往游戏中注册 ''FoliagePlacer'' 需要 ''FoliagePlacerType''
 +
 +和 ''TrunkPlacerType'' 类似,FabricAPI 不提供创建 ''FoliagePlacerType'' 的实用功能。我们的 mixin 看上去几乎相同,同时不要忘记修改你的 mixin 配置!
 +
 +<code java>
 +@Mixin(FoliagePlacerType.class)
 +public interface FoliagePlacerTypeInvoker {
 +    @Invoker
 +    static <P extends FoliagePlacer> FoliagePlacerType<P> callRegister(String id, Codec<P> codec) {
 +        throw new IllegalStateException();
 +    }
 +}
 +</code>
 +
 +==== 创建 FoliagePlacer ====
 +''FoliagePlacer'' 比 ''TrunkPlacer'' 更加复杂一些,包括:
 +
 +  * 用于序列化的编码解码器。在此例中我们展示了如何往编码解码器中添加一个额外的 IntProvider。
 +  * 用于获取 ''FoliagePlacerType'' 的获取器(getter)。
 +  * ''generate'' 方法,该方法创建树叶。
 +  * ''getRandomHeight'' 方法。不管名字是什么,你通常应该返回你的树叶的最大高度。
 +  * ''isInvalidForLeaves'' 方法,可以为放置树叶的地方设置限制。
 +
 +我们的 ''FoliagePlacer'' 会往各个方向(东南西北)创建4行的树叶方块:
 +
 +<code java>
 +
 +public class RichFoliagePlacer extends FoliagePlacer {
 +    // 对于foliageHeight我们使用由 IntProvider.createValidatingCodec 生成的编码解码器
 +    // 方法参数,我们传入 IntProvider 的最小值和最大值
 +    // 为了向你的 TrunkPlacer/FoliagePlacer/TreeDecorator 等添加多个域(fields),可调用多次.and。
 +    //
 +    // 如果想创建属于我们自己的编码解码器类型,可以参考 IntProvider.createValidatingCodec 方法的源代码。
 +    public static final Codec<RichFoliagePlacer> CODEC = RecordCodecBuilder.create(instance ->
 +        fillFoliagePlacerFields(instance)
 +        .and(IntProvider.createValidatingCodec(1, 512).fieldOf("foliage_height").forGetter(RichFoliagePlacer::getFoliageHeight)
 +        .apply(instance, RichFoliagePlacer::new));
 +
 +    private final IntProvider foliageHeight;
 +
 +    public RichFoliagePlacer(IntProvider radius, IntProvider offset, IntProvider foliageHeight) {
 +        super(radius, offset);
 +
 +        this.foliageHeight = foliageHeight;
 +    }
 +
 +    public IntProvider getFoliageHeight() {
 +        return this.foliageHeight;
 +    }
 +
 +    @Override
 +    protected FoliagePlacerType<?> getType() {
 +        return Tutorial.RICH_FOLIAGE_PLACER;
 +    }
 +
 +    @Override
 +    protected void generate(TestableWorld world, BiConsumer<BlockPos, BlockState> replacer, Random random, TreeFeatureConfig config, int trunkHeight, TreeNode treeNode, int foliageHeight, int radius, int offset) {
 +        BlockPos.Mutable center = treeNode.getCenter().mutableCopy();
 +
 +        for (
 +            // 从 X 开始:中心 - 半径
 +            Vec3i vec = center.subtract(new Vec3i(radius, 0, 0));
 +            // 在 X 结束:中心 + 半径
 +            vec.compareTo(center.add(new Vec3i(radius, 0, 0))) == 0;
 +            // 每次移动1
 +            vec.add(1, 0, 0)) {
 +            this.placeFoliageBlock(world, replacer, random, config, new BlockPos(vec));
 +        }
 +
 +        for (Vec3i vec = center.subtract(new Vec3i(0, radius, 0)); vec.compareTo(center.add(new Vec3i(0, radius, 0))) == 0; vec.add(0, 1, 0)) {
 +            this.placeFoliageBlock(world, replacer, random, config, new BlockPos(vec));
 +        }
 +    }
 +
 +    @Override
 +    public int getRandomHeight(Random random, int trunkHeight, TreeFeatureConfig config) {
 +        // 使用 IntProvider 挑选随机高度
 +        return foliageHeight.get(random);
 +    }
 +
 +    @Override
 +    protected boolean isInvalidForLeaves(Random random, int dx, int y, int dz, int radius, boolean giantTrunk) {
 +        // 我们的 FoliagePlacer 不为树叶设置限制
 +        return false;
 +    }
 +}
 +
 +</code>
 +==== 注册并使用你的 FoliagePlacer ====
 +该过程几乎相同,只需要使用你的调用器(invoker)创建并注册 ''FoliagePlacerType''
 +
 +<code java>
 +public static final FoliagePlacerType<RichFoliagePlacer> RICH_FOLIAGE_PLACER = FoliagePlacerTypeInvoker.callRegister("rich_foliage_placer", RichFoliagePlacer.CODEC);
 +</code>
 +
 +并将旧的 ''FoliagePlacer'' 替换成你的新的:
 +
 +<code java>
 +[...]
 +new RichFoliagePlacer(ConstantIntProvider.create(5), ConstantIntProvider.create(0), ConstantIntProvider.create(3)),
 +[...]
 +</code>
 +
 +===== 创建一个 TreeDecorator =====
 +''TreeDecorator'' 允许你添加额外的元素到你的树之中在执行你的 ''TrunkPlacer'' 和 ''FoliagePlacer'' (比如苹果,蜂巢等) //**之后**//。如果你有游戏后台开发经验的话,''TreeDecorator'' 本质上是用于树木的一个后处理器(post-processer),用于修饰树木的额外信息。
 +
 +==== 原版的 TreeDecorators ====
 +原版的 ''TreeDecorator'' 几乎是没办法复用的,除了 ''LeavesVineTreeDecorator''
 +和 ''TrunkVineTreeDecorator''
 +
 +虽然这是一件非常繁琐的事情,但是你还是需要创建你自己的 ''TreeDecorator''
 +
 +==== 创建一个 TreeDecoratorType ====
 +一个 ''TreeDecoratorType'' 是需要注册到你的 ''TreeDecorator'' 之中的。
 +
 +FabricAPI 没有提供任何工具用于创建 ''TreeDecoratorType'', 所以我们需要再次使用 mixin 了。
 +
 +我们的 mixin 大概会看起来非常像是以下内容,同时不要忘记把他们添加到你自己的 mixin 配置文件当中:
 +
 +<code java>
 +@Mixin(TreeDecoratorType.class)
 +public interface TreeDecoratorTypeInvoker {
 +    @Invoker
 +    static <P extends TreeDecorator> TreeDecoratorType<P> callRegister(String id, Codec<P> codec) {
 +        throw new IllegalStateException();
 +    }
 +}
 +</code>
 +
 +==== 创建 TreeDecorator ====
 +''TreeDecorator'' 有一个特别简单的结构:
 +
 +  * 一个可用于序列化的编码解码器。但默认情况下为空,因为构造函数是没有参数的。如果需要,你可以随时扩展(expand)它。
 +  * 你的 ''TreeDecoratorType'' 的获取器(getter)。
 +  * 为修饰树而存在的 ''generate'' 方法。
 +
 +我们的 ''TreeDecorator'' 将在树干周围以 25% 的几率在树干的一侧产生金块(简直不要太爽太炫酷对不对):
 +
 +<code java>
 +public class RichTreeDecorator extends TreeDecorator {
 +    public static final RichTreeDecorator INSTANCE = new RichTreeDecorator();
 +    // 我们的构造函数没有任何参数,所以我们创建一个单元编解码器,让他返回一个单例对象。
 +    public static final Codec<RichTreeDecorator> CODEC = Codec.unit(() -> INSTANCE);
 +
 +    @Override
 +    protected TreeDecoratorType<?> getType() {
 +        return Tutorial.RICH_TREE_DECORATOR;
 +    }
 +
 +    @Override
 +    public void generate(TestableWorld world, BiConsumer<BlockPos, BlockState> replacer, Random random, List<BlockPos> logPositions, List<BlockPos> leavesPositions) {
 +        // 遍历方块位置
 +        for (BlockPos logPosition : logPositions) {
 +            // 选择一个从 0(含)到 4(不含)的值,如果是 0,则继续
 +            // 这是一个让树生成金块从而让我们走向富裕的机会,太爽了。
 +            if (random.nextInt(4) == 0) {
 +                // 选择一个从 0 到 4 的随机值,并使用它确定将放置金块到树的一侧
 +                int sideRaw = random.nextInt(4);
 +                Direction side = switch (sideRaw) {
 +                    case 0 -> Direction.NORTH;
 +                    case 1 -> Direction.SOUTH;
 +                    case 2 -> Direction.EAST;
 +                    case 3 -> Direction.WEST;
 +                    default -> throw new ArithmeticException("The picked side value doesn't fit in the 0 to 4 bounds");
 +                };
 +
 +                // 通过结果边偏移树木位置
 +                BlockPos targetPosition = logPosition.offset(side, 1);
 +
 +                // 使用 BiConsumer replacer 放置金块!
 +                // 这是在 TrunkPlacers、FoliagePlacers 和 TreeDecorators 中放置方块的标准方法。
 +                replacer.accept(targetPosition, Blocks.GOLD_BLOCK.getDefaultState());
 +            }
 +        }
 +    }
 +}
 +</code>
 +
 +==== 注册和使用你的 TreeDecorator ====
 +首先,使用调用器(invoker)创建你的 ''TreeDecoratorType''
 +
 +<code java>
 +public static final TreeDecoratorType<RichTreeDecorator> RICH_TREE_DECORATOR = TreeDecoratorTypeInvoker.callRegister("rich_tree_decorator", RichTreeDecorator.CODEC);
 +</code>
 +
 +然后,在创建你的 ''TreeFeatureConfig.Builder'' 和 ''build'' 方法之间调用这个。
 +
 +<code java>
 +[...]
 +.decorators(Collections.singletonList(RichTreeDecorator.INSTANCE))
 +[...]
 +</code>
 +
 +===== 创建一个高级的 SaplingGenerator =====
 +所以,还记得我告诉过你 ''SaplingGenerator'' 实际上可以包含更复杂的逻辑吗?
 +这是一个例子 - 我们这次来创建几棵原版的树木而不是实际的树:
 +
 +<code java>
 +public class RichSaplingGenerator extends SaplingGenerator {
 +    private final ConfiguredFeature<TreeFeatureConfig, ?> feature;
 +
 +    public RichSaplingGenerator(ConfiguredFeature<?, ?> feature) {
 +        this.feature = (ConfiguredFeature<TreeFeatureConfig, ?>) feature;
 +    }
 +
 +    @Nullable
 +    @Override
 +    protected ConfiguredFeature<TreeFeatureConfig, ?> getTreeFeature(Random random, boolean bees) {
 +        int chance = random.nextInt(100);
 +        
 +        // 每棵树都有 10% 的几率
 +        if (chance < 10) {
 +            return ConfiguredFeatures.OAK;
 +        } else if (chance < 20) {
 +            return ConfiguredFeatures.BIRCH;
 +        } else if (chance < 60) {
 +            return ConfiguredFeatures.SPRUCE;
 +        } else if (chance < 40) {
 +            return ConfiguredFeatures.MEGA_SPRUCE;
 +        } else if (chance < 50) {
 +            return ConfiguredFeatures.PINE;
 +        } else if (chance < 60) {
 +            return ConfiguredFeatures.MEGA_PINE;
 +        } else if (chance < 70) {
 +            return ConfiguredFeatures.MEGA_JUNGLE_TREE;
 +        }
 +        
 +        // 如果这些都没有发生(随机值在 70 到 100 之间),则创建实际的树
 +        return feature;
 +    }
 +}
 +</code>
 +
 +其实这没啥练手的,但是他给你展示了 ''SaplingGenerator'' 可以有更复杂的逻辑。
 +
 +===== 给你的树整点额外逻辑! =====
 +使用额外的 ''TreeFeatureConfig.Builder'' 方法,你可以给你的树添加更多的设定:
 +
 +==== dirtProvider ====
 +还记得巨型云杉木下面的灰化土吗,它就是用这个做到的。
 +
 +设置一下 ''BlockStateProvider'' ,让你的树在周围生成铁块!简直就是五金树不是吗?
 +
 +例子:
 +<code java>
 +[...]
 +.dirtProvider(new SimpleBlockStateProvider(Blocks.IRON_BLOCK.getDefaultState()))
 +[...]
 +</code>
 +
 +==== decorators ====
 +用来在你的树上添加 ''TreeDecorator''
 +本教程的 ''TreeDecorator'' 部分简要的展示了这一点。
 +如果你想,你可以使用像 ''Arrays.asList'' 这样的方法方便的在同一棵树上添加多个 ''TreeDecorator''
 +
 +例子
 +
 +<code java>
 +[...]
 +.decorators(Arrays.asList(
 +    FirstTreeDecorator.INSTANCE,
 +    SecondTreeDecorator.INSTANCE,
 +    ThirdTreeDecorator.INSTANCE
 +))
 +[...]
 +</code>
 +
 +==== ignoreVines ====
 +使树生长时无视藤蔓。
 +
 +例子
 +
 +<code java>
 +[...]
 +.ignoreVines()
 +[...]
 +</code>
 +
 +==== forceDirt ====
 +强制 ''TreeFeature'' 在树下生成泥土。
 +
 +例子:
 +
 +<code java>
 +[...]
 +.forceDirt()
 +[...]
 +</code>
 +
 ===== 创建一个 BlockStateProvider ===== ===== 创建一个 BlockStateProvider =====
 敬请期待! 敬请期待!
  
zh_cn/tutorial/trees.txt · Last modified: 2022/08/18 03:40 by 127.0.0.1