From 26723a993302031cfdbe9ae3008d8d73c0ba3a9a Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Tue, 23 Jun 2026 14:39:44 +0800 Subject: [PATCH] =?UTF-8?q?ci(workflow):=20=E6=B7=BB=E5=8A=A0=20Fabric=202?= =?UTF-8?q?6.2=20=E6=9E=84=E5=BB=BA=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs(agents,claude): 更新项目计数,从 7 个子项目增至 8 个 - 在 build.yml 中新增 build-fabric-26-2 任务,使用 Java 25 和 ubuntu-24.04 - 更新 AGENTS.md 和 CLAUDE.md,反映新增的 Fabric-26.2 子项目 - 补充 Fabric-26.2 特定差异说明:使用 EntityTypes.ITEM_DISPLAY 和 Vec3.atCenterOf() - 更新共享资源说明,shared-resources-fabric 现支持 5 个 Fabric 子项目 - 修正 Java 版本要求说明 --- .github/workflows/build.yml | 26 ++ AGENTS.md | 14 +- Box3JS-NeoForge-1.21.1/README_NEW.md | 79 ++++ Box3JS-NeoForge-1.21.1/docs/en/index.md | 83 ++-- Box3JS-NeoForge-1.21.1/docs/index.md | 83 ++-- CLAUDE.md | 14 +- Fabric-26.2/.gitattributes | 9 + Fabric-26.2/.github/workflows/build.yml | 30 ++ Fabric-26.2/.gitignore | 40 ++ Fabric-26.2/build.gradle | 91 ++++ Fabric-26.2/gradle.properties | 20 + Fabric-26.2/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48462 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + Fabric-26.2/gradlew | 248 ++++++++++ Fabric-26.2/gradlew.bat | 82 ++++ Fabric-26.2/settings.gradle | 13 + .../client/java/com/box3lab/Box3Client.java | 9 + .../client/resources/box3.client.mixins.json | 12 + .../src/main/java/com/box3lab/Box3.java | 31 ++ .../com/box3lab/block/BarrierVoxelBlock.java | 60 +++ .../com/box3lab/block/BouncePadBlock.java | 44 ++ .../java/com/box3lab/block/ConveyorBlock.java | 36 ++ .../com/box3lab/block/GlassVoxelBlock.java | 20 + .../com/box3lab/block/SpiderWebBlock.java | 35 ++ .../java/com/box3lab/block/VoxelBlock.java | 49 ++ .../block/entity/PackModelBlockEntity.java | 276 +++++++++++ .../block/entity/PackModelEntityBlock.java | 156 +++++++ .../java/com/box3lab/command/ModCommands.java | 388 ++++++++++++++++ .../java/com/box3lab/register/ModBlocks.java | 69 +++ .../java/com/box3lab/register/ModItems.java | 11 + .../com/box3lab/register/VoxelExport.java | 148 ++++++ .../com/box3lab/register/VoxelImport.java | 178 +++++++ .../box3lab/register/core/BlockRegistrar.java | 31 ++ .../register/creative/CreativeTabExtras.java | 26 ++ .../creative/CreativeTabRegistrar.java | 132 ++++++ .../PackModelBlockEntityRegistrar.java | 212 +++++++++ .../register/sound/CategorySoundTypes.java | 33 ++ .../register/voxel/VoxelBlockFactories.java | 57 +++ .../voxel/VoxelBlockPropertiesFactory.java | 26 ++ .../register/voxel/VoxelLightLevelMapper.java | 11 + .../com/box3lab/util/BlockIdResolver.java | 135 ++++++ .../java/com/box3lab/util/BlockIndexData.java | 243 ++++++++++ .../java/com/box3lab/util/BlockIndexUtil.java | 98 ++++ .../com/box3lab/util/Box3ImportFiles.java | 38 ++ .../java/com/box3lab/util/ConfigUtil.java | 58 +++ .../src/main/resources/assets/box3/icon.png | Bin 0 -> 10333 bytes .../resources/assets/box3/lang/en_us.json | 434 ++++++++++++++++++ .../resources/assets/box3/lang/zh_cn.json | 422 +++++++++++++++++ .../src/main/resources/box3.mixins.json | 12 + .../src/main/resources/fabric.mod.json | 32 ++ 50 files changed, 4253 insertions(+), 110 deletions(-) create mode 100644 Box3JS-NeoForge-1.21.1/README_NEW.md create mode 100644 Fabric-26.2/.gitattributes create mode 100644 Fabric-26.2/.github/workflows/build.yml create mode 100644 Fabric-26.2/.gitignore create mode 100644 Fabric-26.2/build.gradle create mode 100644 Fabric-26.2/gradle.properties create mode 100644 Fabric-26.2/gradle/wrapper/gradle-wrapper.jar create mode 100644 Fabric-26.2/gradle/wrapper/gradle-wrapper.properties create mode 100755 Fabric-26.2/gradlew create mode 100644 Fabric-26.2/gradlew.bat create mode 100644 Fabric-26.2/settings.gradle create mode 100644 Fabric-26.2/src/client/java/com/box3lab/Box3Client.java create mode 100644 Fabric-26.2/src/client/resources/box3.client.mixins.json create mode 100644 Fabric-26.2/src/main/java/com/box3lab/Box3.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/block/BarrierVoxelBlock.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/block/BouncePadBlock.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/block/ConveyorBlock.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/block/GlassVoxelBlock.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/block/SpiderWebBlock.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/block/VoxelBlock.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/block/entity/PackModelBlockEntity.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/block/entity/PackModelEntityBlock.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/command/ModCommands.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/register/ModBlocks.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/register/ModItems.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/register/VoxelExport.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/register/VoxelImport.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/register/core/BlockRegistrar.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/register/creative/CreativeTabExtras.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/register/modelbe/PackModelBlockEntityRegistrar.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/register/sound/CategorySoundTypes.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/register/voxel/VoxelBlockFactories.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/register/voxel/VoxelBlockPropertiesFactory.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/register/voxel/VoxelLightLevelMapper.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/util/BlockIdResolver.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/util/BlockIndexData.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/util/BlockIndexUtil.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/util/Box3ImportFiles.java create mode 100644 Fabric-26.2/src/main/java/com/box3lab/util/ConfigUtil.java create mode 100644 Fabric-26.2/src/main/resources/assets/box3/icon.png create mode 100644 Fabric-26.2/src/main/resources/assets/box3/lang/en_us.json create mode 100644 Fabric-26.2/src/main/resources/assets/box3/lang/zh_cn.json create mode 100644 Fabric-26.2/src/main/resources/box3.mixins.json create mode 100644 Fabric-26.2/src/main/resources/fabric.mod.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2db888ce..3eb36893 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -111,6 +111,32 @@ jobs: name: Fabric-26.1-artifact path: Fabric-26.1/build/libs/ + build-fabric-26-2: + runs-on: ubuntu-24.04 + steps: + - name: checkout repository + uses: actions/checkout@v4 + - name: validate gradle wrapper + uses: gradle/actions/wrapper-validation@v4 + - name: setup jdk + uses: actions/setup-java@v4 + with: + java-version: "25" + distribution: "microsoft" + - name: make gradle wrapper executable + working-directory: ./Fabric-26.2 + run: chmod +x ./gradlew + + - name: build Fabric 26.2 + working-directory: ./Fabric-26.2 + run: ./gradlew build + + - name: upload artifacts Fabric 26.2 + uses: actions/upload-artifact@v4 + with: + name: Fabric-26.2-artifact + path: Fabric-26.2/build/libs/ + build-neoforge-1-21-1: runs-on: ubuntu-24.04 steps: diff --git a/AGENTS.md b/AGENTS.md index 27fd94d3..4d90ea44 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t Box3Blocks is a Minecraft mod that imports 372 decorative blocks from the Box3 platform into Minecraft, supporting terrain file import/export and model items. It also includes **Box3JS**, a server-side TypeScript/JavaScript scripting engine (Rhino) for creating custom gameplay, mini-games, and world interactions. -The repository is a **multi-project monorepo** with 7 independent subprojects targeting different mod loaders and Minecraft versions. There is no root build system — each subproject has its own Gradle wrapper and `build.gradle`. +The repository is a **multi-project monorepo** with 8 independent subprojects targeting different mod loaders and Minecraft versions. There is no root build system — each subproject has its own Gradle wrapper and `build.gradle`. ## Subprojects @@ -16,11 +16,12 @@ The repository is a **multi-project monorepo** with 7 independent subprojects ta | `Fabric-1.21.1/` | Fabric | 1.21.1 | 21 | `fabric-loom-remap` | | `Fabric-1.21.11/` | Fabric | 1.21.11 | 21 | `fabric-loom-remap` | | `Fabric-26.1/` | Fabric | 26.1 | 25 | `fabric-loom` | +| `Fabric-26.2/` | Fabric | 26.2 | 25 | `fabric-loom` | | `Forge-1.20.1/` | Forge | 1.20.1 | 17 | `net.minecraftforge.gradle` v6.x | | `NeoForge-1.21.1/` | NeoForge | 1.21.1 | 21 | **Box3JS lives here** — NeoForge ModDevGradle | | `NeoForge-26.1/` | NeoForge | 26.1 | 25 | NeoForge ModDevGradle | -Only NeoForge-1.21.1 has the Box3JS scripting engine. The other 6 subprojects are purely the Box3Blocks decorative block mod. +Only NeoForge-1.21.1 has the Box3JS scripting engine. The other 7 subprojects are purely the Box3Blocks decorative block mod. ## Build Commands @@ -36,7 +37,7 @@ cd run/config/box3/script/colorzone npm install && npm run build # esbuild → Babel → Rhino target ``` -**Important:** Forge-1.20.1 requires Java 17. All other subprojects use Java 21+. NeoForge-26.1 uses Java 25. +**Important:** Forge-1.20.1 requires Java 17. Fabric-1.21.1 and Fabric-1.21.11 use Java 21. Fabric-26.1, Fabric-26.2, and NeoForge-26.1 use Java 25. There are no existing tests (`src/test` directories are empty). @@ -45,7 +46,7 @@ There are no existing tests (`src/test` directories are empty). Shared resources are centralized to avoid ~20,000 duplicate asset files: - **`shared-resources/`** — used by ALL subprojects: block textures, models, blockstates, item models, worldgen data, `block-id.json`, `block-spec.json` -- **`shared-resources-fabric/`** — used by all 4 Fabric subprojects: `models/item/` JSONs + lang files +- **`shared-resources-fabric/`** — used by all 5 Fabric subprojects: `models/item/` JSONs + lang files - **`shared-resources-forge/`** — used by Forge + both NeoForge subprojects: `models/item/` JSONs + lang files ## Block Mod Architecture @@ -125,10 +126,11 @@ Consumable/Cooldown/Enchantable/JukeboxPlayable components are NOT available in ## Version Differences -- `VoxelExport` only in Fabric-1.21.11, Fabric-26.1, and Forge/NeoForge variants +- `VoxelExport` only in Fabric-1.21.11, Fabric-26.1, Fabric-26.2, and Forge/NeoForge variants - `VoxelFluidRenderHandler` only in Fabric-1.21.11 - NeoForge-26.1 moved client code to `src/client/java` -- Fabric-26.1 uses `fabric-loom` (not `fabric-loom-remap`) and Java 25 +- Fabric-26.1 and Fabric-26.2 use `fabric-loom` (not `fabric-loom-remap`) and Java 25 +- Fabric-26.2 uses `EntityTypes.ITEM_DISPLAY` (not `EntityType.ITEM_DISPLAY`) and `Vec3.atCenterOf()` (not `BlockPos.getCenter()`) ## Tools diff --git a/Box3JS-NeoForge-1.21.1/README_NEW.md b/Box3JS-NeoForge-1.21.1/README_NEW.md new file mode 100644 index 00000000..5ce6b5f8 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/README_NEW.md @@ -0,0 +1,79 @@ +# Box3JS — Minecraft Scripting Engine + +> **Beta** — APIs may change during early development. Feedback is welcome. + +**No Java, just TypeScript.** Build Minecraft minigames, RPG systems, and world mechanics with scripts that hot-reload in seconds. + +Built on Mozilla Rhino, Box3JS brings the Box3 coding style to Minecraft servers. Write TypeScript, run it instantly — no JDK, no Gradle, no server restarts. + +## Features + +- **TypeScript-first** — Full type declarations (`.d.ts`) with bilingual JSDoc, auto-complete in any editor +- **Box3-compatible API** — World, Entity, Player, Voxels, Storage, Database, HTTP, remoteChannel +- **110+ Minecraft extensions** — Scoreboards, BossBars, teams, world border, particles, fireworks, potions, custom blocks/items/sounds registration, and more +- **Client-side scripting** — Custom HUD, keyboard input, audio playback, client-side SQLite & HTTP +- **Project isolation** — Run multiple script projects independently, each with its own scope and callbacks +- **Sandbox mode** — Test safely; roll back all script-made changes on stop +- **Standalone compilation** — `/box3script compile` packages your script into a distributable JAR + +## Quick Start + +``` +/box3script create mygame +``` + +``` +npm install && npm run build +``` + +``` +/box3script sandbox mygame +/box3script start mygame +``` + +Full guide: [https://docs.box3lab.com/box3js-mc](https://docs.box3lab.com/box3js-mc) + +## License + +Apache License 2.0 + +--- + +# Box3JS(神岛代码)— Minecraft 脚本引擎 + +> **Beta** — 早期测试阶段,API 可能变动,欢迎反馈。 + +**不写 Java,只用 TypeScript。** 在 Minecraft 里用脚本开发小游戏、RPG 玩法、世界机制,热重载秒级生效。 + +Box3JS 基于 Mozilla Rhino 引擎,延续了神奇代码岛的 API 风格。无需 JDK、无需 Gradle、无需重启服务器,写 TypeScript 即写即跑。 + +## 特性 + +- **TypeScript 优先** — 完整类型声明(`.d.ts`),双语 JSDoc,任意编辑器均有自动补全 +- **Box3 兼容 API** — World / Entity / Player / Voxels / Storage / Database / HTTP / remoteChannel +- **110+ MC 扩展** — 记分板、Bossbar、队伍、世界边界、粒子、烟花、药水、自定义方块/物品/音效注册等 +- **客户端脚本** — 自定义 HUD、键盘输入、音效播放、客户端 SQLite & HTTP +- **项目隔离** — 多脚本项目独立运行,各自拥有独立作用域和回调 +- **沙盒模式** — 放心测试,停止后回滚一切脚本改动 +- **独立编译** — `/box3script compile` 一键打包为可分发 JAR + +## 快速开始 + +``` +/box3script create mygame +``` + +``` +npm install && npm run build +``` + +``` +/box3script sandbox mygame +/box3script start mygame +``` + +完整教程:[https://docs.box3lab.com/box3js-mc](https://docs.box3lab.com/box3js-mc) + +## 许可证 + +Apache License 2.0 diff --git a/Box3JS-NeoForge-1.21.1/docs/en/index.md b/Box3JS-NeoForge-1.21.1/docs/en/index.md index 0dd46e73..018e255e 100644 --- a/Box3JS-NeoForge-1.21.1/docs/en/index.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/index.md @@ -3,76 +3,61 @@ layout: home hero: name: "Box3JS" - text: "JS/TS Scripting Engine for Minecraft" - tagline: Build custom gameplay, mini-games, and UIs — no JDK, no Java compilation required. + text: "TypeScript Scripting Engine for Minecraft" + tagline: No Java. No Gradle. No restarts. Write TypeScript, hot-reload in seconds. actions: - theme: brand text: Get Started link: /en/guide/getting-started + - theme: alt + text: Why Box3JS? + link: /en/guide/about-box3js features: + - icon: ⚡ + title: TypeScript, Zero Java + details: Full DTS types with bilingual JSDoc for every API. esbuild + Babel pipeline compiles modern TS to ES5 automatically. No JDK, no Gradle, no IDE setup. + - icon: 🔄 + title: Hot Reload in Seconds + details: Edit → save → see changes instantly. No server restarts. Built-in file watcher auto-reloads on every save. - icon: 🎮 + title: Box3-Compatible API + details: Same clean API style as Box3 (Shenqi Code Island). World, Entity, Player, Voxels, Storage, Database, HTTP, remoteChannel — all as global objects. + - icon: 🧩 + title: 110+ Minecraft Extensions + details: Scoreboards, BossBars, teams, world border, particles, fireworks, lightning, potions, custom blocks/items/sounds, and more. + - icon: 🖥️ title: Server & Client Scripting - details: Server-side world manipulation, entities, recipes. Client-side keyboard input, screen UI, sounds, SQLite storage, and HTTP requests. + details: Server handles game logic and world manipulation. Client handles keyboard input, HUD, audio, custom GUIs. Bidirectional events via remoteChannel. + - icon: 🛡️ + title: Sandbox Protection + details: Enable sandbox to auto-track all script changes. Disable to fully roll back. Test fearlessly — your map is always safe. - icon: 📦 - title: TypeScript First - details: Full DTS type definitions for all 17 global objects. Built-in esbuild + Babel pipeline transpiles modern TS to Rhino-compatible ES5. - - icon: 🔄 - title: Hot Reload - details: Edit scripts and see changes instantly without restarting the server. File watcher auto-reloads on save. - - icon: 🌐 - title: Bidirectional Communication - details: remoteChannel enables server↔client event messaging. Server broadcasts to all players; clients reply independently. + title: Standalone JAR Distribution + details: /box3script compile packages your scripts into a standalone mod. Drop it into mods/ — no Box3JS dependency required. - icon: 🗄️ - title: Dual-Side Storage & Database - details: JSON file persistence and SQLite on both server and client. Pagination, atomic updates, counters, and tagged-template queries. - - icon: 🧩 - title: Custom Blocks & Items - details: Block textures, item models, equipment, sounds, and creative tabs — all registered from JSON configs (standalone/JAR mode). - - icon: 📚 - title: Comprehensive Docs - details: 50+ pages across API reference, progressive tutorials, cookbook recipes, architecture deep-dive, and FAQ — in Chinese and English. - - icon: 🚀 - title: Standalone JAR Mode - details: Compile your script project into a self-contained JAR mod. No runtime dependency on Box3JS — just drop it in your mods folder. + title: Built-in Persistence + details: JSON file storage and SQLite database on both server and client. Leaderboards, player saves, economies — all built-in. --- ## Quick Start ```bash -# In-game: create a new project -/box3script create mygame +/box3script create mygame # scaffold a TypeScript project +``` -# Build and watch +```bash cd config/box3/script/mygame -npm install -npm run build -- --watch - -# TypeScript type checking -npm run check +npm install && npm run build # install deps + build ``` -```ts -// src/server/app.ts — your first script -world.onChat((entity, message) => { - if (message === "!hello") { - entity.player.directMessage(`Hello, ${entity.player.name}!`); - return false; - } - return true; -}); +```bash +/box3script sandbox mygame # enable sandbox (recommended) +/box3script start mygame # start the script ``` -[Read full docs →](/en/guide/getting-started) - -## Version Info +Open `src/server/app.ts`, write your game logic, save, then `/box3script reload mygame`. Enable `/box3script watch` and it auto-reloads on every save. -| Component | Version | -|-----------|---------| -| Minecraft | 1.21.1 | -| Mod Loader | NeoForge | -| Java | 21 | -| JS Engine | Mozilla Rhino 1.9.1 (ES5) | -| TypeScript | via Babel → ES5 | +[Full documentation →](/en/guide/getting-started) diff --git a/Box3JS-NeoForge-1.21.1/docs/index.md b/Box3JS-NeoForge-1.21.1/docs/index.md index 01e61bbc..41e8dcb8 100644 --- a/Box3JS-NeoForge-1.21.1/docs/index.md +++ b/Box3JS-NeoForge-1.21.1/docs/index.md @@ -3,76 +3,61 @@ layout: home hero: name: "Box3JS" - text: "Minecraft JS/TS 脚本引擎" - tagline: 神奇代码岛同款编程体验,用 JS/TS 在 Minecraft 里创造小游戏 + text: "Minecraft TypeScript 脚本引擎" + tagline: 不写 Java,不用 Gradle,不用重启。写 TypeScript,热重载秒级生效。 actions: - theme: brand text: 快速开始 link: /guide/getting-started + - theme: alt + text: 为什么选择 Box3JS? + link: /guide/about-box3js features: + - icon: ⚡ + title: TypeScript,零 Java + details: 所有 API 均提供完整 DTS 类型定义 + 双语 JSDoc。esbuild + Babel 构建管线自动转译现代 TS 到 ES5。无需 JDK、Gradle、IDE 配置。 + - icon: 🔄 + title: 秒级热重载 + details: 修改 → 保存 → 即时生效,不需要重启服务端。内置文件监听,保存即自动重载。 - icon: 🎮 + title: Box3 兼容 API + details: 延续神奇代码岛的简洁 API 风格。World、Entity、Player、Voxels、Storage、Database、HTTP、remoteChannel——全部全局注入,无需 import。 + - icon: 🧩 + title: 110+ MC 扩展 + details: 记分板、Bossbar、队伍、世界边界、粒子、烟花、闪电、药水、自定义方块/物品/音效注册……覆盖常见玩法需求。 + - icon: 🖥️ title: 服务端 & 客户端双端脚本 - details: 服务端:世界操作、实体、合成配方。客户端:键盘输入、屏幕 UI、音效、SQLite 存储、HTTP 请求。 + details: 服务端处理游戏逻辑和世界操作,客户端处理键盘、HUD、音效、自定义 GUI。通过 remoteChannel 双向实时通信。 + - icon: 🛡️ + title: 沙盒保护 + details: 开启沙盒自动追踪脚本所有改动,关闭沙盒完整回滚。放心测试,不破坏地图。 - icon: 📦 - title: TypeScript 优先 - details: 17 个全局对象全部提供 DTS 类型定义。内置 esbuild + Babel 构建管线,将现代 TS 转译为 Rhino 兼容的 ES5。 - - icon: 🔄 - title: 热重载 - details: 修改脚本即时生效,无需重启服务端。文件监听器在保存时自动重载。 - - icon: 🌐 - title: 双向通信 - details: remoteChannel 实现服务端↔客户端事件消息传递。服务端广播至所有玩家,客户端独立回复。 + title: 独立 JAR 分发 + details: /box3script compile 一键编译为独立模组,放入 mods/ 即可运行,无需 Box3JS 依赖。 - icon: 🗄️ - title: 双端存储 & 数据库 - details: 服务端和客户端均支持 JSON 文件持久化与 SQLite。分页查询、原子更新、计数器、标签模板查询。 - - icon: 🧩 - title: 自定义方块 & 物品 - details: 方块纹理、物品模型、装备、音效、创造标签页——全部通过 JSON 配置注册(独立/JAR 模式)。 - - icon: 📚 - title: 完善文档 - details: 50+ 页面,涵盖 API 参考、渐进式教程、常用配方、架构深入解析和常见问题——支持中英双语。 - - icon: 🚀 - title: 独立 JAR 模式 - details: 将脚本项目编译为独立 JAR 模组,无需依赖 Box3JS 运行时——放入 mods 文件夹即可使用。 + title: 内置持久化 + details: 服务端和客户端均支持 JSON 文件存储和 SQLite 数据库。排行榜、玩家存档、经济系统——全部内置。 --- ## 快速开始 ```bash -# 游戏内:创建新项目 -/box3script create mygame +/box3script create mygame # 创建 TypeScript 项目 +``` -# 构建并监听 +```bash cd config/box3/script/mygame -npm install -npm run build -- --watch - -# TypeScript 类型检查 -npm run check +npm install && npm run build # 安装依赖 + 构建 ``` -```ts -// src/server/app.ts — 你的第一个脚本 -world.onChat((entity, message) => { - if (message === "!hello") { - entity.player.directMessage(`你好,${entity.player.name}!`); - return false; - } - return true; -}); +```bash +/box3script sandbox mygame # 开启沙盒(推荐) +/box3script start mygame # 启动脚本 ``` -[查看完整文档 →](/guide/getting-started) - -## 版本信息 +打开 `src/server/app.ts` 写游戏逻辑,保存后 `/box3script reload mygame`。开启 `/box3script watch` 后保存即自动重载。 -| 组件 | 版本 | -|------|------| -| Minecraft | 1.21.1 | -| 模组加载器 | NeoForge | -| Java | 21 | -| JS 引擎 | Mozilla Rhino 1.9.1 (ES5) | -| TypeScript | 通过 Babel → ES5 | +[完整文档 →](/guide/getting-started) diff --git a/CLAUDE.md b/CLAUDE.md index da729076..510d0c2b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Box3Blocks is a Minecraft mod that imports 372 decorative blocks from the Box3 platform into Minecraft, supporting terrain file import/export and model items. It also includes **Box3JS**, a dual-side (server + client) TypeScript/JavaScript scripting engine (Rhino) for creating custom gameplay, mini-games, GUIs, and world interactions. -The repository is a **multi-project monorepo** with 7 independent subprojects targeting different mod loaders and Minecraft versions. There is no root build system — each subproject has its own Gradle wrapper and `build.gradle`. +The repository is a **multi-project monorepo** with 8 independent subprojects targeting different mod loaders and Minecraft versions. There is no root build system — each subproject has its own Gradle wrapper and `build.gradle`. ## Subprojects @@ -16,11 +16,12 @@ The repository is a **multi-project monorepo** with 7 independent subprojects ta | `Fabric-1.21.1/` | Fabric | 1.21.1 | 21 | `fabric-loom-remap` | | `Fabric-1.21.11/` | Fabric | 1.21.11 | 21 | `fabric-loom-remap` | | `Fabric-26.1/` | Fabric | 26.1 | 25 | `fabric-loom` | +| `Fabric-26.2/` | Fabric | 26.2 | 25 | `fabric-loom` | | `Forge-1.20.1/` | Forge | 1.20.1 | 17 | `net.minecraftforge.gradle` v6.x | | `NeoForge-1.21.1/` | NeoForge | 1.21.1 | 21 | **Box3JS lives here** — NeoForge ModDevGradle | | `NeoForge-26.1/` | NeoForge | 26.1 | 25 | NeoForge ModDevGradle | -Only NeoForge-1.21.1 has the Box3JS scripting engine. The other 6 subprojects are purely the Box3Blocks decorative block mod. +Only NeoForge-1.21.1 has the Box3JS scripting engine. The other 7 subprojects are purely the Box3Blocks decorative block mod. ## Build Commands @@ -39,7 +40,7 @@ cd run/config/box3/script/mygame && npm run build cd NeoForge-1.21.1 && node tools/verify-box3js-project.mjs ``` -**Important:** Forge-1.20.1 requires Java 17. All other subprojects use Java 21+. NeoForge-26.1 uses Java 25. +**Important:** Forge-1.20.1 requires Java 17. Fabric-1.21.1 and Fabric-1.21.11 use Java 21. Fabric-26.1, Fabric-26.2, and NeoForge-26.1 use Java 25. There are no existing tests (`src/test` directories are empty). @@ -48,7 +49,7 @@ There are no existing tests (`src/test` directories are empty). Shared resources are centralized to avoid ~20,000 duplicate asset files: - **`shared-resources/`** — used by ALL subprojects: block textures, models, blockstates, item models, worldgen data, `block-id.json`, `block-spec.json` -- **`shared-resources-fabric/`** — used by all 4 Fabric subprojects: `models/item/` JSONs + lang files +- **`shared-resources-fabric/`** — used by all 5 Fabric subprojects: `models/item/` JSONs + lang files - **`shared-resources-forge/`** — used by Forge + both NeoForge subprojects: `models/item/` JSONs + lang files ## Block Mod Architecture @@ -201,10 +202,11 @@ export default [ ## Version Differences -- `VoxelExport` only in Fabric-1.21.11, Fabric-26.1, and Forge/NeoForge variants +- `VoxelExport` only in Fabric-1.21.11, Fabric-26.1, Fabric-26.2, and Forge/NeoForge variants - `VoxelFluidRenderHandler` only in Fabric-1.21.11 - NeoForge-26.1 moved client code to `src/client/java` -- Fabric-26.1 uses `fabric-loom` (not `fabric-loom-remap`) and Java 25 +- Fabric-26.1 and Fabric-26.2 use `fabric-loom` (not `fabric-loom-remap`) and Java 25 +- Fabric-26.2 uses `EntityTypes.ITEM_DISPLAY` (not `EntityType.ITEM_DISPLAY`) and `Vec3.atCenterOf()` (not `BlockPos.getCenter()`) ## Tools diff --git a/Fabric-26.2/.gitattributes b/Fabric-26.2/.gitattributes new file mode 100644 index 00000000..097f9f98 --- /dev/null +++ b/Fabric-26.2/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/Fabric-26.2/.github/workflows/build.yml b/Fabric-26.2/.github/workflows/build.yml new file mode 100644 index 00000000..524fb1bd --- /dev/null +++ b/Fabric-26.2/.github/workflows/build.yml @@ -0,0 +1,30 @@ +# Automatically build the project and run any configured tests for every push +# and submitted pull request. This can help catch issues that only occur on +# certain platforms or Java versions, and provides a first line of defence +# against bad commits. + +name: build +on: [pull_request, push] + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - name: checkout repository + uses: actions/checkout@v6 + - name: validate gradle wrapper + uses: gradle/actions/wrapper-validation@v6 + - name: setup jdk + uses: actions/setup-java@v5 + with: + java-version: '25' + distribution: 'microsoft' + - name: make gradle wrapper executable + run: chmod +x ./gradlew + - name: build + run: ./gradlew build + - name: capture build artifacts + uses: actions/upload-artifact@v7 + with: + name: Artifacts + path: build/libs/ diff --git a/Fabric-26.2/.gitignore b/Fabric-26.2/.gitignore new file mode 100644 index 00000000..c476faf2 --- /dev/null +++ b/Fabric-26.2/.gitignore @@ -0,0 +1,40 @@ +# gradle + +.gradle/ +build/ +out/ +classes/ + +# eclipse + +*.launch + +# idea + +.idea/ +*.iml +*.ipr +*.iws + +# vscode + +.settings/ +.vscode/ +bin/ +.classpath +.project + +# macos + +*.DS_Store + +# fabric + +run/ + +# java + +hs_err_*.log +replay_*.log +*.hprof +*.jfr diff --git a/Fabric-26.2/build.gradle b/Fabric-26.2/build.gradle new file mode 100644 index 00000000..9eb3321e --- /dev/null +++ b/Fabric-26.2/build.gradle @@ -0,0 +1,91 @@ +plugins { + id 'net.fabricmc.fabric-loom' version "${loom_version}" + id 'maven-publish' +} + +version = project.mod_version +group = project.maven_group + +base { + archivesName = project.archives_base_name +} + +repositories { + // Add repositories to retrieve artifacts from in here. + // You should only use this when depending on other mods because + // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. + // See https://docs.gradle.org/current/userguide/declaring_repositories.html + // for more information about repositories. +} + +loom { + splitEnvironmentSourceSets() + + mods { + "box3" { + sourceSet sourceSets.main + sourceSet sourceSets.client + } + } + +} + +dependencies { + // To change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + + implementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. This is technically optional, but you probably want it anyway. + implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + +} + +processResources { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + inputs.property "version", project.version + + filesMatching("fabric.mod.json") { + expand "version": inputs.properties.version + } + from(file("../shared-resources")) + from(file("../shared-resources-fabric")) +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 25 +} + +java { + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. + withSourcesJar() + + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 +} + +jar { + inputs.property "archivesName", project.base.archivesName + + +} + +// configure the maven publication +publishing { + publications { + create("mavenJava", MavenPublication) { + artifactId = project.archives_base_name + from components.java + } + } + + // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. + repositories { + // Add repositories to publish to here. + // Notice: This block does NOT have the same function as the block in the top level. + // The repositories here will be used for publishing your artifact, not for + // retrieving dependencies. + } +} diff --git a/Fabric-26.2/gradle.properties b/Fabric-26.2/gradle.properties new file mode 100644 index 00000000..fdc31cdb --- /dev/null +++ b/Fabric-26.2/gradle.properties @@ -0,0 +1,20 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G +org.gradle.parallel=true + +# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349 +org.gradle.configuration-cache=false + +# Fabric Properties +# check these on https://fabricmc.net/develop +minecraft_version=26.2 +loader_version=0.19.3 +loom_version=1.17-SNAPSHOT + +# Mod Properties +mod_version=1.4.3-mc26.2 +maven_group=com.box3lab +archives_base_name=box3 + +# Dependencies +fabric_api_version=0.153.0+26.2 diff --git a/Fabric-26.2/gradle/wrapper/gradle-wrapper.jar b/Fabric-26.2/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..b1b8ef56b44f16b14dc800fa8103a6d89abb526f GIT binary patch literal 48462 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 R2np~f1o-$>Q}y+){|A@R9n$~+ literal 0 HcmV?d00001 diff --git a/Fabric-26.2/gradle/wrapper/gradle-wrapper.properties b/Fabric-26.2/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..df6a6ad7 --- /dev/null +++ b/Fabric-26.2/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/Fabric-26.2/gradlew b/Fabric-26.2/gradlew new file mode 100755 index 00000000..b9bb139f --- /dev/null +++ b/Fabric-26.2/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/Fabric-26.2/gradlew.bat b/Fabric-26.2/gradlew.bat new file mode 100644 index 00000000..24c62d56 --- /dev/null +++ b/Fabric-26.2/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/Fabric-26.2/settings.gradle b/Fabric-26.2/settings.gradle new file mode 100644 index 00000000..622ce07c --- /dev/null +++ b/Fabric-26.2/settings.gradle @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +// Should match your modid +rootProject.name = 'box3' diff --git a/Fabric-26.2/src/client/java/com/box3lab/Box3Client.java b/Fabric-26.2/src/client/java/com/box3lab/Box3Client.java new file mode 100644 index 00000000..649d7d52 --- /dev/null +++ b/Fabric-26.2/src/client/java/com/box3lab/Box3Client.java @@ -0,0 +1,9 @@ +package com.box3lab; + +import net.fabricmc.api.ClientModInitializer; + +public class Box3Client implements ClientModInitializer { + @Override + public void onInitializeClient() { + } +} \ No newline at end of file diff --git a/Fabric-26.2/src/client/resources/box3.client.mixins.json b/Fabric-26.2/src/client/resources/box3.client.mixins.json new file mode 100644 index 00000000..74c22c56 --- /dev/null +++ b/Fabric-26.2/src/client/resources/box3.client.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "package": "com.box3lab.client.mixin", + "compatibilityLevel": "JAVA_25", + "client": [], + "injectors": { + "defaultRequire": 1 + }, + "overwrites": { + "requireAnnotations": true + } +} \ No newline at end of file diff --git a/Fabric-26.2/src/main/java/com/box3lab/Box3.java b/Fabric-26.2/src/main/java/com/box3lab/Box3.java new file mode 100644 index 00000000..e3962bd5 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/Box3.java @@ -0,0 +1,31 @@ +package com.box3lab; + +import com.box3lab.command.ModCommands; +import com.box3lab.register.ModBlocks; +import com.box3lab.register.ModItems; + +import net.fabricmc.api.ModInitializer; + +// import org.slf4j.Logger; +// import org.slf4j.LoggerFactory; + +public class Box3 implements ModInitializer { + public static final String MOD_ID = "box3"; + + // This logger is used to write text to the console and the log file. + // It is considered best practice to use your mod id as the logger's name. + // That way, it's clear which mod wrote info, warnings, and errors. + // public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + @Override + public void onInitialize() { + // This code runs as soon as Minecraft is in a mod-load-ready state. + // However, some things (like resources) may still be uninitialized. + // Proceed with mild caution. + ModBlocks.initialize(); + ModItems.initialize(); + ModCommands.register(); + + // LOGGER.info("Hello Fabric world!"); + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/block/BarrierVoxelBlock.java b/Fabric-26.2/src/main/java/com/box3lab/block/BarrierVoxelBlock.java new file mode 100644 index 00000000..90e1a7f7 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/block/BarrierVoxelBlock.java @@ -0,0 +1,60 @@ +package com.box3lab.block; + +import com.box3lab.util.ConfigUtil; +import com.google.gson.JsonObject; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; + +import static com.box3lab.util.ConfigUtil.CONFIG_DIR_NAME; + +public class BarrierVoxelBlock extends VoxelBlock { + + private static volatile boolean visible = false; + + static { + loadConfig(); + } + + public BarrierVoxelBlock(BlockBehaviour.Properties properties) { + super(properties); + } + + @Override + public float getShadeBrightness(BlockState state, BlockGetter level, BlockPos pos) { + return visible ? 0F : 1.0F; + } + + public static boolean isVisible() { + return visible; + } + + public static void setVisible(boolean value) { + visible = value; + saveConfig(); + } + + private static void loadConfig() { + JsonObject obj = ConfigUtil.readConfig(CONFIG_DIR_NAME); + if (obj == null) { + return; + } + if (obj.has("barrierVisible")) { + visible = obj.get("barrierVisible").getAsBoolean(); + } + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return visible ? RenderShape.MODEL : RenderShape.INVISIBLE; + } + + private static void saveConfig() { + JsonObject obj = new JsonObject(); + obj.addProperty("barrierVisible", visible); + ConfigUtil.writeConfig(CONFIG_DIR_NAME, obj); + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/block/BouncePadBlock.java b/Fabric-26.2/src/main/java/com/box3lab/block/BouncePadBlock.java new file mode 100644 index 00000000..69119653 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/block/BouncePadBlock.java @@ -0,0 +1,44 @@ +package com.box3lab.block; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; + +public class BouncePadBlock extends VoxelBlock { + + public BouncePadBlock(BlockBehaviour.Properties properties) { + super(properties); + } + + @Override + public void stepOn(Level level, BlockPos pos, BlockState state, Entity entity) { + super.stepOn(level, pos, state, entity); + + if (entity.getY() < pos.getY() + 0.5D) { + return; + } + + Vec3 vel = entity.getDeltaMovement(); + + double bounce = 0.7D + level.getRandom().nextDouble() * 0.3D; + + if (vel.y < bounce) { + vel = new Vec3(vel.x, bounce, vel.z); + } + + entity.setDeltaMovement(vel); + } + + @Override + public void fallOn(Level level, BlockState state, BlockPos pos, Entity entity, double fallDistance) { + + if (entity.isSuppressingBounce()) { + super.fallOn(level, state, pos, entity, fallDistance); + } else { + entity.resetFallDistance(); + } + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/block/ConveyorBlock.java b/Fabric-26.2/src/main/java/com/box3lab/block/ConveyorBlock.java new file mode 100644 index 00000000..43a5855f --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/block/ConveyorBlock.java @@ -0,0 +1,36 @@ +package com.box3lab.block; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; + +public class ConveyorBlock extends VoxelBlock { + + public ConveyorBlock(BlockBehaviour.Properties properties) { + super(properties); + } + + @Override + public void stepOn(Level level, BlockPos pos, BlockState state, Entity entity) { + super.stepOn(level, pos, state, entity); + + Direction facing = state.getValue(HORIZONTAL_FACING); + double speed = 0.06D; + + Vec3 vel = entity.getDeltaMovement(); + switch (facing) { + case NORTH -> vel = new Vec3(vel.x, vel.y, vel.z - speed); + case SOUTH -> vel = new Vec3(vel.x, vel.y, vel.z + speed); + case WEST -> vel = new Vec3(vel.x - speed, vel.y, vel.z); + case EAST -> vel = new Vec3(vel.x + speed, vel.y, vel.z); + default -> { } + } + + entity.setDeltaMovement(vel); + + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/block/GlassVoxelBlock.java b/Fabric-26.2/src/main/java/com/box3lab/block/GlassVoxelBlock.java new file mode 100644 index 00000000..9c03be1d --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/block/GlassVoxelBlock.java @@ -0,0 +1,20 @@ +package com.box3lab.block; + +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; + +public class GlassVoxelBlock extends VoxelBlock { + + public GlassVoxelBlock(BlockBehaviour.Properties properties) { + super(properties); + } + + @Override + public boolean skipRendering(BlockState state, BlockState adjacentState, Direction side) { + if (adjacentState.getBlock() == state.getBlock()) { + return true; + } + return super.skipRendering(state, adjacentState, side); + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/block/SpiderWebBlock.java b/Fabric-26.2/src/main/java/com/box3lab/block/SpiderWebBlock.java new file mode 100644 index 00000000..1b1e1d75 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/block/SpiderWebBlock.java @@ -0,0 +1,35 @@ +package com.box3lab.block; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; + +public class SpiderWebBlock extends VoxelBlock { + + public SpiderWebBlock(BlockBehaviour.Properties properties) { + super(properties); + } + + @Override + public void stepOn(Level level, BlockPos pos, BlockState state, Entity entity) { + super.stepOn(level, pos, state, entity); + + if (entity instanceof LivingEntity) { + LivingEntity living = (LivingEntity) entity; + + + boolean isSprinting = living.isSprinting(); + + if (isSprinting) { + Vec3 movement = living.getDeltaMovement(); + living.setDeltaMovement(movement.multiply(0.15, 1.0, 0.15)); + } else { + living.setDeltaMovement(Vec3.ZERO); + } + } + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/block/VoxelBlock.java b/Fabric-26.2/src/main/java/com/box3lab/block/VoxelBlock.java new file mode 100644 index 00000000..66c1886f --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/block/VoxelBlock.java @@ -0,0 +1,49 @@ +package com.box3lab.block; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.EnumProperty; + +public class VoxelBlock extends Block { + public static final EnumProperty HORIZONTAL_FACING = BlockStateProperties.HORIZONTAL_FACING; + + public VoxelBlock(BlockBehaviour.Properties properties) { + super(properties); + this.registerDefaultState(this.stateDefinition.any().setValue(HORIZONTAL_FACING, Direction.NORTH)); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(HORIZONTAL_FACING); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(HORIZONTAL_FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(HORIZONTAL_FACING, rotation.rotate(state.getValue(HORIZONTAL_FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(HORIZONTAL_FACING))); + } + + @Override + public boolean isCollisionShapeFullBlock(BlockState state, BlockGetter level, BlockPos pos) { + return super.isCollisionShapeFullBlock(state, level, pos); + } + +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/block/entity/PackModelBlockEntity.java b/Fabric-26.2/src/main/java/com/box3lab/block/entity/PackModelBlockEntity.java new file mode 100644 index 00000000..4ab3efa7 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/block/entity/PackModelBlockEntity.java @@ -0,0 +1,276 @@ +package com.box3lab.block.entity; + +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import com.box3lab.register.modelbe.PackModelBlockEntityRegistrar; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.permissions.PermissionSet; +import net.minecraft.world.entity.Display; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityTypes; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.AABB; + +public class PackModelBlockEntity extends BlockEntity { + private static final long RESPAWN_INTERVAL_TICKS = 20L; + private static final String DISPLAY_TAG_PREFIX = "box3_pack_model:"; + + private static final float SCALE_STEP = 0.1F; + private static final float SCALE_MIN = 0.1F; + private static final float SCALE_MAX = 4.0F; + private static final float OFFSET_STEP = 0.05F; + private static final float ROTATION_STEP = 15.0F; + private static final Map CONFIG_CLIPBOARD = new ConcurrentHashMap<>(); + + private float scale = 1.0F; + private float offsetX = 0.0F; + private float offsetY = 0.0F; + private float offsetZ = 0.0F; + private float rotationOffset = 0.0F; + private int modeIndex = 0; + + public PackModelBlockEntity(BlockPos pos, BlockState state) { + super(PackModelBlockEntityRegistrar.typeFor(state.getBlock()), pos, state); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.scale = clamp(input.getFloatOr("scale", 1.0F), SCALE_MIN, SCALE_MAX); + this.offsetX = input.getFloatOr("offset_x", 0.0F); + this.offsetY = input.getFloatOr("offset_y", 0.0F); + this.offsetZ = input.getFloatOr("offset_z", 0.0F); + this.rotationOffset = input.getFloatOr("rotation_offset", 0.0F); + this.modeIndex = Math.floorMod(input.getIntOr("config_mode", 0), Mode.values().length); + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putFloat("scale", this.scale); + output.putFloat("offset_x", this.offsetX); + output.putFloat("offset_y", this.offsetY); + output.putFloat("offset_z", this.offsetZ); + output.putFloat("rotation_offset", this.rotationOffset); + output.putInt("config_mode", this.modeIndex); + } + + @Override + public void setRemoved() { + removeDisplaysAt(this.level, this.getBlockPos()); + super.setRemoved(); + } + + public static void serverTick(Level level, BlockPos pos, BlockState state, PackModelBlockEntity blockEntity) { + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + + String tag = displayTag(pos); + var displays = findDisplays(serverLevel, pos, tag); + + if (displays.isEmpty()) { + if (serverLevel.getGameTime() % RESPAWN_INTERVAL_TICKS != 0) { + return; + } + spawnDisplay(serverLevel, pos, state, blockEntity, tag); + return; + } + + for (Display.ItemDisplay display : displays) { + blockEntity.applyPose(serverLevel, pos, state, display); + } + } + + public void cycleMode(net.minecraft.world.entity.player.Player player) { + this.modeIndex = (this.modeIndex + 1) % Mode.values().length; + this.setChanged(); + player.sendSystemMessage(Component.translatable( + "message.box3.model.config.mode", + Component.translatable(currentMode().translationKey()))); + } + + public void adjustCurrentMode(ServerLevel level, BlockPos pos, BlockState state, int direction, + net.minecraft.world.entity.player.Player player) { + Mode mode = currentMode(); + switch (mode) { + case SCALE -> this.scale = clamp(this.scale + SCALE_STEP * direction, SCALE_MIN, SCALE_MAX); + case OFFSET_X -> this.offsetX += OFFSET_STEP * direction; + case OFFSET_Y -> this.offsetY += OFFSET_STEP * direction; + case OFFSET_Z -> this.offsetZ += OFFSET_STEP * direction; + case ROTATION -> this.rotationOffset = normalizeDegrees(this.rotationOffset + ROTATION_STEP * direction); + } + + this.setChanged(); + player.sendSystemMessage(statusComponent()); + applyToDisplays(level, pos, state); + } + + public void copyConfig(net.minecraft.world.entity.player.Player player) { + CONFIG_CLIPBOARD.put(player.getUUID(), + new ConfigSnapshot(this.scale, this.offsetX, this.offsetY, this.offsetZ, this.rotationOffset)); + player.sendSystemMessage(Component.translatable("message.box3.model.config.copy.success")); + } + + public void pasteConfig(ServerLevel level, BlockPos pos, BlockState state, + net.minecraft.world.entity.player.Player player) { + ConfigSnapshot snapshot = CONFIG_CLIPBOARD.get(player.getUUID()); + if (snapshot == null) { + player.sendSystemMessage(Component.translatable("message.box3.model.config.copy.empty")); + return; + } + + this.scale = clamp(snapshot.scale, SCALE_MIN, SCALE_MAX); + this.offsetX = snapshot.offsetX; + this.offsetY = snapshot.offsetY; + this.offsetZ = snapshot.offsetZ; + this.rotationOffset = normalizeDegrees(snapshot.rotationOffset); + this.setChanged(); + + applyToDisplays(level, pos, state); + player.sendSystemMessage(Component.translatable("message.box3.model.config.copy.pasted")); + player.sendSystemMessage(statusComponent()); + } + + public static void removeDisplaysAt(Level level, BlockPos pos) { + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + + String tag = displayTag(pos); + for (Display.ItemDisplay display : findDisplays(serverLevel, pos, tag)) { + display.discard(); + display.setCustomName(null); + } + } + + private static void spawnDisplay(ServerLevel level, BlockPos pos, BlockState state, PackModelBlockEntity be, + String tag) { + Display.ItemDisplay display = EntityTypes.ITEM_DISPLAY.create(level, EntitySpawnReason.SPAWN_ITEM_USE); + if (display == null) { + return; + } + + display.setNoGravity(true); + display.setInvulnerable(true); + display.getSlot(0).set(new ItemStack(state.getBlock())); + display.setCustomName(Component.literal(tag)); + display.setCustomNameVisible(false); + + be.applyPose(level, pos, state, display); + level.addFreshEntity(display); + } + + private void applyPose(ServerLevel level, BlockPos pos, BlockState state, Display.ItemDisplay display) { + double x = pos.getX() + 0.5D; + double y = pos.getY() + this.offsetY; + double z = pos.getZ() + 0.5D; + display.setPos(x, y, z); + + float baseYaw = 0.0F; + if (state.hasProperty(PackModelEntityBlock.HORIZONTAL_FACING)) { + Direction facing = state.getValue(PackModelEntityBlock.HORIZONTAL_FACING); + baseYaw = facing.toYRot(); + } + display.setYRot(normalizeDegrees(baseYaw + this.rotationOffset)); + + applyDisplayTransformation(level, display); + } + + private void applyDisplayTransformation(ServerLevel level, Display.ItemDisplay display) { + MinecraftServer server = level.getServer(); + CommandSourceStack source = server.createCommandSourceStack().withSuppressedOutput() + .withPermission(PermissionSet.ALL_PERMISSIONS); + + String cmd = String.format( + Locale.ROOT, + "data merge entity %s {item_display:\"fixed\",transformation:{translation:[%sf,%sf,%sf],left_rotation:[0f,0f,0f,1f],scale:[%sf,%sf,%sf],right_rotation:[0f,0f,0f,1f]}}", + display.getStringUUID(), + fmt(this.offsetX), fmt(0.0F), fmt(this.offsetZ), + fmt(this.scale), fmt(this.scale), fmt(this.scale)); + server.getCommands().performPrefixedCommand(source, cmd); + } + + private static String fmt(float value) { + return String.format(Locale.ROOT, "%.3f", value); + } + + private static java.util.List findDisplays(ServerLevel level, BlockPos pos, String tag) { + return level.getEntitiesOfClass( + Display.ItemDisplay.class, + new AABB(pos).inflate(0.25D), + display -> { + Component name = display.getCustomName(); + return name != null && tag.equals(name.getString()); + }); + } + + private void applyToDisplays(ServerLevel level, BlockPos pos, BlockState state) { + for (Display.ItemDisplay display : findDisplays(level, pos, displayTag(pos))) { + applyPose(level, pos, state, display); + } + } + + private Mode currentMode() { + return Mode.values()[this.modeIndex]; + } + + private Component statusComponent() { + return Component.translatable( + "message.box3.model.config.status", + Component.translatable(currentMode().translationKey()), + String.format(Locale.ROOT, "%.2f", this.scale), + String.format(Locale.ROOT, "%.2f", this.offsetX), + String.format(Locale.ROOT, "%.2f", this.offsetY), + String.format(Locale.ROOT, "%.2f", this.offsetZ), + String.format(Locale.ROOT, "%.1f", this.rotationOffset)); + } + + private static float clamp(float value, float min, float max) { + return Math.max(min, Math.min(max, value)); + } + + private static float normalizeDegrees(float value) { + float v = value % 360.0F; + return v < 0.0F ? v + 360.0F : v; + } + + private static String displayTag(BlockPos pos) { + return DISPLAY_TAG_PREFIX + pos.asLong(); + } + + private enum Mode { + SCALE("scale"), + OFFSET_X("offset_x"), + OFFSET_Y("offset_y"), + OFFSET_Z("offset_z"), + ROTATION("rotation"); + + private final String keyPart; + + Mode(String keyPart) { + this.keyPart = keyPart; + } + + public String translationKey() { + return "message.box3.model.config.mode." + this.keyPart; + } + } + + private record ConfigSnapshot(float scale, float offsetX, float offsetY, float offsetZ, float rotationOffset) { + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/block/entity/PackModelEntityBlock.java b/Fabric-26.2/src/main/java/com/box3lab/block/entity/PackModelEntityBlock.java new file mode 100644 index 00000000..bd449cad --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/block/entity/PackModelEntityBlock.java @@ -0,0 +1,156 @@ +package com.box3lab.block.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.phys.BlockHitResult; +import org.jspecify.annotations.Nullable; + +public class PackModelEntityBlock extends Block implements EntityBlock { + public static final EnumProperty HORIZONTAL_FACING = BlockStateProperties.HORIZONTAL_FACING; + + public PackModelEntityBlock(BlockBehaviour.Properties properties) { + super(properties); + this.registerDefaultState(this.stateDefinition.any().setValue(HORIZONTAL_FACING, Direction.NORTH)); + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new PackModelBlockEntity(pos, state); + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } + + @Override + protected void spawnDestroyParticles(Level level, Player player, BlockPos pos, BlockState state) { + // This block is rendered by ItemDisplay instead of block model, + // so vanilla block-break particles would resolve to missing textures. + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(HORIZONTAL_FACING); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(HORIZONTAL_FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(HORIZONTAL_FACING, rotation.rotate(state.getValue(HORIZONTAL_FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(HORIZONTAL_FACING))); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, + BlockHitResult hitResult) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof PackModelBlockEntity modelBe) { + modelBe.cycleMode(player); + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + + @Override + protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, + InteractionHand hand, BlockHitResult hitResult) { + if (stack.is(Items.PAPER)) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof PackModelBlockEntity modelBe) { + modelBe.copyConfig(player); + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + + if (stack.is(Items.BOOK)) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof PackModelBlockEntity modelBe && level instanceof net.minecraft.server.level.ServerLevel serverLevel) { + modelBe.pasteConfig(serverLevel, pos, state, player); + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + + int direction; + if (stack.is(Items.STICK)) { + direction = 1; + } else if (stack.is(Items.BLAZE_ROD)) { + direction = -1; + } else { + return InteractionResult.TRY_WITH_EMPTY_HAND; + } + + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof PackModelBlockEntity modelBe && level instanceof net.minecraft.server.level.ServerLevel serverLevel) { + modelBe.adjustCurrentMode(serverLevel, pos, state, direction, player); + return InteractionResult.SUCCESS; + } + + return InteractionResult.PASS; + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + if (level.isClientSide()) { + return null; + } + + BlockEntityType expectedType = com.box3lab.register.modelbe.PackModelBlockEntityRegistrar.typeFor(state.getBlock()); + if (blockEntityType != expectedType) { + return null; + } + + return (lvl, pos, blockState, be) -> PackModelBlockEntity.serverTick( + lvl, + pos, + blockState, + (PackModelBlockEntity) be); + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/command/ModCommands.java b/Fabric-26.2/src/main/java/com/box3lab/command/ModCommands.java new file mode 100644 index 00000000..d41febaf --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/command/ModCommands.java @@ -0,0 +1,388 @@ +package com.box3lab.command; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.box3lab.block.BarrierVoxelBlock; +import com.box3lab.register.VoxelExport; +import com.box3lab.register.VoxelImport; +import com.box3lab.util.Box3ImportFiles; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.suggestion.SuggestionProvider; + +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.commands.CommandSourceStack; +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.phys.Vec3; + +public final class ModCommands { + private ModCommands() { + } + + private static final String DEFAULT_EXPORT_MARKER_BLOCK = "minecraft:redstone_block"; + private static final int MAX_MARKER_SCAN_RADIUS = 1024; + private static final int MARKER_Y_TOLERANCE = 512; + + private static final SuggestionProvider BOX3_FILE_SUGGESTIONS = (context, builder) -> { + try { + List files = Box3ImportFiles.listJsonFiles(); + for (String file : files) { + String name = file; + if (name.endsWith(".gz")) { + name = name.substring(0, name.length() - 3); + } + builder.suggest(name); + } + } catch (IOException ignored) { + + } + return builder.buildFuture(); + }; + + public static void register() { + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { + dispatcher.register( + literal("box3import") + .executes(context -> listBox3ImportFiles(context.getSource())) + .then(argument("fileName", StringArgumentType.word()) + .suggests(BOX3_FILE_SUGGESTIONS) + // /box3import + .executes(context -> executeBox3Import( + context.getSource(), + StringArgumentType.getString( + context, + "fileName"), + 0, + false, + false)) + // /box3import + .then(argument("offsetY", + IntegerArgumentType.integer()) + .executes(context -> executeBox3Import( + context.getSource(), + StringArgumentType + .getString( + context, + "fileName"), + IntegerArgumentType + .getInteger( + context, + "offsetY"), + false, + false)) + // /box3import + // + .then(argument("ignoreBarrier", + BoolArgumentType.bool()) + .executes(context -> executeBox3Import( + context.getSource(), + StringArgumentType + .getString( + context, + "fileName"), + IntegerArgumentType + .getInteger( + context, + "offsetY"), + BoolArgumentType.getBool( + context, + "ignoreBarrier"), + false)) + // /box3import + // + // + // + // + .then(argument("ignoreWater", + BoolArgumentType.bool()) + .executes(context -> executeBox3Import( + context.getSource(), + StringArgumentType + .getString( + context, + "fileName"), + IntegerArgumentType + .getInteger( + context, + "offsetY"), + BoolArgumentType.getBool( + context, + "ignoreBarrier"), + BoolArgumentType.getBool( + context, + "ignoreWater")))))))); + + dispatcher.register( + literal("box3barrier") + .executes(context -> showBarrierStatus(context.getSource())) + .then(argument("value", BoolArgumentType.bool()) + .executes(context -> setBarrierVisible( + context.getSource(), + BoolArgumentType.getBool( + context, + "value"))) + .then(literal("toggle") + .executes(context -> toggleBarrierVisible( + context.getSource()))))); + + dispatcher.register( + literal("box3export") + .executes(context -> showBox3ExportUsage(context.getSource())) + .then(argument("fileName", StringArgumentType.word()) + .executes(context -> executeBox3ExportByMarkers( + context.getSource(), + StringArgumentType.getString( + context, + "fileName"))))); + }); + } + + private static int listBox3ImportFiles(CommandSourceStack source) { + var dir = Box3ImportFiles.getImportDir(); + + try { + List files = Box3ImportFiles.listJsonFiles(); + + if (files.isEmpty()) { + source.sendSuccess( + () -> Component.translatable( + "command.box3.box3import.list.empty", + dir.toString()), + false); + } else { + String joined = String.join(", ", files); + source.sendSuccess( + () -> Component.translatable( + "command.box3.box3import.list.success", + dir.toString(), joined), + false); + } + } catch (IOException e) { + source.sendFailure( + Component.translatable( + "command.box3.box3import.list.error", + dir.toString(), e.getMessage())); + } + + return 1; + } + + private static int showBox3ExportUsage(CommandSourceStack source) { + source.sendFailure(Component.translatable("command.box3.box3export.usage")); + return 0; + } + + private static String resolveMapName(String fileName) { + if (fileName != null && fileName.startsWith("Box3-")) { + String suffix = fileName.substring("Box3-".length()); + if (!suffix.isEmpty()) { + return "https://box3lab.com/mc/build/" + suffix + ".gz"; + } + } + return fileName; + } + + private static int executeBox3Import(CommandSourceStack source, String fileName, + int offsetY, boolean ignoreBarrier, boolean useVanillaWater) { + ServerLevel level = source.getServer().overworld(); + try { + ServerPlayer player = source.getPlayer(); + String mapName = resolveMapName(fileName); + var basePos = player != null ? player.position() : Vec3.atCenterOf(new BlockPos(0, 0, 0)); + var offsetPos = basePos.add(0, offsetY, 0); + + VoxelImport.apply(null, level, mapName, + offsetPos, + player, + ignoreBarrier, + useVanillaWater); + + source.sendSuccess( + () -> Component.translatable("command.box3.box3import.success", + mapName), + false); + } catch (Exception e) { + source.sendFailure( + Component.translatable("command.box3.box3import.failure", e.getMessage())); + } + return 1; + } + + private static int showBarrierStatus(CommandSourceStack source) { + boolean visible = BarrierVoxelBlock.isVisible(); + source.sendSuccess( + () -> Component.translatable("command.box3.box3barrier.status", + String.valueOf(visible)), + false); + return 1; + } + + private static int setBarrierVisible(CommandSourceStack source, boolean value) { + BarrierVoxelBlock.setVisible(value); + source.sendSuccess( + () -> Component.translatable("command.box3.box3barrier.set", String.valueOf(value)), + false); + return 1; + } + + private static int toggleBarrierVisible(CommandSourceStack source) { + boolean current = BarrierVoxelBlock.isVisible(); + boolean next = !current; + BarrierVoxelBlock.setVisible(next); + source.sendSuccess( + () -> Component.translatable("command.box3.box3barrier.toggled", + String.valueOf(next)), + false); + return 1; + } + + private static int executeBox3Export(CommandSourceStack source, String fileName, + int x1, int y1, int z1, int x2, int y2, int z2) { + ServerLevel level = source.getLevel(); + BlockPos from = new BlockPos(x1, y1, z1); + BlockPos to = new BlockPos(x2, y2, z2); + + try { + VoxelExport.ExportResult result = VoxelExport.exportRegion(level, from, to, fileName); + source.sendSuccess( + () -> Component.translatable( + "command.box3.box3export.success", + result.output().toString(), + result.scannedBlocks(), + result.exportedBlocks()), + false); + } catch (Exception e) { + source.sendFailure( + Component.translatable("command.box3.box3export.failure", e.getMessage())); + } + return 1; + } + + private static int executeBox3ExportByMarkers(CommandSourceStack source, String fileName) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.translatable("command.box3.box3export.player_only")); + return 0; + } + + Block markerBlock = resolveMarkerBlock(DEFAULT_EXPORT_MARKER_BLOCK); + if (markerBlock == null) { + source.sendFailure(Component.translatable("command.box3.box3export.marker_invalid", + DEFAULT_EXPORT_MARKER_BLOCK)); + return 0; + } + + List positions = findMarkerPositions(source.getLevel(), player.blockPosition(), markerBlock, + MAX_MARKER_SCAN_RADIUS, MARKER_Y_TOLERANCE, 2); + if (positions.size() < 2) { + source.sendFailure(Component.translatable( + "command.box3.box3export.marker_count_invalid", + MAX_MARKER_SCAN_RADIUS, + positions.size(), + BuiltInRegistries.BLOCK.getKey(markerBlock).toString())); + return 0; + } + + BlockPos p1 = positions.get(0); + BlockPos p2 = positions.get(1); + return executeBox3Export(source, fileName, p1.getX(), p1.getY(), p1.getZ(), p2.getX(), p2.getY(), + p2.getZ()); + } + + private static Block resolveMarkerBlock(String blockId) { + Identifier id = Identifier.tryParse(blockId); + if (id == null) { + return null; + } + if (!BuiltInRegistries.BLOCK.containsKey(id)) { + return null; + } + return BuiltInRegistries.BLOCK.get(id).map(holder -> holder.value()).orElse(null); + } + + private static List findMarkerPositions(ServerLevel level, BlockPos center, Block markerBlock, + int maxRadius, int yTolerance, int maxResults) { + List positions = new ArrayList<>(); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + int cx = center.getX(); + int cy = center.getY(); + int cz = center.getZ(); + + for (int radius = 0; radius <= maxRadius; radius++) { + int minX = cx - radius; + int maxX = cx + radius; + int minZ = cz - radius; + int maxZ = cz + radius; + + if (radius == 0) { + if (scanMarkerColumn(level, markerBlock, cy, yTolerance, cx, cz, cursor, positions, + maxResults)) { + return positions; + } + continue; + } + + for (int x = minX; x <= maxX; x++) { + if (scanMarkerColumn(level, markerBlock, cy, yTolerance, x, minZ, cursor, positions, + maxResults)) { + return positions; + } + if (scanMarkerColumn(level, markerBlock, cy, yTolerance, x, maxZ, cursor, positions, + maxResults)) { + return positions; + } + } + + for (int z = minZ + 1; z <= maxZ - 1; z++) { + if (scanMarkerColumn(level, markerBlock, cy, yTolerance, minX, z, cursor, positions, + maxResults)) { + return positions; + } + if (scanMarkerColumn(level, markerBlock, cy, yTolerance, maxX, z, cursor, positions, + maxResults)) { + return positions; + } + } + } + return positions; + } + + private static boolean scanMarkerColumn(ServerLevel level, Block markerBlock, int centerY, int yTolerance, int x, int z, + BlockPos.MutableBlockPos cursor, List positions, int maxResults) { + for (int dy = 0; dy <= yTolerance; dy++) { + int y1 = centerY + dy; + cursor.set(x, y1, z); + if (level.hasChunkAt(cursor) && level.getBlockState(cursor).getBlock() == markerBlock) { + positions.add(cursor.immutable()); + if (positions.size() >= maxResults) { + return true; + } + } + + if (dy == 0) { + continue; + } + + int y2 = centerY - dy; + cursor.set(x, y2, z); + if (level.hasChunkAt(cursor) && level.getBlockState(cursor).getBlock() == markerBlock) { + positions.add(cursor.immutable()); + if (positions.size() >= maxResults) { + return true; + } + } + } + return false; + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/register/ModBlocks.java b/Fabric-26.2/src/main/java/com/box3lab/register/ModBlocks.java new file mode 100644 index 00000000..2853bc1b --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/register/ModBlocks.java @@ -0,0 +1,69 @@ +package com.box3lab.register; + +import java.util.HashMap; +import java.util.Map; + +import com.box3lab.Box3; +import com.box3lab.register.core.BlockRegistrar; +import com.box3lab.register.creative.CreativeTabRegistrar; +import com.box3lab.register.modelbe.PackModelBlockEntityRegistrar; +import com.box3lab.register.sound.CategorySoundTypes; +import com.box3lab.register.voxel.VoxelBlockFactories; +import com.box3lab.register.voxel.VoxelBlockPropertiesFactory; +import com.box3lab.register.voxel.VoxelLightLevelMapper; +import com.box3lab.util.BlockIndexData; +import com.box3lab.util.BlockIndexUtil; + +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; + +public class ModBlocks { + public static final Map BLOCKS = new HashMap<>(); + + public static void initialize() { + + BlockIndexData data = BlockIndexData.get(); + + for (int i = 0; i < data.ids.length; i++) { + int id = data.ids[i]; + if (id == 0) { + continue; + } + if (BlockIndexUtil.isFluid(id)) { + continue; + } + + String voxelName = data.names[i]; + String texturePart = voxelName.toLowerCase(java.util.Locale.ROOT); + + String category = data.categoryByName.getOrDefault(texturePart, ""); + SoundType soundType = CategorySoundTypes.soundTypeForCategory(category); + + int emissive = BlockIndexUtil.blockEmissiveLight(id); + final int lightLevel = VoxelLightLevelMapper.lightLevelFromEmissivePacked(emissive); + + boolean solid = BlockIndexUtil.isSolid(id); + boolean transparent = !solid; + + int index = data.indexById.get(id); + float hardness = data.blockHardness[index]; + float resistance = data.blockResistance[index]; + float friction = data.blockFriction[index]; + + var props = VoxelBlockPropertiesFactory.create(solid, soundType, lightLevel, hardness, resistance, + friction); + + Block block = BlockRegistrar.register( + Box3.MOD_ID, + texturePart, + VoxelBlockFactories.factoryFor(texturePart, transparent), + props, + true); + BLOCKS.put(texturePart, block); + } + + CreativeTabRegistrar.registerCreativeTabs(Box3.MOD_ID, BLOCKS, data); + PackModelBlockEntityRegistrar.registerAll(); + } + +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/register/ModItems.java b/Fabric-26.2/src/main/java/com/box3lab/register/ModItems.java new file mode 100644 index 00000000..f8f2634c --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/register/ModItems.java @@ -0,0 +1,11 @@ +package com.box3lab.register; + +public final class ModItems { + + private ModItems() { + } + + public static void initialize() { + // Reserved for future item registrations. + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/register/VoxelExport.java b/Fabric-26.2/src/main/java/com/box3lab/register/VoxelExport.java new file mode 100644 index 00000000..68735942 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/register/VoxelExport.java @@ -0,0 +1,148 @@ +package com.box3lab.register; + +import static com.box3lab.Box3.MOD_ID; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.GZIPOutputStream; + +import com.box3lab.util.BlockIdResolver; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; + +public final class VoxelExport { + + private VoxelExport() { + } + + public static ExportResult exportRegion(ServerLevel level, BlockPos from, BlockPos to, String fileName) throws IOException { + BlockPos min = new BlockPos( + Math.min(from.getX(), to.getX()), + Math.min(from.getY(), to.getY()), + Math.min(from.getZ(), to.getZ())); + BlockPos max = new BlockPos( + Math.max(from.getX(), to.getX()), + Math.max(from.getY(), to.getY()), + Math.max(from.getZ(), to.getZ())); + + int sizeX = max.getX() - min.getX() + 1; + int sizeY = max.getY() - min.getY() + 1; + int sizeZ = max.getZ() - min.getZ() + 1; + + List indices = new ArrayList<>(); + List data = new ArrayList<>(); + List rot = new ArrayList<>(); + + for (int z = 0; z < sizeZ; z++) { + for (int y = 0; y < sizeY; y++) { + for (int x = 0; x < sizeX; x++) { + BlockPos pos = min.offset(x, y, z); + BlockState state = level.getBlockState(pos); + int id = BlockIdResolver.getIdByBlock(state.getBlock()); + if (id == 0) { + continue; + } + + int idx = x + y * sizeX + z * sizeX * sizeY; + indices.add(idx); + data.add(id); + rot.add(toRotationIndex(state)); + } + } + } + + JsonObject root = new JsonObject(); + root.add("shape", intArray(sizeX, sizeY, sizeZ)); + root.add("dir", intArray(1, 1, 1)); + root.add("indices", toJsonArray(indices)); + root.add("data", toJsonArray(data)); + root.add("rot", toJsonArray(rot)); + + Path output = resolveOutput(fileName); + Files.createDirectories(output.getParent()); + writeGzipJson(output, root.toString()); + + return new ExportResult(output, sizeX * sizeY * sizeZ, indices.size()); + } + + private static int toRotationIndex(BlockState state) { + Direction dir = null; + if (state.hasProperty(BlockStateProperties.HORIZONTAL_FACING)) { + dir = state.getValue(BlockStateProperties.HORIZONTAL_FACING); + } else if (state.hasProperty(BlockStateProperties.FACING)) { + Direction facing = state.getValue(BlockStateProperties.FACING); + if (facing.getAxis().isHorizontal()) { + dir = facing; + } + } + + if (dir == null) { + return 0; + } + + Rotation rotation = switch (dir) { + case EAST -> Rotation.CLOCKWISE_90; + case SOUTH -> Rotation.CLOCKWISE_180; + case WEST -> Rotation.COUNTERCLOCKWISE_90; + default -> Rotation.NONE; + }; + + return switch (rotation) { + case CLOCKWISE_90 -> 1; + case CLOCKWISE_180 -> 2; + case COUNTERCLOCKWISE_90 -> 3; + default -> 0; + }; + } + + private static JsonArray intArray(int a, int b, int c) { + JsonArray array = new JsonArray(); + array.add(a); + array.add(b); + array.add(c); + return array; + } + + private static JsonArray toJsonArray(List values) { + JsonArray array = new JsonArray(); + for (Integer value : values) { + array.add(value); + } + return array; + } + + private static Path resolveOutput(String fileName) { + String cleaned = (fileName == null || fileName.isBlank()) ? "export" : fileName.trim(); + if (!cleaned.endsWith(".gz")) { + cleaned = cleaned + ".gz"; + } + return FabricLoader.getInstance() + .getConfigDir() + .resolve(MOD_ID) + .resolve(cleaned); + } + + private static void writeGzipJson(Path outputPath, String json) throws IOException { + try (OutputStream fos = Files.newOutputStream(outputPath); + GZIPOutputStream gos = new GZIPOutputStream(fos)) { + gos.write(json.getBytes(StandardCharsets.UTF_8)); + gos.finish(); + } + } + + public record ExportResult(Path output, int scannedBlocks, int exportedBlocks) { + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/register/VoxelImport.java b/Fabric-26.2/src/main/java/com/box3lab/register/VoxelImport.java new file mode 100644 index 00000000..a95ce28e --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/register/VoxelImport.java @@ -0,0 +1,178 @@ +package com.box3lab.register; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.GZIPInputStream; + +import static com.box3lab.Box3.MOD_ID; +import com.box3lab.util.BlockIdResolver; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.phys.Vec3; + +public final class VoxelImport { + + public static void apply(Object voxels, Object world, String mapName, Vec3 customOrigin, + ServerPlayer player, boolean ignoreBarrier, boolean ignoreWater) throws Exception { + JsonObject cfg = loadConfig(mapName); + if (cfg == null) { + throw new Exception(Component + .translatable("command.box3.box3import.config_invalid") + .getString()); + } + + int[] shape = toIntArray(cfg.getAsJsonArray("shape")); + int[] dir = toIntArray(cfg.getAsJsonArray("dir")); + int[] indices = toIntArray(cfg.getAsJsonArray("indices")); + int[] data = toIntArray(cfg.getAsJsonArray("data")); + int[] rot = toIntArray(cfg.getAsJsonArray("rot")); + + int total = Math.min(indices.length, data.length); + int lastProgress = -1; + + int[] origin = new int[] { + (int) Math.floor(customOrigin.x), + (int) Math.floor(customOrigin.y), + (int) Math.floor(customOrigin.z) + }; + + for (int i = 0; i < indices.length && i < data.length; i++) { + int idx = indices[i]; + int id = data[i]; + + if (ignoreBarrier && BlockIdResolver.isBarrierId(id)) { + continue; + } + + int x = idx % shape[0]; + int y = (idx / shape[0]) % shape[1]; + int z = idx / (shape[0] * shape[1]); + int r = rot.length > i ? rot[i] : 0; + int wx = origin[0] + dir[0] * x; + int wy = origin[1] + dir[1] * y; + int wz = origin[2] + dir[2] * z; + + if (world instanceof ServerLevel level) { + BlockPos pos = new BlockPos(wx, wy, wz); + Block block = BlockIdResolver.getBlockById(id, ignoreWater); + Rotation rotation = switch (r & 3) { + case 1 -> Rotation.CLOCKWISE_90; + case 2 -> Rotation.CLOCKWISE_180; + case 3 -> Rotation.COUNTERCLOCKWISE_90; + default -> Rotation.NONE; + }; + + level.setBlock(pos, block.defaultBlockState().rotate(rotation), 3); + + if (player != null) { + int progress = (i + 1) * 100 / total; + if (progress / 10 > lastProgress / 10) { + lastProgress = progress; + player.sendSystemMessage(Component.translatable( + "command.box3.box3import.progress", mapName, progress)); + } + } + } + } + } + + private static JsonObject loadConfig(String mapName) { + if (mapName.startsWith("http://") || mapName.startsWith("https://")) { + try { + String json = readGzipJsonFromUrl(mapName); + return JsonParser.parseString(json).getAsJsonObject(); + } catch (IOException | JsonParseException e) { + System.err.println(Component + .translatable("command.box3.box3import.config_read_failed", mapName) + .getString()); + return null; + } + } + + // 否则仍然从本地 config 目录读取 + Path configPath = FabricLoader.getInstance() + .getConfigDir() + .resolve(MOD_ID) + .resolve(mapName.endsWith(".gz") ? mapName : mapName + ".gz"); + + if (!Files.exists(configPath)) { + System.err.println(Component + .translatable("command.box3.box3import.config_missing", configPath.toString()) + .getString()); + return null; + } + + try { + String json = readGzipJson(configPath.toString()); + return JsonParser.parseString(json).getAsJsonObject(); + } catch (IOException | JsonParseException e) { + System.err.println(Component + .translatable("command.box3.box3import.config_read_failed", configPath.toString()) + .getString()); + return null; + } + } + + private static String readGzipJson(String path) throws IOException { + try (FileInputStream fis = new FileInputStream(path); + GZIPInputStream gis = new GZIPInputStream(fis); + ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + + byte[] buffer = new byte[8192]; + int n; + while ((n = gis.read(buffer)) > 0) { + baos.write(buffer, 0, n); + } + return baos.toString(StandardCharsets.UTF_8); + } + } + + private static String readGzipJsonFromUrl(String urlString) throws IOException { + URI uri = URI.create(urlString); + URL url = uri.toURL(); + URLConnection connection = url.openConnection(); + connection.setConnectTimeout(10_000); + connection.setReadTimeout(30_000); + + try (InputStream is = connection.getInputStream(); + GZIPInputStream gis = new GZIPInputStream(is); + ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + + byte[] buffer = new byte[8192]; + int n; + while ((n = gis.read(buffer)) > 0) { + baos.write(buffer, 0, n); + } + return baos.toString(StandardCharsets.UTF_8); + } + } + + private static int[] toIntArray(JsonArray array) { + if (array == null) { + return new int[0]; + } + int[] result = new int[array.size()]; + for (int i = 0; i < array.size(); i++) { + result[i] = array.get(i).getAsInt(); + } + return result; + } +} \ No newline at end of file diff --git a/Fabric-26.2/src/main/java/com/box3lab/register/core/BlockRegistrar.java b/Fabric-26.2/src/main/java/com/box3lab/register/core/BlockRegistrar.java new file mode 100644 index 00000000..a8659619 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/register/core/BlockRegistrar.java @@ -0,0 +1,31 @@ +package com.box3lab.register.core; + +import java.util.function.Function; + +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockBehaviour; + +public final class BlockRegistrar { + private BlockRegistrar() { + } + + public static Block register(String modId, String name, Function blockFactory, BlockBehaviour.Properties settings, boolean shouldRegisterItem) { + ResourceKey blockKey = ResourceKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath(modId, name)); + Block block = blockFactory.apply(settings.setId(blockKey)); + + if (shouldRegisterItem) { + ResourceKey itemKey = ResourceKey.create(Registries.ITEM, Identifier.fromNamespaceAndPath(modId, name)); + BlockItem blockItem = new BlockItem(block, new Item.Properties().setId(itemKey).useBlockDescriptionPrefix()); + Registry.register(BuiltInRegistries.ITEM, itemKey, blockItem); + } + + return Registry.register(BuiltInRegistries.BLOCK, blockKey, block); + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/register/creative/CreativeTabExtras.java b/Fabric-26.2/src/main/java/com/box3lab/register/creative/CreativeTabExtras.java new file mode 100644 index 00000000..a8f97da5 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/register/creative/CreativeTabExtras.java @@ -0,0 +1,26 @@ +package com.box3lab.register.creative; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.minecraft.world.level.ItemLike; + +public final class CreativeTabExtras { + private static final Map> EXTRAS = new HashMap<>(); + + private CreativeTabExtras() { + } + + public static void add(String categoryPath, ItemLike item) { + if (categoryPath == null || categoryPath.isBlank() || item == null) { + return; + } + EXTRAS.computeIfAbsent(categoryPath, k -> new ArrayList<>()).add(item); + } + + public static Map> extras() { + return EXTRAS; + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java b/Fabric-26.2/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java new file mode 100644 index 00000000..89c02c56 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/register/creative/CreativeTabRegistrar.java @@ -0,0 +1,132 @@ +package com.box3lab.register.creative; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import com.box3lab.util.BlockIndexData; + +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.ItemLike; + +public final class CreativeTabRegistrar { + public static final String DEFAULT_MODEL_TAB = "models"; + + private CreativeTabRegistrar() { + } + + private static String sanitizeCategoryPath(String category) { + if (category == null || category.isBlank()) { + return ""; + } + String lower = category.toLowerCase(Locale.ROOT); + return lower.replaceAll("[^a-z0-9_\\-]+", "_"); + } + + private static ItemStack defaultIcon(Map blocks) { + Block iconBlock = blocks.get("stone"); + if (iconBlock == null && !blocks.isEmpty()) { + iconBlock = blocks.values().iterator().next(); + } + return iconBlock == null ? new ItemStack(Items.STONE) : new ItemStack(iconBlock); + } + + public static void registerCreativeTabs(String modId, Map blocks, BlockIndexData data) { + Map> categoryToRegistryNames = new HashMap<>(); + for (String rn : blocks.keySet()) { + String voxelName = rn; + String category = data.categoryByName.getOrDefault(voxelName, ""); + String categoryPath = sanitizeCategoryPath(category); + if (categoryPath != null && !categoryPath.isBlank()) { + categoryToRegistryNames.computeIfAbsent(categoryPath, k -> new ArrayList<>()).add(rn); + } + } + + List categories = new ArrayList<>(categoryToRegistryNames.keySet()); + categories.sort(Comparator.naturalOrder()); + + int tabColumn = 0; + for (String categoryPath : categories) { + List registryNames = categoryToRegistryNames.get(categoryPath); + if (registryNames == null || registryNames.isEmpty()) { + continue; + } + registryNames.sort(Comparator.naturalOrder()); + + Block categoryIconBlock = blocks.get(registryNames.get(0)); + + ResourceKey key = ResourceKey.create( + BuiltInRegistries.CREATIVE_MODE_TAB.key(), + Identifier.fromNamespaceAndPath(modId, "creative_tab_" + categoryPath)); + CreativeModeTab tab = CreativeModeTab.builder(CreativeModeTab.Row.TOP, tabColumn++) + .icon(() -> categoryIconBlock == null ? defaultIcon(blocks) : new ItemStack(categoryIconBlock)) + .title(Component.translatable("itemGroup." + modId + "." + categoryPath)) + .displayItems((params, output) -> { + for (String rn : registryNames) { + Block b = blocks.get(rn); + if (b != null) { + output.accept(b.asItem()); + } + } + + List extras = CreativeTabExtras.extras().get(categoryPath); + if (extras != null) { + for (ItemLike extra : extras) { + if (extra != null) { + output.accept(extra); + } + } + } + }) + .build(); + + Registry.register(BuiltInRegistries.CREATIVE_MODE_TAB, key, tab); + } + } + + public static void registerModelTab(String modId) { + String categoryPath = sanitizeCategoryPath(DEFAULT_MODEL_TAB); + if (categoryPath.isBlank()) { + return; + } + if (BuiltInRegistries.CREATIVE_MODE_TAB + .containsKey(Identifier.fromNamespaceAndPath(modId, "creative_tab_" + categoryPath))) { + return; + } + + List extras = CreativeTabExtras.extras().get(categoryPath); + if (extras == null || extras.isEmpty()) { + return; + } + + ItemLike iconItem = extras.get(0); + ResourceKey key = ResourceKey.create( + BuiltInRegistries.CREATIVE_MODE_TAB.key(), + Identifier.fromNamespaceAndPath(modId, "creative_tab_" + categoryPath)); + + CreativeModeTab tab = CreativeModeTab.builder(CreativeModeTab.Row.BOTTOM, 0) + .icon(() -> new ItemStack(iconItem)) + .title(Component.translatable("itemGroup." + modId + "." + categoryPath)) + .displayItems((params, output) -> { + for (ItemLike extra : extras) { + if (extra != null) { + output.accept(extra); + } + } + }) + .build(); + + Registry.register(BuiltInRegistries.CREATIVE_MODE_TAB, key, tab); + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/register/modelbe/PackModelBlockEntityRegistrar.java b/Fabric-26.2/src/main/java/com/box3lab/register/modelbe/PackModelBlockEntityRegistrar.java new file mode 100644 index 00000000..04e3625b --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/register/modelbe/PackModelBlockEntityRegistrar.java @@ -0,0 +1,212 @@ +package com.box3lab.register.modelbe; + +import static com.box3lab.Box3.MOD_ID; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import com.box3lab.block.entity.PackModelEntityBlock; +import com.box3lab.register.creative.CreativeTabExtras; +import com.box3lab.register.creative.CreativeTabRegistrar; + +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.MapColor; + +public final class PackModelBlockEntityRegistrar { + private static final String ASSET_PREFIX = "assets/" + MOD_ID + "/"; + + private static final Map> TYPES_BY_BLOCK = new LinkedHashMap<>(); + + private PackModelBlockEntityRegistrar() { + } + + public static void registerAll() { + Set modelNames = discoverPairedModelNames(); + if (modelNames.isEmpty()) { + return; + } + + for (String name : modelNames) { + Identifier id = Identifier.tryBuild(MOD_ID, name); + if (id == null) { + continue; + } + + ResourceKey blockKey = ResourceKey.create(Registries.BLOCK, id); + if (BuiltInRegistries.BLOCK.containsKey(blockKey)) { + continue; + } + + Block block = new PackModelEntityBlock( + BlockBehaviour.Properties.of() + .setId(blockKey) + .mapColor(MapColor.STONE) + .strength(1.5F, 6.0F) + .noOcclusion() + .isViewBlocking((state, level, pos) -> false) + .isSuffocating((state, level, pos) -> false) + .isRedstoneConductor((state, level, pos) -> false)); + Registry.register(BuiltInRegistries.BLOCK, blockKey, block); + + ResourceKey itemKey = ResourceKey.create( + Registries.ITEM, + Identifier.fromNamespaceAndPath(MOD_ID, name)); + if (!BuiltInRegistries.ITEM.containsKey(itemKey)) { + Item item = new BlockItem(block, new Item.Properties().setId(itemKey).useItemDescriptionPrefix()); + Registry.register(BuiltInRegistries.ITEM, itemKey, item); + CreativeTabExtras.add(CreativeTabRegistrar.DEFAULT_MODEL_TAB, item); + } + + ResourceKey> blockEntityKey = ResourceKey.create(Registries.BLOCK_ENTITY_TYPE, id); + if (BuiltInRegistries.BLOCK_ENTITY_TYPE.containsKey(blockEntityKey)) { + continue; + } + + BlockEntityType type = FabricBlockEntityTypeBuilder + .create(com.box3lab.block.entity.PackModelBlockEntity::new, block) + .build(); + + Registry.register(BuiltInRegistries.BLOCK_ENTITY_TYPE, blockEntityKey, type); + TYPES_BY_BLOCK.put(block, type); + } + + CreativeTabRegistrar.registerModelTab(MOD_ID); + } + + public static BlockEntityType typeFor(Block block) { + BlockEntityType type = TYPES_BY_BLOCK.get(block); + if (type == null) { + throw new IllegalStateException("No block entity type bound for block: " + block); + } + return type; + } + + private static Set discoverPairedModelNames() { + Set result = new LinkedHashSet<>(); + Path packsRoot = FabricLoader.getInstance().getGameDir().resolve("resourcepacks"); + if (!Files.isDirectory(packsRoot)) { + return result; + } + + try (var entries = Files.list(packsRoot)) { + entries.forEach(entry -> { + if (Files.isDirectory(entry)) { + collectFromDirectory(entry, result); + } else if (isArchive(entry)) { + collectFromArchive(entry, result); + } + }); + } catch (IOException ignored) { + } + + return result; + } + + private static void collectFromDirectory(Path packDir, Set out) { + Path assetsRoot = packDir.resolve("assets").resolve(MOD_ID); + if (!Files.isDirectory(assetsRoot)) { + return; + } + + Set models = collectBaseNamesFromDirectory(assetsRoot, ".json"); + if (models.isEmpty()) { + return; + } + + Set textures = collectBaseNamesFromDirectory(assetsRoot, ".png"); + if (textures.isEmpty()) { + return; + } + + for (String model : models) { + if (textures.contains(model)) { + out.add(model); + } + } + } + + private static Set collectBaseNamesFromDirectory(Path root, String suffix) { + Set names = new LinkedHashSet<>(); + try (var files = Files.walk(root)) { + files.filter(Files::isRegularFile).forEach(path -> { + String fileName = path.getFileName().toString(); + if (!fileName.toLowerCase(Locale.ROOT).endsWith(suffix)) { + return; + } + + String base = fileName.substring(0, fileName.length() - suffix.length()).toLowerCase(Locale.ROOT); + if (!base.isBlank()) { + names.add(base); + } + }); + } catch (IOException ignored) { + } + return names; + } + + private static void collectFromArchive(Path archive, Set out) { + try (ZipFile zip = new ZipFile(archive.toFile())) { + Set models = new LinkedHashSet<>(); + Set textures = new LinkedHashSet<>(); + + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + continue; + } + + String name = entry.getName(); + if (!name.startsWith(ASSET_PREFIX)) { + continue; + } + + String fileName = name.substring(name.lastIndexOf('/') + 1); + String lower = fileName.toLowerCase(Locale.ROOT); + if (lower.endsWith(".json")) { + String base = fileName.substring(0, fileName.length() - 5).toLowerCase(Locale.ROOT); + if (!base.isBlank()) { + models.add(base); + } + } else if (lower.endsWith(".png")) { + String base = fileName.substring(0, fileName.length() - 4).toLowerCase(Locale.ROOT); + if (!base.isBlank()) { + textures.add(base); + } + } + } + + for (String model : models) { + if (textures.contains(model)) { + out.add(model); + } + } + } catch (IOException ignored) { + } + } + + private static boolean isArchive(Path path) { + String name = path.getFileName().toString().toLowerCase(Locale.ROOT); + return name.endsWith(".zip") || name.endsWith(".jar"); + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/register/sound/CategorySoundTypes.java b/Fabric-26.2/src/main/java/com/box3lab/register/sound/CategorySoundTypes.java new file mode 100644 index 00000000..aff3f8bb --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/register/sound/CategorySoundTypes.java @@ -0,0 +1,33 @@ +package com.box3lab.register.sound; + +import java.util.Locale; + +import net.minecraft.world.level.block.SoundType; + +public final class CategorySoundTypes { + private CategorySoundTypes() { + } + + public static SoundType soundTypeForCategory(String category) { + if (category == null) { + return SoundType.STONE; + } + String c = category.toLowerCase(Locale.ROOT); + + return switch (c) { + case "structure" -> SoundType.STONE; + case "nature" -> SoundType.GRASS; + + case "symbol", "number", "letter", "color" -> SoundType.STONE; + + case "wood", "plant", "tree", "leaf", "leaves" -> SoundType.WOOD; + case "metal", "machine" -> SoundType.METAL; + case "glass" -> SoundType.GLASS; + case "wool", "cloth" -> SoundType.WOOL; + case "sand" -> SoundType.SAND; + case "snow" -> SoundType.SNOW; + case "slime" -> SoundType.SLIME_BLOCK; + default -> SoundType.STONE; + }; + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/register/voxel/VoxelBlockFactories.java b/Fabric-26.2/src/main/java/com/box3lab/register/voxel/VoxelBlockFactories.java new file mode 100644 index 00000000..75cb8588 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/register/voxel/VoxelBlockFactories.java @@ -0,0 +1,57 @@ +package com.box3lab.register.voxel; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; + +import com.box3lab.block.BarrierVoxelBlock; +import com.box3lab.block.BouncePadBlock; +import com.box3lab.block.ConveyorBlock; +import com.box3lab.block.GlassVoxelBlock; +import com.box3lab.block.SpiderWebBlock; +import com.box3lab.block.VoxelBlock; + +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockBehaviour; + +public final class VoxelBlockFactories { + private static final Function DEFAULT_FACTORY = VoxelBlock::new; + private static final Map> FACTORIES = new HashMap<>(); + + static { + register("conveyor", ConveyorBlock::new); + register("bounce_pad", BouncePadBlock::new); + register("spiderweb", SpiderWebBlock::new); + register("barrier", BarrierVoxelBlock::new); + } + + private VoxelBlockFactories() { + } + + public static void register(String texturePart, Function factory) { + if (texturePart == null || texturePart.isBlank() || factory == null) { + return; + } + FACTORIES.put(texturePart.toLowerCase(Locale.ROOT), factory); + } + + public static Function factoryFor(String texturePart, boolean transparent) { + if (texturePart == null) { + return DEFAULT_FACTORY; + } + + String key = texturePart.toLowerCase(Locale.ROOT); + + Function factory = FACTORIES.get(key); + if (factory != null) { + return factory; + } + + if (transparent) { + return GlassVoxelBlock::new; + } + + return DEFAULT_FACTORY; + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/register/voxel/VoxelBlockPropertiesFactory.java b/Fabric-26.2/src/main/java/com/box3lab/register/voxel/VoxelBlockPropertiesFactory.java new file mode 100644 index 00000000..60fb8ba2 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/register/voxel/VoxelBlockPropertiesFactory.java @@ -0,0 +1,26 @@ +package com.box3lab.register.voxel; + +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.MapColor; + +public final class VoxelBlockPropertiesFactory { + private VoxelBlockPropertiesFactory() { + } + + public static BlockBehaviour.Properties create(boolean solid, SoundType soundType, int lightLevel, float hardness, + float resistance, float friction) { + BlockBehaviour.Properties props = BlockBehaviour.Properties.of() + .sound(soundType) + .mapColor(MapColor.COLOR_CYAN) + .lightLevel(state -> lightLevel) + .strength(hardness, resistance) + .friction(friction); + + if (!solid) { + props = props.noOcclusion(); + } + + return props; + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/register/voxel/VoxelLightLevelMapper.java b/Fabric-26.2/src/main/java/com/box3lab/register/voxel/VoxelLightLevelMapper.java new file mode 100644 index 00000000..ef88bf45 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/register/voxel/VoxelLightLevelMapper.java @@ -0,0 +1,11 @@ +package com.box3lab.register.voxel; + +public final class VoxelLightLevelMapper { + private VoxelLightLevelMapper() { + } + + public static int lightLevelFromEmissivePacked(int emissivePacked) { + int rawLight = emissivePacked == 0 ? 0 : (int) Math.round(15.0 * (0.8 + 0.2 * emissivePacked / 4095.0)); + return Math.max(0, Math.min(15, rawLight)); + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/util/BlockIdResolver.java b/Fabric-26.2/src/main/java/com/box3lab/util/BlockIdResolver.java new file mode 100644 index 00000000..2b4b6490 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/util/BlockIdResolver.java @@ -0,0 +1,135 @@ +package com.box3lab.util; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import com.box3lab.register.ModBlocks; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; + +public final class BlockIdResolver { + + private static JsonObject blockIdMapping = null; + private static Map blockToIdMapping = null; + + private BlockIdResolver() { + } + + private static void loadBlockIdMapping() { + if (blockIdMapping != null) { + return; + } + + try (InputStream is = BlockIdResolver.class.getClassLoader().getResourceAsStream("block-id.json")) { + if (is == null) { + throw new RuntimeException(Component + .translatable("command.box3.block_id.missing_file") + .getString()); + } + try (InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + blockIdMapping = JsonParser.parseReader(reader).getAsJsonObject(); + } + } catch (Exception e) { + throw new RuntimeException( + Component.translatable("command.box3.block_id.read_failed").getString(), + e); + } + } + + private static void loadReverseMapping() { + loadBlockIdMapping(); + if (blockToIdMapping != null) { + return; + } + + Map reverse = new HashMap<>(); + for (var entry : blockIdMapping.entrySet()) { + String registryKey = entry.getValue().getAsString().toLowerCase(Locale.ROOT); + reverse.putIfAbsent(registryKey, Integer.parseInt(entry.getKey())); + } + blockToIdMapping = reverse; + } + + public static Block getBlockById(int id) { + return getBlockById(id, false); + } + + public static Block getBlockById(int id, boolean ignoreWater) { + loadBlockIdMapping(); + + String idStr = String.valueOf(id); + if (!blockIdMapping.has(idStr)) { + System.err.println(Component + .translatable("command.box3.block_id.no_mapping_for_id", idStr) + .getString()); + return Blocks.STONE; + } + + // 364: spec_water_block (water) + // 412-430: spec_*_juice_block, milk, soy_sauce, coffee, peach_juice + if (id == 364 + || id == 412 + || id == 414 + || id == 416 + || id == 418 + || id == 420 + || id == 422 + || id == 424 + || id == 426 + || id == 428 + || id == 430) { + if (ignoreWater) { + return Blocks.AIR; + } + return Blocks.WATER; + } + + String registryKey = blockIdMapping.get(idStr).getAsString(); + String normalizedKey = registryKey.toLowerCase(Locale.ROOT); + + Block block = ModBlocks.BLOCKS.get(normalizedKey); + if (block == null) { + System.err.println(Component + .translatable("command.box3.block_id.missing_registered_block", registryKey) + .getString()); + return Blocks.STONE; + } + + return block; + } + + public static boolean isBarrierId(int id) { + loadBlockIdMapping(); + + String idStr = String.valueOf(id); + if (!blockIdMapping.has(idStr)) { + return false; + } + String registryKey = blockIdMapping.get(idStr).getAsString(); + return "barrier".equalsIgnoreCase(registryKey); + } + + public static int getIdByBlock(Block block) { + loadReverseMapping(); + String key = BuiltInRegistries.BLOCK.getKey(block).getPath().toLowerCase(Locale.ROOT); + Integer id = blockToIdMapping.get(key); + if (id != null) { + return id; + } + + if (block == Blocks.WATER) { + return 364; + } + + return 0; + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/util/BlockIndexData.java b/Fabric-26.2/src/main/java/com/box3lab/util/BlockIndexData.java new file mode 100644 index 00000000..149eab2e --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/util/BlockIndexData.java @@ -0,0 +1,243 @@ +package com.box3lab.util; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Locale; +import java.util.Set; + +public final class BlockIndexData { + public static final class FluidInfo { + public final int id; + public final int mass; + public final long info; + public final double fluidExtinction; + + public FluidInfo(int id, int mass, long info, double fluidExtinction) { + this.id = id; + this.mass = mass; + this.info = info; + this.fluidExtinction = fluidExtinction; + } + } + + public final int[] ids; + public final String[] names; + public final int[] emissive; + public final float[] blockHardness; + public final float[] blockResistance; + public final float[] blockFriction; + public final Map categoryByName; + public final Set notSolidIds; + public final Map fluidsById; + + public final Map indexById; + public final Map idByName; + + private BlockIndexData( + int[] ids, + String[] names, + int[] emissive, + float[] blockHardness, + float[] blockResistance, + float[] blockFriction, + Map categoryByName, + Set notSolidIds, + Map fluidsById) { + this.ids = ids; + this.names = names; + this.emissive = emissive; + this.blockHardness = blockHardness; + this.blockResistance = blockResistance; + this.blockFriction = blockFriction; + this.categoryByName = categoryByName; + this.notSolidIds = notSolidIds; + this.fluidsById = fluidsById; + + Map indexByIdTmp = new HashMap<>(ids.length * 2); + for (int i = 0; i < ids.length; i++) { + indexByIdTmp.put(ids[i], i); + } + this.indexById = indexByIdTmp; + + Map idByNameTmp = new HashMap<>(names.length * 2); + int len = Math.min(ids.length, names.length); + for (int i = 0; i < len; i++) { + idByNameTmp.put(names[i], ids[i]); + } + this.idByName = idByNameTmp; + } + + private static volatile BlockIndexData INSTANCE; + + public static BlockIndexData get() { + BlockIndexData inst = INSTANCE; + if (inst != null) { + return inst; + } + synchronized (BlockIndexData.class) { + if (INSTANCE == null) { + INSTANCE = load(); + } + return INSTANCE; + } + } + + private static BlockIndexData load() { + JsonObject root; + try (InputStream is = BlockIndexData.class.getClassLoader().getResourceAsStream("block-spec.json")) { + if (is == null) { + throw new IllegalStateException("Missing resource: block-spec.json"); + } + root = JsonParser.parseReader(new InputStreamReader(is, StandardCharsets.UTF_8)).getAsJsonObject(); + } catch (Exception e) { + throw new RuntimeException("Failed to load block-spec.json", e); + } + + final class Entry { + final String name; + final int id; + final int emissive; + final String category; + final boolean transparent; + final boolean fluid; + final int mass; + final int fluidR; + final int fluidG; + final int fluidB; + final double fluidExtinction; + final float hardness; + final float resistance; + final float friction; + + Entry(String name, int id, int emissive, String category, boolean transparent, boolean fluid, int mass, + int fluidR, int fluidG, int fluidB, double fluidExtinction, float hardness, float resistance, + float friction) { + this.name = name; + this.id = id; + this.emissive = emissive; + this.category = category; + this.transparent = transparent; + this.fluid = fluid; + this.mass = mass; + this.fluidR = fluidR; + this.fluidG = fluidG; + this.fluidB = fluidB; + this.fluidExtinction = fluidExtinction; + this.hardness = hardness; + this.resistance = resistance; + this.friction = friction; + } + } + + List entries = new ArrayList<>(root.size()); + for (Map.Entry e : root.entrySet()) { + String name = e.getKey(); + JsonObject obj = e.getValue().getAsJsonObject(); + + int id = obj.has("id") ? obj.get("id").getAsInt() : -1; + String category = obj.has("category") ? obj.get("category").getAsString() : ""; + boolean transparent = obj.has("transparent") && obj.get("transparent").getAsBoolean(); + boolean fluid = obj.has("fluid") && obj.get("fluid").getAsBoolean(); + int mass = obj.has("mass") ? obj.get("mass").getAsInt() : 0; + + int emissivePacked = 0; + if (obj.has("emissive") && obj.get("emissive").isJsonArray() + && obj.getAsJsonArray("emissive").size() >= 3) { + double er = obj.getAsJsonArray("emissive").get(0).getAsDouble(); + double eg = obj.getAsJsonArray("emissive").get(1).getAsDouble(); + double eb = obj.getAsJsonArray("emissive").get(2).getAsDouble(); + double max = Math.max(er, Math.max(eg, eb)); + emissivePacked = (int) Math.round(Math.max(0.0, Math.min(1.0, max / 15.0)) * 4095.0); + } + + int fr = 0, fg = 0, fb = 0; + if (obj.has("fluidColor") && obj.get("fluidColor").isJsonArray() + && obj.getAsJsonArray("fluidColor").size() >= 3) { + double r = obj.getAsJsonArray("fluidColor").get(0).getAsDouble(); + double g = obj.getAsJsonArray("fluidColor").get(1).getAsDouble(); + double b = obj.getAsJsonArray("fluidColor").get(2).getAsDouble(); + + fr = (int) Math.round((r <= 1.0 ? r * 255.0 : r)); + fg = (int) Math.round((g <= 1.0 ? g * 255.0 : g)); + fb = (int) Math.round((b <= 1.0 ? b * 255.0 : b)); + + fr = Math.max(0, Math.min(255, fr)); + fg = Math.max(0, Math.min(255, fg)); + fb = Math.max(0, Math.min(255, fb)); + } + + double fluidExtinction = 0.0; + if (obj.has("fluidExtinction")) { + fluidExtinction = obj.get("fluidExtinction").getAsDouble(); + } + + // Parse strength values + float hardness = 1.0f; // default hardness + float resistance = 1.0f; // default resistance + + if (obj.has("strength") && obj.get("strength").isJsonObject()) { + JsonObject strengthObj = obj.get("strength").getAsJsonObject(); + hardness = strengthObj.has("hardness") ? strengthObj.get("hardness").getAsFloat() : 1.0f; + resistance = strengthObj.has("resistance") ? strengthObj.get("resistance").getAsFloat() : 1.0f; + } + + // Parse friction value (default 1.0 if missing) + float friction = 1.0f; + if (obj.has("friction")) { + friction = obj.get("friction").getAsFloat(); + } + + if (id >= 0) { + entries.add(new Entry(name, id, emissivePacked, category, transparent, fluid, mass, fr, fg, fb, + fluidExtinction, hardness, resistance, friction)); + } + } + + entries.sort((a, b) -> Integer.compare(a.id, b.id)); + + int[] ids = new int[entries.size()]; + String[] names = new String[entries.size()]; + int[] emissive = new int[entries.size()]; + float[] blockHardness = new float[entries.size()]; + float[] blockResistance = new float[entries.size()]; + float[] blockFriction = new float[entries.size()]; + Map categoryByName = new HashMap<>(entries.size() * 2); + + Set notSolidSet = new HashSet<>(); + Map fluidsById = new HashMap<>(); + + for (int i = 0; i < entries.size(); i++) { + Entry en = entries.get(i); + ids[i] = en.id; + names[i] = en.name; + emissive[i] = en.emissive; + blockHardness[i] = en.hardness; + blockResistance[i] = en.resistance; + blockFriction[i] = en.friction; + categoryByName.put(en.name.toLowerCase(Locale.ROOT), en.category == null ? "" : en.category); + + if (en.transparent || en.fluid) { + notSolidSet.add(en.id); + } + if (en.fluid) { + int a = 255; + long info = (en.fluidR & 255L) | ((en.fluidG & 255L) << 8) | ((en.fluidB & 255L) << 16) + | ((a & 255L) << 24); + fluidsById.put(en.id, new FluidInfo(en.id, en.mass, info, en.fluidExtinction)); + } + } + + return new BlockIndexData(ids, names, emissive, blockHardness, blockResistance, blockFriction, categoryByName, + notSolidSet, fluidsById); + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/util/BlockIndexUtil.java b/Fabric-26.2/src/main/java/com/box3lab/util/BlockIndexUtil.java new file mode 100644 index 00000000..2d4539b0 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/util/BlockIndexUtil.java @@ -0,0 +1,98 @@ +package com.box3lab.util; + +import java.util.Locale; + +public final class BlockIndexUtil { + private BlockIndexUtil() { + } + + public static final class RGBA { + public final int r; + public final int g; + public final int b; + public final float a; + + public RGBA(int r, int g, int b, float a) { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + } + + public static int normalizeId(int blockId) { + return blockId & 4095; + } + + public static String getNameById(int blockId) { + int id = normalizeId(blockId); + BlockIndexData data = BlockIndexData.get(); + Integer idx = data.indexById.get(id); + if (idx == null) { + return null; + } + if (idx < 0 || idx >= data.names.length) { + return null; + } + return data.names[idx]; + } + + public static Integer getIdByName(String name) { + if (name == null) { + return null; + } + BlockIndexData data = BlockIndexData.get(); + return data.idByName.get(name); + } + + public static boolean isFluid(int blockId) { + int id = normalizeId(blockId); + return BlockIndexData.get().fluidsById.containsKey(id); + } + + public static boolean isSolid(int blockId) { + int id = normalizeId(blockId); + return !BlockIndexData.get().notSolidIds.contains(id); + } + + public static boolean isSoild(int blockId) { + return isSolid(blockId); + } + + public static int blockEmissiveLight(int blockId) { + int id = normalizeId(blockId); + BlockIndexData data = BlockIndexData.get(); + Integer idx = data.indexById.get(id); + if (idx == null) { + return 0; + } + if (idx < 0 || idx >= data.emissive.length) { + return 0; + } + return data.emissive[idx]; + } + + public static RGBA getFluidColor(int blockId) { + int id = normalizeId(blockId); + BlockIndexData.FluidInfo fluid = BlockIndexData.get().fluidsById.get(id); + if (fluid == null) { + return null; + } + long info = fluid.info; + + int r = (int) (info & 255L); + int g = (int) ((info & 65280L) >> 8); + int b = (int) ((info & 16711680L) >>> 16); + float a = (float) (((info >>> 24) & 255L) / 255.0); + + return new RGBA(r, g, b, a); + } + + public static String getVoxelNameLowerCaseById(int blockId) { + String name = getNameById(blockId); + if (name == null) { + return null; + } + return name.toLowerCase(Locale.ROOT); + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/util/Box3ImportFiles.java b/Fabric-26.2/src/main/java/com/box3lab/util/Box3ImportFiles.java new file mode 100644 index 00000000..bb2c1c13 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/util/Box3ImportFiles.java @@ -0,0 +1,38 @@ +package com.box3lab.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import net.fabricmc.loader.api.FabricLoader; + +import static com.box3lab.Box3.MOD_ID; + +public final class Box3ImportFiles { + + private Box3ImportFiles() { + } + + public static Path getImportDir() { + return FabricLoader.getInstance() + .getConfigDir() + .resolve(MOD_ID); + } + + public static List listJsonFiles() throws IOException { + Path dir = getImportDir(); + if (!Files.exists(dir)) { + return Collections.emptyList(); + } + + try (var stream = Files.list(dir)) { + return stream + .filter(path -> path.getFileName().toString().endsWith(".gz")) + .map(path -> path.getFileName().toString()) + .sorted() + .toList(); + } + } +} diff --git a/Fabric-26.2/src/main/java/com/box3lab/util/ConfigUtil.java b/Fabric-26.2/src/main/java/com/box3lab/util/ConfigUtil.java new file mode 100644 index 00000000..2c4c97f5 --- /dev/null +++ b/Fabric-26.2/src/main/java/com/box3lab/util/ConfigUtil.java @@ -0,0 +1,58 @@ +package com.box3lab.util; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +import net.fabricmc.loader.api.FabricLoader; + +import static com.box3lab.Box3.MOD_ID; + +public final class ConfigUtil { + + public static final String CONFIG_DIR_NAME = "config.json"; + + private ConfigUtil() { + } + + public static JsonObject readConfig(String fileName) { + try { + Path dir = FabricLoader.getInstance().getConfigDir().resolve(MOD_ID); + Path path = dir.resolve(fileName); + if (!Files.exists(path)) { + return null; + } + + try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + return JsonParser.parseReader(reader).getAsJsonObject(); + } + } catch (IOException | JsonParseException e) { + return null; + } + } + + public static void writeConfig(String fileName, JsonObject obj) { + if (obj == null) { + return; + } + try { + Path dir = FabricLoader.getInstance().getConfigDir().resolve(MOD_ID); + if (!Files.exists(dir)) { + Files.createDirectories(dir); + } + Path path = dir.resolve(fileName); + + try (Writer writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { + writer.write(obj.toString()); + } + } catch (IOException e) { + } + } +} diff --git a/Fabric-26.2/src/main/resources/assets/box3/icon.png b/Fabric-26.2/src/main/resources/assets/box3/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8ef4da98ef9aa226f7e86eb386f33f7bc6a9375b GIT binary patch literal 10333 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 NUDYkqMn5|G{{WJbHbwvd literal 0 HcmV?d00001 diff --git a/Fabric-26.2/src/main/resources/assets/box3/lang/en_us.json b/Fabric-26.2/src/main/resources/assets/box3/lang/en_us.json new file mode 100644 index 00000000..37a935c5 --- /dev/null +++ b/Fabric-26.2/src/main/resources/assets/box3/lang/en_us.json @@ -0,0 +1,434 @@ +{ + "block.box3.spec_air_block": "Air", + "block.box3.spec_blueberry_juice_block": "Blueberry Juice", + "block.box3.spec_coffee_block": "Coffee", + "block.box3.spec_grape_juice_block": "Grape Juice", + "block.box3.spec_lemon_juice_block": "Lemon Juice", + "block.box3.spec_lime_juice_block": "Lime Juice", + "block.box3.spec_milk_block": "Milk", + "block.box3.spec_orange_juice_block": "Orange Juice", + "block.box3.spec_peach_juice_block": "Peach Juice", + "block.box3.spec_soy_sauce_block": "Soy Sauce", + "block.box3.spec_strawberry_juice_block": "Strawberry Juice", + "block.box3.spec_water_block": "Water", + "block.box3.a": "A", + "block.box3.acacia": "Acacia", + "block.box3.add": "Add", + "block.box3.air_duct": "Air Duct", + "block.box3.ampersand": "Ampersand", + "block.box3.asterisk": "Asterisk", + "block.box3.at": "At", + "block.box3.b": "B", + "block.box3.backslash": "Backslash", + "block.box3.bamboo": "Bamboo", + "block.box3.barrier": "Barrier", + "block.box3.bat_window": "Bat Window", + "block.box3.bear_footprint": "Bear Footprint", + "block.box3.biscuit": "Biscuit", + "block.box3.black": "Black", + "block.box3.black_glass": "Black Glass", + "block.box3.blue": "Blue", + "block.box3.blue_decorative_light": "Blue Decorative Light", + "block.box3.blue_gift": "Blue Gift", + "block.box3.blue_glass": "Blue Glass", + "block.box3.blue_grass": "Blue Grass", + "block.box3.blue_grass_all": "Blue Grass All", + "block.box3.blue_light": "Blue Light", + "block.box3.blue_surface_01": "Blue Surface 01", + "block.box3.blue_surface_02": "Blue Surface 02", + "block.box3.board0": "board0", + "block.box3.board1": "board1", + "block.box3.board10": "board10", + "block.box3.board11": "board11", + "block.box3.board12": "board12", + "block.box3.board13": "board13", + "block.box3.board14": "board14", + "block.box3.board15": "board15", + "block.box3.board2": "board2", + "block.box3.board3": "board3", + "block.box3.board4": "board4", + "block.box3.board5": "board5", + "block.box3.board6": "board6", + "block.box3.board7": "board7", + "block.box3.board8": "board8", + "block.box3.board9": "board9", + "block.box3.board_01": "Board 01", + "block.box3.board_02": "Board 02", + "block.box3.board_03": "Board 03", + "block.box3.board_04": "Board 04", + "block.box3.board_05": "Board 05", + "block.box3.board_06": "Board 06", + "block.box3.board_07": "Board 07", + "block.box3.bookshelf": "Bookshelf", + "block.box3.bounce_pad": "Bounce Pad", + "block.box3.bracket_close": "Bracket Close", + "block.box3.bracket_open": "Bracket Open", + "block.box3.brick_01": "Brick 01", + "block.box3.brick_02": "Brick 02", + "block.box3.brick_red": "Brick Red", + "block.box3.button": "Button", + "block.box3.c": "C", + "block.box3.cadet_blue": "Cadet Blue", + "block.box3.candy": "Candy", + "block.box3.caret": "Caret", + "block.box3.carpet_01": "Carpet 01", + "block.box3.carpet_02": "Carpet 02", + "block.box3.carpet_03": "Carpet 03", + "block.box3.carpet_04": "Carpet 04", + "block.box3.carpet_05": "Carpet 05", + "block.box3.carpet_06": "Carpet 06", + "block.box3.carpet_07": "Carpet 07", + "block.box3.carpet_08": "Carpet 08", + "block.box3.carpet_09": "Carpet 09", + "block.box3.carpet_10": "Carpet 10", + "block.box3.carpet_11": "Carpet 11", + "block.box3.carpet_12": "Carpet 12", + "block.box3.carpet_13": "Carpet 13", + "block.box3.coffee_gray": "Coffee Gray", + "block.box3.colon": "Colon", + "block.box3.color_glass": "Color Glass", + "block.box3.comma": "Comma", + "block.box3.conveyor": "Conveyor", + "block.box3.crane_lantern": "Crane Lantern", + "block.box3.crane_roof_01": "Crane Roof 01", + "block.box3.crane_roof_02": "Crane Roof 02", + "block.box3.cross_window": "Cross Window", + "block.box3.d": "D", + "block.box3.dark_blue_grass": "Dark Blue Grass", + "block.box3.dark_blue_grass_all": "Dark Blue Grass All", + "block.box3.dark_brick_00": "Dark Brick 00", + "block.box3.dark_brick_01": "Dark Brick 01", + "block.box3.dark_brick_02": "Dark Brick 02", + "block.box3.dark_grass": "Dark Grass", + "block.box3.dark_grass_all": "Dark Grass All", + "block.box3.dark_grass_rock": "Dark Grass Rock", + "block.box3.dark_grass_sand": "Dark Grass Sand", + "block.box3.dark_gray": "Dark Gray", + "block.box3.dark_orchid": "Dark Orchid", + "block.box3.dark_purple_grass": "Dark Purple Grass", + "block.box3.dark_purple_grass_all": "Dark Purple Grass All", + "block.box3.dark_red": "Dark Red", + "block.box3.dark_red_grass": "Dark Red Grass", + "block.box3.dark_red_grass_all": "Dark Red Grass All", + "block.box3.dark_salmon": "Dark Salmon", + "block.box3.dark_sand": "Dark Sand", + "block.box3.dark_slate_blue": "Dark Slate Blue", + "block.box3.dark_stone": "Dark Stone", + "block.box3.dark_surface": "Dark Surface", + "block.box3.dark_volcanic_rock": "Dark Volcanic Rock", + "block.box3.dark_yellow_grass": "Dark Yellow Grass", + "block.box3.dark_yellow_grass_all": "Dark Yellow Grass All", + "block.box3.dirt": "Dirt", + "block.box3.divide": "Divide", + "block.box3.dollar": "Dollar", + "block.box3.e": "E", + "block.box3.eight": "Eight", + "block.box3.equal": "Equal", + "block.box3.exclamation_mark": "Exclamation Mark", + "block.box3.express_box": "Express Box", + "block.box3.f": "F", + "block.box3.fan": "Fan", + "block.box3.firecracker": "Firecracker", + "block.box3.five": "Five", + "block.box3.four": "Four", + "block.box3.fu": "Fu", + "block.box3.g": "G", + "block.box3.geometric_window_01": "Geometric Window 01", + "block.box3.geometric_window_02": "Geometric Window 02", + "block.box3.glass": "Glass", + "block.box3.gold_trim_brick": "Gold Trim Brick", + "block.box3.grass": "Grass", + "block.box3.grass_all": "Grass All", + "block.box3.grass_rock": "Grass Rock", + "block.box3.grass_sand": "Grass Sand", + "block.box3.greater_than": "Greater Than", + "block.box3.green_decorative_light": "Green Decorative Light", + "block.box3.green_glass": "Green Glass", + "block.box3.green_leaf": "Green Leaf", + "block.box3.green_light": "Green Light", + "block.box3.greenbelt_l": "Greenbelt L", + "block.box3.greenbelt_l1": "Greenbelt l1", + "block.box3.grey_stone_brick": "Grey Stone Brick", + "block.box3.h": "H", + "block.box3.honeycomb_01": "Honeycomb 01", + "block.box3.honeycomb_02": "Honeycomb 02", + "block.box3.i": "I", + "block.box3.ice": "Ice", + "block.box3.ice_brick": "Ice Brick", + "block.box3.ice_wall": "Ice Wall", + "block.box3.indigo_light": "Indigo Light", + "block.box3.j": "J", + "block.box3.k": "K", + "block.box3.l": "L", + "block.box3.lab_lamp_01": "Lab Lamp 01", + "block.box3.lab_lamp_02": "Lab Lamp 02", + "block.box3.lab_lamp_03": "Lab Lamp 03", + "block.box3.lab_material_01": "Lab Material 01", + "block.box3.lab_material_02": "Lab Material 02", + "block.box3.lab_material_03": "Lab Material 03", + "block.box3.lab_material_04": "Lab Material 04", + "block.box3.lab_material_05": "Lab Material 05", + "block.box3.lab_material_06": "Lab Material 06", + "block.box3.lab_material_07": "Lab Material 07", + "block.box3.lab_material_08": "Lab Material 08", + "block.box3.lab_material_09": "Lab Material 09", + "block.box3.lab_material_10": "Lab Material 10", + "block.box3.lab_material_11": "Lab Material 11", + "block.box3.lab_material_12": "Lab Material 12", + "block.box3.lab_material_13": "Lab Material 13", + "block.box3.lab_material_14": "Lab Material 14", + "block.box3.lab_material_15": "Lab Material 15", + "block.box3.lab_screen": "Lab Screen", + "block.box3.lab_wire": "Lab Wire", + "block.box3.lantern_01": "Lantern 01", + "block.box3.lantern_02": "Lantern 02", + "block.box3.lava01": "lava01", + "block.box3.lava02": "lava02", + "block.box3.leaf_01": "Leaf 01", + "block.box3.leaf_02": "Leaf 02", + "block.box3.leaf_03": "Leaf 03", + "block.box3.leaf_04": "Leaf 04", + "block.box3.leaf_05": "Leaf 05", + "block.box3.leaf_06": "Leaf 06", + "block.box3.ledfloor01": "ledfloor01", + "block.box3.ledfloor02": "ledfloor02", + "block.box3.lemon": "Lemon", + "block.box3.less_than": "Less Than", + "block.box3.light_blue_grass": "Light Blue Grass", + "block.box3.light_blue_grass_all": "Light Blue Grass All", + "block.box3.light_dirt": "Light Dirt", + "block.box3.light_gray": "Light Gray", + "block.box3.light_grey_stone_brick": "Light Grey Stone Brick", + "block.box3.light_purple_grass_": "Light Purple Grass", + "block.box3.light_purple_grass_all": "Light Purple Grass All", + "block.box3.light_sand": "Light Sand", + "block.box3.light_volcanic_rock": "Light Volcanic Rock", + "block.box3.m": "M", + "block.box3.macaroon": "Macaroon", + "block.box3.maroon": "Maroon", + "block.box3.medium_gray": "Medium Gray", + "block.box3.medium_green": "Medium Green", + "block.box3.medium_orchid": "Medium Orchid", + "block.box3.medium_purple": "Medium Purple", + "block.box3.medium_spring_green": "Medium Spring Green", + "block.box3.medium_violet_red": "Medium Violet Red", + "block.box3.medium_yellow": "Medium Yellow", + "block.box3.mint_green": "Mint Green", + "block.box3.mint_green_light": "Mint Green Light", + "block.box3.multiply": "Multiply", + "block.box3.n": "N", + "block.box3.navajo_white": "Navajo White", + "block.box3.nine": "Nine", + "block.box3.o": "O", + "block.box3.olive_green": "Olive Green", + "block.box3.one": "One", + "block.box3.orange": "Orange", + "block.box3.orange_grass": "Orange Grass", + "block.box3.orange_grass_all": "Orange Grass All", + "block.box3.orange_light": "Orange Light", + "block.box3.orange_red": "Orange Red", + "block.box3.p": "P", + "block.box3.palace_carving": "Palace Carving", + "block.box3.palace_cloud": "Palace Cloud", + "block.box3.palace_eaves_01": "Palace Eaves 01", + "block.box3.palace_eaves_02": "Palace Eaves 02", + "block.box3.palace_eaves_03": "Palace Eaves 03", + "block.box3.palace_eaves_04": "Palace Eaves 04", + "block.box3.palace_eaves_05": "Palace Eaves 05", + "block.box3.palace_eaves_06": "Palace Eaves 06", + "block.box3.palace_eaves_07": "Palace Eaves 07", + "block.box3.palace_eaves_08": "Palace Eaves 08", + "block.box3.palace_floor": "Palace Floor", + "block.box3.palace_lamp": "Palace Lamp", + "block.box3.palace_roof": "Palace Roof", + "block.box3.palace_window": "Palace Window", + "block.box3.pale_green": "Pale Green", + "block.box3.palm": "Palm", + "block.box3.paren_close": "Paren Close", + "block.box3.paren_open": "Paren Open", + "block.box3.percent": "Percent", + "block.box3.period": "Period", + "block.box3.peru": "Peru", + "block.box3.pink": "Pink", + "block.box3.pink_cake": "Pink Cake", + "block.box3.pink_grass": "Pink Grass", + "block.box3.pink_grass_all": "Pink Grass All", + "block.box3.pink_light": "Pink Light", + "block.box3.plank_01": "Plank 01", + "block.box3.plank_02": "Plank 02", + "block.box3.plank_03": "Plank 03", + "block.box3.plank_04": "Plank 04", + "block.box3.plank_05": "Plank 05", + "block.box3.plank_06": "Plank 06", + "block.box3.plank_07": "Plank 07", + "block.box3.polar_ice": "Polar Ice", + "block.box3.polar_region": "Polar Region", + "block.box3.pound": "Pound", + "block.box3.powder_blue": "Powder Blue", + "block.box3.pumpkin": "Pumpkin", + "block.box3.pumpkin_lantern": "Pumpkin Lantern", + "block.box3.purple": "Purple", + "block.box3.purple_grass": "Purple Grass", + "block.box3.purple_grass_all": "Purple Grass All", + "block.box3.purple_surface_01": "Purple Surface 01", + "block.box3.purple_surface_02": "Purple Surface 02", + "block.box3.q": "Q", + "block.box3.quartz_brick": "Quartz Brick", + "block.box3.question_mark": "Question Mark", + "block.box3.quotation_mark": "Quotation Mark", + "block.box3.r": "R", + "block.box3.rainbow_cube": "Rainbow Cube", + "block.box3.red": "Red", + "block.box3.red_brick": "Red Brick", + "block.box3.red_brick_floor": "Red Brick Floor", + "block.box3.red_brick_wall": "Red Brick Wall", + "block.box3.red_decorative_light": "Red Decorative Light", + "block.box3.red_gift": "Red Gift", + "block.box3.red_glass": "Red Glass", + "block.box3.red_light": "Red Light", + "block.box3.rock": "Rock", + "block.box3.roof_blue_04": "Roof Blue 04", + "block.box3.roof_green": "Roof Green", + "block.box3.roof_grey": "Roof Grey", + "block.box3.roof_purple": "Roof Purple", + "block.box3.roof_red": "Roof Red", + "block.box3.roof_yellow": "Roof Yellow", + "block.box3.s": "S", + "block.box3.sakura_pink": "Sakura Pink", + "block.box3.sand": "Sand", + "block.box3.sand_stones": "Sand Stones", + "block.box3.semicolon": "Semicolon", + "block.box3.seven": "Seven", + "block.box3.sienna": "Sienna", + "block.box3.six": "Six", + "block.box3.sky_blue": "Sky Blue", + "block.box3.slash": "Slash", + "block.box3.snow": "Snow", + "block.box3.snow_grass": "Snow Grass", + "block.box3.snow_land": "Snow Land", + "block.box3.snowflake_lamp": "Snowflake Lamp", + "block.box3.snowland": "Snowland", + "block.box3.snowman_body": "Snowman Body", + "block.box3.snowman_head": "Snowman Head", + "block.box3.special_grass_01": "Special Grass 01", + "block.box3.special_grass_02": "Special Grass 02", + "block.box3.special_grass_03": "Special Grass 03", + "block.box3.special_grass_04": "Special Grass 04", + "block.box3.special_grass_05": "Special Grass 05", + "block.box3.special_grass_06": "Special Grass 06", + "block.box3.special_grass_07": "Special Grass 07", + "block.box3.special_grass_08": "Special Grass 08", + "block.box3.special_grass_09": "Special Grass 09", + "block.box3.special_grass_10": "Special Grass 10", + "block.box3.special_grass_11": "Special Grass 11", + "block.box3.special_grass_12": "Special Grass 12", + "block.box3.special_grass_13": "Special Grass 13", + "block.box3.special_grass_14": "Special Grass 14", + "block.box3.special_grass_15": "Special Grass 15", + "block.box3.special_grass_16": "Special Grass 16", + "block.box3.special_land_01": "Special Land 01", + "block.box3.special_sand_01": "Special Sand 01", + "block.box3.special_sand_02": "Special Sand 02", + "block.box3.special_sand_03": "Special Sand 03", + "block.box3.special_sand_04": "Special Sand 04", + "block.box3.special_sand_05": "Special Sand 05", + "block.box3.spiderweb": "Spiderweb", + "block.box3.stained_glass": "Stained Glass", + "block.box3.stainless_steel": "Stainless Steel", + "block.box3.star_lamp": "Star Lamp", + "block.box3.stone": "Stone", + "block.box3.stone_brick_01": "Stone Brick 01", + "block.box3.stone_brick_02": "Stone Brick 02", + "block.box3.stone_pillar_03": "Stone Pillar 03", + "block.box3.stone_pillar_04": "Stone Pillar 04", + "block.box3.stone_pillar_05": "Stone Pillar 05", + "block.box3.stone_pillar_06": "Stone Pillar 06", + "block.box3.stone_wall": "Stone Wall", + "block.box3.stone_wall_01": "Stone Wall 01", + "block.box3.stripe_01": "Stripe 01", + "block.box3.stripe_02": "Stripe 02", + "block.box3.stripe_03": "Stripe 03", + "block.box3.stripe_04": "Stripe 04", + "block.box3.stripe_05": "Stripe 05", + "block.box3.subtract": "Subtract", + "block.box3.t": "T", + "block.box3.television": "Television", + "block.box3.three": "Three", + "block.box3.tilde": "Tilde", + "block.box3.toolbox": "Toolbox", + "block.box3.traditional_window": "Traditional Window", + "block.box3.treasure_chest": "Treasure Chest", + "block.box3.turquoise": "Turquoise", + "block.box3.two": "Two", + "block.box3.u": "U", + "block.box3.v": "V", + "block.box3.w": "W", + "block.box3.warm_yellow_light": "Warm Yellow Light", + "block.box3.white": "White", + "block.box3.white_grass": "White Grass", + "block.box3.white_light": "White Light", + "block.box3.window": "Window", + "block.box3.windygrass": "Windygrass", + "block.box3.winter_leaf": "Winter Leaf", + "block.box3.withered_grass": "Withered Grass", + "block.box3.withered_grass_land": "Withered Grass Land", + "block.box3.wood": "Wood", + "block.box3.wooden_box": "Wooden Box", + "block.box3.woodstone_12": "Woodstone 12", + "block.box3.x": "X", + "block.box3.y": "Y", + "block.box3.yellow_decorative_light": "Yellow Decorative Light", + "block.box3.yellow_grass": "Yellow Grass", + "block.box3.yellow_green": "Yellow Green", + "block.box3.yellow_light": "Yellow Light", + "block.box3.z": "Z", + "block.box3.zero": "Zero", + "itemGroup.box3.letter": "Box3:Letters", + "itemGroup.box3.number": "Box3:Numbers", + "itemGroup.box3.symbol": "Box3:Symbols", + "itemGroup.box3.color": "Box3:Colors", + "itemGroup.box3.element": "Box3:Elements", + "itemGroup.box3.food": "Box3:Food", + "itemGroup.box3.light": "Box3:Lighting", + "itemGroup.box3.nature": "Box3:Nature", + "itemGroup.box3.structure": "Box3:Structures", + "itemGroup.box3.models": "Box3:Models", + "command.box3.box3import.usage": "Usage: /box3import (reads from config/box3/.gz),It will generate a building at your current location. ", + "command.box3.box3export.usage": "Usage: /box3export (place 2 redstone blocks as opposite corners)", + "command.box3.box3import.success": "Imported building from config/box3/%s", + "command.box3.box3import.progress": "Importing %s... %s%%", + "command.box3.box3import.failure": "Import failed: %s", + "command.box3.box3import.config_invalid": "Config file is invalid, please check it exists and has correct format.", + "command.box3.box3import.config_missing": "Config file does not exist: %s", + "command.box3.box3import.config_read_failed": "Failed to read config file: %s", + "command.box3.block_id.missing_file": "Cannot find block-id.json in the mod resources.", + "command.box3.block_id.read_failed": "Failed to read block-id.json.", + "command.box3.block_id.no_mapping_for_id": "No block-id.json mapping found for ID %s.", + "command.box3.block_id.missing_fluid_block": "Missing fluid block for %s, using water instead.", + "command.box3.block_id.missing_registered_block": "Missing registered voxel block: %s.", + "command.box3.box3import.list.empty": "No Terrain files found in %s (excluding %s)", + "command.box3.box3import.list.success": "Available Terrain files in %s: %s", + "command.box3.box3import.list.error": "Failed to list Terrain files in %s: %s", + "command.box3.box3barrier.status": "Barrier visible: %s", + "command.box3.box3barrier.set": "Barrier visibility set to: %s", + "command.box3.box3barrier.toggled": "Barrier visibility toggled to: %s (re-enter the world to fully apply)", + "command.box3.box3export.success": "Exported successfully: %s (scanned %s blocks, exported %s)", + "command.box3.box3export.failure": "Export failed: %s", + "command.box3.box3export.player_only": "This command can only be used by a player.", + "command.box3.box3export.selection_incomplete": "Selection is incomplete. Set both pos1 and pos2 first.", + "command.box3.box3export.selection_cleared": "Export selection cleared.", + "command.box3.box3export.pos_set": "Set %s to (%s, %s, %s).", + "command.box3.box3export.marker_invalid": "Invalid marker block id: %s", + "command.box3.box3export.marker_count_invalid": "Within radius %s, found %s marker blocks (%s), but exactly 2 are required.", + "message.box3.model.config.mode": "Model config mode: %s (Stick: +, Blaze Rod: -, Paper: copy, Book: paste)", + "message.box3.model.config.status": "mode=%s scale=%s offset=(%s, %s, %s) rot=%s", + "message.box3.model.config.mode.scale": "Scale", + "message.box3.model.config.mode.offset_x": "Offset X", + "message.box3.model.config.mode.offset_y": "Offset Y", + "message.box3.model.config.mode.offset_z": "Offset Z", + "message.box3.model.config.mode.rotation": "Rotation", + "message.box3.model.config.copy.success": "Copied current model config.", + "message.box3.model.config.copy.empty": "No copied config found.", + "message.box3.model.config.copy.pasted": "Pasted copied model config.", + "flat_world_preset.box3.box3_plains_world": "Box3 Plains" +} diff --git a/Fabric-26.2/src/main/resources/assets/box3/lang/zh_cn.json b/Fabric-26.2/src/main/resources/assets/box3/lang/zh_cn.json new file mode 100644 index 00000000..5f82f635 --- /dev/null +++ b/Fabric-26.2/src/main/resources/assets/box3/lang/zh_cn.json @@ -0,0 +1,422 @@ +{ + "block.box3.a": "A", + "block.box3.acacia": "金合欢", + "block.box3.add": "加", + "block.box3.air_duct": "通风管", + "block.box3.ampersand": "和号", + "block.box3.asterisk": "星号", + "block.box3.at": "艾特", + "block.box3.b": "B", + "block.box3.backslash": "反斜杠", + "block.box3.bamboo": "竹子", + "block.box3.barrier": "屏障", + "block.box3.bat_window": "蝙蝠窗", + "block.box3.bear_footprint": "熊脚印", + "block.box3.biscuit": "饼干", + "block.box3.black": "黑色", + "block.box3.black_glass": "黑色玻璃", + "block.box3.blue": "蓝色", + "block.box3.blue_decorative_light": "蓝色装饰灯", + "block.box3.blue_gift": "蓝色礼物", + "block.box3.blue_glass": "蓝色玻璃", + "block.box3.blue_grass": "蓝色草", + "block.box3.blue_grass_all": "蓝色全草", + "block.box3.blue_light": "蓝色灯", + "block.box3.blue_surface_01": "蓝色表面01", + "block.box3.blue_surface_02": "蓝色表面02", + "block.box3.board0": "木板0", + "block.box3.board1": "木板1", + "block.box3.board10": "木板10", + "block.box3.board11": "木板11", + "block.box3.board12": "木板12", + "block.box3.board13": "木板13", + "block.box3.board14": "木板14", + "block.box3.board15": "木板15", + "block.box3.board2": "木板2", + "block.box3.board3": "木板3", + "block.box3.board4": "木板4", + "block.box3.board5": "木板5", + "block.box3.board6": "木板6", + "block.box3.board7": "木板7", + "block.box3.board8": "木板8", + "block.box3.board9": "木板9", + "block.box3.board_01": "木板01", + "block.box3.board_02": "木板02", + "block.box3.board_03": "木板03", + "block.box3.board_04": "木板04", + "block.box3.board_05": "木板05", + "block.box3.board_06": "木板06", + "block.box3.board_07": "木板07", + "block.box3.bookshelf": "书架", + "block.box3.bounce_pad": "弹跳垫", + "block.box3.bracket_close": "右方括号", + "block.box3.bracket_open": "左方括号", + "block.box3.brick_01": "砖块01", + "block.box3.brick_02": "砖块02", + "block.box3.brick_red": "红砖", + "block.box3.button": "按钮", + "block.box3.c": "C", + "block.box3.cadet_blue": "军蓝", + "block.box3.candy": "糖果", + "block.box3.caret": "脱字符", + "block.box3.carpet_01": "地毯01", + "block.box3.carpet_02": "地毯02", + "block.box3.carpet_03": "地毯03", + "block.box3.carpet_04": "地毯04", + "block.box3.carpet_05": "地毯05", + "block.box3.carpet_06": "地毯06", + "block.box3.carpet_07": "地毯07", + "block.box3.carpet_08": "地毯08", + "block.box3.carpet_09": "地毯09", + "block.box3.carpet_10": "地毯10", + "block.box3.carpet_11": "地毯11", + "block.box3.carpet_12": "地毯12", + "block.box3.carpet_13": "地毯13", + "block.box3.coffee_gray": "咖啡灰", + "block.box3.colon": "冒号", + "block.box3.color_glass": "彩色玻璃", + "block.box3.comma": "逗号", + "block.box3.conveyor": "传送带", + "block.box3.crane_lantern": "起重机灯笼", + "block.box3.crane_roof_01": "起重机屋顶01", + "block.box3.crane_roof_02": "起重机屋顶02", + "block.box3.cross_window": "十字窗", + "block.box3.d": "D", + "block.box3.dark_blue_grass": "深蓝草", + "block.box3.dark_blue_grass_all": "深蓝全草", + "block.box3.dark_brick_00": "深砖00", + "block.box3.dark_brick_01": "深砖01", + "block.box3.dark_brick_02": "深砖02", + "block.box3.dark_grass": "深草", + "block.box3.dark_grass_all": "深全草", + "block.box3.dark_grass_rock": "深草岩", + "block.box3.dark_grass_sand": "深草沙", + "block.box3.dark_gray": "深灰", + "block.box3.dark_orchid": "深兰花", + "block.box3.dark_purple_grass": "深紫草", + "block.box3.dark_purple_grass_all": "深紫全草", + "block.box3.dark_red": "深红", + "block.box3.dark_red_grass": "深红草", + "block.box3.dark_red_grass_all": "深红全草", + "block.box3.dark_salmon": "深鲑鱼", + "block.box3.dark_sand": "深沙", + "block.box3.dark_slate_blue": "深板岩蓝", + "block.box3.dark_stone": "深石", + "block.box3.dark_surface": "深表面", + "block.box3.dark_volcanic_rock": "深火山岩", + "block.box3.dark_yellow_grass": "深黄草", + "block.box3.dark_yellow_grass_all": "深黄全草", + "block.box3.dirt": "泥土", + "block.box3.divide": "除", + "block.box3.dollar": "美元", + "block.box3.e": "E", + "block.box3.eight": "八", + "block.box3.equal": "等号", + "block.box3.exclamation_mark": "感叹号", + "block.box3.express_box": "快递箱", + "block.box3.f": "F", + "block.box3.fan": "风扇", + "block.box3.firecracker": "鞭炮", + "block.box3.five": "五", + "block.box3.four": "四", + "block.box3.fu": "福", + "block.box3.g": "G", + "block.box3.geometric_window_01": "几何窗01", + "block.box3.geometric_window_02": "几何窗02", + "block.box3.glass": "玻璃", + "block.box3.gold_trim_brick": "金边砖", + "block.box3.grass": "草", + "block.box3.grass_all": "全草", + "block.box3.grass_rock": "草岩", + "block.box3.grass_sand": "草沙", + "block.box3.greater_than": "大于号", + "block.box3.green_decorative_light": "绿色装饰灯", + "block.box3.green_glass": "绿色玻璃", + "block.box3.green_leaf": "绿叶", + "block.box3.green_light": "绿色灯", + "block.box3.greenbelt_l": "绿化带L", + "block.box3.greenbelt_l1": "绿化带L1", + "block.box3.grey_stone_brick": "灰石砖", + "block.box3.h": "H", + "block.box3.honeycomb_01": "蜂巢01", + "block.box3.honeycomb_02": "蜂巢02", + "block.box3.i": "I", + "block.box3.ice": "冰", + "block.box3.ice_brick": "冰砖", + "block.box3.ice_wall": "冰墙", + "block.box3.indigo_light": "靛蓝灯", + "block.box3.j": "J", + "block.box3.k": "K", + "block.box3.l": "L", + "block.box3.lab_lamp_01": "实验室灯01", + "block.box3.lab_lamp_02": "实验室灯02", + "block.box3.lab_lamp_03": "实验室灯03", + "block.box3.lab_material_01": "实验室材料01", + "block.box3.lab_material_02": "实验室材料02", + "block.box3.lab_material_03": "实验室材料03", + "block.box3.lab_material_04": "实验室材料04", + "block.box3.lab_material_05": "实验室材料05", + "block.box3.lab_material_06": "实验室材料06", + "block.box3.lab_material_07": "实验室材料07", + "block.box3.lab_material_08": "实验室材料08", + "block.box3.lab_material_09": "实验室材料09", + "block.box3.lab_material_10": "实验室材料10", + "block.box3.lab_material_11": "实验室材料11", + "block.box3.lab_material_12": "实验室材料12", + "block.box3.lab_material_13": "实验室材料13", + "block.box3.lab_material_14": "实验室材料14", + "block.box3.lab_material_15": "实验室材料15", + "block.box3.lab_screen": "实验室屏幕", + "block.box3.lab_wire": "实验室电线", + "block.box3.lantern_01": "灯笼01", + "block.box3.lantern_02": "灯笼02", + "block.box3.lava01": "熔岩01", + "block.box3.lava02": "熔岩02", + "block.box3.leaf_01": "叶子01", + "block.box3.leaf_02": "叶子02", + "block.box3.leaf_03": "叶子03", + "block.box3.leaf_04": "叶子04", + "block.box3.leaf_05": "叶子05", + "block.box3.leaf_06": "叶子06", + "block.box3.ledfloor01": "LED地板01", + "block.box3.ledfloor02": "LED地板02", + "block.box3.lemon": "柠檬", + "block.box3.less_than": "小于号", + "block.box3.light_blue_grass": "浅蓝草", + "block.box3.light_blue_grass_all": "浅蓝全草", + "block.box3.light_dirt": "浅土", + "block.box3.light_gray": "浅灰", + "block.box3.light_grey_stone_brick": "浅灰石砖", + "block.box3.light_purple_grass_": "浅紫草", + "block.box3.light_purple_grass_all": "浅紫全草", + "block.box3.light_sand": "浅沙", + "block.box3.light_volcanic_rock": "浅火山岩", + "block.box3.m": "M", + "block.box3.macaroon": "马卡龙", + "block.box3.maroon": "栗色", + "block.box3.medium_gray": "中灰", + "block.box3.medium_green": "中绿", + "block.box3.medium_orchid": "中兰花", + "block.box3.medium_purple": "中紫", + "block.box3.medium_spring_green": "中春绿", + "block.box3.medium_violet_red": "中紫红", + "block.box3.medium_yellow": "中黄", + "block.box3.mint_green": "薄荷绿", + "block.box3.mint_green_light": "薄荷绿灯", + "block.box3.multiply": "乘", + "block.box3.n": "N", + "block.box3.navajo_white": "纳瓦霍白", + "block.box3.nine": "九", + "block.box3.o": "O", + "block.box3.olive_green": "橄榄绿", + "block.box3.one": "一", + "block.box3.orange": "橙色", + "block.box3.orange_grass": "橙草", + "block.box3.orange_grass_all": "橙全草", + "block.box3.orange_light": "橙色灯", + "block.box3.orange_red": "橙红", + "block.box3.p": "P", + "block.box3.palace_carving": "宫殿雕刻", + "block.box3.palace_cloud": "宫殿云", + "block.box3.palace_eaves_01": "宫殿屋檐01", + "block.box3.palace_eaves_02": "宫殿屋檐02", + "block.box3.palace_eaves_03": "宫殿屋檐03", + "block.box3.palace_eaves_04": "宫殿屋檐04", + "block.box3.palace_eaves_05": "宫殿屋檐05", + "block.box3.palace_eaves_06": "宫殿屋檐06", + "block.box3.palace_eaves_07": "宫殿屋檐07", + "block.box3.palace_eaves_08": "宫殿屋檐08", + "block.box3.palace_floor": "宫殿地板", + "block.box3.palace_lamp": "宫殿灯", + "block.box3.palace_roof": "宫殿屋顶", + "block.box3.palace_window": "宫殿窗", + "block.box3.pale_green": "淡绿", + "block.box3.palm": "棕榈", + "block.box3.paren_close": "右圆括号", + "block.box3.paren_open": "左圆括号", + "block.box3.percent": "百分号", + "block.box3.period": "句号", + "block.box3.peru": "秘鲁色", + "block.box3.pink": "粉色", + "block.box3.pink_cake": "粉色蛋糕", + "block.box3.pink_grass": "粉草", + "block.box3.pink_grass_all": "粉全草", + "block.box3.pink_light": "粉色灯", + "block.box3.plank_01": "木板01", + "block.box3.plank_02": "木板02", + "block.box3.plank_03": "木板03", + "block.box3.plank_04": "木板04", + "block.box3.plank_05": "木板05", + "block.box3.plank_06": "木板06", + "block.box3.plank_07": "木板07", + "block.box3.polar_ice": "极地冰", + "block.box3.polar_region": "极地区域", + "block.box3.pound": "井号", + "block.box3.powder_blue": "粉蓝", + "block.box3.pumpkin": "南瓜", + "block.box3.pumpkin_lantern": "南瓜灯", + "block.box3.purple": "紫色", + "block.box3.purple_grass": "紫草", + "block.box3.purple_grass_all": "紫全草", + "block.box3.purple_surface_01": "紫色表面01", + "block.box3.purple_surface_02": "紫色表面02", + "block.box3.q": "Q", + "block.box3.quartz_brick": "石英砖", + "block.box3.question_mark": "问号", + "block.box3.quotation_mark": "引号", + "block.box3.r": "R", + "block.box3.rainbow_cube": "彩虹方块", + "block.box3.red": "红色", + "block.box3.red_brick": "红砖", + "block.box3.red_brick_floor": "红砖地板", + "block.box3.red_brick_wall": "红砖墙", + "block.box3.red_decorative_light": "红色装饰灯", + "block.box3.red_gift": "红色礼物", + "block.box3.red_glass": "红色玻璃", + "block.box3.red_light": "红色灯", + "block.box3.rock": "岩石", + "block.box3.roof_blue_04": "蓝色屋顶04", + "block.box3.roof_green": "绿色屋顶", + "block.box3.roof_grey": "灰色屋顶", + "block.box3.roof_purple": "紫色屋顶", + "block.box3.roof_red": "红色屋顶", + "block.box3.roof_yellow": "黄色屋顶", + "block.box3.s": "S", + "block.box3.sakura_pink": "樱花粉", + "block.box3.sand": "沙子", + "block.box3.sand_stones": "沙石", + "block.box3.semicolon": "分号", + "block.box3.seven": "七", + "block.box3.sienna": "赭石", + "block.box3.six": "六", + "block.box3.sky_blue": "天蓝", + "block.box3.slash": "斜杠", + "block.box3.snow": "雪", + "block.box3.snow_grass": "雪草", + "block.box3.snow_land": "雪地", + "block.box3.snowflake_lamp": "雪花灯", + "block.box3.snowland": "雪境", + "block.box3.snowman_body": "雪人身体", + "block.box3.snowman_head": "雪人头", + "block.box3.special_grass_01": "特殊草01", + "block.box3.special_grass_02": "特殊草02", + "block.box3.special_grass_03": "特殊草03", + "block.box3.special_grass_04": "特殊草04", + "block.box3.special_grass_05": "特殊草05", + "block.box3.special_grass_06": "特殊草06", + "block.box3.special_grass_07": "特殊草07", + "block.box3.special_grass_08": "特殊草08", + "block.box3.special_grass_09": "特殊草09", + "block.box3.special_grass_10": "特殊草10", + "block.box3.special_grass_11": "特殊草11", + "block.box3.special_grass_12": "特殊草12", + "block.box3.special_grass_13": "特殊草13", + "block.box3.special_grass_14": "特殊草14", + "block.box3.special_grass_15": "特殊草15", + "block.box3.special_grass_16": "特殊草16", + "block.box3.special_land_01": "特殊陆地01", + "block.box3.special_sand_01": "特殊沙01", + "block.box3.special_sand_02": "特殊沙02", + "block.box3.special_sand_03": "特殊沙03", + "block.box3.special_sand_04": "特殊沙04", + "block.box3.special_sand_05": "特殊沙05", + "block.box3.spiderweb": "蜘蛛网", + "block.box3.stained_glass": "染色玻璃", + "block.box3.stainless_steel": "不锈钢", + "block.box3.star_lamp": "星星灯", + "block.box3.stone": "石头", + "block.box3.stone_brick_01": "石砖01", + "block.box3.stone_brick_02": "石砖02", + "block.box3.stone_pillar_03": "石柱03", + "block.box3.stone_pillar_04": "石柱04", + "block.box3.stone_pillar_05": "石柱05", + "block.box3.stone_pillar_06": "石柱06", + "block.box3.stone_wall": "石墙", + "block.box3.stone_wall_01": "石墙01", + "block.box3.stripe_01": "条纹01", + "block.box3.stripe_02": "条纹02", + "block.box3.stripe_03": "条纹03", + "block.box3.stripe_04": "条纹04", + "block.box3.stripe_05": "条纹05", + "block.box3.subtract": "减", + "block.box3.t": "T", + "block.box3.television": "电视", + "block.box3.three": "三", + "block.box3.tilde": "波浪号", + "block.box3.toolbox": "工具箱", + "block.box3.traditional_window": "传统窗", + "block.box3.treasure_chest": "宝箱", + "block.box3.turquoise": "绿松石", + "block.box3.two": "二", + "block.box3.u": "U", + "block.box3.v": "V", + "block.box3.w": "W", + "block.box3.warm_yellow_light": "暖黄灯", + "block.box3.white": "白色", + "block.box3.white_grass": "白草", + "block.box3.white_light": "白灯", + "block.box3.window": "窗户", + "block.box3.windygrass": "风草", + "block.box3.winter_leaf": "冬叶", + "block.box3.withered_grass": "枯草", + "block.box3.withered_grass_land": "枯草地", + "block.box3.wood": "木头", + "block.box3.wooden_box": "木箱", + "block.box3.woodstone_12": "木石12", + "block.box3.x": "X", + "block.box3.y": "Y", + "block.box3.yellow_decorative_light": "黄色装饰灯", + "block.box3.yellow_grass": "黄草", + "block.box3.yellow_green": "黄绿", + "block.box3.yellow_light": "黄灯", + "block.box3.z": "Z", + "block.box3.zero": "零", + "itemGroup.box3.letter": "Box3:字母", + "itemGroup.box3.number": "Box3:数字", + "itemGroup.box3.symbol": "Box3:符号", + "itemGroup.box3.color": "Box3:颜色", + "itemGroup.box3.element": "Box3:元素", + "itemGroup.box3.food": "Box3:食物", + "itemGroup.box3.light": "Box3:灯光", + "itemGroup.box3.nature": "Box3:自然", + "itemGroup.box3.structure": "Box3:建筑", + "itemGroup.box3.models": "Box3:模型", + "command.box3.box3import.usage": "用法:/box3import <文件名>(从 config/box3/<文件名>.gz 读取),会在你的当前位置生成建筑", + "command.box3.box3export.usage": "用法:/box3export <文件名>(放置2个红石块作为对角点)", + "command.box3.box3import.success": "已从 %s 导入建筑", + "command.box3.box3import.progress": "正在导入 %s... %s%%", + "command.box3.box3import.failure": "导入失败:%s", + "command.box3.box3import.config_invalid": "配置文件读取失败,请检查文件是否存在且格式正确。", + "command.box3.box3import.config_missing": "配置文件不存在:%s", + "command.box3.box3import.config_read_failed": "读取配置失败:%s", + "command.box3.block_id.missing_file": "无法找到 block-id.json 文件。", + "command.box3.block_id.read_failed": "读取 block-id.json 失败。", + "command.box3.block_id.no_mapping_for_id": "未找到 ID %s 对应的方块映射。", + "command.box3.block_id.missing_fluid_block": "未找到流体方块:%s,使用水方块代替。", + "command.box3.block_id.missing_registered_block": "未找到注册的方块:%s。", + "command.box3.box3import.list.empty": "在 %s 中没有找到可用的地形文件(不包括 %s)", + "command.box3.box3import.list.success": "%s 中可用的地形文件:%s", + "command.box3.box3import.list.error": "列出 %s 中的地形文件失败:%s", + "command.box3.box3barrier.status": "屏障可见状态:%s", + "command.box3.box3barrier.set": "屏障可见状态已设置为:%s", + "command.box3.box3barrier.toggled": "屏障可见状态已切换为:%s(重新进入世界以完全生效)", + "command.box3.box3export.success": "导出成功:%s(扫描 %s 方块,导出 %s 个)", + "command.box3.box3export.failure": "导出失败:%s", + "command.box3.box3export.player_only": "该命令只能由玩家执行。", + "command.box3.box3export.selection_incomplete": "选区不完整,请先设置 pos1 和 pos2。", + "command.box3.box3export.selection_cleared": "已清空导出选区。", + "command.box3.box3export.pos_set": "已设置 %s:(%s, %s, %s)", + "command.box3.box3export.marker_invalid": "无效的标记方块ID:%s", + "command.box3.box3export.marker_count_invalid": "在半径 %s 内找到 %s 个标记方块(%s),需要刚好 2 个。", + "message.box3.model.config.mode": "模型配置模式:%s(木棍:增加,烈焰棒:减少,纸:复制,书:粘贴)", + "message.box3.model.config.status": "模式=%s 缩放=%s 偏移=(%s, %s, %s) 旋转=%s", + "message.box3.model.config.mode.scale": "缩放", + "message.box3.model.config.mode.offset_x": "X偏移", + "message.box3.model.config.mode.offset_y": "Y偏移", + "message.box3.model.config.mode.offset_z": "Z偏移", + "message.box3.model.config.mode.rotation": "旋转", + "message.box3.model.config.copy.success": "已复制当前模型参数。", + "message.box3.model.config.copy.empty": "没有可粘贴的已复制参数。", + "message.box3.model.config.copy.pasted": "已粘贴模型参数。", + "flat_world_preset.box3.box3_plains_world": "神岛平原" +} diff --git a/Fabric-26.2/src/main/resources/box3.mixins.json b/Fabric-26.2/src/main/resources/box3.mixins.json new file mode 100644 index 00000000..a28781d5 --- /dev/null +++ b/Fabric-26.2/src/main/resources/box3.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "package": "com.box3lab.mixin", + "compatibilityLevel": "JAVA_25", + "mixins": [], + "injectors": { + "defaultRequire": 1 + }, + "overwrites": { + "requireAnnotations": true + } +} \ No newline at end of file diff --git a/Fabric-26.2/src/main/resources/fabric.mod.json b/Fabric-26.2/src/main/resources/fabric.mod.json new file mode 100644 index 00000000..4831713d --- /dev/null +++ b/Fabric-26.2/src/main/resources/fabric.mod.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "id": "box3", + "version": "${version}", + "name": "Box3Blocks(神岛材质包)", + "description": "Box3 方块模组为 Minecraft 添加 372 款独特装饰方块,涵盖字母、符号、色块与发光方块,并统一整理至创造模式标签页。同时支持将神奇代码岛中的建筑/模型一键迁移到 Minecraft 中。", + "authors": ["神岛实验室"], + "contact": { + "homepage": "https://box3lab.com/", + "sources": "https://github.com/box3lab/Fabric-mcMod" + }, + "license": "Apache License 2.0", + "icon": "assets/box3/icon.png", + "environment": "*", + "entrypoints": { + "main": ["com.box3lab.Box3"], + "client": ["com.box3lab.Box3Client"] + }, + "mixins": [ + "box3.mixins.json", + { + "config": "box3.client.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": ">=0.19.3", + "minecraft": "~26.2", + "java": ">=25", + "fabric-api": "*" + } +}