跳转至内容
  • 版块
  • 最新
  • 热门
  • 标签
  • 群组
皮肤
  • Light
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • 默认(Zephyr)
  • 不使用皮肤
折叠
品牌标识

VariedMC 魔改论坛

登录以发布

  • 全部板块
  • 忆然忆
    [原创][KubeJS]kubejs获取玩家所处坐标的结构
    忆然忆 忆然

    本文使用:CC-BY-NC-SA 4.0协议
    直接上代码
    在此推荐下来自ZZZank的ProbeJS Legacy
    (已经支持1.20.1了(喜))

    /**
     * 获取实体坐标的所有结构
     * @param {Internal.Entity} entity
     * returns {$StructureStart_[]}
     */
    function getAllStructuresAtentity {
      let structureList = []
      /**@type {$ServerLevel_} */
      let serverLevel = entity.level
      let entityPos = entity.block.pos
      /** @type {Internal.Structure[]} */
      let structureArray = serverLevel.structureManager().getAllStructuresAt(entityPos).keySet().toArray()
      for (let structure of structureArray) {
        let structureStart =
          serverLevel.structureManager().getStructureAt(entityPos, structure)
        if (structureStart.isValid()) {
          structureList.push(structureStart)
        }
      }
      return structureList
    }
    

    0 0 0 回复
  • LirxOwOL
    [原创][KubeJS][Mod]如何在1.21.1NeoForge编写一个KubeJS的附属Mod
    LirxOwOL LirxOwO

    KubeJS 模组附属开发指南 (NeoForge 1.21.1)

    目录

    • 简介
    • 项目结构
    • 核心组件
      • 主插件类
      • 事件系统
      • 实用工具类
    • 配置文件

    简介

    KubeJS是一个允许玩家使用JavaScript修改Minecraft游戏的模组,为其开发附属模组可以进一步扩展其功能,为JavaScript脚本提供更多的API,让开发者拥有更多的操作可能性,例如偷走你的钱包。

    项目结构

    一个标准的KubeJS附属模组项目结构如下(1.21.1 NeoForge),导入模组不用多说了吧。。。
    你都写KubeJS附属了,还不会搭环境?
    依赖的模组为KubeJS和Rhino蠢牛(1.21.1):

    src/main/
    ├── java/
    │   └── com/example/kubejsaddon/
    │       ├── YourAddonPlugin.java (主插件类)
    │       ├── YourAddonEvents.java (自定义事件)
    │       └── utils/
    │           ├── SomeUtils.java (实用工具类)
    │           └── ...其他Event类
    ├── resources/
    │   ├── kubejs.plugins.txt (KubeJS插件声明)---最重要!!
    │   ├── youraddon.mixins.json (可选的Mixin配置)
    │   └── META-INF/
    │       └── neoforge.mods.toml (模组信息)
    └── templates/
        └── META-INF/
            └── neoforge.mods.toml (模板文件)
    

    核心组件

    主插件类

    主插件类是KubeJS附属模组的入口点,需要implements KubeJSPlugin接口,相较于1.20.1为 extend kubeJSPlugin:

    package com.example.kubejsaddon;
    
    import dev.latvian.mods.kubejs.plugin.KubeJSPlugin;
    import dev.latvian.mods.kubejs.script.BindingRegistry;
    import dev.latvian.mods.kubejs.event.EventGroupRegistry;
    
    public class YourAddonPlugin implements KubeJSPlugin {
        
        @Override
        public void init() {
            // 初始化代码,会在KubeJS加载时执行
            System.out.println("YourAddon Plugin initialized!");
        }
        
        Override
        public void registerEventsEventGroupRegistry registry {
            // 注册自定义事件组
            registry.register(YourAddonEvents.GROUP);
        }
        
        Override
        public void registerBindingsBindingRegistry bindings {
            // 绑定Java类和对象到JS环境
            bindings.add("YourUtils", YourUtils.class);
            
            // 绑定Minecraft原生类
            bindings.add("SomeMinecraftClass", net.minecraft.SomeClass.class);
        }
    }
    

    事件系统

    KubeJS使用事件系统来允许JavaScript脚本响应游戏中的各种事件。要创建自定义事件,需要定义事件组和事件处理器:

    package com.example.kubejsaddon;
    
    import dev.latvian.mods.kubejs.event.EventGroup;
    import dev.latvian.mods.kubejs.event.EventHandler;
    import dev.latvian.mods.kubejs.event.KubeEvent;
    
    public interface YourAddonEvents {
        
        // 创建事件组
        EventGroup GROUP = EventGroup.of("YourAddonEvents");
        
        // 定义启动时事件
        EventHandler STARTUP_EVENT = GROUP.startup("customStartup", () -> CustomStartupEvent.class);
        
        // 定义服务器事件
        EventHandler SERVER_EVENT = GROUP.server("customServer", () -> CustomServerEvent.class);
        
        // 定义客户端事件
        EventHandler CLIENT_EVENT = GROUP.client("customClient", () -> CustomClientEvent.class);
    }
    
    // 自定义事件类
    class CustomStartupEvent implements KubeEvent {
        private final String message;
        
        public CustomStartupEvent(String message) {
            this.message = message;
        }
        
        public String getMessage() {
            return message;
        }
    }
    

    在JavaScript中使用自定义事件:

    // 在startup_scripts目录下的JS文件中
    YourAddonEvents.customStartup(event => {
        console.log("Custom startup event received: " + event.message)
    })
    
    // 在server_scripts目录下的JS文件中
    YourAddonEvents.customServer(event => {
        console.log("Custom server event with data: " + event.data)
    })
    

    触发自定义事件的Java代码:

    // 在适当的时机触发事件
    YourAddonEvents.STARTUP_EVENT.post(new CustomStartupEvent("Hello from Java!"));
    

    工具类编写

    为了向JavaScript环境提供有用的功能,通常需要创建各种工具类,丰富自己在开发时有更多的工具选择,而不局限于KubeJS提供的Utils:

    package com.example.kubejsaddon.utils;
    
    import dev.latvian.mods.kubejs.typings.Info;
    
    public class YourUtils {
        
        // 使用@Info注解提供JavaScript文档
        //使用ProbeJS补全时会显示该方法调用时是用于什么
        Info"一个简单的方法描述"
        public static String doSomething(String input) {
            return "Processed: " + input;
        }
        
        Info"另一个带有参数说明的方法"
        public static int calculate(int a, int b) {
            return a + b;
        }
        
        // 静态内部类也可以被暴露给JavaScript
        public static class InnerHelper {
            Info"内部类的方法"
            public static boolean check(String value) {
                return value != null && !value.isEmpty();
            }
        }
    }
    

    配置文件

    kubejs.plugins.txt

    在src/main/resources/kubejs.plugins.txt文件中声明你的KubeJS插件,这个txt非常重要,这是你KubeJS Plugins能被运行的重要文件之一!如果缺失,KubeJS将无法检测到你的附属Mod:

    # KubeJS插件声明
    # 格式:完整类名 [依赖模组ID] or [client]
    
    # 主插件类
    com.example.kubejsaddon.YourAddonPlugin
    
    # 如果有客户端专用插件
    # com.example.kubejsaddon.YourAddonClientPlugin client
    
    # 如果依赖其他模组
    # com.example.kubejsaddon.IntegrationPlugin some_mod_id
    

    编写完后就可以build为jar,放到生产环境中测试,建议搭配 ProbeJS 食用你自己编写的第一个KubeJS Plugins,在游戏中使用/probejs dump来检测ProbeJS是否能将你的方法导出为ts文件,并在vscode中补全,编写一些方法来测试你的附属是否有效。如果上述都成功实现了,那么恭喜你成功开发了一个KubeJS附属!


    7 0 0 回复
  • QiHuang02Q
    [原创][1.21.X][Neoforge][modding]CodecCodec你的!
    QiHuang02Q QiHuang02

    总览
    • 1 数据驱动的基石
      • 1.1 Codec 的核心概念
      • 1.2 工作流程
      • 1.3 为什么我们要使用 Codec?
    • 2 Codec 工具包:关键类与接口
      • 2.1 Codec<A>:双向转换器
      • 2.2 DynamicOps<T>:适配数据格式 (JsonOps, NbtOps)
      • 2.3 DataResult<A>:处理结果与错误
    • 3 如何构建 Codec
      • 3.1 基本类型的 Codec (String, Int, Bool 等)
      • 3.2 使用 RecordCodecBuilder 定义复杂数据
        • 3.2.1 构建自定义对象和记录的结构
        • 3.2.2 定义字段:fieldOf() 与 forGetter()
        • 3.2.3 处理可选性:optionalFieldOf() 与默认值
      • 3.3 转换与组合 Codec
        • 3.3.1 类型间映射:xmap() 及其变体 (flatXMap, comapFlatMap)
        • 3.3.2 实现多态分发:Codec.dispatch()
        • 3.3.3 组合 Codec:Codec.pair() 与 Codec.either()
    • 4 Codec 在 Minecraft 生态中的应用

    本文使用:CC-BY-NC-SA 4.0协议
    原文链接

    1 数据驱动的基石

    在目前的Minecraft(1.13+)模组开发中,数据驱动已成为主流。无论是自定义生物群系、新的配方类型还是某些复杂的方块行为,我们都倾向于将配置信息存储在 JSON 文件中,而不是硬编码在Java代码里。这极大地提高了模组的灵活性和可配置性。

    然而,如何安全、高效地在 JSON 数据和游戏内的 Java 对象之间进行转换呢?这正是 Codec 发挥作用的地方。本文将简单地探讨 Codec 的概念、用途和优势,并结合我的 PortalTransform 模组中的代码进行实例分析。所以有一些不完全正确的地方请指出。

    1.1 Codec 的核心概念

    Codec(编解码器)这个词在百度百科中的解释是一个能够对一个信号或者一个数据流进行变换的设备或者程序。而我的世界中的 Codec 指的是一个由Mojang开发并深度集成到Minecraft中的双向数据转换框架。

    你可以将其简单的理解为一个高度智能化的“翻译官”,它的职责是在两种完全不同的“语言”之间进行精确翻译:

    • 语言A: 游戏内存中的 Java 对象 (Object)。这是程序逻辑可以直接理解和操作的结构。
    • 语言B: 用于存储和传输的数据格式 (Data Format),通常是 JSON 或 NBT。

    Codec 的本质是定义一套完整的、双向的映射规则,用来规定一个 Java 对象是如何被序列化(保存)为 Json,以及一个 Json 文件是如何被反序列化(加载)成一个 Java 对象。

    1.2 工作流程

    Codec 的的工作包含两个方向:

    1、编码 / 序列化:

    • 方向:Java 对象 -> 某种 数据格式 (JSON/NBT)
    • 用途:当你需要将游戏中的一个对象保存到玩家的硬盘(如世界存档)或通过网络发送给客户端时。

    2、解码 / 反序列化:

    • 方向:某种 数据格式 (JSON/NBT) -> Java 对象
    • 用途:当游戏启动或加载资源时,从数据包的 JSON 文件或世界存档的 NBT 中读取数据,并将其转换为游戏逻辑可以使用的对象。

    1.3 为什么我们要使用 Codec?

    在处理数据序列化,特别是 JSON 或 NBT 这类格式时,部分开发者可能会考虑直接使用如 GSON、Jackson 这样的库,或者手动遍历数据结构进行解析和构建。然而,Minecraft 的 Codec 系统提供了超越这些基本功能的诸多优势,使其成为在 Minecraft 模组开发中更推荐的选择:

    • 类型安全: Codec 强制在编译时就定义数据结构和 Java 类型之间的映射。这大大减少了因类型不匹配或数据格式错误导致的运行时错误。手动解析 JSON 时,很容易因忘记检查字段是否存在、类型是否正确而引入 bug。
    • 强大的错误处理: Codec 的核心是 DataResult,它提供了一种优雅的方式来处理解析过程中可能出现的成功、失败以及部分成功的情况。相比手动解析中繁琐的 try-catch 和空值检查,DataResult 使得错误处理逻辑更清晰、更集中,并且能提供更具上下文的错误信息。
    • 双向转换: 一个 Codec 定义了从 Java 对象到序列化形式(编码)以及从序列化形式到 Java 对象(解码)的双向逻辑。这意味着你只需要定义一次数据结构,就可以同时用于读取和写入,减少了代码冗余和潜在的不一致性。手动处理时,编码和解码逻辑往往是分开实现的。
    • 与游戏数据格式的无缝集成: Codec 通过 DynamicOps (如 JsonOps, NbtOps) 的抽象,使得同一套 Codec 定义可以轻松应用于 JSON、NBT 等多种 Minecraft 常用的数据格式,而无需为每种格式重写解析逻辑。
    • 可组合性与可重用性: Codec 可以像积木一样组合起来。基础类型的 Codec 可以用来构建复杂对象的 Codec (通过 RecordCodecBuilder),列表的 Codec,映射的 Codec 等。这种声明式的方式使得复杂数据结构的定义更为简洁和模块化。
    • 数据演进与版本控制: 作为 DataFixerUpper (DFU) 库的一部分,Codec 的设计初衷就包含了对数据版本迁移的考量。虽然模组开发者可能不直接使用完整的 DFU 框架,但 Codec 本身的设计理念鼓励思考数据的长期演进,并提供了一些机制(如 optionalFieldOf、xmap 的变体)来处理数据结构的变化。
    • 与 Minecraft 核心系统的兼容性: Minecraft 的许多核心系统,如数据包加载、配方、世界生成、数据组件等,都深度依赖 Codec。使用 Codec 可以确保你的自定义数据与这些系统更好地集成,并能利用游戏提供的验证和加载机制。
      虽然对于非常简单、一次性的 JSON 解析任务,手动处理或使用通用库可能看起来更直接,但一旦涉及到需要持久化、网络同步、与游戏系统交互或未来可能需要演进的复杂数据结构,Codec 所提供的结构化、类型安全和错误处理能力将带来巨大的开发效率和代码质量提升。

    2 Codec 工具包:关键类与接口

    Codec 系统由一系列协同工作的类和接口组成,它们共同构成了 Minecraft 数据序列化的基石。

    2.1 Codec<A>:双向转换器

    Codec<A> 接口定义了一个针对特定类型 A 的对象的双向转换逻辑,该逻辑能够普适于任何层级化的序列化格式。它实质上结合了一个 Encoder<A>(编码器)和一个 Decoder<A>(解码器)的功能。Codec 的实例被设计为不可变的,并且一旦构建完成,通常建议将其存储在静态(static final)字段中,以便复用和确保线程安全。

    2.2 DynamicOps<T>:适配数据格式 (JsonOps, NbtOps)

    DynamicOps<T> 扮演着层级化序列化格式(例如 JSON、NBT)的适配器角色。它允许从底层序列化形式中提取和插入简单数据类型。常见的实现有用于处理 JSON 数据的 JsonOps 和用于处理 NBT 数据的 NbtOps。Codec 通常与一个 DynamicOps 实例协同工作,以完成实际的编解码操作。

    DynamicOps.png

    2.3 DataResult<A>:处理结果与错误

    使用 Codec 进行编码或解码操作后,返回的是一个 DataResult<A> 对象。该对象封装了转换操作的结果,其中可能包含成功转换的实例,或者在转换失败时包含错误数据。DataResult 可以表示成功或失败状态,其 resultOrPartial 方法在处理自定义数据包(datapack)资源等场景时特别有用,它允许在记录错误的同时,仍可能获取部分成功转换的结果。

    3 如何构建 Codec

    构建复杂的 Codec 通常始于对基本数据类型和常见集合类型的处理。

    3.1 基本类型的 Codec (String, Int, Bool 等)

    Mojang 提供了用于处理 Java 基本数据类型及其包装类的内置 Codec,例如 Codec.BOOL、Codec.INT、Codec.STRING、Codec.FLOAT 等。这些是构建更复杂 Codec 的基础构件。

    因为本篇教程我是基于Neoforge开发角度,所以你可以在Neoforge官方的Wiki中找到更多的Codec类型。

    3.2 使用 RecordCodecBuilder 定义复杂数据

    对于具有明确命名字段的自定义类或 Java 记录(Record),RecordCodecBuilder 是创建其 Codec 的主要工具。

    3.2.1 构建自定义对象和记录的结构

    RecordCodecBuilder 是为具有显式命名字段的类或 Java Record 创建 Codec 的核心工具。它允许通过 instance.group(...) 方法定义多个字段。Java Record 因其固有的不可变性和简洁性而特别适合与 RecordCodecBuilder 配合使用。

    3.2.2 定义字段:fieldOf() 与 forGetter()

    在 RecordCodecBuilder.group(...)内部,每个字段都通过其类型的 Codec、后跟 .fieldOf("json_key_name")(指定序列化格式中的键名)以及 .forGetter(MyClass::getFieldName)(提供用于序列化的 getter 函数)来定义。这些方法将 Java 字段(通过其 getter 和类型的 Codec)与其在序列化数据中的表示(键名)关联起来。

    // 假设有一个 Java Record:
    // public record MyData(String name, int value) {}
    
    // MyData 的 Codec
    public static final Codec MY_DATA_CODEC = RecordCodecBuilder.create(instance ->
        instance.group(
            Codec.STRING.fieldOf("name").forGetter(MyData::name),
            Codec.INT.fieldOf("value").forGetter(MyData::value)
        ).apply(instance, MyData::new)
    );
    

    3.2.3 处理可选性:optionalFieldOf() 与默认值

    为了处理数据中可能缺失的字段,可以使用 someCodec.optionalFieldOf("field_name") 来创建一个对应 Optional<Type> 类型的字段。如果希望在字段缺失时提供一个默认值,则可以使用 someCodec.optionalFieldOf("field_name", defaultValue)。需要注意的是,如果一个可选字段存在但数据格式错误,错误可能会被静默捕获,并使用默认值代替。若希望在可选元素解析出错时消费掉该错误,可以使用 lenientOptionalFieldOf。

    // 假设有一个 Java Record:
    // public record ConfigData(String id, Optional description, int timeout) {}
    
    // ConfigData 的 Codec
    public static final Codec CONFIG_DATA_CODEC = RecordCodecBuilder.create(instance ->
        instance.group(
            Codec.STRING.fieldOf("id").forGetter(ConfigData::id),
            Codec.STRING.optionalFieldOf("description").forGetter(ConfigData::description), // 返回 Optional
            Codec.INT.optionalFieldOf("timeout", 30).forGetter(ConfigData::timeout) // 如果缺失,timeout 默认为 30
        ).apply(instance, ConfigData::new)
    );
    

    3.3 转换与组合 Codec

    Codec 系统提供了强大的机制来转换现有 Codec 以适应新的数据类型,或将多个 Codec 组合成更复杂的结构。

    3.3.1 类型间映射:xmap() 及其变体 (flatXMap, comapFlatMap)

    WIP

    3.3.2 实现多态分发:Codec.dispatch()

    WIP

    3.3.3 组合 Codec:Codec.pair() 与 Codec.either()

    WIP

    4 Codec 在 Minecraft 生态中的应用

    随着 Minecraft 版本的迭代,Codec 的应用范围不断扩大,深入到模组开发的各个方面。

    • 数据组件
      数据组件(Data Components)是 Minecraft 1.20.5 版本引入的一项重要特性,用以替代旧的 ItemStack NBT 系统来存储物品栈的附加数据。Codec 在定义这些自定义数据组件的持久化和网络同步方式方面起着核心作用。
      对于 NeoForge,数据组件将数据作为实际对象存储在物品栈上。定义一个 DataComponentType 时,需要为其提供一个用于持久化的 Codec(通过 .persistent() 方法指定)和/或一个用于网络同步的 StreamCodec(通过 .networkSynchronized() 方法指定)。RecordCodecBuilder 是创建这些 Codec 的常用工具。

    • 自定义配方序列化器
      模组可以通过自定义配方序列化器(Recipe Serializer)来定义全新的合成机制或处理类型,这些通常具有自定义的 JSON 结构。Codec 在此过程中负责解析这些 JSON 并将其转换为游戏逻辑中可用的 Java 对象。
      对于 NeoForge,自定义的 RecipeSerializer 需要提供一个 MapCodec 用于 JSON 的序列化和反序列化,以及一个 StreamCodec 用于网络同步。通常使用 RecordCodecBuilder.mapCodec(...) 来创建所需的 MapCodec。

    • 网络数据包序列化
      为了实现高效的网络通信,Minecraft 使用 StreamCodec<B, V>(其中 B 通常是 ByteBuf 或 RegistryFriendlyByteBuf)进行数据包内容的序列化和反序列化。它与用于磁盘/JSON 序列化的通用 Codec<T> 是不同的。DataComponentType.Builder 接受一个 StreamCodec 用于 networkSynchronized 配置。RecipeSerializer 同样需要一个 StreamCodec。


    0 0 1 回复
  • 昨天没有做东西,今天做K
    [原创][Mod]通过特定格式文字进行查找结构
    昨天没有做东西,今天做K 昨天没有做东西,今天做

    故事的起因是有帮twf写模组时,需要用到查找结构指令显示坐标给玩家,但是玩家不一定有权限或者ftbq发送并不会sendmessage给玩家,所以有了此代码。
    我也将结构查找的功能写成了一个类,可以直接拿去用。

    public class StructureLocator {
        private static final ResourceKey<Registry<ConfiguredStructureFeature<?, ?>>> STRUCTURE_REGISTRY_KEY =
                Registry.CONFIGURED_STRUCTURE_FEATURE_REGISTRY;
    
        /**
         * 根据单个结构 ResourceLocation(如 "minecraft:village" 或者自定义 mod:id)查找最近的那个点。
         *
         * @param level     当前维度
         * @param center    中心搜索点
         * @param id        结构的 ResourceLocation
         * @param radius    搜索半径(方块数)
         * @param skipKnown 是否跳过已探索过的结构
         * return 如果找到,返回 Pair结构坐标, Holder<该结构>; 找不到则 empty()
         */
        public static Optional<Pair<BlockPos, Holder<ConfiguredStructureFeature<?, ?>>>> findNearest(
                ServerLevel level,
                BlockPos center,
                ResourceLocation id,
                int radius,
                boolean skipKnown
        ) {
            Registry<ConfiguredStructureFeature<?, ?>> registry =
                    level.registryAccess().registryOrThrow(STRUCTURE_REGISTRY_KEY);
    
            ResourceKey<ConfiguredStructureFeature<?, ?>> key =
                    ResourceKey.create(STRUCTURE_REGISTRY_KEY, id);
    
            Holder<ConfiguredStructureFeature<?, ?>> holder =
                    registry.getHolder(key).orElse(null);
            if (holder == null) return Optional.empty();
    
            HolderSet<ConfiguredStructureFeature<?, ?>> holderSet = HolderSet.direct(holder);
            Pair<BlockPos, Holder<ConfiguredStructureFeature<?, ?>>> result =
                    level.getChunkSource()
                            .getGenerator()
                            .findNearestMapFeature(level, holderSet, center, radius, skipKnown);
    
            return Optional.ofNullable(result);
        }
    
        /**
         * 根据 TagKey(像 "#minecraft:village")来查找最近的结构。
         *
         * @param level     当前维度
         * @param center    中心搜索点
         * @param tagKey    结构 TagKey(Registry.CONFIGURED_STRUCTURE_FEATURE_REGISTRY 下的 Tag)
         * @param radius    搜索半径
         * @param skipKnown 是否跳过已探索结构
         * return 如果找到,返回 Pair结构坐标, Holder<该结构>; 找不到则 empty()
         */
        public static Optional<Pair<BlockPos, Holder<ConfiguredStructureFeature<?, ?>>>> findNearestByTag(
                ServerLevel level,
                BlockPos center,
                TagKey<ConfiguredStructureFeature<?, ?>> tagKey,
                int radius,
                boolean skipKnown
        ) {
            Registry<ConfiguredStructureFeature<?, ?>> registry =
                    level.registryAccess().registryOrThrow(STRUCTURE_REGISTRY_KEY);
    
            HolderSet<ConfiguredStructureFeature<?, ?>> holderSet =
                    registry.getOrCreateTag(tagKey);
            Pair<BlockPos, Holder<ConfiguredStructureFeature<?, ?>>> result =
                    level.getChunkSource()
                            .getGenerator()
                            .findNearestMapFeature(level, holderSet, center, radius, skipKnown);
    
            return Optional.ofNullable(result);
        }
    
        /**
         * 把查到的结果格式化成一个聊天用的组件(绿色坐标 + 距离)。
         *
         * @param structureName 你想显示的结构名称(如 "minecraft:village" 或 "#minecraft:village")
         * @param origin        搜索中心
         * @param pair          findNearest 返回的 Pair
         * param translateKey  翻译 key,通常用 "commands.locate.success"
         */
        public static Component formatLocateResult
                String structureName,
                BlockPos origin,
                Pair<BlockPos, Holder<ConfiguredStructureFeature<?, ?>>> pair,
                String translateKey
         {
            BlockPos found = pair.getFirst();
            int distance = Mth.floor(
                    dist(origin.getX(), origin.getZ(), found.getX(), found.getZ())
            );
    
            MutableComponent coords = ComponentUtils.wrapInSquareBrackets(
                            new TranslatableComponent("chat.coordinates", found.getX(), "~", found.getZ())
                    );
    
            return new TranslatableComponent(
                    translateKey, structureName, coords, distance
            );
        }
    
        private static float dist(int x1, int z1, int x2, int z2) {
            int dx = x2 - x1, dz = z2 - z1;
            return Mth.sqrt((float) (dx * dx + dz * dz));
        }
    }
    

    这部分代码就是查找结构有关的类了,通过获取ConfiguredStructureFeature的注册表,然后再获取结构的Holder类,再给ChunkGenerator的findNearestMapFeature方法去获取到BlockPos。

    MixinServerPlayer.class
    public class ServerPlayerMixin {
        ModifyArgmethod = "sendMessage(Lnet/minecraft/network/chat/Component;Lnet/minecraft/network/chat/ChatType;Ljava/util/UUID;V", at = Atvalue = "INVOKE", target = "Lnet/minecraft/network/protocol/game/ClientboundChatPacket;<init>(Lnet/minecraft/network/chat/Component;Lnet/minecraft/network/chat/ChatType;Ljava/util/UUID;V"), index = 0)
        public Component modifyMessage(Component component){
            MutableComponent mutableComponent = component.copy();
            String message = mutableComponent.getString();
            ServerPlayer serverPlayer = (ServerPlayer) (Object) this;
            if (message.contains("<structure>") && message.contains("</structure>")) {
                String structure = message.replaceAll(".*<structure>(.*?)</structure>.*", "$1");
                BlockPos center = serverPlayer.blockPosition();
                Optional<Pair<BlockPos, Holder<ConfiguredStructureFeature<?, ?>>>> holderPair = StructureLocator.findNearest(
                        serverPlayer.getLevel(),
                        center,
                        new ResourceLocation(structure),
                        100,
                        false
                );
                AtomicReference<String> replace = new AtomicReference<>();
                holderPair.ifPresentOrElse(pair -> {
                    Component component1 = StructureLocator.formatLocateResult(structure, center, pair, "commands.locate.success");
                    replace.set(component1.getString());
                }, () -> {
                    replace.set(new TranslatableComponent("commands.locate.failed", structure).getString());
                });
                Style style = component.getStyle();
                mutableComponent = new TextComponent(message.replaceAll("<structure>(.*?)</structure>", "§a%s§r".formatted(replace.get()))).withStyle(style);
            }
            return mutableComponent;
        }
    }
    

    这段mixin则是注入ServerPlayer的sendMessage方法,修改new ClientboundChatPacket(message, type, sender)的message参数,||因为我偷懒,所以没用MixinExtra||,用正则表达式进行匹配到对应的结构格式文字,然后修改原文字中的内容。

    这差不多都就是本次代码的全部内容了。


    0 0 0 回复
  • 昨天没有做东西,今天做K
    [原创][Mod]基于opengl取色的hsv色盘屏幕
    昨天没有做东西,今天做K 昨天没有做东西,今天做

    直接上代码。

    import com.mojang.blaze3d.systems.RenderSystem;
    import com.mojang.blaze3d.vertex.*;
    import net.minecraft.client.Minecraft;
    import net.minecraft.client.gui.GuiGraphics;
    import net.minecraft.client.gui.screens.Screen;
    import net.minecraft.client.renderer.GameRenderer;
    import net.minecraft.network.chat.Component;
    import net.minecraft.util.FastColor;
    import org.joml.Matrix4f;
    import org.lwjgl.BufferUtils;
    import org.lwjgl.opengl.GL11;
    
    import java.awt.*;
    import java.nio.ByteBuffer;
    
    public class ColorPickerScreen extends Screen {
        private int pickColor = 0xFFFFFFFF;
        private int hueColor = Color.HSBtoRGB(0f, 1f, 1f);
        private double pendingMouseX = -1, pendingMouseY = -1;
    
        // 色板与色相条的位置/尺寸常量
        private static final int PANEL_X = 0, PANEL_Y = 0, PANEL_W = 200, PANEL_H = 100;
        private static final int HUE_X = 10, HUE_H = 20, HUE_W = 200, HUE_Y_OFFSET = 30;
        private static final int PREVIEW_X = 300, PREVIEW_Y = 0, PREVIEW_W = 50, PREVIEW_H = 50;
        private static final int HUE_SEGMENTS = 100;
    
        public ColorPickerScreen() {
            super(Component.literal("HSV Color Picker"));
        }
    
        Override
        public void renderGuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks {
            super.render(guiGraphics, mouseX, mouseY, partialTicks);
    
            Matrix4f mat = guiGraphics.pose().last().pose();
            Tesselator tess = Tesselator.getInstance();
            BufferBuilder buf = tess.getBuilder();
    
            RenderSystem.setShader(GameRenderer::getPositionColorShader);
            buf.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
            drawColorPanel(mat, buf);
            drawHueBar(mat, buf);
            drawPreview(mat, buf);
            tess.end();
    
            // 在所有内容绘制完后再执行拾色
            handlePendingPick(guiGraphics);
        }
    
        Override
        public boolean mouseClickeddouble mouseX, double mouseY, int button {
            this.pendingMouseX = mouseX;
            this.pendingMouseY = mouseY;
            return super.mouseClicked(mouseX, mouseY, button);
        }
    
        // === 私有封装方法 ===
    
        /** 绘制从黑→黑→选中色→白的渐变色板 */
        private void drawColorPanel(Matrix4f mat, BufferBuilder buf) {
            buf.vertex(mat, PANEL_X, PANEL_Y + PANEL_H, 0).color(0f, 0f, 0f, 1f).endVertex();
            buf.vertex(mat, PANEL_X + PANEL_W, PANEL_Y + PANEL_H, 0).color(0f, 0f, 0f, 1f).endVertex();
    
            float rHue = FastColor.ARGB32.red(hueColor)   / 255f;
            float gHue = FastColor.ARGB32.green(hueColor) / 255f;
            float bHue = FastColor.ARGB32.blue(hueColor)  / 255f;
            buf.vertex(mat, PANEL_X + PANEL_W, PANEL_Y, 0).color(rHue, gHue, bHue, 1f).endVertex();
    
            buf.vertex(mat, PANEL_X, PANEL_Y, 0).color(1f, 1f, 1f, 1f).endVertex();
        }
    
        /** 绘制水平色相条 */
        private void drawHueBar(Matrix4f mat, BufferBuilder buf) {
            int hueY = this.height - HUE_Y_OFFSET;
            for (int i = 0; i < HUE_SEGMENTS; i++) {
                float h1 = (float) i / HUE_SEGMENTS;
                float h2 = (float)(i + 1) / HUE_SEGMENTS;
                int c1 = Color.HSBtoRGB(h1, 1f, 1f);
                int c2 = Color.HSBtoRGB(h2, 1f, 1f);
                float r1 = ((c1>>16)&0xFF)/255f, g1 = ((c1>>8)&0xFF)/255f, b1 = (c1&0xFF)/255f;
                float r2 = ((c2>>16)&0xFF)/255f, g2 = ((c2>>8)&0xFF)/255f, b2 = (c2&0xFF)/255f;
    
                int x1 = HUE_X + i * HUE_W / HUE_SEGMENTS;
                int x2 = HUE_X + (i+1) * HUE_W / HUE_SEGMENTS;
                buf.vertex(mat, x1, hueY, 0).color(r1, g1, b1, 1f).endVertex();
                buf.vertex(mat, x2, hueY, 0).color(r2, g2, b2, 1f).endVertex();
                buf.vertex(mat, x2, hueY + HUE_H, 0).color(r2, g2, b2, 1f).endVertex();
                buf.vertex(mat, x1, hueY + HUE_H, 0).color(r1, g1, b1, 1f).endVertex();
            }
        }
    
        /** 在右侧绘制当前 pickColor 预览小矩形 */
        private void drawPreview(Matrix4f mat, BufferBuilder buf) {
            float r = FastColor.ARGB32.red(pickColor)   / 255f;
            float g = FastColor.ARGB32.green(pickColor) / 255f;
            float b = FastColor.ARGB32.blue(pickColor)  / 255f;
    
            buf.vertex(mat, PREVIEW_X,               PREVIEW_Y + PREVIEW_H, 0).color(r, g, b, 1f).endVertex();
            buf.vertex(mat, PREVIEW_X + PREVIEW_W,   PREVIEW_Y + PREVIEW_H, 0).color(r, g, b, 1f).endVertex();
            buf.vertex(mat, PREVIEW_X + PREVIEW_W,   PREVIEW_Y,             0).color(r, g, b, 1f).endVertex();
            buf.vertex(mat, PREVIEW_X,               PREVIEW_Y,             0).color(r, g, b, 1f).endVertex();
        }
    
        /** 处理 pendingMouseX/Y,进行边界检查并调用 glReadPixels 拾色 */
        private void handlePendingPick(GuiGraphics guiGraphics) {
            if (pendingMouseX < 0) return;
    
            // 边界判断:面板 或 色相条
            boolean inPanel = pendingMouseX >= PANEL_X && pendingMouseX <= PANEL_X + PANEL_W
                    && pendingMouseY >= PANEL_Y && pendingMouseY <= PANEL_Y + PANEL_H;
            int hueY = this.height - HUE_Y_OFFSET;
            boolean inHue  = pendingMouseX >= HUE_X && pendingMouseX <= HUE_X + HUE_W
                    && pendingMouseY >= hueY   && pendingMouseY <= hueY + HUE_H;
            if (!inPanel && !inHue) {
                pendingMouseX = pendingMouseY = -1;
                return;
            }
    
            // 计算 OpenGL 像素坐标
            double scale = Minecraft.getInstance().getWindow().getGuiScale();
            if (scale < 1) scale = 1;
            int px = (int)(pendingMouseX * scale);
            int py = (int)(pendingMouseY * scale);
            int glY = Minecraft.getInstance().getWindow().getHeight() - py - 1;
            guiGraphics.flush();
    
            ByteBuffer buf = BufferUtils.createByteBuffer(3);
            GL11.glReadPixels(px, glY, 1, 1, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buf);
            int r = buf.get(0)&0xFF, g = buf.get(1)&0xFF, b = buf.get(2)&0xFF;
    
            if (inHue) {
                hueColor = FastColor.ARGB32.color(255, r, g, b);
            } else {
                pickColor = FastColor.ARGB32.color(255, r, g, b);
            }
    
            pendingMouseX = pendingMouseY = -1;
        }
    }
    

    具体的注释都在代码里面了,一些魔法数字我也写成了字段。
    项目地址在我的github仓库, 都是LGPLv3的代码


    4 0 0 回复
  • 草莓呜咩B
    养老向的物品抽奖机:虚空草莓
    草莓呜咩B 草莓呜咩

    冰中有宝和紧凑型虚空采矿机这两个模组真的很有意思,快乐的全物品抽奖可以玩好久好久好久。
    不过他俩啊,一个是探索向,一个是科技向,倾向有所不同。
    而我正好还有一个养老向的点子呢,那就是虚空草莓!

    // startup_scripts
    StartupEvents.registry('block', event => {
      event.create("void_strawberry", "crop")
        .dropSeed(false)
        .age(7)// 从 0 开始
        .crop("kubejs:void_strawberry")
        .texture("0", "kubejs:block/void_strawberry_crop_stage0")
        .texture("1", "kubejs:block/void_strawberry_crop_stage1")
        .texture("2", "kubejs:block/void_strawberry_crop_stage1")
        .texture("3", "kubejs:block/void_strawberry_crop_stage1")
        .texture("4", "kubejs:block/void_strawberry_crop_stage2")
        .texture("5", "kubejs:block/void_strawberry_crop_stage2")
        .texture("6", "kubejs:block/void_strawberry_crop_stage2")
        .texture("7", "kubejs:block/void_strawberry_crop_stage3")
        .item(item => {
          item.displayName("虚空草莓种子")
            .texture("kubejs:item/void_strawberry_seed")
            // .tooltip("贯通虚空的神奇作物,它究竟能带来什么呢?")
            .rarity("epic")
        })
        .displayName("虚空草莓");
    });
    StartupEvents.registry("item", event => {
      event.create("void_strawberry")
        .rarity("epic")
        .maxStackSize(64)
        .displayName("虚空草莓")
    });
    

    好了,这样一来就简单注册了虚空草莓的作物,贴图就先忽略不计啦。

    接下来就是实现功能的部分了,在KubeJS获取物品合集还是蛮方便的:

    // server_scripts
    ItemEvents.rightClicked("kubejs:void_strawberry", event => {
      let player = event.player;
      let list = Item.typeList.toArray();
      // list = list.filter(item => ...);
      let loot = event.level.createEntity("minecraft:item");
      let id = list[Math.floor(Math.random() * list.length)];
      loot.setNbt(`{Item:{id:"${id}",Count:1b}}`);
      loot.setPosition(player.x, player.y, player.z);
      loot.spawn();
      event.item.setCount(event.item.count - 1);
    });
    

    这样就完成了,是相对简单的物品实体生成形式,可以很方便地筛选,把创造马达呀什么的都丢掉。
    或许留着更好?


    1 0 0 回复
  • 忆然忆
    [原创][KubeJS][持续更新?]在kjs中使用createCustom进行注册物品
    忆然忆 忆然

    本文使用:CC-BY-NC-SA 4.0协议
    kjs本身提供的物品注册比较有限,在注册某些kjs未提供的item的时候我们就需要用到createCustom
    如果有需要的item注册可以在本文下方留言,会考虑更新
    createCustom注册model是没有的,也就是你还得去写一份model.json(这部分可以参考原版wiki)
    下面是使用createCustom去注册一个弓的例子

    let $BowItem = Java.loadClass("net.minecraft.world.item.BowItem")
    let $Item$Properties = Java.loadClass("net.minecraft.world.item.Item$Properties")
    
    StartupEvents.registry("item", event => {
      event.createCustom("modid:item_name", () => {
        let properties = new $Item$Properties()
        //修改耐久 为0则无耐久属性
        properties.durability(0)
        //修改最大堆叠数量
        properties.stacksTo(1)
        //修改稀有度
        properties.rarity("epic")
        //创建新的bowitem
        let item = new $BowItem(properties)
        //返回新的bowitem进行注册
        return item
      })
    })
    

    0 0 0 回复
  • 半梦半
    论坛怎么蓝蓝的
    半梦半 半梦

    论坛现在蓝蓝的欸


    1 0 0 回复
  • 忆然忆
    [原创][KubeJS 7][BrokenClassFilter]关于Nashorn在kjs中的应用
    忆然忆 忆然

    前言
    ClassFilter是kjs的一个保护机制,笔者在这里不建议进行对ClassFilter动手脚
    此篇仅以替换kjs的ClassFilter为例子展示Nashorn在kjs的使用
    本文使用:CC-BY-NC-SA 4.0协议

    因为1.21.1的kubejs ban了反射,因此只能在nashorn里面进行操作
    Nashorn比较复杂,这次就仅展示成品

    let $KubeJS = Java.loadClass("dev.latvian.mods.kubejs.KubeJS")
    
    let $ServerLifecycleHooks = Java.loadClass("net.neoforged.neoforge.server.ServerLifecycleHooks")
    /**@type {$MinecraftServer_} */
    let Server = $ServerLifecycleHooks.getCurrentServer()
    //所有的scriptmanager获取
    let ServerScriptMagager = Server.getServerResources().managers().kjs$getServerScriptManager()
    let ClientScriptMagager = $KubeJS.getClientScriptManager()
    let StartupScriptMagager = $KubeJS.getStartupScriptManager()
    
    
    
    let $ScriptEngineManager = Java.loadClass("javax.script.ScriptEngineManager")
    let Nashorn = new $ScriptEngineManager().getEngineFactories()[0].getScriptEngine()
    
    
    Nashorn.eval(`
      var Clazz = Java.type("java.lang.Class")
      var ClassFilter = Java.type("dev.latvian.mods.kubejs.plugin.ClassFilter")
      var HashSet = Java.type("java.util.HashSet")
      var ArrayList = Java.type("java.util.ArrayList")
    
      var ScriptManager$Clazz = Clazz.forName("dev.latvian.mods.kubejs.script.ScriptManager")
      var ScriptManager$classFilter$Field = ScriptManager$Clazz.getDeclaredField("classFilter")
    
      var ClassFilter$Clazz = Clazz.forName("dev.latvian.mods.kubejs.plugin.ClassFilter")
      var ClassFilter$denyStrong$Field = ClassFilter$Clazz.getDeclaredField("denyStrong")
      var ClassFilter$denyWeak$Field = ClassFilter$Clazz.getDeclaredField("denyWeak")
      
      ScriptManager$classFilter$Field.setAccessible(true)
      ClassFilter$denyStrong$Field.setAccessible(true)
      ClassFilter$denyWeak$Field.setAccessible(true)
    
      var unlock=function(ScriptMagager){
        var ClassFilter = ScriptManager$classFilter$Field.get(ScriptMagager)
        ClassFilter$denyStrong$Field.set(ClassFilter,new HashSet())
        ClassFilter$denyWeak$Field.set(ClassFilter,new ArrayList())
      }
    `)
    //参数传入scriptmanager即可爆破classfilter
    Nashorn.invokeFunction("unlock",ServerScriptMagager)
    let $File = Java.loadClass("java.io.File")
    

    2 0 0 回复
  • 忆然忆
    [原创]使用kjs进行文件的删除
    忆然忆 忆然

    本文使用:CC-BY-NC-SA 4.0协议

    因为kjs的JsonIO并未限制文件名称的后缀
    因此这个操作并不安全

    kubejs的jsonio可以将第二个参数填入空字符串来删除文件
    JsonIO.write('kubejs/assets/test.nbt', "")
    如此便删除了.minecraft/kubejs/assets里面的test.nbt文件


    3 0 0 回复
  • sdjgeS
    [原创?][KJS6][EE.JS]较为方便地为矿物提供注册与通用配方修改的轮子
    sdjgeS sdjge

    EEJS 是N-Withe的整合包Omniworld-R的EEJS部分分支且添加了一些模组的支持,如有需要可以麻烦我或者自己写更多支持😺
    由于原仓库未标注协议,默认视为ARR,所以获得了原作者同意后才使用MIT协议。
    它可以让你优雅的编辑、统一矿物与材料。
    推荐与Almost unified一起使用!! (因为我懒得补全这部分)
    材质来自于Emendatus Enigmatica
    项目与本文使用MIT协议!!🤗

    存储库详见这里

    前置需求

    • minecraft => 1.20.1也许不止1.20.1
    • (NEO)Forge => any
    • KubeJS => 6
    • LootJS => any
    • KubeJS Mekanism 如果有Mekanism
    • KubeJS Immersive Engineering 如果有Immersive Enginnering
    • KubeJS Create 如果有Create
    • KubeJS Blood Magic 如果有Blood Magic
    • KubeJS Thermal 如果有Thermal Series
    • KubeJS-EnderIO 如果有Ender IO
    • Occultism KubeJS 如果有Occultism

    功能

    • 可自定义的容矿岩/材料
    • 便于编辑
    • 不错的兼容性

    使用文档

    注册部分

    自定义容矿岩

    添加新的定义在 EE_STRATAS ,它在 strata.js里

    stone: {
    	name: "stone", // 命名材料
    	texture: "minecraft:block/stone", // 容矿岩材质
    	fill: "minecraft:stone",
    	hardness: 1.5, // 矿物硬度
    	resistance: 6, // 矿物爆炸抗性
    	tool: "pickaxe", // 工具类型
    }
    

    添加或修改 material_def.js 的开头,类似于下文
    e.g.

    let glodStratas = ["andesite", "diorite", "granite", "end_stone"];
    ...
    let EE_MATERIALS = [
        ...
    ] 
    

    自定义材料

    添加新的定义在 EE_MATERIALS ,它在 material_def.js里

    * 代表非必须

    [1^]ProcessedTypes

    { 
    name: "coal", // 命名材料
    type: "dust", // 你的材料分类 -- ("dust","metal","gem")
    baseItem: "dust", //*基础类型是?
    processedTypes: ["dust","fluid"], //*处理物种类在下面的表格
    color: ["#393e46", "#2e2e2e", "#261e24", "#1f1721", "#1c1c1e"], //*你需要5种颜色才能正常生成材质
    burnTime: 1600, //*如果可以烧 / 时间单位是 tick
    strata: vanillaComplementStratas, //*容矿岩种类 -- *如果你加了"ore"类型,你需要有这条*
    fluidType:"thin" 
    //*types: "thin","thick" and "custom", if it's thin or thick it will use first color(color[0]), 
    // but custom has more configs! that need another color to color the fluid if you want, 
    // first color is the bucket color, and you need more textures named to "${name}_still" and "${name}_flowing" like vanilla.
    // 我懒得写一遍中文教程了,简单来说就是写了这个只会最多用2种颜色,三种type,只有custom会用第二种颜色然后作为材质第二次上色的颜色,第二种不写也没问题😁
    drop: {item: "minecraft:raw_iron",min: 1,max: 1,}, //*矿物掉落物
    harvestLevel: "stone", //*挖掘等级
    smallStorageBlock: true,//*存储方块合成的配方是4*4还是9*9
    gemTemplate: 1, //*纹理模板
    toolProperties: { //*工具属性 *加上了的话下面都要出现,不加就没有*
    damage: 9, // 伤害
    durability: 1800, // 耐久
    harvestLevel: 4, // 挖掘等级
    enchantValue: 24, // 附魔等级
    miningSpeed: 12 // 挖掘速度
    },
    armorProperties: { //*护甲属性 *加上了的话下面都要出现,不加就没有*
    durability: 1000, // 护甲耐久
    durabilityMultiplier: 70, // 护甲耐久乘数
    slotProtections: [4, 7, 9, 4], // 护甲部位的护甲值
    equipSound: 'minecraft:item.armor.equip_iron', // 穿戴声音
    toughness: 3.5, // 护甲抗性
    knockbackResistance: 0 // 击退减免
    }}
    

    太长不看

    clone或者下载压缩包后粘贴到kjs文件夹内后即可在material_def.js与strata.js中添加新的材料与容矿岩,格式详见文档。

    已知问题

    地物生成绝对有问题,但是我不知道怎么优雅地实现。

    画廊

    44b56256-f642-44e3-9620-d20a4f180440-image.png


    3 0 0 回复
  • 不是椰浆不
    [搬运][Windows][JSON-i18n]一款JSON文件快速本地化的翻译工具
    不是椰浆不 不是椰浆

    项目地址:MonianHello/JSON-i18n: JSON文件快速本地化的翻译工具
    以下介绍为项目地址readme,因介绍已较为详细,故不另行编写
    本帖内不提供二次分发文件,请前往项目地址自行下载!


    JSON-i18n

    JSON文件快速本地化的翻译工具

    部分功能参考自CFPATools/Minecraft-Mods-Translator: Mods Translator for Minecraft 1.16+ (github.com)

    image-20230608112735258

    主要功能:

    • 对接Minecraft模组翻译参考词典 (mcmod.cn),可以在程序内直接进行查询
    • 对接文本翻译_机器翻译-百度AI开放平台 (baidu.com),可以一键对全文进行机器翻译
    • 可以快速替换值中的单词而不会影响键名
    • 支持切换深色模式与浅色模式,支持修改ui文字样式大小、保存布局后再次启动后自动应用、(进阶)提供对ui的高度自定义设置

    程序提供五个快捷键:

    • Ctrl+F 跳转至搜索
    • Ctrl+H 跳转至替换
    • Ctrl+Shift+A 全选替换候选项
    • Ctrl+Up(方向键上) 跳转至审阅模式上一个键名
    • Ctrl+Down(方向键下) 跳转至审阅模式下一个键名

    详细说明:

    首次启动:

    首次启动后需要设置工作目录,设置完成后也可以在左上角 选项-首选项中修改

    注意:请避免通过直接修改config.ini的方式修改配置,由于不同设备的文本编辑器编码方式可能不同,错误的编码方式会导致程序异常。如果出现此类情况请删除config.ini以初始化程序

    外观设置:

    程序内支持修改全局字体及大小,在左上角 选项-首选项中修改。使用鼠标滚轮可以快速调整不同的字体及大小,方便预览。同时支持切换深色模式

    界面说明:

    程序左侧为文件浏览区域,可以快速选择需要打开的json文件

    程序右侧分别是查询栏和替换栏

    程序中间为操作区域,用来编辑json文件,在浏览模式下点击行号可以快速跳转到审阅模式

    菜单功能:

    上方菜单栏中有以下四个功能:

    • 首选项
    • 保存当前布局
    • 一键清空空格
    • 安全模式保存(在保存文件按钮失效时使用)

    左侧文件浏览区域中可以右键文件呼出菜单,有以下四个功能:

    • 在资源管理器中打开
    • 移动到回收站
    • 复制并重命名
    • 重命名

    翻译功能:

    翻译功能需要首先前往百度智能云-管理中心 (baidu.com)获取免费测试资源(500万字符/年),后续将陆续支持其他翻译api。

    使用前需要在配置文件中填入获取到的接口ak/sk,具体步骤见下文。

    测试成功后,可以点击左下角 翻译文件按钮,程序将自动对全文进行翻译。此时翻译并不会直接替换原文,需要用户进行进一步校对。如不需要,可以点击左下角 复制机翻按钮,一键替换译文与原文。完成后点击左下角 保存文件按钮即可保存更改。

    image-20230608115821872

    翻译接口领取步骤:

    1、登录百度账号,选择领取 文本翻译-通用版

    image-20230608115139348

    2、创建应用,接口选择 文本翻译-通用版,其他内容无要求

    image-20230608115356481

    3、复制ak与sk到程序中,测试成功后即可使用

    image-20230608115533624

    image-20230608115621227


    0 0 0 回复
  • 不是椰浆不
    [原创][1.20.1][EidolonJS开发实录]基于链式调用达成在Schema注册中使用回调函数构建配方
    不是椰浆不 不是椰浆

    项目地址:PickAID/EidolonJS: as the name says


    在为Eidolon中的坩埚注册相应的Schema时,我发现了一个问题:

    坩埚配方的输入并不能够使用简单的RecipeKey添加,相关的代码如下:

    public static class Step {
            public final List<Ingredient> matches = new ArrayList<>();
            public final int stirs;
    
            public Step(int stirs, List<Ingredient> matches) {
                this.stirs = stirs;
                this.matches.addAll(matches);
            }
        }
    

    而最初一版的RecipeKey与Schema设计如下:

    public interface CrucibleSchema {
        RecipeKey<OutputItem> OUTPUT = ItemComponents.OUTPUT_ID_WITH_COUNT.key("result");
        RecipeKey<OutputItem[]> OUTPUT = ItemComponents.OUTPUT_ARRAY.key("result");
    
        RecipeComponentBuilder STEP_BUILDER = new RecipeComponentBuilder(2)
                .add(NumberComponent.INT.key("stirs").optional(1))
                .add(NumberComponent.INT.key("stirs").defaultOptional())
                .add(ItemComponents.INPUT_ARRAY.key("items"));
        RecipeKey<RecipeComponentBuilderMap[]> STEPS = STEP_BUILDER.inputRole().asArray().key("steps");
        RecipeSchema SCHEMA = new RecipeSchema(OUTPUT, STEPS);
    

    进行注册后简单对代码进行测试:

    ServerEvents.recipes(event => {
        event.recipes.eidolon.crucible("2x stone", [
            {stirs: 1, items: ["#forge:dusts/redstone", "2x eidolon:soul_shard"]},
            {items: ["#forge:dusts/redstone", "eidolon:soul_shard"]},
            {stirs: 1},
            {stirs: 3}
        ])
    }
    

    配方的确可以正常注册,但是可以看到,Step部分的格式与json几乎一致,不够优雅。
    于是笔者开始尝试在RecipeKey中使用回调函数,然而失败了(也有可能是我菜),
    而我在阅读了SummoningRitual的源代码之后(AlmostReliable/summoningrituals at 1.20.1-forge)有了不一样的想法。
    或许可以使用链式调用形式的配方,并在链式调用的方法中使用回调函数。
    完整的代码实现如下:

    //篇幅原因省去import
    
    //CrucibleSchema.java
    public interface CrucibleSchema {
        FunctionalInterface
        interface StepBuilderCallback {
            void applyStepBuilderJS builder;
        }
        class CrucibleRecipeJS extends RecipeJS {
            public CrucibleRecipeJS steps(StepBuilderCallback callback) {
                var builder = new StepBuilderJS();
                callback.apply(builder);
                setValue(STEPS, builder.getStepList().toArray(CrucibleRecipe.Step[]::new));
                return this;
            }
    
    
        }
    
        RecipeKey<CrucibleRecipe.Step[]> STEPS = new RecipeComponent<CrucibleRecipe.Step[]>() {
            @Override
            public Class<?> componentClass() {
                return CrucibleRecipe.Step[].class;
            }
    
            Override
            public JsonElement writeRecipeJS recipe, CrucibleRecipe.Step[] value {
                if (value == null) {
                    return null;
                }
    
                JsonArray stepsArray = new JsonArray();
                for (CrucibleRecipe.Step step : value) {
                    JsonObject stepObj = new JsonObject();
    
                    // Add stirs
                    stepObj.addProperty("stirs", step.stirs);
    
                    // Add items
                    if (!step.matches.isEmpty()) {
                        JsonArray itemsArray = new JsonArray();
                        for (Ingredient ingredient : step.matches) {
                            itemsArray.add(ingredient.toJson());
                        }
                        stepObj.add("items", itemsArray);
                    }
    
                    stepsArray.add(stepObj);
                }
    
                return stepsArray;
            }
    
            Override
            public CrucibleRecipe.Step[] readRecipeJS recipe, Object from {
                if (from instanceof JsonElement) {
                    JsonArray stepsArray = ((JsonElement) from).getAsJsonArray();
                    List<CrucibleRecipe.Step> steps = new ArrayList<>();
    
                    for (JsonElement element : stepsArray) {
                        if (!element.isJsonObject()) {
                            throw new JsonSyntaxException("Each step must be a JSON object");
                        }
    
                        JsonObject stepObj = element.getAsJsonObject();
                        int stirs = stepObj.has("stirs") ? stepObj.get("stirs").getAsInt() : 0;
    
                        List<Ingredient> ingredients = new ArrayList<>();
                        if (stepObj.has("items")) {
                            JsonArray itemsArray = stepObj.getAsJsonArray("items");
                            for (JsonElement item : itemsArray) {
                                ingredients.add(Ingredient.fromJson(item));
                            }
                        }
    
                        steps.add(new CrucibleRecipe.Step(stirs, ingredients));
                    }
    
                    return steps.toArray(CrucibleRecipe.Step[]::new);
                } else {
                    throw new JsonSyntaxException("Each step must be a JSON object");
                }
            }
        }.key("steps").noBuilders();
    
        RecipeKey<OutputItem> OUTPUT = ItemComponents.OUTPUT.key("result").noBuilders();
    
        RecipeSchema SCHEMA = new RecipeSchema(CrucibleRecipeJS.class, CrucibleRecipeJS::new, OUTPUT, STEPS).constructor(((recipe, schemaType, keys, from) -> {
            recipe.setValue(OUTPUT, from.getValue(recipe, OUTPUT));
            recipe.setValue(STEPS, new CrucibleRecipe.Step[0]);
        }), OUTPUT);
    
    }
    
    
    //StepBuilderJS.java
    public class StepBuilderJS {
        List<CrucibleRecipe.Step> stepList = new ArrayList<>();
        List<Ingredient> item = new ArrayList<>();
        int stirs = 0;
    
        public StepBuilderJS stirs(int count) {
            stirs = count;
            return this;
        }
    
        public StepBuilderJS stirs() {
            return stirs(1);
        }
    
        public StepBuilderJS items(Ingredient... items) {
            item.addAll(List.of(items));
            return this;
        }
    
        public void step(int count, Ingredient... items) {
            var step = new CrucibleRecipe.Step(count, item);
            stepList.add(step);
        }
    
        public void build(){
            var step = new CrucibleRecipe.Step(stirs, item);
            stepList.add(step);
            stirs(0);
            item.clear();
        }
    
        @HideFromJS
        public List<CrucibleRecipe.Step> getStepList() {
            return stepList;
        }
    }
    

    最终实现的效果如下:

    event.recipes.eidolon.crucible("2x stone").steps((step) => {
            step.stirs(1).items("#forge:dusts/redstone", "eidolon:soul_shard").build()
            step.items("#forge:dusts/redstone","eidolon:soul_shard").build()
            step.stirs(3).build()
        })
    

    或许直接使用链式调用传入每一步的设计更为直接,但是出于个人偏好,笔者仍然选择使用回调函数的形式处理这部分。
    (//TODO 代码详解施工中)


    1 0 2 回复
  • 不是椰浆不
    【发帖必读】妙妙工具版版规与发帖规范
    不是椰浆不 不是椰浆

    总览
    • 第一章 板块定位
    • 第二章 发帖规范
    • 第三章 违规行为
    • 第四章 附则

    第一章 板块定位

    第一条 核心宗旨

    妙妙工具板块致力于分享实用技术资源,包括但不限于:

    • 原创或转载的程序、网页工具
    • KubeJS/CrT魔改脚本、工具集、配置集、Lib库
    • 可复用的代码轮子、开发模板
    • 其他可提升效率的技术解决方案

    第二条 内容导向

    鼓励发布完整度高、逻辑清晰、注释规范的作品,倡导技术交流与开源精神。


    第二章 发帖规范

    第三条 标题格式

    一、基础格式

    [原创/搬运][工具平台][工具英文名——工具译名] 其他说明与描述
    说明:

    1. 原创/搬运:必填,标明内容性质(搬运需注明原出处);
    2. 工具平台:必填,标明工具类型或适用平台(如 Windows / Forge / KubeJS 6 / 在线工具 等);
    3. 工具名称:
      • 有中英文名:格式为 英文名——中文译名;
      • 无中文译名:可仅写英文名;
      • 无英文名:可仅写中文名;
    4. 其他说明:选填,补充版本、功能亮点或短评(如 v3.2.1更新 / 便捷翻译工具)。

    二、注意事项

    1. 标题中英文间用 短破折号 ——(非短横线 -);
    2. 标题中方括号使用英文字符[],而不使用中文字符【】
    3. 禁止添加夸张符号(如 !!! 【爆款】);
    4. 平台分类需简洁明确,避免模糊表述(如 [Minecraft] 应改为 [Minecraft 1.20.1-1.21.X])。

    三、格式示例

    • [转载][Windows][JSON-i18n]一款JSON文件快速本地化的翻译工具
    • [转载][CrT 1.12.2][GrassUtils]基于CraftTweaker的便捷工具类集合

    第四条 版权声明

    1. 原创内容:需在正文显著位置标注原创声明以及授权协议;
    2. 搬运内容:
      • 一般情况下禁止二次转载(即“二转”),原搬运贴年久失修例外(超过三个大版本更新未同步内容);
      • 若原作者注明转载需要授权,或原作品协议不允许转载,需在正文顶部展示授权协议截图/链接;
      • 二次分发需要在协议允许的情况下进行,或获取原作者授权,需在正文顶部展示授权协议截图/链接;
    3. 衍生作品:若涉及二改、二次分发或反向工程,必须符合原始协议条款,否则视为剽窃。

    第五条 安全警示

    内容若包含以下操作,必须添加 unsafe 标签并在正文详细说明风险:

    • 修改系统关键文件
    • 静默执行的操作
    • 涉及敏感权限
    • 存在数据丢失/泄露风险的功能
      (示例):
      本脚本涉及对系统文件进行修改的操作!

    第六条 质量管控

    禁止发布以下低质量内容:

    1. 功能重复:与已有工具核心功能高度相似;
    2. 代码简陋:无异常处理、无注释、逻辑混乱的“玩具代码”;
    3. 可替代性强:可通过现有工具组合或简单命令实现同等效果;
    4. 描述模糊:未提供使用场景、参数说明或效果演示。
      注:若承诺改进,需在标题添加 [持续更新] 并公示开发计划(如更新日志)。

    第三章 违规行为

    第七条 绝对禁止

    1. 剽窃:直接复制他人代码未标注来源,或篡改版权声明;
    2. 黑箱分发:对闭源工具进行逆向工程并违规传播;
    3. 恶意组件:植入后门、挖矿代码、数据爬虫等危害性内容;
    4. 虚假更新:标注 [持续更新] 但超30天无实质进展且未说明原因。

    第八条 内容删除标准

    1. 未标注 [unsafe] 的高风险工具;
    2. 未提供授权证明的搬运内容;
    3. 被3名以上用户举报核实为低创作品。

    第九条 违规处罚

    违规类型 处理措施
    未标注[unsafe] 警告+强制编辑
    低创内容 删帖+禁止3日内发同类主题
    剽窃/黑箱分发 永久封禁账号+全论坛置顶晒尸

    第四章 附则

    第十条 本规则自发布之日起生效,最终解释权归VariedMC管理组所有。


    0 0 0 回复
  • 真冬M
    [1.20.1]强制加载并渲染特定区块
    真冬M 真冬

    我也不知道有什么实用价值但反正先写着。

    思路:
    1)强加载区块
    2)主动发送区块数据
    3)强制渲染区块

    强加载

    首先是强加载区块,我写了一个简单的类实现:

    public class ChunkLoader {
        private static final HashMap<String, ArrayList<ChunkPos>> loaders = new HashMap<>();
    
        public static void add(ServerLevel level, ChunkPos center) {
            String key = level.dimension().toString();
            if (!loaders.containsKey(key)) loaders.put(key, new ArrayList<>());
            if (!loaders.get(key).contains(center)) {
                loaders.get(key).add(center);
                ForgeChunkManager.forceChunk(level, MODID, center.getMiddleBlockPosition(0), center.x, center.z, true, true);
            }
        }
        public static void removeAll(ServerLevel level) {
            String key = level.dimension().toString();
            if (!loaders.containsKey(key)) return;
            if (loaders.get(key).isEmpty()) return;
            Iterator<ChunkPos> iterator = loaders.get(key).iterator();
            while (iterator.hasNext()) {
                ChunkPos center = iterator.next();
                iterator.remove();
                ForgeChunkManager.forceChunk(level, MODID, center.getMiddleBlockPosition(0), center.x, center.z, false, false);
            }
        }
    }
    

    写得比较草率,主打一个能用就行。
    这是在服务端运行的,区块要加载到服务端的区块缓存(ServerChunkCache)中,才能发送对应的区块数据。
    如果想安全地强加载区块,请使用Ticket系统,我这里只是分享思路,就简单用forceChunk方法敷衍过去了。

    发送区块数据

    这部分是mixin得到的,把ChunkMap中的playerLoadedChunk方法拿出来用,最终发送的是一个ClientboundLevelChunkWithLightPacket包。

    Mixinvalue = ChunkMap.class
    Implements@Interface(iface = IChunkMap .class, prefix = "lazy$")
    public abstract class ChunkMapMixin implements IChunkMap {
        @Shadow Nullable protected abstract ChunkHolder getVisibleChunkIfPresentlong p_140328_;
    
        Shadow protected abstract void playerLoadedChunkServerPlayer p_183761_, MutableObject<ClientboundLevelChunkWithLightPacket> p_183762_, LevelChunk p_183763_;
    
        public void loadLevelChunk(ServerPlayer player, ChunkPos chunkPos) {
            ChunkHolder chunkholder = this.getVisibleChunkIfPresent(chunkPos.toLong());
            if (chunkholder == null) return;
            LevelChunk levelchunk = chunkholder.getTickingChunk();
            if (levelchunk == null) return;
            this.playerLoadedChunk(player, new MutableObject<>(), levelchunk);
        }
    }
    

    默认情况下,区块更新是惰性的,要使用playerLoadedChunk方法,除了单独拎出来用,也可以插入到move方法中:

        Injectmethod = "move", at = @At("HEAD")
        private void justMove(ServerPlayer player, CallbackInfo ci) {
            loadLevelChunk(player, ChunkPos.ZERO);
        }
    

    这个方法是默认情况下玩家更新区块的方法,插入到这里,等同于为玩家更新额外的区块。

    强制渲染

    这部分是最复杂的,需要进行非常非常多的mixin。
    让我们一步步走。
    首先,客户端收到ClientboundLevelChunkWithLightPacket包后需要进行处理,将区块数据存进客户端的区块缓存(ClientChunkCache)中,等待帧渲染将它抓出来渲染。
    这里出现了第一次渲染判定,读取区块缓存的时候,要检测区块坐标是否在视距范围内。
    那么我们将它干掉。

    Mixintargets = "net.minecraft.client.multiplayer.ClientChunkCache$Storage"
    public class ClientChunkCache$StorageMixin {
        Injectmethod = "inRange", at = @At("HEAD", cancellable = true)
        private void modifyRange(int x, int z, CallbackInfoReturnable<Boolean> cir) {
            if (new ChunkPos(*****).equals(new ChunkPos(x, z))) {
                cir.setReturnValue(true);
            }
        }
    }
    

    如果轮到检测的这个区块和你想渲染的区块是同一个,就强制通过检测。
    说完了缓存,接下来就是渲染。
    最核心的渲染是在LevelRenderer里,这个类超级超级长。
    我其实不太想介绍这个部分,因为Embeddium在这个类中用Overwrite重写了非常多的方法,比如我下面要说的setupRender:

    MixinLevelRenderer.class
    public class LevelRendererMixin {
        ModifyVariablemethod = "setupRender",at = @At("STORE", ordinal = 0)
        private double modifyX(double x) {
            // 改一下x。
            return x;
        }
        // 把y和z也改了,此处省略。
    }
    

    这么做可以强制转移渲染的中心点,转移到你想渲染的地方。
    是的,这个方法被重写了,重写后以客户端实例的摄像机位置为渲染中心了。

    Minecraft.getInstance().gameRenderer.getMainCamera().getPosition();
    

    如果要修改渲染中心就去改摄像机位置吧,反正强制渲染特定区块大概率是伴随着摄像机移动的。
    如果要考虑不安装Embeddium的情况,就用MixinPlugin区分一下:

    public class MixinPlugin implements IMixinConfigPlugin {
        Override
        public boolean shouldApplyMixinString targetClassName, String mixinClassName {
            if (mixinClassName.equals("com.mafuyu404.examplemod.mixin.LevelRendererMixin")) {
                return !isClassLoaded("me.jellysquid.mods.sodium.client.SodiumClientMod"); // Sodium/Embeddium核心类
            }
            return true;
        }
    
        private static boolean isClassLoaded(String className) {
            try {
                Class.forName(className, false, getClassLoader());
                return true;
            } catch (ClassNotFoundException e) {
                return false;
            }
        }
        // 必要的补全此处省略。
    }
    

    不过,真有人会不装Embeddium吗?我测试的时候32视距给我提了快100帧……

    在处理完区块渲染,其实还有最后一步,那就是实体渲染。

    Mixintargets = "net.minecraft.server.level.ChunkMap$TrackedEntity"
    public class ChunkMap$TrackedEntityMixin {
        ModifyVariablemethod = "updatePlayer", at = @At(value = "STORE")
        private Vec3 wwa(Vec3 direction) {
            // 改一下direction。
            return direction;
        }
    }
    

    很多优化模组都有远处实体剔除的功能,实体渲染其实可以看作与区块渲染独立。
    上面这个方法叫updatePlayer,其实是指更新本地玩家区块数据中的实体追踪数据,这里面会计算实体与玩家的距离,从而决定实体是否要渲染。
    direction其实就是玩家坐标与实体坐标构成的向量,设为零向量即可。

    结语

    内容就这么多了,其实是相当鸡肋的东西,之后想到什么再来补充吧。


    4 0 1 回复
  • QiHuang02Q
    谁来测测我的?
    QiHuang02Q QiHuang02

    来测测孩子的模组吧


    1 0 0 回复
  • 半梦半
    【典型案例】崩溃求解答
    半梦半 半梦

    a518faf6-6e0c-4dc1-8062-4dfa2641190a-295C4E22D524B23617F8D8390FD15DC2.png

    做包新人,什么也不懂,加了大概300+模组,然后就这么崩了。请问大佬注入失败是什么意思啊,这个模组对我挺重要的有没有办法不删🥺


    2 0 0 回复
  • 不是客服M
    在这测试一下各种功能
    不是客服M 不是客服

    测试


    7 0 0 回复
  • 草莓呜咩B
    美食再就业:用紫颂果慕斯进行定点传送
    草莓呜咩B 草莓呜咩

    美食们可不止能饱腹而已!
    紫颂果慕斯既然是紫颂果做的,就和传送脱不了关系?
    现在可以将它在铁砧上改名为坐标,逗号间隔,吃了就可以传送到该坐标,是不是很方便呀。
    不过还是要消耗一点点经验呢。

    ItemEvents.foodEaten("kitchenkarrot:chorus_mousse", event => {
      let position = event.player.mainHandItem.displayName.getString().slice(1, -1).split(",");
      if (event.player.xpLevel < 3)
        event.player.statusMessage = "经验等级不足, 需要三级经验!";
      else if (position.length == 3) {
        event.player.setPosition(position[0], position[1], position[2]);
        event.player.addXPLevels(-3);
      }
    });
    

    那么草莓慕斯会是什么效果呢?要是能召唤草莓军团就太帅了!


    0 0 3 回复
  • 昨天没有做东西,今天做K
    已经,没有modding的理由了
    昨天没有做东西,今天做K 昨天没有做东西,今天做

    😩


    4 0 0 回复
  • 真冬M
    程序猿梗图
    真冬M 真冬

    Image_1061376123293768.jpg
    Image_1061371682756269.jpg
    Image_1061332528610442.jpg
    Image_1061301561331800.jpg
    Image_976141317588545.jpg
    1638441314448-2021-12-02-18.23.38.webp
    1648277883321-22118451-3961-4997-ae8b-de32f03ef7f9-qq图片20220326145519.webp


    0 0 0 回复
  • 登录

  • 没有帐号? 注册

  • 登录或注册以进行搜索。
  • 第一个帖子
    最后一个帖子
0
  • 版块
  • 最新
  • 热门
  • 标签
  • 群组