From 96c3a34fc7213e78658ac871774206ad80655b0b Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 10 Aug 2026 17:56:25 +0800 Subject: [PATCH 01/38] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20port/1.21.1?= =?UTF-8?q?=20=E8=BF=81=E7=A7=BB=E8=87=B3=20dev/26.1=20=E7=9A=84=E5=AF=B9?= =?UTF-8?q?=E6=AF=94=E4=B8=8E=E8=BF=81=E7=A7=BB=E8=AE=A1=E5=88=92=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增分支对比报告:分支拓扑、提交主题、模块清单与逐模块差异分类 - 新增 Minecraft 与 NeoForge 版本差异文档:13 个 API 差异主题,含旧→新对照、涉及文件与迁移注意 - 新增迁移计划文档:8 个回迁项(含 module.yukkuri 整模块)的目标、步骤、API 适配点与验收方式 - 三个文档回迁项编号与提交引用保持一致,无法本地验证的点均标注待验证 --- .gitignore | 1 + docs/branch-comparison.md | 593 +++++++ docs/migration-plan-dev-26.1.md | 909 +++++++++++ docs/migration-plan-port-1.21.1.md | 1397 +++++++++++++++++ .../minecraft-neoforge-version-differences.md | 1140 ++++++++++++++ 5 files changed, 4040 insertions(+) create mode 100644 docs/branch-comparison.md create mode 100644 docs/migration-plan-dev-26.1.md create mode 100644 docs/migration-plan-port-1.21.1.md create mode 100644 docs/minecraft-neoforge-version-differences.md diff --git a/.gitignore b/.gitignore index 0a5cf974..2f8c2370 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,4 @@ run/ !gradle-wrapper.jar reference/ +ref/ diff --git a/docs/branch-comparison.md b/docs/branch-comparison.md new file mode 100644 index 00000000..2c94b196 --- /dev/null +++ b/docs/branch-comparison.md @@ -0,0 +1,593 @@ +# AnvilLib 分支对比报告:`port/1.21.1` ↔ `dev/26.1` + +> 文档信息 +> +> - 生成日期:2026-08-10 +> - 分析对象:`port/1.21.1` @ `fab551b`(Minecraft 1.21.1 / NeoForge 21.1.226)、`dev/26.1` @ `0b7e45f`(Minecraft 26.1.2 / + NeoForge 26.1.2.76) +> - 共同祖先(merge-base):`81c5a9c` +> - 数据来源:git 实测(`git log` / `git diff` / `git show` 只读命令)+ 8 个并行子代理模块级调研(见附录 D) +> - 关联文档:本文是 **分支对比报告**,聚焦「两侧关系与差异分类」;回迁执行细节见迁移计划文档(其中回迁项使用与本报告一致的 + canonical 编号 #1–#8) +> - 结论性质:除标注「待验证」处外,均基于实测提交与文件内容得出 + +--- + +## 1. 概述与结论 + +AnvilLib 的两个长期分支在共同祖先 `81c5a9c` 处分化后走向了不同的生命周期:`dev/26.1` 是 **演进主线**(在 merge-base 之后新增 +170 个提交),它沿 1.21.2 → 1.21.11 → 26.1 的版本链一路升级、引入 `module.gradle` 聚合构建体系、新建 rendering / rpc / +sync / explosion / font / collision / space-select 等一批新模块,并通过 `7019b38` 一次性移植了当时 1.21.1 侧的 +multiblock / util / network 后持续演进;`port/1.21.1` 则是 **维护与特性分支**(在 merge-base 之后新增 46 个提交),它在冻结的 +1.21.1 平台上继续落地了网络库、wheel 模块、multiblock 动态系统、油库(yukkuri)模块等特性并修复了一批 bug。迁移方向为 +**单向(port → dev)**:port 侧 46 个独有提交中,除约 23 个与 dev 早期提交同主题的镜像提交外,真正需要回迁的独有内容收敛为 +**8 项(#1–#8)**,其中 `module.yukkuri` 整模块(#1)是唯一的大项,其余为小型 bugfix 与可选 API 补充;codec / network / +config / integration / registrum / wheel / moveable-entity-block 七个共享模块判定为 **dev 超集或同源重写,无需回迁** +。结论是:以 dev/26.1 为最终目标,仅按 #1–#8 执行受控回迁,不做整分支合并。 + +### 1.1 结论要点 + +| # | 结论 | 依据 | +|----|----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------| +| C1 | 迁移方向单向:`port/1.21.1` → `dev/26.1`,不做反向合并 | dev/26.1 为演进主线(170 独有提交),port 侧为 1.21.1 平台维护分支(46 独有提交) | +| C2 | 回迁收敛为 8 项(#1–#8),唯一大项是 `module.yukkuri` 整模块(#1) | 逐模块双向 diff 分类结果(见第 5 章) | +| C3 | 七个共享模块无需回迁:codec / network / config / integration / registrum / wheel / moveable-entity-block | dev 侧为超集或同源重写;port 侧 bugfix 在新架构下天然规避 | +| C4 | 共享模块中 util / recipe / multiblock / test / main 存在回迁点,共 8 项中的 7 项(#2–#8) | 逐文件 diff 定位(见 5.2) | +| C5 | `module.yukkuri` 迁移需在「继续使用已弃用流体 API」与「适配新 `ResourceHandler`」之间二选一,推荐后者 | NeoForge 26.1.2.76 sources jar 实测(见 5.3) | +| C6 | port 侧独立构建脚本、lang datagen 产物、`ASYNC_MULTIBLOCK_CHECK_PLAN.md` 不回迁 | 构建体系已被 `module.gradle`(`ea4301f`)取代;文档已标注「已完成实现」 | + +--- + +## 2. 分支拓扑 + +### 2.1 拓扑数据 + +以下数据均为 2026-08-10 在本地仓库实测(只读命令): + +| 项 | 值 | +|----------------------------------------------|---------------------------------------------------------| +| merge-base(共同祖先) | `81c5a9c`(`81c5a9c5246a8d3642ea5cfddaf5298f885f7872`) | +| `port/1.21.1` HEAD | `fab551b`(`fab551bbda48837bf43b4423006b391e4f049221`) | +| `dev/26.1` HEAD | `0b7e45f`(`0b7e45f1bd52561f4ad42e468a72bfdcbee5d23c`) | +| dev 侧独有提交数(`port/1.21.1..dev/26.1`) | **170** | +| port 侧独有提交数(`dev/26.1..port/1.21.1`) | **46** | + +即:`port/1.21.1` 领先 `dev/26.1` **46 个提交**(port 独有),落后 **170 个提交**(dev 独有)。两侧从 merge-base +之后不再有共同提交,处于完全分叉状态。 + +```text + merge-base + 81c5a9c + │ + ┌──────────────────┼──────────────────┐ + │ (46 commits) │ (170 commits) │ + ▼ │ ▼ + port/1.21.1 │ dev/26.1 + fab551b │ 0b7e45f + MC 1.21.1 │ MC 26.1.2 + NeoForge 21.1.226 │ NeoForge 26.1.2.76 + │ + port 独有 46 提交 / dev 独有 170 提交 +``` + +### 2.2 版本与构建环境对比 + +| 项 | `port/1.21.1` | `dev/26.1` | +|-------------------|--------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------| +| Minecraft | 1.21.1(`gradle.properties`:`minecraft_version_range=[1.21.1,1.21.2)`) | 26.1.2(`gradle/libs.versions.toml`:`minecraft = "26.1.2"`) | +| NeoForge | 21.1.226 | 26.1.2.76(`gradle/libs.versions.toml`:`neoForge = "26.1.2.76"`) | +| Java | 21(`gradle.properties`:`java_version=21`) | 25(`gradle.properties`:`java_version=25`) | +| 映射(Parchment) | 1.21.1 / 2024.11.17 | 无 Parchment(26.1 时代不再使用) | +| 模组版本 | 2.0.0 | 2.0.0 | +| 许可证 | MIT | MIT | +| 构建体系 | 各模块独立 `build.gradle` + 根级 `gradle.properties` 传参 | 根级 `module.gradle` 聚合(`ea4301f`)+ `createModule` 任务(`16b7bb5`)+ 依赖坐标后缀 `-neoforge-26.1` | +| 依赖版本管理 | 无统一版本目录 | `gradle/libs.versions.toml`(modDevGradle 2.0.141、lombok 9.2.0、machete 2.0.1、jreleaser 1.23.0、mod-publish 1.1.0) | +| 模块结构 | 13 个 `module.*`(含 `module.yukkuri`) | 19 个 `module.*` + `module.gradle` + `renderdoc-loader`(不含 yukkuri) | + +dev/26.1 侧 `gradle.properties` 另有关键开关 `interface_injection=false`,port 侧无此项。 + +### 2.3 历史脉络与里程碑提交 + +两侧在 merge-base `81c5a9c` 之后的关键里程碑(按时间顺序): + +| 日期 | 提交 | 分支 | 事件 | +|------------|------------------|------|-------------------------------------------------------------------------------------------------------------------------------| +| 2026-03-26 | `6f2685a` | dev | `chore: update project to use Minecraft and NeoForge version 26.1`——正式切入 26.1 | +| 2026-04-14 | `5fc6f65` | port | `feat(multiblock): add dynamic multiblock system`——port 侧原创动态多方块系统 | +| 2026-05-02 | `7019b38` | dev | `chore: Port multiblock and util from 1.21.1`——一次性移植当时 1.21.1 侧的 multiblock / util / network,随后完成 26.1 API 适配 | +| 2026-05-11 | `ea4301f` | dev | `feat(build): update build configuration and add module.gradle for modular setup`——构建体系重构 | +| — | `569e01d`(#88) | port | `添加油库(里)`——新增 `module.yukkuri` 整模块 | +| — | `fab551b`(#91) | port | `修油库`——port 侧当前 HEAD | + +两侧在 merge-base 之后的完整升级链(dev 侧)与特性链(port 侧)见第 3 章提交主题对比。 + +### 2.4 拓扑解读 + +1. **dev/26.1 的 170 个独有提交**中,前段(`81d1f59` → `6f2685a`)是 1.21.2 → 26.1 的连续版本升级链(共 11 次版本更新提交),中段是 + 26.1 API 适配与模块演进,后段(`7019b38` 之后)是移植与新建模块的持续开发。170 的数字里包含大量重构/清理提交,实际「功能面」比表面小。 +2. **port/1.21.1 的 46 个独有提交**中,约 23 个与 dev 早期提交 **同主题镜像**(如网络库、license 改 MIT、bump 2.0.0、wheel + 等,对照表见附录 C),是两侧在分叉后分别重做/挑选的相同内容;真正 port 独有、dev 无对应物的内容集中在 multiblock + 动态系统、yukkuri、piston bugfix 与 util/recipe 的少量修复上——这正是回迁清单 #1–#8 的来源。 +3. 由于两侧无共同提交, **直接 merge 会引入大规模冲突且方向错误**(会把 dev 侧 26.1 演进全部回退到 1.21.1 + 语义),故采用「受控回迁」而非分支合并。 + +--- + +## 3. 提交主题对比 + +本章基于两条只读命令的完整输出归纳: + +```bash +git log --oneline port/1.21.1..dev/26.1 # dev 侧独有,170 条 +git log --oneline dev/26.1..port/1.21.1 # port 侧独有,46 条 +``` + +### 3.1 dev/26.1 侧关键演进主题(170 提交,7 个主题) + +#### 主题 D1:26.1 移植与版本升级链 + +dev 分支在 merge-base 之后的第一要务是版本升级:从 1.21.2 起步,经 1.21.3 → 1.21.11 共 11 次版本提交,最终在 `6f2685a` +(2026-03-26)切入 26.1;随后 `7019b38`(2026-05-02)移植 port 侧 multiblock / util / network,并做了一系列 26.1 API +适配(ItemStackTemplate、ResourceHandler 缓存、ResourceKey 配方标识、FMLLoader.getCurrent () 环境检查等)。 + +| 提交 | 主题 | +|-----------|----------------------------------------------------------------------------------------------------------------| +| `81d1f59` | `chore(build): 更新所有模块及相关配置至Minecraft 1.21.2版本`——升级链起点 | +| `d6fb290` | 1.21.5 升级 + `ItemSubPredicate → DataComponentPredicate` 重构 | +| `b3728b6` | 1.21.11 / 21.11.40-beta 升级 | +| `6f2685a` | `chore: update project to use Minecraft and NeoForge version 26.1`——正式切入 26.1 | +| `7019b38` | `chore: Port multiblock and util from 1.21.1`——一次性移植三模块 | +| `6e00420` | `refactor: replace ItemStack with ItemStackTemplate in various classes`——26.1 API 适配 | +| `2033fd9` | `refactor(recipe-cache): 替换物品处理器缓存为资源处理器缓存`——26.1 资源 API 适配 | +| `f14cde3` | `refactor: update RecipeMapMixin to use ResourceKey for recipe identification` | +| `f03fbd7` | `refactor: update BlockEntityBuilder and related classes to use FMLLoader.getCurrent() for environment checks` | + +#### 主题 D2:构建体系重构(`module.gradle` 聚合) + +dev 侧将各模块独立构建脚本统一为根级 `module.gradle` 聚合,配套 CI、发布配置同步重构。该体系 **不回迁**(port 侧不回迁 dev +的构建体系,见 5.4)。 + +| 提交 | 主题 | +|-----------|-----------------------------------------------------------------------------------------------------| +| `ea4301f` | `feat(build): update build configuration and add module.gradle for modular setup`——聚合构建核心提交 | +| `16b7bb5` | `feat(build): add createModule task for modular project setup` | +| `f97ec94` | `feat(build): add exclusive content configuration for Anvil Lib repository` | +| `1063ee9` | `Add sync processor module and refactor build configuration` | +| `aa59e37` | `refactor(ci): 重构CI工作流以支持模块化构建` | +| `7f27908` | `feat(ci): add synchronization workflows for build and Maven Central deployment` | +| `a26fde5` | `feat(build): 添加Maven发布仓库配置支持` | +| `ae2acd0` | `feat(build): add publishing configuration for CurseForge and Modrinth` | + +#### 主题 D3:新模块建设(rendering / rpc / sync / explosion / font / collision / space-select) + +dev 侧新建了一批 1.21.1 侧没有的模块:字体渲染(font)、SDF 渲染管线(rendering,含 RenderDoc 集成)、注解式 RPC、同步框架(sync,含独立 +processor 子模块)、爆炸模拟(explosion)、AABB/三角形相交检测(collision)、空间选择(space-select)。 + +| 提交 | 主题 | +|-----------|---------------------------------------------------------------------------------------| +| `1d56a4e` | `添加自定义文字渲染模块 module.font`(#22) | +| `7121bf6` | `feat(rendering): Add bloom post-processing and add RenderDoc integration`(#17) | +| `3ce2611` | `feat(sync): implement synchronization framework with SyncManager and annotations` | +| `bf49789` | `feat(sync): implement SyncConfigManager and payload handling for synchronization` | +| `d153e58` | `Implement RPC module with annotation-based remote method calls`(#60) | +| `4910620` | `feat(rpc): 添加对更多数据类型的网络传输支持`(#87) | +| `435e23f` | `Implement explosion module with features and optimizations`(#58) | +| `82b4f6a` | `feat(collision): add AnvilLibCollision for AABB and triangle intersection detection` | +| `1335fc5` | `Add space selection and district management features`(#24) | + +#### 主题 D4:rendering 模块持续演进(SDF / GUI / Bloom / 兼容性) + +rendering 是 dev 侧迭代最密集的模块:缓存型 Block Entity 渲染管线(视锥剔除、半透明排序、Bloom)、SDF 共享参数化、GUI 坐标 +int→float 破坏性重构、AMD/Mac 与 Iris 兼容修复等。 + +| 提交 | 主题 | +|-----------------------------------------------|----------------------------------------------------------------------------------------------------| +| `2751be4` | `feat(rendering): 引入缓存型 Block Entity Rendering 管线,支持视锥剔除、半透明排序与 Bloom`(#26) | +| `5b9cdcd` | `refactor(rendering)!: migrate GUI coordinate parameters from int to float`(#34)——破坏性重构 | +| `b5f5420` | `Implement bloom post-processing effect`(#20) | +| `8a818f0` | `feat(rendering): add compound submit node infrastructure with dirty tracking for bloom`(#23) | +| `6b5a544` / `6d5a97b` | SDF 渲染共享参数管理重构与索引修复(#44 / #45) | +| `d7a8f5d` | `fix(sdf): fix sdf not working on AMD/Mac` | +| `d121047` / `48bfe55` / `fba1027` / `0e8290d` | Iris 兼容(#68)、日志刷屏修复(#74)、Iris 顶点格式(#76)、bloom/compute 稳定性(#92) | + +#### 主题 D5:registrum 模块演进 + +registrum 在 dev 侧经历了注册表项修复、数据组件谓词构建器、CreativeTabBuilder、音效/配方注册、类重命名与文档链接修复等一系列演进。 + +| 提交 | 主题 | +|-----------------------------------|-------------------------------------------------------------------------------| +| `9b2d0e3` | `更多注册表注册`(#31) | +| `9627358` / `da801d2` / `8026804` | 修复误删的注册表项 + 补全 MIT 协议声明(#30) | +| `252922e` | `Registrum 添加数据组件谓词构建器支持`(#47) | +| `79965dc` | `refactor(registrum): 更新数据组件谓词方法的泛型类型定义`(#77) | +| `9c89bfa` | `Add CreativeTabBuilder for custom tabs and update item registration`(#27) | +| `c307a4c` | `添加了音效注册表,添加了配方注册`(#28) | +| `655ad78` | `refactor: rename classes and update original file links in Registrum module` | + +#### 主题 D6:multiblock 动态系统在 dev 侧的后续演进 + +`7019b38` 移植后,dev 侧继续演进动态多方块:定义获取改用 RegistryAccess、状态管理从 Long 换为 +BlockPos、新增成形/解体事件(#69)、修复未加载区块快照问题(#73)。 + +| 提交 | 主题 | +|-----------|------------------------------------------------------------------------------------------------------| +| `1b0a652` | `fix(multiblock): update definition retrieval to use registry access in form and unform packets` | +| `22091d4` | `fix(multiblock): update multiblock state management to use BlockPos instead of Long` | +| `1e5bfb3` | `feat(multiblock): implement dynamic multiblock event handling for formation and unformation`(#69) | +| `d888714` | `Fix bug where DynamicMultiblockManager attempts to load unloaded blocks`(#73) | +| `f40b016` | `feat(network): add Network annotation to multiblock package-info` | + +#### 主题 D7:代码质量与生态统一 + +| 提交 | 主题 | +|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `f94126f` | `refactor(nullness): replace javax.annotation with org.jspecify.annotations for nullability annotations`——@NullMarked 统一(注:另有 `3bf9b6c` 为「注册表更多种类添加 #25」,勿混淆) | +| `6fb6335` | `chore(license): update mod license from GNU LGPL 3.0 to MIT and change copyright holder` | +| `c72aa30` | `chore(annotations): 添加 ApiStatus.Internal 注解到内部接口和类`(#93) | +| `0b7e45f` | `refactor(common): 优化代码导入`(#96)——dev 当前 HEAD | +| `fd18fe9` / `b1484bc` | UnlimitedItemStack 弃用与构造优化(#55 / #86) | +| `113423a` | `feat(mod-description): update mod_description to provide a detailed overview of the space selection library` | + +### 3.2 port/1.21.1 侧关键演进主题(46 提交,6 个主题) + +#### 主题 P1:网络模块落地(1.21.1 平台) + +port 侧在分叉后完成了网络库的完整落地:模块创建、CI 补充、打包/mixin/自动注册三个修复。该系列与 dev 早期提交(`8d8ac8c` +等)同主题镜像,dev 侧为等价实现, **不回迁**。 + +| 提交 | 主题 | +|-----------------------|--------------------------------------------------| +| `a222300` | `feat(network): 添加网络库` | +| `19d98e3` / `10069a0` | 网络库描述与 javadoc 优化 | +| `0d6c1b4` | `feat(ci): 补充网络库缺失的ci` | +| `c7a790b` | `fix(packaging): 修复网络库不会打包进主库的问题` | +| `00d65d0` | `fix(mixin): 修复因缺失mixin文件导致的崩溃问题` | +| `6d2741f` | `fix(network): 修复网络包自动注册的问题` | +| `23719fb` / `d88b8a6` | `BoolAndInt` 紧凑编解码 record 与 long 编码改进 | + +#### 主题 P2:multiblock 动态系统(port 侧原创) + +动态多方块系统最初在 port 侧实现(`5fc6f65`,2026-04-14),后被 `7019b38` 移植到 dev。port 侧的独特贡献集中在异步检查、错误处理、位置修正与 +datagen 本地化。 + +| 提交 | 主题 | +|-----------|------------------------------------------------------------------------------------------------| +| `5fc6f65` | `feat(multiblock): add dynamic multiblock system`——动态系统核心 | +| `ba395e7` | `feat(multiblock): add data generation and localization support for multiblock configurations` | +| `2634e49` | `feat(multiblock): implement asynchronous multiblock checking with configurable parameters` | +| `3832895` | `feat(multiblock): enhance error handling in controller retrieval and logging` | +| `4aa9d13` | `feat(multiblock): improve position correction logic in multiblock controller detection` | + +#### 主题 P3:wheel 模块 + +wheel 在两侧均有提交(port `18834b5`/`3d0a364` ↔ dev `0d73ab0`/`1ae3411` 同主题),dev 侧为 26.1 等价实现, **不回迁**。 + +| 提交 | 主题 | +|-----------|-----------------------------------------------------------------------------------------------| +| `18834b5` | `feat(wheel): Implement Wheel module with API, documentation, and build updates`(#16) | +| `3d0a364` | `feat(wheel): Refactor WheelMenu and WheelEntry for improved action handling and null safety` | + +#### 主题 P4:util 模块扩展与修复 + +| 提交 | 主题 | +|-----------|--------------------------------------------------------------------------------------------------| +| `9d61997` | `feat(util): add ClientTickRecorder and utility methods for dimension and player name retrieval` | +| `9874db7` | `fix(tick): ensure tick recording only occurs when the level is not null`——**回迁项 #4** | +| `c8ee900` | `feat(util): update ISerializer package structure and add WeightedChanceBlockStates class` | +| `0efcd3d` | `feat(shape): add cut methods for VoxelShape and AABB to remove specified shapes` | +| `95bd5ca` | `feat(util): refactor utility classes to use UtilityClass annotation and improve organization` | + +#### 主题 P5:piston / moveable-entity-block 功能与 bugfix + +| 提交 | 主题 | +|-----------|---------------------------------------------------------------------------------------------| +| `53eaa11` | `feat(piston): implement moveable entity block functionality and rendering enhancements` | +| `1acdcf8` | `fix(piston): Fixed the issue of PoseStack not being emptied due to premature returns` | +| `9c437b1` | `feat(piston): enhance PistonBaseBlockMixin with shared block entity handling` | +| `f2b60b6` | `fix(cache): make setting the simulated item stack would properly handle empty type stacks` | + +结论:该模块 dev 侧以 26.1 渲染架构 **同源重写**(`83477ab` 等),port 侧 `1acdcf8` 修复的「PoseStack 提前 return +未清空」问题在新架构下天然规避, **无需回迁**(详见 5.4)。 + +#### 主题 P6:油库(yukkuri)模块与发布收尾 + +yukkuri 是 port 侧唯一整模块独有内容,也是回迁清单的最大项(#1);`1311782` 的 CurseForge 环境声明是 main 模块的回迁项(#8)。 + +| 提交 | 主题 | +|-----------|------------------------------------------------------------------------------------------------| +| `569e01d` | `添加油库(里)`(#88)——新增 `module.yukkuri`(14 个 Java 文件 + build + mods.toml + README) | +| `18b29ca` | `ci: publish yukkuri before aggregate module`(#89) | +| `1311782` | `fix: declare CurseForge environments`(#90)——**回迁项 #8** | +| `fab551b` | `修油库`(#91)——port 当前 HEAD,yukkuri 收尾修复 | + +#### 主题 P7:recipe / codec 杂项修复 + +| 提交 | 主题 | +|-----------------------|---------------------------------------------------------------------------------------------------------------| +| `edd71ec` | `修复鱼缸加工bug`(#83)——**回迁项 #2** | +| `e78d020` | `修复鱼缸加工bug`(#84)——**回迁项 #3** | +| `0b56109` | `Add DamageSourcePredicate StreamCodec and its relative StreamCodecs`(#67)——dev 侧 `3b5dfea` 同主题,不回迁 | +| `50aeac7` | `feat(codec): add zomListMap method ...`(#82)——dev 侧 `2e1b537` 同主题,不回迁 | +| `6a0e8a9` | `修复三个bug`(#79)——dev 侧 `922d523` 同主题,不回迁 | +| `6078b7b` / `88d5800` | en_ud 颠倒英语修复(port/`6078b7b` ↔ dev/`88d5800` 镜像) | + +### 3.3 两侧主题汇总对比 + +| 维度 | `port/1.21.1` | `dev/26.1` | +|--------------|------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| +| 平台 | 冻结在 MC 1.21.1 / NeoForge 21.1.226 | 升级链至 MC 26.1.2 / NeoForge 26.1.2.76 | +| 模块建设 | yukkuri(整模块)、wheel、网络库落地 | rendering / rpc / sync / explosion / font / collision / space-select / renderdoc-loader | +| 构建 | 各模块独立构建脚本(不回迁) | `module.gradle` 聚合 + `-neoforge-26.1` 坐标 + toml 版本目录(dev 特有) | +| multiblock | 动态系统原创、异步检查、datagen 本地化 | 移植后演进:成形/解体事件(#69)、未加载区块快照修复(#73)、BlockPos 状态 | +| bugfix 密度 | 高(网络打包、mixin 崩溃、鱼缸加工 ×2、PoseStack、ClientTickRecorder) | 高(误删注册表项、SDF AMD/Mac、Iris 兼容、log spam、gnome/xwayland 崩溃) | +| 代码质量动作 | UtilityClass 注解化、license MIT | @NullMarked(jspecify)统一、@ApiStatus.Internal、MIT | + +--- + +## 4. 模块清单对比 + +基于 `git ls-tree --name-only <分支>` 实测(见附录 A)。模块目录前缀均为 `module.*`;`module.gradle` 为 dev 侧构建聚合文件,不属于模块。 + +### 4.1 dev/26.1 独有模块(8 项) + +| 模块 | 一句话职责 | +|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------| +| `module.collision` | AABB 与三角形相交检测(`82b4f6a` 引入) | +| `module.explosion` | 爆炸模拟与优化(`435e23f` 引入) | +| `module.font` | 自定义文字渲染(`1d56a4e` 引入) | +| `module.rendering` | SDF 渲染管线、GUI 渲染、Bloom 后处理、RenderDoc 集成(`7121bf6` 等) | +| `module.rpc` | 注解式远程方法调用(`d153e58` 引入) | +| `module.space-select` | 空间选择与 DistrictKey 管理(`1335fc5` / `e75b10f`) | +| `module.sync` | 同步框架:SyncManager、SyncConfigManager、payload 处理,含独立 `processor/` 子模块(`3ce2611` / `1063ee9`) | +| `renderdoc-loader` | RenderDoc 原生加载器(`renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/RenderDocAgent.java`,与 `module.rendering` 配套) | + +### 4.2 port/1.21.1 独有模块(1 项) + +| 模块 | 一句话职责 | 状态 | +|------------------|-----------------------------------------------------------------------------------------------------------------------|---------------| +| `module.yukkuri` | 油库/汽化 API(`569e01d` #88 引入,14 个 Java 文件 + build.gradle + gradle.properties + META-INF/mods.toml + README) | **回迁项 #1** | + +### 4.3 共享模块(12 项) + +| 模块 | 结论(详见第 5 章) | 回迁项 | +|--------------------------------|----------------------------------|------------| +| `module.codec` | dev 超集 | 无 | +| `module.config` | dev 演进 | 无 | +| `module.integration` | dev 演进 | 无 | +| `module.main` | dev 演进 + port 侧 2 行回迁 | #8 | +| `module.moveable-entity-block` | 同源重写,无需回迁 | 无 | +| `module.multiblock` | dev 演进 + 可选回迁 | #6(可选) | +| `module.network` | dev 超集 | 无 | +| `module.recipe` | dev 演进 + 2 处 bugfix 回迁 | #2、#3 | +| `module.registrum` | dev 演进 | 无 | +| `module.test` | dev 演进 + multiblock 测试组回迁 | #7 | +| `module.util` | dev 演进 + 2 处回迁 | #4、#5 | +| `module.wheel` | dev 超集 | 无 | + +--- + +## 5. 逐模块差异分类总表 + +### 5.1 共享模块差异分类总表 + +对每个共享模块执行 `git diff port/1.21.1 dev/26.1 -- module.xxx`(见附录 B)并逐文件核对后,分类结论如下。标注【需回迁】的模块存在 +port 侧独有、dev 侧缺失的内容;标注【dev 超集/同源】的模块无需回迁。 + +| 模块 | 结论 | 关键依据 | 回迁项 | +|--------------------------------|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------| +| `module.codec` | **dev 超集** | port 侧 `0b56109`/`50aeac7`/`6a0e8a9` 均有 dev 同主题提交(`3b5dfea`/`2e1b537`/`922d523`);dev 另有 `173a454`(portable Codec/MapCodec 方法)、`52c3f8b`/`cf75837`(PositionPredicate AT 补充)、`3b5dfea` 等独有内容 | 无 | +| `module.network` | **dev 超集** | port 侧网络提交与 dev 早期提交同主题镜像(附录 C);dev 侧继续演进:`20721b1`(泛型检查优化)、`5733a9d`(发包工具)、`6220427`(PacketProtocol 公开)、`395495d`(双向注册 handler 参数) | 无 | +| `module.config` | **dev 演进** | dev 侧独有:`3d50d76`(Config 注解 group 属性)、`c0a619b`(配置管理器模组容器获取修复)、`946938f`/`1ddfd24`(配置管理增强与文档);port 侧无独有内容 | 无 | +| `module.integration` | **dev 演进** | dev 侧独有:`dc4d583`(data loader 重命名与类型处理)、`e8efbd4`(实例计数器自增);port 侧无独有内容 | 无 | +| `module.registrum` | **dev 演进** | dev 侧大幅演进(见主题 D5);port 侧无独有内容 | 无 | +| `module.wheel` | **dev 超集** | 两侧同主题提交(port `18834b5`/`3d0a364` ↔ dev `0d73ab0`/`1ae3411`),dev 侧在 26.1 API 上等价实现;port 侧无独有内容 | 无 | +| `module.moveable-entity-block` | **同源重写,无需回迁** | dev 侧 `83477ab`/`b5b27aa`/`2d4cd84` 以 26.1 渲染架构重写;port 侧 `1acdcf8`(PoseStack 未清空)在新架构下天然规避;`53eaa11`/`9c437b1` 与 dev `2d4cd84`/`b5b27aa` 对应 | 无 | +| `module.util` | **dev 演进 + 2 处回迁** | port 独有:`9874db7`(ClientTickRecorder 修复)、`5fc6f65` 中的 `BlockStatePredicate.Builder.with(BlockState)`;其余 port 提交(`9d61997`/`c8ee900`/`0efcd3d`/`95bd5ca`)dev 侧已有等价或超集实现 | **#4、#5** | +| `module.recipe` | **dev 演进 + 2 处 bugfix 回迁** | port 独有:`edd71ec`(InWorldRecipe 谓词栈回滚)、`e78d020`(ItemResourceHandlerCacheElement.sync() 短路/差量写入);dev 侧其余为演进(`2033fd9` 资源处理器缓存、`0d0649f` 配方代码优化等) | **#2、#3** | +| `module.multiblock` | **dev 演进 + 可选回迁** | dev 侧为移植后的超集(事件、快照修复、RegistryAccess);port 独有:`5fc6f65` 中 `MultiblockDefinition` 的 16 个 `BlockState` 重载——dev 侧是否保留待与维护者确认(见 #6) | **#6(可选)** | +| `module.test` | **dev 演进 + 测试组回迁** | dev 侧 module.test 为 26.1 新结构;port 侧独有的 multiblock 动态系统测试组(TestControllerBlock + LibBlocks/LibItemGroups/LibMultiblockControllers/LibMultiblocks + datagen 注册)在 dev 侧缺失 | **#7** | +| `module.main` | **dev 演进 + 2 行回迁** | port 独有:`1311782` 在 publishMods 块声明的 `clientRequired=true` / `serverRequired=true` 两行 CurseForge 环境配置;dev 侧 publishMods 块无此两行 | **#8** | + +### 5.2 回迁项详解(canonical 编号 #1–#8) + +> 编号、来源提交、落点与难度为既定事实(两侧调研一致);「迁移步骤 / 验收」为执行建议,供迁移计划文档引用。 + +#### #1 `module.yukkuri` 整模块(高 · 最大项) + +- **内容**:`module.yukkuri` 全部 14 个 Java 文件 + `build.gradle` + `gradle.properties` + `META-INF/mods.toml` + README。 +- **来源提交**:`569e01d`(#88,添加油库)、`18b29ca`(#89,CI 发布顺序——CI 配置部分不回迁,见 5.4)。 +- **落点**:dev/26.1 新模块 `module.yukkuri`(按 dev 侧 `module.gradle` 聚合结构落位,注册到根级构建)。 +- **API 适配点**(已核实,见 5.3): + - `ResourceLocation` → `Identifier`(26.1 重命名); + - 流体能力 API 二选一: (A) 继续使用已弃用的 `IFluidHandler`/`FluidStack`(可编译、有告警); (B) 适配新 + `ResourceHandler`(推荐,与 dev/26.1 物品能力迁移方向一致); + - `Level.getCapability(...)` 调用签名核对(26.1 下 capability 查询方式有调整,需对照 + `BlockCapability.createSided(Identifier, Class)` 现状)。 +- **迁移步骤**:① 从 `port/1.21.1:module.yukkuri/` 导出全部源码与资源 → ② 按 dev 侧包结构(`dev.anvilcraft.lib.v2` + 体系)落位 → ③ 全局替换 `ResourceLocation`/`Identifier` → ④ 按选项 B 重写流体交互层 → ⑤ 注册进 `module.gradle` + 聚合与主模块依赖 → ⑥ 编译 + 运行时冒烟。 +- **验收**:`gradlew :module.yukkuri:build` 通过;游戏内油库/汽化功能在 26.1.2 客户端与服务器端可正常交互;无弃用 API 告警(若选 + B)。 +- **难度**:高(涉及 26.1 流体新 API,需对照 sources jar 逐方法适配)。 + +#### #2 `module.recipe`:`InWorldRecipe.matches()` 谓词栈回滚(低) + +- **来源提交**:`edd71ec`(#83)。 +- **落点**:`module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java`(dev 侧同名文件)。 +- **内容**:`matches()` 中谓词计算栈的回滚修复(纯逻辑搬运,无 API 变更)。 +- **适配点**:无;两侧文件同名同包,按 diff 直接搬逻辑。 +- **验收**:port 侧 #83 对应的鱼缸加工回归场景在 dev 侧复现测试通过(dev 侧 module.test 补一条回归用例)。 + +#### #3 `module.recipe`:`ItemResourceHandlerCacheElement.sync()` 短路/差量写入(中) + +- **来源提交**:`e78d020`(#84)。 +- **落点**:`module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/ItemResourceHandlerCacheElement.java` + (port 侧原名 `ItemHandlerCacheElement.java`,dev 侧已随 `2033fd9` 改名并换用资源处理器缓存体系)。 +- **内容**:`sync()` 中避免无谓写入的短路与差量写入逻辑。 +- **适配点**:dev 侧 `ResourceHandler` 无 `setResource` 方法,需改为在 Transaction 内 extract + insert 实现差量写入(与 dev + 侧现有缓存写入模式对齐)。 +- **验收**:缓存同步逻辑在 dev 侧单元/集成测试通过;无空转写入路径(可日志/断言验证调用次数)。 + +#### #4 `module.util`:`ClientTickRecorder` 修复(低) + +- **来源提交**:`9874db7`(主菜单计 tick / 退出归零修复)。 +- **落点**:`module.util/src/main/java/dev/anvilcraft/lib/v2/util/ClientTickRecorder.java`(dev 侧同路径)。 +- **内容**:仅当 level 非空时记录 tick;退出时归零(`9874db7` 为 +9/-2 行的小修复)。 +- **适配点**:保留 dev 侧 `@EventBusSubscriber(modid = AnvilLibUtil.MOD_ID, ...)` 写法, **不要**搬 port 侧无 modid 的写法。 +- **验收**:主菜单不累计 tick、进世界正常计数、退出世界归零;dev 侧该文件编译通过。 + +#### #5 `module.util`:`BlockStatePredicate.Builder.with(BlockState)`(低–中) + +- **来源提交**:`5fc6f65`(与 #6 同提交)。 +- **落点**:`module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java`。 +- **内容**:恢复 `Builder.with(BlockState)` 便捷重载(16 个 `BlockState` 重载的底层支撑)。 +- **适配点**:port 实现使用 `Property.getName(T)`(26.1 已移除)→ 改用 dev 侧现有泛型重载 `with(Property, T)` 实现。 +- **联动决策**:与 #6 联动——若 #6 判定不迁(dev 有意精简 API),#5 应一并放弃;反之 #5 是 #6 的前置。 +- **验收**:`BlockStatePredicate.Builder` 以 `BlockState` 入参构造谓词编译通过并可正确匹配。 + +#### #6 `module.multiblock`:`MultiblockDefinition` 的 16 个 `BlockState` 重载(中 · 可选) + +- **来源提交**:`5fc6f65`。 +- **落点**: + `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/definition/MultiblockDefinition.java`(dev + 侧同路径)。 +- **内容**:16 个以 `BlockState` 直接入参的重载构造/方法。 +- **前置**:依赖 #5 恢复 `with(BlockState)`。 +- **决策点**:若判定 dev 侧删除是有意的 API 精简(`@ApiStatus.Internal` 化趋势,见 `c72aa30`),则 **放弃整项**——保留 dev + 现状,不引入 port 侧便捷层。 +- **验收**(若迁):dev 侧 `MultiblockDefinition` 以 `BlockState` 入参编译通过;动态多方块测试组(#7)覆盖该路径。 + +#### #7 `module.test`:multiblock 动态系统测试组(中–高) + +- **内容**:port 侧独有的动态多方块测试组:`TestControllerBlock` + `LibBlocks` / `LibItemGroups` / + `LibMultiblockControllers` / `LibMultiblocks` + datagen 注册。 +- **落点**:按 dev 侧 `module.test` 新结构落位(dev 侧 module.test 已按 26.1 重构)。 +- **适配点**:`@Mod` 构造器去 ModContainer 参数(26.1 签名变化);`REGISTRUM.addDataGenerator(...)` 替代 GatherDataEvent(dev + 侧现有注册模式);`ResourceLocation` → `Identifier`。 +- **验收**:dev 侧 `:module.test` 构建与测试运行通过,动态多方块成形/解体用例覆盖 #2/#6 涉及的逻辑。 + +#### #8 `module.main`:curseforge 环境两行(低) + +- **来源提交**:`1311782`(#90)。 +- **落点**:`module.main/build.gradle` 的 `publishMods` 块。 +- **内容**:`clientRequired = true` / `serverRequired = true` 两行 CurseForge 环境声明。 +- **适配点**:按 dev 侧 `publishMods` 现有结构落位(`ae2acd0` 引入的发布配置); **不要**把 port 侧 `jarJar(api ...)` 写法搬回。 +- **验收**:`module.main` 发布任务 dry-run 通过,CurseForge 元数据含环境声明。 + +### 5.3 油库(yukkuri)流体能力 API 迁移选项(已核实事实) + +以下事实来自本地 NeoForge **26.1.2.76 sources jar**(`neoforge-26.1.2.76-sources.jar`,路径见附录 D),可直接作为 #1 的适配依据: + +1. **旧 API 仍存在但已弃用**:`net.neoforged.neoforge.fluids.capability.IFluidHandler` 与 + `net.neoforged.neoforge.fluids.FluidStack` 仍可编译,但 `IFluidHandler` 标注 + `@Deprecated(since = "1.21.9", forRemoval = true)`,并提供了迁移包装器 `IFluidHandler.of(ResourceHandler)`。 +2. **新 API**: + - `net.neoforged.neoforge.transfer.ResourceHandler`:方法为 `getResource` / `extract` / `insert` / `isValid` / + `getCapacityAsInt` / `size`, **无 `setResource`**; + - `net.neoforged.neoforge.transfer.fluid.FluidResource`:不可变,实现 `DataComponentHolderResource`,静态字段 + `CODEC` / `OPTIONAL_CODEC` / `STREAM_CODEC` / `EMPTY`。 +3. **Capability 查询**:`BlockCapability.createSided(Identifier, Class)` 在 26.1 仍存在,仅标识符类型改为 `Identifier`。 + +| 选项 | 做法 | 代价 | 建议 | +|----------------------|----------------------------------------------------------------------|------------------------------------------------------------------------|--------------------| +| A 最小移植 | 继续使用已弃用的 `IFluidHandler` / `FluidStack` | 可编译但有弃用告警;未来 `forRemoval` 后必须再迁 | 不推荐(仅作兜底) | +| B 适配新 API(推荐) | 改用 `ResourceHandler`,Transaction 内 insert/extract | 与 dev/26.1 侧物品能力迁移方向(`2033fd9` 资源处理器化)一致,一步到位 | **推荐** | + +### 5.4 不回迁清单与理由 + +| 内容 | 理由 | +|------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `module.codec` / `module.network` / `module.config` / `module.integration` / `module.registrum` / `module.wheel` | dev 侧为超集或已独立演进(见 5.1 各模块行) | +| `module.moveable-entity-block`(含 `1acdcf8` PoseStack 修复) | dev 侧以 26.1 渲染架构同源重写(`83477ab` 等),port 侧 bugfix 在新架构下天然规避 | +| port 侧各模块独立构建脚本、`gradle.properties` | 已被 dev 侧 `module.gradle` 聚合 + toml 版本目录取代(`ea4301f`),回迁即倒退 | +| lang datagen 产物(`zh_cn.json` / `en_us.json`) | dev 侧统一由 datagen 生成(`e3ba2cd` 起块物品自行提供 lang),直接提交产物与 dev 工作流冲突 | +| `module.multiblock/ASYNC_MULTIBLOCK_CHECK_PLAN.md` | 文档已标注「已完成实现」;dev 侧异步检查实现等价且更完善(`d888714` 等),建议不迁移 | +| `18b29ca`(CI 发布顺序) | CI 体系 dev 侧已整体重构(`aa59e37`/`7f27908`),port 侧 CI 片段不适用 | +| dev 侧特有演进(不回迁方向) | 动态多方块事件 `1e5bfb3`(#69)、未加载区块快照修复 `d888714`(#73)、@NullMarked 统一 `f94126f`、`module.gradle` 构建体系 `ea4301f`、@ApiStatus.Internal `c72aa30`(#93)——均为 dev 独有,port 侧不反向吸收 | + +--- + +## 6. 附录:研究方法 + +### A. merge-base 分析 + +```bash +# 共同祖先与两侧 HEAD +git merge-base port/1.21.1 dev/26.1 # → 81c5a9c5246a8d3642ea5cfddaf5298f885f7872 +git rev-parse port/1.21.1 dev/26.1 # → fab551bbda... / 0b7e45f1bd... + +# 双向独有提交计数 +git rev-list --count port/1.21.1..dev/26.1 # → 170(dev 独有) +git rev-list --count dev/26.1..port/1.21.1 # → 46(port 独有) + +# 模块清单(顶层 module.* 目录) +git ls-tree --name-only dev/26.1 | grep '^module\.' +git ls-tree --name-only port/1.21.1 | grep '^module\.' +``` + +解读:merge-base 之后两侧完全分叉;dev 独有 170 提交中前段为版本升级链,port 独有 46 提交中约一半为 dev 早期提交的镜像(见附录 +C)。 + +### B. 双向 diff(逐模块差异分类) + +对每个共享模块执行双向 diff,人工核对「port 独有 vs dev 独有」的实质内容: + +```bash +# 两侧差异(粗粒度定位) +git diff port/1.21.1 dev/26.1 -- module.util +# port 侧独有修改(回迁候选) +git diff port/1.21.1..dev/26.1 -- module.util # 即 dev 相对 port 的变更 +# 按文件核对提交归属 +git log --oneline dev/26.1..port/1.21.1 -- module.recipe +``` + +差异分类规则: + +| 分类 | 判据 | 动作 | +|---------------|-------------------------------------------------------------------|-----------------------| +| port 独有内容 | port 侧有修改/文件,dev 侧无对应 | 进入回迁候选(#1–#8) | +| dev 超集 | dev 侧包含 port 侧全部实质内容且有更多演进 | 不回迁 | +| 同源重写 | 两侧同一功能在不同 API 架构上分别实现(如 moveable-entity-block) | 不回迁 | +| 同主题镜像 | port 提交与 dev 早期提交主题一致(如网络库、wheel) | 不回迁,以 dev 为准 | + +### C. 提交对照(port 独有 46 提交的镜像/独有判定) + +对 `dev/26.1..port/1.21.1` 的 46 条提交逐条与 dev 侧 170 条比对主题后,同主题镜像关系如下(节选): + +| port 提交 | 镜像主题 | dev 侧对应提交 | +|-----------------------------------------------------------------------------------|----------------------------------------|-----------------------------------------------------------------------------------| +| `a222300` / `19d98e3` / `10069a0` / `0d6c1b4` / `c7a790b` / `00d65d0` / `6d2741f` | 网络库落地/优化/CI/打包/mixin/自动注册 | `8d8ac8c` / `548cecd` / `0fc80e5` / `baac9a0` / `d85e3d5` / `0e86a9d` / `09e435c` | +| `18834b5` / `3d0a364` | wheel 模块 | `0d73ab0` / `1ae3411` | +| `bcefcb9` | license LGPL→MIT | `6fb6335` | +| `a74671a` | README 格式 | `371d5db` | +| `1dca02f` | bump 2.0.0 | `65a605d` | +| `6078b7b` / `109a935` | en_ud 修复 / 风格优化 | `88d5800` / `42f816d` | +| `23719fb` / `d88b8a6` | BoolAndInt 编解码 | `3fc6054` / `8e01120` | +| `d7985f6` / `f2b60b6` | ItemCache 范围/空栈处理 | `ec16ccb` / `1c523ee` | +| `53eaa11` / `9c437b1` | moveable entity block | `83477ab` / `2d4cd84` | +| `0b56109` | DamageSourcePredicate StreamCodec | `3b5dfea` | +| `50aeac7` | zomListMap | `2e1b537` | +| `6a0e8a9` | 三个 bug 修复 | `922d523` | + +判定为 port 真正独有(无 dev 对应)的提交:`5fc6f65`(动态多方块系统,dev 经 `7019b38` 移植后已改写)、`ba395e7` / `2634e49` / +`3832895` / `4aa9d13`(异步检查/错误处理/位置修正/datagen 本地化——其中部分已被 dev 侧等价实现吸收)、`9874db7` +(ClientTickRecorder 修复)、`edd71ec` / `e78d020`(鱼缸加工 bugfix ×2)、`569e01d` / `18b29ca` / `1311782` / `fab551b` +(yukkuri 系列)、`9d61997` / `c8ee900` / `0efcd3d` / `95bd5ca`(util 扩展——dev 侧已有等价或超集,不回迁)。 + +### D. 参考资料 + +| 资料 | 位置 | +|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 8 个并行子代理模块级调研报告(codec/util/network/recipe/registrum/multiblock/moveable-entity-block/main/config/integration/wheel/test 差异分类与 API 变化点明细) | `C:/Users/Administrator/.kimi-code/sessions/wd_anvillib_031cb785b7e3/session_6033f19b-f4ef-481a-b18c-d626cc22c314/agents/main/tool-results/AgentSwarm-call_00_nh47UIFAaqo9d96levr46332-594fb004-9f1f-4c9e-b230-43bc1440211d.txt` | +| NeoForge 26.1.2.76 sources jar(流体能力 API 核实依据) | `C:/Users/Administrator/.gradle/caches/modules-2/files-2.1/net.neoforged/neoforge/26.1.2.76/1d1dabe31afb953e46fb278e1cd8f04ab8e0d9a0/neoforge-26.1.2.76-sources.jar` | +| 只读 git 命令(随时可用) | `git show <分支>:<路径>`、`git diff port/1.21.1 dev/26.1 -- <路径>`、`git log --oneline port/1.21.1..dev/26.1 -- <路径>`、`git grep -n <关键字> <分支> -- <路径>` | + +### E. 待验证事项 + +| # | 事项 | 验证方法 | +|----|--------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| +| V1 | #5/#6 的 `with(BlockState)` 在 26.1 的可用性(`Property.getName(T)` 已移除,改用泛型重载后行为等价性) | dev 分支上编译运行 `BlockStatePredicate` 相关测试 | +| V2 | #1 选项 B 的 `ResourceHandler` 迁移完整性(14 个 Java 文件逐一核对 API 用法) | `gradlew :module.yukkuri:compileJava` 编译输出对照 26.1.2.76 sources jar | +| V3 | #7 测试组在 dev 侧 `module.test` 新结构的落位方式(`REGISTRUM.addDataGenerator` 注册模式) | 对照 dev 侧 module.test 现有注册代码 + 构建运行 | +| V4 | `module.yukkuri` 的 mods.toml 依赖声明与 `module.main` 聚合依赖是否需要调整 | 检查 dev 侧其他模块的 mods.toml 依赖写法 | +| V5 | #8 两行配置在 dev 侧 `publishMods` 块的结构兼容性 | `module.main` 发布任务 dry-run | diff --git a/docs/migration-plan-dev-26.1.md b/docs/migration-plan-dev-26.1.md new file mode 100644 index 00000000..81da3c3b --- /dev/null +++ b/docs/migration-plan-dev-26.1.md @@ -0,0 +1,909 @@ +# AnvilLib 迁移计划:`port/1.21.1` → `dev/26.1`(回迁执行方案) + +> 文档信息 +> +> - 生成日期:2026-08-10 +> - 分析对象:`port/1.21.1` @ `fab551b`(Minecraft 1.21.1 / NeoForge 21.1.226)、`dev/26.1` @ `0b7e45f`(Minecraft 26.1.2 / + NeoForge 26.1.2.76) +> - 共同祖先(merge-base):`81c5a9c` +> - 数据来源:git 实测(`git show` / `git diff` / `git log` 只读命令,2026-08-10 本地仓库)+ NeoForge 26.1.2.76 sources jar + 实测 + 8 个并行子代理模块级调研 +> - 关联文档:本文是 **迁移计划文档**,与 `docs/branch-comparison.md`(分支对比报告)配套;回迁项使用与对比报告一致的 + canonical 编号 #1–#8 +> - 结论性质:除标注「待验证」处外,均基于实测提交、文件内容与 26.1.2.76 sources jar 得出 +> - 执行约束:本计划只描述 **内容回迁**,不含 dev 侧既有演进的搬运;本文件仅为计划,执行时需在 dev/26.1 分支上以受控提交落地 + +--- + +## 1. 概述与范围 + +### 1.1 迁移背景 + +AnvilLib 的两个长期分支在 merge-base `81c5a9c` 之后完全分叉:`dev/26.1` 是演进主线(独有 170 提交),沿 1.21.2 → 1.21.11 → +26.1 的版本链升级,并完成了 `module.gradle` 聚合构建体系重构;`port/1.21.1` 是维护与特性分支(独有 46 提交),在冻结的 1.21.1 +平台上落地了网络库、wheel、multiblock 动态系统、油库(yukkuri)模块并修复了一批 bug。 + +`docs/branch-comparison.md` 已对两侧关系做了完整分类:port 侧 46 个独有提交中,约 23 个与 dev 早期提交同主题镜像,真正需要回迁的独有内容收敛为 +**8 项(canonical 编号 #1–#8)**。其中 `module.yukkuri` 整模块(#1)是唯一大项,其余为小型 bugfix、API 补充与发布配置。 + +本文档即这 8 项的执行计划: **每项给出目标、步骤(含具体 git 命令)、API 适配点与验收方式**,并汇总构建/CI +接入点、不做清单、建议执行顺序与总体验收标准。 + +### 1.2 范围界定 + +本计划 **包含**: + +- 8 个回迁项(#1–#8)的完整执行方案与验收判据; +- 回迁涉及的构建 / CI / README 联动修改(仅 #1 与 #8 涉及); +- 每个回迁项与 port 侧来源提交、dev/26.1 落点文件的一一对应关系。 + +本计划 **不包含**(即「不做清单」的完整理由见第 5 章): + +- dev 侧既有演进的任何反向搬运(动态多方块事件 `1e5bfb3` #69、未加载区块快照修复 `d888714` #73、`@NullMarked` 统一 + `3bf9b6c`/`f94126f`、`module.gradle` 构建体系 `ea4301f`、`@ApiStatus.Internal` `c72aa30` #93 等); +- codec / network / config / integration / registrum / wheel / moveable-entity-block 七个共享模块的任何内容(dev + 侧为超集或同源重写,见第 5 章); +- port 侧各模块独立构建脚本与 lang datagen 产物(构建体系已被 dev 侧聚合取代,lang 由 datagen 统一生成)。 + +**迁移方式**:两侧无共同提交,直接 merge 方向错误且冲突不可控,故采用 **受控回迁**——在 dev/26.1 分支上按 #1–#8 +逐个以独立提交落地,每个提交只改本项内容,便于评审与回滚。 + +### 1.3 前置条件与约定 + +| 项 | 约定 | +|----------|---------------------------------------------------------------------------------------------------------------------------------| +| 编号 | 回迁项一律使用 canonical 编号 **#1–#8**,与 `docs/branch-comparison.md` 第 5.2 节一致 | +| 基线 | dev/26.1 @ `0b7e45f`(文档描述的全部落点均以该提交为准;执行时如 HEAD 已前进,需先以 `git log` 复核落点文件未变) | +| 提取方式 | 一律用 `git show port/1.21.1:<路径>` 提取源码,**不**进行分支切换、不产生 git 写入操作 | +| 工作区 | 回迁在 dev/26.1 分支工作树执行;只读对比可用 `git diff port/1.21.1 dev/26.1 -- <路径>` 或临时工作树(`git -C <临时目录> diff`) | +| 验证基线 | dev 侧构建命令:`gradlew :module.xxx:build`、`gradlew :module.xxx:compileJava`、`gradlew runData`(模块需要 datagen 时) | +| 待验证 | 所有标注「待验证」的条目均已给出验证方法,执行时须完成验证后才能标记该项完成 | +| API 事实 | NeoForge 26.1.2.76 的流体能力 API 事实见 3.1.4 小节(已实测),其余 26.1 API 差异以编译期核对为准 | + +### 1.4 回迁项一览(结论先行) + +| # | 内容 | 难度 | 一句话结论 | +|----|--------------------------------------------------------|-------|------------------------------------------------------------------------------------------------------------| +| #1 | `module.yukkuri` 整模块 | 高 | 唯一大项;14 个 Java 文件 + 构建/元数据/README,需按 dev 聚合结构落位并做 26.1 流体 API 适配(推荐选项 B) | +| #2 | `InWorldRecipe.matches()` 谓词栈回滚 | 低 | 纯逻辑搬运,无 API 变更 | +| #3 | `ItemResourceHandlerCacheElement.sync()` 短路/差量写入 | 中 | dev 侧已换 `ResourceHandler` 体系,需 Transaction 内 extract+insert 重写 | +| #4 | `ClientTickRecorder` 修复 | 低 | 判空 + 退出归零两处小改,保留 dev 侧 modid 写法 | +| #5 | `BlockStatePredicate.Builder.with(BlockState)` | 低-中 | 改走 dev 侧泛型重载实现;与 #6 联动决策 | +| #6 | `MultiblockDefinition` 16 个便捷重载 | 中 | 依赖 #5;若判定 dev 删除属有意精简则整体放弃 | +| #7 | `module.test` multiblock 测试组 | 中-高 | 按 dev 侧 module.test 新结构落位,适配 26.1 注册 API | +| #8 | `module.main` CurseForge 环境两行 | 低 | `publishMods` 块加两行,无其他影响 | + +--- + +## 2. 回迁项总表 + +### 2.1 总表(含前置依赖) + +> 内容、来源提交、落点与难度与 `docs/branch-comparison.md` 第 5.2 节完全一致;「前置依赖」列为本文档新增,用于编排执行顺序。 + +| # | 内容 | 来源提交 | 落点(dev/26.1) | 难度 | API 适配点 | 前置依赖 | +|---|------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|------------------|------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------| +| 1 | module.yukkuri 整模块(14 Java 文件 + build + mods.toml + README) | `569e01d` #88、`18b29ca` #89 | 新模块 `module.yukkuri/` | 高(最大项) | `ResourceLocation`→`Identifier`;流体 API(见 3.1.4);`Level.getCapability` 签名核对 | 无代码前置;需 `settings.gradle` / `module.main` 依赖 / CI(`modules.json`)/ README 四处联动(见 3.1.3) | +| 2 | module.recipe:`InWorldRecipe.matches()` 谓词栈回滚 | `edd71ec` #83 | `InWorldRecipe.java`(同名同包) | 低(纯逻辑搬运) | 无 | 无 | +| 3 | module.recipe:`ItemResourceHandlerCacheElement.sync()` 短路/差量写入 | `e78d020` #84 | `cache/item/ItemResourceHandlerCacheElement.java`(port 侧原名 `ItemHandlerCacheElement`,dev 侧已随 `2033fd9` 改名) | 中 | dev 侧 `ResourceHandler` 无 `setResource`,需在 Transaction 内 extract+insert 实现原子替换 | 无 | +| 4 | module.util:`ClientTickRecorder` 修复(主菜单计 tick / 退出归零) | `9874db7` | `util/ClientTickRecorder.java` | 低 | 无;保留 dev 侧 `@EventBusSubscriber(modid=AnvilLibUtil.MOD_ID,...)` 写法,勿搬 port 侧无 modid 写法 | 无 | +| 5 | module.util:`BlockStatePredicate.Builder.with(BlockState)` | `5fc6f65` | `predicate/BlockStatePredicate.java` | 低-中 | port 实现用 `Property.getName(T)`(26.1 已移除)→ 改用 dev 侧泛型重载 `with(Property, T)`;与 #6 联动决策 | 无 | +| 6 | module.multiblock:`MultiblockDefinition` 的 16 个 `BlockState` 便捷重载 | `5fc6f65` | `dynamic/definition/MultiblockDefinition.java` | 中 | 依赖 #5 恢复 `with(BlockState)`;若判定 dev 侧删除是有意 API 精简则放弃整项 | **#5** | +| 7 | module.test:multiblock 动态系统测试组(TestControllerBlock + LibBlocks/LibItemGroups/LibMultiblockControllers/LibMultiblocks + datagen 注册) | port 特有(`5fc6f65` 动态系统配套,`ba395e7` datagen) | 按 dev 侧 module.test 新结构落位 | 中-高 | `@Mod` 构造器去 ModContainer;`REGISTRUM.addDataGenerator` 替代 GatherDataEvent;`ResourceLocation`→`Identifier` | **#6**(测试组用到 `MultiblockDefinition.builder()` 便捷重载与 `BlockStatePredicate`) | +| 8 | module.main/build.gradle:curseforge `clientRequired=true`/`serverRequired=true` | `1311782` #90 | `module.main/build.gradle` publishMods 块 | 低(两行) | 按 dev 侧结构落位;勿把 port 侧 `jarJar(api ...)` 写法搬回 | 无 | + +### 2.2 编号一致性说明 + +- 本表 #1–#8 与 `docs/branch-comparison.md` 5.2 节一一对应,本次回迁以本表为唯一执行依据。 +- #5 与 #6 同源(均出自 port 提交 `5fc6f65`),#5 是 #6 的 **代码级前置**(`MultiblockDefinition` 的 `BlockState` 重载内部调用 + `BlockStatePredicate.builder().with(state)`);二者共享同一个「放弃则一并放弃」的联动决策(见 3.6.3)。 +- #1 无代码前置,但牵涉 4 处工程联动(`settings.gradle`、`module.main/build.gradle`、`.github/modules.json` + 、README),建议最先执行以尽早暴露 26.1 流体 API 适配风险。 +- #7 依赖 #6 的便捷重载(测试定义 `LibMultiblocks` 使用 `addController(...)` / `add(...)` / + `seriaBuilder().mapController(...)` 等),故排在 #6 之后。 +- #8 独立,可随时执行,安排在最后作为收尾。 + +--- + +## 3. 逐项详细计划 + +> 每节统一包含四部分: **目标**(一句话)、 **步骤**(具体到 git 命令与落点文件)、 **API 适配点**(旧 → 新)、 **验收方式**。步骤中的 +> `git show port/1.21.1:<路径>` 均为只读操作。 + +### 3.1 迁移项 #1:`module.yukkuri` 整模块(高 · 最大项) + +#### 3.1.1 目标 + +把 port 侧独有的油库/汽化运行时 `module.yukkuri`(14 个 Java 文件 + 构建文件 + mods.toml + README)整体迁移到 dev/26.1,按 +dev 侧 `module.gradle` 聚合结构落位为独立模块 `module.yukkuri/`(坐标 `anvillib-yukkuri-neoforge-26.1`),完成 26.1 API +适配并接入构建、CI 发布顺序与 README。 + +#### 3.1.2 来源与现状(已核实) + +| 项 | 值 | +|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 来源提交 | `569e01d`(#88「添加油库(里)」,21 文件 / 739 行新增)、`18b29ca`(#89「ci: publish yukkuri before aggregate module」,CI 发布顺序)、`fab551b`(#91「修油库」,port 当前 HEAD,收尾修复) | +| port 侧文件清单 | 18 个:`module.yukkuri/README.md`、`build.gradle`(270 行独立脚本)、`gradle.properties`(3 属性)、`src/main/resources/META-INF/neoforge.mods.toml`(25 行),以及 14 个 Java 文件 | +| Java 文件(14) | `Yukkuri.java`;`api/event/LargeCauldronProcessEvent.java`;`api/vapor/` 下 `IVaporConsumer` / `VaporAction` / `VaporStack` / `VaporizationCauldron` / `VaporizationContext` / `VaporizationManager` / `VaporizationOffer` / `VaporizationSource` / `VaporizationSources` / `YukkuriCapabilities` / `YukkuriVaporTypes` | +| 包名 | `dev.anvilcraft.lib.v2.yukkuri`(与 dev 侧 `v2` 包体系一致,无需改包) | +| 模块间依赖 | **无**(已逐文件核对 import:仅引用自身包 + MC/NeoForge 类,不依赖 util/codec 等 anvillib 模块)→ CI 拓扑 `needs: []`,Level 0 | +| port 侧独立构建脚本 | 不回迁(见第 5 章);dev 侧由根 `build.gradle` 的 `subprojects` 块统一 `apply from: rootProject.file("module.gradle")` 接管 | +| 行为契约 | `VaporizationManager.tick(ServerLevel, VaporizationCauldron)` 是运行时入口,由外部大锅实现(AnvilCraft)在服务器 tick 调用;`YukkuriCapabilities.VAPOR_CONSUMER` 通过 `BlockCapability.createSided` 注册于大锅正上方方块,查询方向 `Direction.DOWN` | +| port 收尾修复(fab551b) | `VaporizationManager.java`(+24/-2)、`VaporizationSource.java`(+6/-1)——迁移时直接以 `port/1.21.1` 侧文件为准,天然包含该修复 | + +#### 3.1.3 迁移步骤 + +**阶段 A:源码导出与落位** + +```bash +# 1) 在 dev/26.1 分支工作树创建目录(执行时在 dev/26.1 分支,以下为只读提取) +mkdir -p module.yukkuri/src/main/java/dev/anvilcraft/lib/v2/yukkuri/api/{event,vapor} +mkdir -p module.yukkuri/src/main/resources/META-INF + +# 2) 逐文件从 port 侧导出(示例;共 14 个 Java 文件,路径见 3.1.2 表) +git show port/1.21.1:module.yukkuri/src/main/java/dev/anvilcraft/lib/v2/yukkuri/Yukkuri.java \ + > module.yukkuri/src/main/java/dev/anvilcraft/lib/v2/yukkuri/Yukkuri.java +# 其余 13 个 Java 文件同理,逐一落位;可用循环批量提取: +for f in $(git ls-tree -r --name-only port/1.21.1 -- module.yukkuri | grep '\.java$'); do + git show "port/1.21.1:$f" > "${f/port\/1.21.1\//}" # 按 dev 工作树相对路径落位 +done + +# 3) 导出 README 与 mods.toml 作为内容底稿(build.gradle / gradle.properties 不回迁,见阶段 B) +git show port/1.21.1:module.yukkuri/README.md > module.yukkuri/README.md +git show port/1.21.1:module.yukkuri/src/main/resources/META-INF/neoforge.mods.toml \ + > module.yukkuri/src/main/resources/META-INF/neoforge.mods.toml +``` + +**阶段 B:构建文件按 dev 聚合结构重写(不搬 port 侧 270 行独立脚本)** + +- `module.yukkuri/build.gradle`:dev 侧普通模块的 build.gradle 只保留 `dependencies {}` 块(如 `module.util/build.gradle` + 的形态),其余(版本、neoForge 配置、runs、processResources 替换、sourcesJar/javadocJar、publishing、jreleaser)全部由根级 + `module.gradle` 聚合提供: + +```groovy +// module.yukkuri/build.gradle(按 dev 侧 module.util/build.gradle 精简) +dependencies { + // yukkuri 无 anvillib 模块间依赖,此块可为空;未来引入依赖时按 NOT_DEV 双分支写法 +} +``` + +- `module.yukkuri/gradle.properties`:dev 侧模块统一只含 3 个属性(参照 `module.util/gradle.properties`): + +```properties +## Mod Properties +mod_id=anvillib_yukkuri +mod_name=AnvilLib-Yukkuri +mod_description=Large-cauldron vaporization API and runtime for AnvilCraft addons +``` + +> 注:dev 侧 `module.gradle` 会读取 `anvillib.needRunConfig*` 系列属性(如 `module.explosion/gradle.properties` 设 +> `anvillib.needRunConfig=true`、`anvillib.needRunConfig.data=true`),未设置的模块等价于不生成 run 配置,yukkuri 默认 +> **不设置**(无 datagen / gametest 需求);如迁移后需要本地冒烟运行,可临时加 `anvillib.needRunConfig=true`,验证完删除。 + +- `module.yukkuri/src/main/resources/META-INF/neoforge.mods.toml`:以 dev 侧模块模板为准(参照 `module.util` 的 + mods.toml),保留 `[[mods]]` 与 `[[dependencies]]`(neoforge / minecraft 两条);port 侧版本无 mixin, **删除模板中的 + `[[mixins]]` 块**(模板默认含 `[[mixins]] config = "${mod_id}.mixins.json"`,yukkuri 没有 mixin 配置文件,保留会触发加载失败); + `displayURL`、`authors` 等占位符由 `processResources` 统一替换(`module.gradle` 已配置)。 + +**阶段 C:`settings.gradle` 注册(对照 `569e01d` 对 port 侧 settings.gradle 的改动,改到 dev 侧对应位置)** + +```groovy +// settings.gradle:include 列表加一行(与 dev 侧模块并列) +include 'module.yukkuri' +// settings.gradle:project 改名表加一行(dev 侧坐标为 -neoforge-26.1 后缀) +project(':module.yukkuri').name = 'anvillib-yukkuri-neoforge-26.1' +``` + +**阶段 D:`module.main/build.gradle` 聚合依赖(使用 dev 侧既有写法,NOT_DEV 双分支各加一行)** + +```groovy +// module.main/build.gradle:dependencies 块,NOT_DEV == 'true' 分支 +jarJar(api("dev.anvilcraft.lib:anvillib-yukkuri-neoforge-26.1:latest.release")) +// module.main/build.gradle:dependencies 块,else(本地开发)分支 +jarJar(implementation project(":anvillib-yukkuri-neoforge-26.1")) +``` + +> 严格沿用 dev 侧现有 19 个模块的两行写法(见 `module.main/build.gradle` 的 `dependencies` 块); **不要**把 port 侧 +> `569e01d` 中顺带做的 `jarJar(implementation ...)` → `jarJar(api ...)` 批量改写搬回(那是 port 侧的历史改动,dev 侧保持现状)。 + +**阶段 E:CI 发布顺序(对照 `18b29ca`,按 dev 侧机制落位)** + +- port 侧 `18b29ca` 的做法:在 `ci.yml` 手工新增 `yukkuri` 与 `yukkuri-maven-central-deploy` 两个 job,并把 `yukkuri` 加入 + `main` job 的 `needs` 列表,确保 yukkuri 在聚合模块 `main` 之前构建并发布。 +- dev 侧机制不同(`aa59e37` 重构 + `7f27908` 同步工作流):`ci.yml` 的 `prepare` job 执行 + `.github/workflows/generate-matrix.js`,读取 `.github/modules.json` 做拓扑分层,自动生成 `build-l0/l1/l2` 与 + `deploy-l0/l1/l2` 矩阵;`main` job `needs: [build-l2]` 传递依赖所有层级。因此 **只需在 `.github/modules.json`增加一行**: + +```json +{ + "module": "yukkuri", + "needs": [] +} +``` + +- `generate-matrix.js` 会从模块名自动推导 `module_id=anvillib-yukkuri`、`mod_id=anvillib_yukkuri`(与 port 侧手工声明一致); + `build_and_test.yml` 默认 `GRADLE_PROJECT=anvillib-yukkuri-neoforge-26.1`、`MODULE_DIR=module.yukkuri`,无需额外参数。 +- 效果等价于 `18b29ca` 的意图:yukkuri 位于 Level 0(无依赖),进入 `build-l0` / `deploy-l0`,先于 `main`(`needs: [build-l2]` + )构建发布;`main` 侧无需改动 `needs`。 +- 验收:本地 `node .github/workflows/generate-matrix.js` 逻辑核对(设置 `GITHUB_WORKSPACE` 后运行,确认 `level_0` 包含 + `yukkuri`);或在 PR 分支观察 CI 的 `build-l0` 出现 yukkuri 任务。 **待验证**:`modules.json` 是否需要在 `needs` + 中补充(yukkuri 无模块间依赖,理论为 `[]`;如编译期发现引用了其他模块则补入对应 needs)。 + +**阶段 F:README 更新(对照 `569e01d` 对 README.md / README.en.md 的 4 处改动,改到 dev 侧对应位置)** + +| 落点 | 改动 | +|----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `README.md` | 模块功能表加一行 `\| **Yukkuri** \| 大锅汽化 API 与运行时 \|`(对照 port 侧 README 的中文表样式);聚合模块清单(`- yukkuri`)与依赖坐标示例(`implementation "dev.anvilcraft.lib:anvillib-yukkuri-neoforge-26.1:2.0.0"`)各加一处,坐标后缀用 **`-neoforge-26.1`**(dev 侧坐标,勿写 `-1.21.1`) | +| `README.en.md` | 英文版同位置同步 4 处(模块表 / 模块清单 / 两处依赖坐标示例),坐标后缀 `-neoforge-26.1` | +| `module.yukkuri/README.md` | 直接采用 port 侧版本;其中「Dependency」小节引用的聚合坐标 `anvillib-neoforge-1.21.1` 需改为 dev 侧 `anvillib-neoforge-26.1`(或按 dev README 的当前聚合命名核对后改写) | + +**阶段 G:26.1 API 适配(核心工作,详见 3.1.4)** + +- 全局替换 `ResourceLocation` → `Identifier`(涉及 5 个文件:`Yukkuri.java`、`VaporStack.java`、`VaporizationSource.java`、 + `VaporizationSources.java`、`YukkuriVaporTypes.java`)。 +- 流体交互层二选一(选项 A / 选项 B,推荐 B),见 3.1.4。 +- `Level.getCapability` 调用点核对(`VaporizationManager.java` 第 40 行附近),见 3.1.4。 +- 全部文件过一遍 `gradlew :module.yukkuri:compileJava` 编译,逐个消灭报错。 + +#### 3.1.4 API 适配点 + +以下事实来自本地 NeoForge **26.1.2.76 sources jar** 实测(`net/neoforged/neoforge/...` 源码,2026-08-10 核对): + +**(a) `ResourceLocation` → `Identifier`(1.21.5+ 官方改名,必改)** + +| 文件 | port 侧用法 | 26.1 写法 | +|----------------------------|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------| +| `Yukkuri.java` | `ResourceLocation.fromNamespaceAndPath(...)`、`public static ResourceLocation of(String path)` | `Identifier.fromNamespaceAndPath(...)`;方法返回类型改 `Identifier` | +| `VaporStack.java` | `record VaporStack(ResourceLocation type, int amount)` | `record VaporStack(Identifier type, int amount)` | +| `VaporizationSource.java` | `ResourceLocation id();` | `Identifier id();` | +| `VaporizationSources.java` | `Map` | `Map` | +| `YukkuriVaporTypes.java` | `public static final ResourceLocation GASEOUS_OIL = Yukkuri.of("gaseous_oil")` 等 | `Identifier`;`isStandard(@Nullable ResourceLocation id)` 同步改 | +| `YukkuriCapabilities.java` | `BlockCapability.createSided(Yukkuri.of("vapor_consumer"), IVaporConsumer.class)` | `createSided` 首参类型即 `Identifier`(见下),**签名不变** | + +**(b) 流体能力 API 二选一(已实测)** + +旧 API 仍存在但已弃用: + +- `net.neoforged.neoforge.fluids.capability.IFluidHandler` 标注 `@Deprecated(since = "1.21.9", forRemoval = true)` + (sources jar 第 25 行),其各方法(fill/drain 等)同步弃用; +- `net.neoforged.neoforge.fluids.FluidStack` 仍可编译使用; +- 提供迁移包装器 `static IFluidHandler of(ResourceHandler handler)`(第 35 行)。 + +新 API: + +- `net.neoforged.neoforge.transfer.ResourceHandler`:方法为 `size()` / `getResource(int)` / + `getCapacityAsInt(int, Resource)` / `isValid(int, Resource)` / `insert(...)` / `extract(...)`, **无 `setResource`**; +- `net.neoforged.neoforge.transfer.fluid.FluidResource`:不可变,实现 `DataComponentHolderResource`,静态字段 + `CODEC` / `OPTIONAL_CODEC` / `STREAM_CODEC` / `EMPTY`。 + +| 选项 | 做法 | 影响面 | 建议 | +|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------| +| **A 最小移植** | 继续使用已弃用的 `IFluidHandler` / `FluidStack`(port 侧代码几乎不改) | 可编译但有弃用告警;`forRemoval` 后必须再迁一次;与 dev 侧物品能力已资源处理器化的现状(`2033fd9`)不一致 | 不推荐,仅作兜底 | +| **B 适配新 API(推荐)** | 用 `ResourceHandler` 重写 yukkuri 的流体读写层:槽位为 `index`(对应 port 侧 `IFluidHandler` 的 tank 序号,注意 26.1 无 `getTanks()`,用 `size()`);读写经 `getResource(index)` / `extract(index, resource, amount, transaction)` / `insert(index, resource, amount, transaction)`,`Transaction.openRoot()` + `commit()` 包裹;`FluidStack` 相关转换经 `FluidResource.toStack(...)` / `FluidResource.of(...)` 完成 | 一步到位,与 dev/26.1 物品能力迁移方向一致;`IVaporConsumer` 接口若暴露流体类型给外部(AnvilCraft 大锅实现),需同步约定新类型 | **推荐**(理由:与 dev 侧既有迁移方向一致、无弃用债、类型不可变更安全) | + +> yukkuri 中实际触碰流体的点:`VaporizationManager` 的 `context.topFluid()`(`FluidStack`)与 +> `VaporizationSource.createOffer(...)` 的 `FluidStack available` 出入参;选择 B 后这些签名改为 `FluidResource` +> (或保留内部转换、接口面最小化—— **待验证**:与 AnvilCraft 大锅实现的对接契约,需在 dev 侧仓库外确认调用方期望)。迁移完整性核对方法:14 +> 个 Java 文件逐一 grep `FluidStack` / `IFluidHandler`,确认零残留后编译。 + +**(c) Capability 查询与注册** + +- `BlockCapability.createSided(Identifier, Class)` 在 26.1.2.76 **仍存在**(sources jar `BlockCapability.java` 第 119 + 行:`public static BlockCapability createSided(Identifier name, Class typeClass)` + ),仅标识符类型改为 `Identifier`——`YukkuriCapabilities.VAPOR_CONSUMER` 声明只需改 `ResourceLocation`→`Identifier`。 +- `VaporizationManager.findConsumer(...)` 中的 + `context.level().getCapability(YukkuriCapabilities.VAPOR_CONSUMER, context.outletPos(), Direction.DOWN)`: + `Level.getCapability` 在 26.1 的签名与上下文参数(BlockCapability / BlockPos / Direction) **待验证**——NeoForge sources + jar 不含 MC patched 源码(`world/level/Level.java` 不在 jar 内),验证方法:① 迁移后直接编译,以编译错误定位签名;② + `gradlew :module.yukkuri:compileJava` 通过后,在 `runData` / 运行环境用 IDE 反编译 `Level.getCapability` 确认语义(返回 + `@Nullable`、查询 side 传参顺序)。 + +#### 3.1.5 验收方式 + +1. **编译**:dev/26.1 分支执行 `gradlew :module.yukkuri:build` 通过(含 `compileJava`、`jar`、`sourcesJar`、`javadocJar`);若选 + B,构建日志 **无弃用 API 告警**(`IFluidHandler` / `FluidStack` 相关 deprecation 警告为零)。 +2. **产物**:`module.yukkuri/build/libs/` 生成 `anvillib-yukkuri-neoforge-26.1-.jar`;`unzip -l` 核对包含 14 + 个类文件、`META-INF/neoforge.mods.toml`(替换后变量正确)、README; **不含** mixin 配置引用。 +3. **聚合**:`gradlew :module.main:build` 通过,主库 jar 的 `META-INF/jarjar/` 内含`anvillib-yukkuri-neoforge-26.1-*.jar`。 +4. **运行时行为**(#1 特有验收):在 dev/26.1 运行 `gradlew :module.main:runServer`(或 gameTest),构造大锅 + 上方消费者环境,验证: + - `VaporizationManager.tick(ServerLevel, VaporizationCauldron)` 每服务器 tick 被调用且无异常; + - 汽化流程:源(`VaporizationSources`)→ 流体抽取 → `IVaporConsumer.receiveVapor`(SIMULATE 不改变状态)→ 执行态差额入账; + - 出口密封(`isOutletBlocked` / `sealsOutlet`)回压路径行为与 port 侧一致; + - `YukkuriCapabilities.VAPOR_CONSUMER` 在 `Direction.DOWN` 查询正确返回消费者。 + - 若 module.test 迁入后(#7),可在 dev 侧为 yukkuri 补一条 gameTest 冒烟( **可选,不在 #1 范围**)。 +5. **CI**:PR 流水线 `build-l0` 矩阵出现 `yukkuri` 且通过;`deploy-l0` 发布 + `dev.anvilcraft.lib:anvillib-yukkuri-neoforge-26.1` 成功(若在发布分支触发);`main` 构建不受影响。 +6. **git 比对**:`git diff port/1.21.1 dev/26.1 -- module.yukkuri` 在迁移完成后应只剩「预期差异」(API + 适配、构建脚本形态、坐标后缀),无意外缺漏。 + +#### 3.1.6 文件级适配核对表(14 个 Java 文件逐一核对) + +> 下表基于对 `port/1.21.1` 侧 14 个文件的逐文件 import / 用法扫描(2026-08-10)。执行时以此表为清单,按「已适配 / +> 无需适配」逐项打勾,勾满后进入编译验收。 + +| 文件 | API 触点(port 侧实测) | 26.1 适配内容 | 选项 B 附加影响 | +|--------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| `Yukkuri.java` | `ResourceLocation.fromNamespaceAndPath`(`of(String)` 工厂,1 处) | `ResourceLocation`→`Identifier`,`of` 返回类型同步改 | 无 | +| `api/event/LargeCauldronProcessEvent.java` | 无(纯事件 record,携带 context 与阶段) | 无 | 无 | +| `api/vapor/IVaporConsumer.java` | 无(vapor 层面接口:`receiveVapor(VaporStack, VaporAction, VaporizationContext)`,不触碰流体类型) | 无 | 无 | +| `api/vapor/VaporAction.java` | 无(`EXECUTE` / `SIMULATE` 枚举) | 无;语义与 26.1 Transaction 的 execute/simulate 对应(见下) | 语义映射:EXECUTE→commit,SIMULATE→不 commit / 嵌套回滚 | +| `api/vapor/VaporStack.java` | `record VaporStack(ResourceLocation type, int amount)` | `ResourceLocation`→`Identifier` | 无 | +| `api/vapor/VaporizationCauldron.java` | **大锅契约接口**:`FluidStack getTopVaporizationFluid()`、`FluidStack drainVaporizationFluid(FluidStack, IFluidHandler.FluidAction)`(import `IFluidHandler`) | 弃用 API 面最大的一处:接口暴露 `IFluidHandler.FluidAction`(随 `IFluidHandler` 弃用) | 接口签名重设计:`FluidResource` + `boolean simulate`(或 Transaction 语义),**对外契约变化,需 AnvilCraft 大锅实现同步(见 V-Y3)** | +| `api/vapor/VaporizationContext.java` | `FluidStack topFluid()`(第 25 行,封装大锅流体读取) | 保持 `FluidStack`(选 A)或改 `FluidResource`(选 B) | 签名改 `FluidResource`,调用方(Manager / Source)同步 | +| `api/vapor/VaporizationManager.java` | `FluidStack`(`processFirstSource` 的 `available` 等)+ `level.getCapability(...)`(`findConsumer`,第 40 行) | `Level.getCapability` 签名核对(见 3.1.4-c,V-Y1);流体类型随选项 | 核心改造文件:`topFluid()` / `createOffer` 出入参全链改 `FluidResource`;`fab551b` 收尾修复已包含在 port 侧最终版中,勿丢 | +| `api/vapor/VaporizationOffer.java` | `record VaporizationOffer(FluidStack input, VaporStack output)` | 随选项 | 组件类型改 `FluidResource` | +| `api/vapor/VaporizationSource.java` | `ResourceLocation id()` + `createOffer(VaporizationContext, FluidStack available, int maxAmount)` | `ResourceLocation`→`Identifier`;流体随选项 | `createOffer` 入参改 `FluidResource`(源实现方 API 变化) | +| `api/vapor/VaporizationSources.java` | `Map SOURCES`(注册表,`register` / `getSources`) | `ResourceLocation`→`Identifier` | 无 | +| `api/vapor/YukkuriCapabilities.java` | `BlockCapability.createSided(Yukkuri.of("vapor_consumer"), IVaporConsumer.class)` | 仅 `Identifier` 类型变化,**签名不变**(26.1 实测 `createSided(Identifier, Class)` 存在) | 无 | +| `api/vapor/YukkuriVaporTypes.java` | `GASEOUS_OIL` / `GASEOUS_WATER` 常量 + `isStandard(ResourceLocation)` | `ResourceLocation`→`Identifier`;标准 ID 字符串 `yukkuri:gaseous_oil` / `yukkuri:gaseous_water` **保持不变**(历史存档与配方兼容,见 port README 约定) | 无 | + +> 核对方法:迁移后执行 `grep -rn "ResourceLocation\|IFluidHandler\|FluidStack" module.yukkuri/src`——`ResourceLocation` 应为 +> 0 匹配;`IFluidHandler` / `FluidStack` 选 B 时应为 0 匹配(选 A 时允许存在但必须逐处加 `@SuppressWarnings("deprecation")` +> 并登记到待验证 V-Y2)。 + +--- + +### 3.2 迁移项 #2:`module.recipe`:`InWorldRecipe.matches()` 谓词栈回滚(低) + +#### 3.2.1 目标 + +把 `edd71ec`(#83)对 `InWorldRecipe.matches()` 的谓词栈回滚修复搬到 dev 侧同名同包文件,修复「匹配失败后未回滚谓词栈导致后续匹配串栈」的鱼缸加工 +bug。 + +#### 3.2.2 步骤 + +```bash +# 1) 查看 port 侧修复的完整 diff(已核实:+17/-5,单文件) +git show edd71ec +# 2) 查看 dev 侧现状(已核实:仍是修复前的旧逻辑——失败路径 context.getStack().clear(), +# 成功路径 getStack().forEach(predicate -> predicate.clearStack(context))) +git show dev/26.1:module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java +# 3) 在 dev/26.1 工作树按 diff 语义改 module.recipe/.../InWorldRecipe.java 的 matches(): +# - 进入 matches() 先记录 initialStackSize = context.getStack().size(); +# - 两处失败路径改调新增私有方法 rollbackPredicates(context, initialStackSize): +# while (stack.size() > initialStackSize) context.pop(stack.getLast()); +# - 成功路径改为仅清理由本次匹配新增的谓词: +# for (int i = initialStackSize; i < stack.size(); i++) stack.get(i).clearStack(context); +# - 文件末尾补换行(port 侧修复顺带修掉了 "\\ No newline at end of file") +``` + +**API 适配点**:无。两侧 `InWorldRecipe.java` 同名同包(`dev.anvilcraft.lib.v2.recipe`),`InWorldRecipeContext` / +`IRecipePredicate` / `ShapelessMatcher` 接口在 dev 侧均存在且语义一致;纯逻辑搬运,仅需确认 dev 侧 `context.pop(...)` 与 +`getStack().getLast()` 可用(`List.getLast()` 为 JDK 21+,dev 侧 Java 25 工具链无问题;`pop` 方法是否存在 **待验证** +,验证方法:改完直接 `gradlew :module.recipe:compileJava`,如无 `pop` 则对照 dev 侧 `InWorldRecipeContext` 的栈操作 API +等价实现)。 + +#### 3.2.3 验收方式 + +1. `gradlew :module.recipe:compileJava` 通过。 +2. 行为核对:对照 port 侧 `edd71ec` 的 diff 逐行比对 dev 侧改动一致(`git diff` 语义等价)。 +3. 回归:dev 侧 `module.test` 补一条鱼缸加工场景回归用例( **待验证**:dev 侧 module.test 是否已有 InWorldRecipe + 相关测试设施;若有则扩展,若无则按 #7 方式新建最小用例),验证:匹配失败后 `context.getStack()` + 尺寸回到初始值、后续配方可正常匹配;成功后仅新增谓词被清理。 +4. `gradlew :module.recipe:test`(若有测试)通过。 + +--- + +### 3.3 迁移项 #3:`module.recipe`:`ItemResourceHandlerCacheElement.sync()` 短路/差量写入(中) + +#### 3.3.1 目标 + +把 `e78d020`(#84)对缓存元素 `sync()` 的「无谓写入短路 + 差量写入」逻辑迁移到 dev 侧已资源处理器化的 +`ItemResourceHandlerCacheElement.sync()`,避免每次同步都执行清空后重写的全量操作。 + +#### 3.3.2 步骤 + +```bash +# 1) 查看 port 侧修复的完整 diff(已核实:+20,单文件 ItemHandlerCacheElement.java) +git show e78d020 +# 2) 查看 dev 侧现状(已核实:ItemResourceHandlerCacheElement.java,sync() 为无条件 +# extract-all + insert simulate,Transaction 包裹;无短路/差量) +git show dev/26.1:module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/ItemResourceHandlerCacheElement.java +# 3) 在 dev/26.1 工作树改写 sync(),语义对照 port 侧: +# 短路:getResource(slot) 与 simulate 转换后的 ItemStack 完全一致(数量 + 组件)→ 直接 return; +# 差量:isSameItemSameComponents 但数量不同 → 仅 extract/insert 差额; +# 全量替换:类型不同 → extract 全部后 insert simulate(即 dev 现有逻辑兜底)。 +``` + +**API 适配点**(旧 → 新): + +| port 侧(1.21.1) | dev 侧(26.1) | +|------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| +| `IItemHandler.getStackInSlot(slot)` | `ResourceHandler.getResource(slot)`(空时返回 `ItemResource.EMPTY`,需 `toStack()` 转 `ItemStack` 做比较) | +| `IItemHandlerModifiable.setStackInSlot(...)`(快捷整槽替换) | `ResourceHandler` **无 setResource**,此快捷路径不存在;统一走「extract 全量 + insert 目标」的 Transaction 原子替换(dev 现状即此模式) | +| `IItemHandler.insertItem(slot, stack, false)` / `extractItem(slot, amount, false)` | `insert(slot, resource, amount, transaction)` / `extract(slot, resource, amount, transaction)`,`Transaction.openRoot()` + `transaction.commit()` | +| `ItemStack.isSameItemSameComponents(a, b)` | 26.1 仍存在(`ItemStack` 静态方法,**待验证**:以编译为准;若移除则用 `ItemResource` 等价比较或 `ItemStack` 组件比对) | +| `stack.copyWithCount(n)` | 26.1 `ItemStack.copyWithCount` 仍存在(**待验证**,同上) | + +> 注意 dev 侧类内已有私有辅助 `extract(ResourceHandler, int)`(`ItemResource` → `ItemStack` +> 转换),差量实现可复用该模式;短路比较建议在 `Transaction` 开启 **之前**完成(只读 `getResource`,无需事务),确认需写入时再开事务。 + +#### 3.3.3 验收方式 + +1. `gradlew :module.recipe:compileJava` 通过。 +2. 单元/集成测试(dev 侧 module.recipe 或 module.test):构造 `ItemResourceHandlerCacheElement`,覆盖四类输入—— (a) 槽位与 + simulate 完全一致(期望:零写入调用); (b) 同类型差量(期望:仅 extract/insert 差额,不触碰多余数量); (c) 不同类型(期望:全量替换); + (d) 槽位空 / simulate 空(期望:与 port 侧行为一致)。 +3. 写入次数可观测:在测试中包裹 `ResourceHandler` 实现并计数 `extract` / `insert` 调用,断言短路场景为 0 次、差量场景各 1 + 次(避免空转写入路径回归)。 +4. 行为回归:鱼缸加工(#83/#84 同源场景)在 dev 侧缓存流程中物品数量与组件与 simulate 一致。 + +--- + +### 3.4 迁移项 #4:`module.util`:`ClientTickRecorder` 修复(低) + +#### 3.4.1 目标 + +把 `9874db7`(+9/-2)的两处修复搬到 dev 侧 `ClientTickRecorder`:主菜单(level 为 null)不累计 tick;退出世界时 tick 归零。 + +#### 3.4.2 步骤 + +```bash +# 1) 查看 port 侧修复 diff(已核实:onTick 加 level != null 判断 + 新增 LoggingOut 事件归零) +git show 9874db7 +# 2) 查看 dev 侧现状(已核实:@EventBusSubscriber(modid = AnvilLibUtil.MOD_ID, value = Dist.CLIENT), +# onTick 仅 isPaused() 判断,无判空、无归零) +git show dev/26.1:module.util/src/main/java/dev/anvilcraft/lib/v2/util/ClientTickRecorder.java +# 3) 在 dev/26.1 工作树修改: +# - onTick 判断改为:if (Minecraft.getInstance().level != null && !Minecraft.getInstance().isPaused()) +# - 新增 @SubscribeEvent onClientExit(ClientPlayerNetworkEvent.LoggingOut e) { ticks = 0; } +# - 新增 import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent +``` + +**API 适配点**: + +| 项 | port 侧(9874db7 后) | dev 侧应保留 | +|--------|--------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 类注解 | `@EventBusSubscriber(value = Dist.CLIENT)`(无 modid) | **保留 dev 现状** `@EventBusSubscriber(modid = AnvilLibUtil.MOD_ID, value = Dist.CLIENT)`——dev 侧 `AnvilLibUtil.MOD_ID` 存在,带 modid 更精确(只接收本 mod 事件总线),**不搬** port 侧无 modid 写法 | +| 事件类 | `ClientPlayerNetworkEvent.LoggingOut` | 26.1 同包同类仍存在(**待验证**:以编译为准;若改名则对照 dev 侧 `ClientPlayerNetworkEvent` 现状) | + +> 搬逻辑(判空 + 归零)而不搬注解写法;`ClientPlayerNetworkEvent.LoggingOut` 在 26.1 客户端事件体系的可用性以 +> `gradlew :module.util:compileJava` 验证。 + +#### 3.4.3 验收方式 + +1. `gradlew :module.util:compileJava` 通过。 +2. 运行验收(`gradlew :module.main:runClient`):主菜单停留 N 秒 → `/time` 或调试输出确认 `ClientTickRecorder.ticks` + 不增长;进入世界 → 正常累计(每 24 小时取模重置不变);退出世界回主菜单 → 归零。 +3. 源码核对:`git diff` 语义与 `9874db7` 等价(除保留 dev 侧 modid 写法外无其他差异)。 + +--- + +### 3.5 迁移项 #5:`module.util`:`BlockStatePredicate.Builder.with(BlockState)`(低-中) + +#### 3.5.1 目标 + +在 dev 侧 `BlockStatePredicate.Builder` 恢复 `with(BlockState)` 便捷重载(#6 的代码级前置),为动态多方块提供「直接以方块状态描述谓词」的入口。 + +#### 3.5.2 步骤 + +```bash +# 1) 查看 port 侧实现(已核实:port 侧 Builder 有 5 个 with 重载,含 with(BlockState); +# dev 侧有 4 个(String/int/boolean/泛型 T),缺 with(BlockState)) +git show port/1.21.1:module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java +git show dev/26.1:module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java +# 2) 在 dev/26.1 工作树 BlockStatePredicate.Builder 内、现有 with(Property, String) 之前 +# (或其他合适位置)新增: +# public Builder with(BlockState state) { +# for (Property> property : state.getProperties()) { +# this.with(property, state.getValue(property)); // 走 dev 侧泛型重载,见适配点 +# } +# return this; +# } +``` + +**API 适配点**(旧 → 新): + +- port 侧实现为 `this.with(property, property.getName(Util.cast(state.getValue(property))))`——`Property.getName(T)` 在 + 26.1 已移除,且依赖 `Util.cast` 辅助。 +- dev 侧已有泛型重载 `public > Builder with(Property property, T value)`,其内部处理 + `StringRepresentable → getSerializedName()`、其余 `String.valueOf(value)`——与 1.21.1 的 `Property.getName` 行为 + **完全等价**(`getName` 的 1.21.1 实现即为 + `value instanceof StringRepresentable ? ((StringRepresentable) value).getSerializedName() : String.valueOf(value)`)。 +- 因此新实现直接调用 `this.with(property, state.getValue(property))`(泛型推断 `T` 为属性值类型), **不**使用 + `property.getName`,也不引入 `Util.cast`(dev 侧 `Util` 类是否保留该辅助 **待验证**,但新实现无需它)。 +- 泛型细节:`state.getProperties()` 返回 `Collection>>`,`state.getValue(property)`可推断为对应 + `T`;若编译器因通配符无法推断,可照 dev 侧泛型重载签名做强转(`@SuppressWarnings` 或 `Util.cast`,以编译为准)。 **待验证** + :通配符泛型调用的编译通过性(方法签名 `with(Property, T)` 的 `T` 需绑定到 `Comparable` 的实际类型,Dev 侧如遇编译错误,参照 + port 侧同文件其余泛型写法的强转模式解决)。 + +#### 3.5.3 验收方式 + +1. `gradlew :module.util:compileJava` 通过。 +2. 行为测试:`BlockStatePredicate.builder().with(Blocks.IRON_DOOR.defaultBlockState())` + 构造谓词,对同状态与改属性后的状态分别断言匹配/不匹配(属性级精确匹配);含 `StringRepresentable` 属性(如 + `FurnaceBlock.FACING`)与枚举/整型属性各一例。 +3. 与 #6 联动:若 #6 判定放弃(见 3.6.3),本项 **一并放弃**(回滚本项改动);决策前本项保持 pending 状态。 + +--- + +### 3.6 迁移项 #6:`module.multiblock`:`MultiblockDefinition` 的 16 个 `BlockState` 便捷重载(中 · 可选) + +#### 3.6.1 目标 + +在 dev 侧 `MultiblockDefinition` 恢复 port 侧 `5fc6f65` 引入的 16 个便捷重载(`add` / `addController` 各 8 个,其中 8 个直接以 +`BlockState` 入参),使动态多方块定义可用「具体方块状态」而非仅「方块类型」描述; **可选**——若判定 dev 侧删除为有意 API +精简则整体放弃(见 3.6.3)。 + +#### 3.6.2 步骤 + +```bash +# 1) 对比两侧 Builder 方法集(已核实): +# port 侧:add ×8(predicate/block/BlockState/block+BlockState/tag/block+tag/BlockState+tag/block+BlockState+tag) +# + addController ×8(同构) +# dev 侧:add ×4(predicate/block/tag/block+tag)+ addController ×4(同构)——缺全部 BlockState 入参变体 +git show port/1.21.1:module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/definition/MultiblockDefinition.java +git show dev/26.1:module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/definition/MultiblockDefinition.java +# 2) 在 dev/26.1 工作树 MultiblockDefinition.java 的 Builder 内补 8 个重载: +# add(Vec3i, BlockState) / add(Vec3i, Block, BlockState) / add(Vec3i, BlockState, CompoundTag) / +# add(Vec3i, Block, BlockState, CompoundTag) 与 addController 对应 4 个; +# 实现均为委托 BlockStatePredicate.builder().with(state)(依赖 #5),如: +# public Builder add(Vec3i localPos, BlockState state) { +# return this.add(localPos, BlockStatePredicate.builder().with(state)); +# } +# 3) 同步核对 dev 侧 SeriaBuilder(map/mapController)是否需要 BlockState 变体: +# port 侧 SeriaBuilder 无 BlockState 变体(已核实 map ×4 / mapController ×4 均无), +# 故 dev 侧 SeriaBuilder 保持现状,不动。 +``` + +**API 适配点**: + +- 依赖 #5 的 `with(BlockState)`;若 #5 按新实现落地,本项重载编译即通。 +- dev 侧 `MultiblockDefinition` 其余结构(record 定义、`toGlobal`、`isController`、SeriaBuilder)与 port 侧一致(已核实),无其他 + 26.1 API 适配点。 +- `add(Vec3i, Block, BlockState)` 语义与 port 一致:`BlockStatePredicate.builder().of(block).with(state)`(先限定方块再限定属性)。 + +#### 3.6.3 联动决策(执行前置) + +| 判据 | 决策 | +|----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------| +| dev 侧删除 `BlockState` 重载是**无意的迁移遗漏**(`7019b38` 移植时改写掉了) | 迁:本项 + #5 一起落地,测试组(#7)覆盖该路径 | +| dev 侧删除是**有意的 API 精简**(对齐 `@ApiStatus.Internal` 化趋势,见 `c72aa30` #93;或维护者认为只暴露 `block` 级 API 更干净) | **放弃整项**:保留 dev 现状,不引入 port 侧便捷层,#5 一并放弃 | + +> 该决策需在迁移执行前与 dev 侧维护者确认(本计划不替维护者做决定);确认前 #5/#6 保持 pending。 + +#### 3.6.4 验收方式(若迁) + +1. `gradlew :module.multiblock:compileJava` 通过。 +2. 行为测试:以 `Blocks.IRON_DOOR.defaultBlockState()` 等含属性状态构造 + `MultiblockDefinition.builder().addController(state)`,与 `BlockStatePredicate` 匹配断言一致(#7 测试组覆盖成形/解体路径)。 +3. 无行为回归:dev 侧现有 `MultiblockDefinition` 用例(如有)不受影响。 + +--- + +### 3.7 迁移项 #7:`module.test`:multiblock 动态系统测试组(中-高) + +#### 3.7.1 目标 + +把 port 侧独有的动态多方块测试组(`TestControllerBlock` + `LibBlocks` / `LibItemGroups` / `LibMultiblockControllers` / +`LibMultiblocks` + datagen 注册)迁入 dev 侧 `module.test`,按 dev 侧 26.1 +新结构落位,为动态多方块系统提供游戏内可运行验证(成形/解体行为 + 定义数据生成)。 + +#### 3.7.2 步骤 + +```bash +# 1) 查看 port 侧测试组文件(已核实清单): +# module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/block/TestControllerBlock.java +# module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/init/LibBlocks.java +# module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/init/LibItemGroups.java +# module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/init/LibMultiblockControllers.java +# module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/init/LibMultiblocks.java +# module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTestDatagen.java(datagen 注册入口) +# datagen 产物:src/generated/resources/data/anvillib_test/anvillib/definitions/{simple,complicated,waht}.json +# 2) 查看 dev 侧 module.test 新结构(已核实):all/{TestBlocks,TestItems,TestItemGroups,TestTiles}、 +# data/TestLangGenerator、AnvilLibTest(@Mod(AnvilLibTest.MOD_ID) 无 ModContainer 参数; +# REGISTRUM.addDataGenerator(ProviderType.LANG, ...) 替代 GatherDataEvent) +git ls-tree -r --name-only dev/26.1 -- module.test +# 3) 落位决策:新建包 module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/(与 port 同构), +# 或并入 dev 侧 all 包(待维护者偏好);以下按「新建 multiblock 包」给出,代码语义与 dev all 包一致 +# 4) 适配并落位(各文件适配点见 3.7.3),在 AnvilLibTest 构造器中注册: +# LibItemGroups.setupRegistration(); LibBlocks.setupRegistration(); LibMultiblockControllers.setupRegistration(); +# 并在 setupDataGeneration() 中注册定义数据生成(见适配点 d) +# 5) datagen 产物:dev 侧产物由 runData 生成(module.test 的 gradle.properties 需开 +# anvillib.needRunConfig.data=true,参照 dev 侧已有模块),不手工提交 port 侧 JSON; +# 生成后核对 simple/complicated/waht 三个定义文件内容 +``` + +**API 适配点**(旧 → 新): + +| 点 | port 侧 | dev 侧(26.1) | +|-------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| (a) `@Mod` 构造器 | `AnvilLibTest(ModContainer container, IEventBus bus)`(port 侧现状) | `AnvilLibTest(IEventBus modBus)`——26.1 已去掉 ModContainer 参数(dev `AnvilLibTest` 现状即此签名) | +| (b) datagen 注册 | `@EventBusSubscriber` + `GatherDataEvent`(`AnvilLibTestDatagen.gatherData` 空壳)+ `REGISTRUM.getDataGenInitializer().add(LibRegistries.DEFINITIONS_KEY, LibMultiblocks::bootstrap)` | `REGISTRUM.addDataGenerator(...)` 体系(dev 现状);`DataProviderInitializer` 在 dev 侧仍存在(`AbstractRegistrum.getDataGenInitializer()` 第 656 行)且 `RegistrumDatapackProvider` 支持 datapack registry(`getDatapackRegistryProviders()`)——`DEFINITIONS_KEY` 的 datapack 数据生成落位**待验证**(add 方法签名与 port 是否一致,以 dev 侧 registrum 源码 + 编译为准) | +| (c) `ResourceLocation` | `TestControllerBlock.getDefinitionId()` 返回 `ResourceLocation`;`LibMultiblocks` 的 `ResourceKey` 构造 | 全部 `Identifier`;`LibMultiblocks.key(...)` 的 `Identifier.fromNamespaceAndPath` 同步 | +| (d) 注册链式 API | `LibBlocks`:`REGISTRUM.block(...).blockstate((ctx, provider) -> {}).item().model((ctx, provider) -> {}).build().register()`;`LibItemGroups`:`DeferredRegister` + `REGISTRUM.addLang` | dev 侧 `TestBlocks` 用 `.properties(p -> ...).blockstate(() -> ...).simpleItem().register()`、`TestItemGroups` 用 `REGISTRUM.creativeTab("test_tab", () -> ...)`——**待验证**:dev 侧 `BlockEntry` builder 是否保留 `.blockstate(...)` / `.item()` / `.model()` / `.build()` 形态(对照 `module.registrum` 源码;若无则按 dev 现有链式 API 改写语义) | +| (e) `IController` / `ControllerRecord` / `SimpleController` | `onFormed` / `onUnformed` 回调 | dev 侧 multiblock 已演进:`1e5bfb3`(#69)新增动态多方块事件、`22091d4` 状态管理改 `BlockPos`——`MultiblockState.getControllerPos()` 等接口**待验证**(以 dev 侧 `MultiblockState` / `IController` 现状为准微调测试回调体) | +| (f) 定义构造 | `MultiblockDefinition.builder().addController(Blocks.FURNACE).add(new Vec3i(0,1,0), Blocks.IRON_BLOCK)` | dev 侧 Builder 基础形态一致;若使用 `BlockState` 重载则依赖 #6 | + +> 包内 `package-info.java`(`@NullMarked` 风格)按 dev 侧模块惯例补充(dev 侧 26.1 已统一 jspecify `@NullMarked`,见 +> `3bf9b6c`/`f94126f` 演进)。 + +#### 3.7.3 验收方式 + +1. `gradlew :module.test:compileJava` 通过。 +2. `gradlew :module.test:runData`(需开 `anvillib.needRunConfig.data=true`)生成产物,核对: + - `data/anvillib_test/anvillib/definitions/simple.json`(熔炉控制器 + 铁块)、`complicated.json`(TestControllerBlock + 控制器 + 铁块)、`waht.json`(白床 + 分层 A/0)与 port 侧产物语义一致(序列化格式以 dev 侧 `DefinitionSerialization` + 为准); + - lang 产物(en_us / en_ud)含新增条目(若有)。 +3. 运行验收(`gradlew :module.test:runServer` 或 gameTest):进世界摆放 SIMPLE / COMPLICATED / WAHT 结构: + - 成形:控制器方块被识别,`onFormed` 触发(如铁锭弹出 64 个); + - 解体:破坏结构,`onUnformed` 触发(铁栏杆弹出); + - 控制器方块(TestControllerBlock)本身可放置、可查询 `getDefinitionId`。 +4. 覆盖联动:测试组运行路径覆盖 #2/#6 涉及的 `MultiblockDefinition` 便捷重载与谓词匹配逻辑。 + +--- + +### 3.8 迁移项 #8:`module.main`:CurseForge 环境两行(低) + +#### 3.8.1 目标 + +在 dev 侧 `module.main/build.gradle` 的 `publishMods.curseforge` 块补充 `clientRequired = true` /`serverRequired = true` +两行环境声明(port 侧 `1311782` #90 已修复的发布元数据问题)。 + +#### 3.8.2 步骤 + +```bash +# 1) 查看 port 侧改动(已核实:publishMods.curseforge 块内 +2 行) +git show 1311782 +# 2) 查看 dev 侧 publishMods 块(已核实:dev 侧结构与 port 同构—— +# file/changelog/type/modLoaders/additionalFiles/curseforge{accessToken,projectId,minecraftVersions}/modrinth) +git show dev/26.1:module.main/build.gradle +# 3) 在 dev/26.1 工作树 module.main/build.gradle 的 curseforge 块内 +# minecraftVersions.add(libs.versions.minecraft.get()) 之后追加两行: +# clientRequired = true +# serverRequired = true +``` + +**API 适配点**: + +- 无 API 适配;`publishMods` 扩展属性(mod-publish-plugin 1.1.0,与 port 侧同版本,见 `gradle/libs.versions.toml`)在 dev + 侧可用。 +- **不要**把 port 侧 `jarJar(api ...)` 批量写法或任何其他 port 侧 `module.main/build.gradle` 内容搬回(dev 侧保持现状)。 +- **待验证**:mod-publish 的 `curseforge` 块中 `clientRequired` / `serverRequired` 属性在 1.1.0 版本的确切类型(boolean), + `gradlew :module.main:compileGroovy` / 配置阶段即报错可发现;另注意 dev 侧是否已在其他发布块声明过环境(已核对 `0b7e45f` + 无)。 + +#### 3.8.3 验收方式 + +1. `gradlew :module.main:build` 配置阶段通过(publishMods 扩展解析无误)。 +2. 发布 dry-run:`gradlew :module.main:publishMods`(或对应 dry-run 任务)在无 token 环境下不报扩展错误;CI + 发布分支实际发布后,CurseForge 元数据含「客户端必需 / 服务端必需」声明( **待验证**:需一次真实发布核对页面元数据,或核对 + mod-publish 输出日志)。 +3. `git diff` 与 `1311782` 语义一致(两行,无多余改动)。 + +--- + +## 4. 构建 / CI 接入点汇总表 + +> 仅 #1 与 #8 涉及工程接入;#2–#7 均为模块内源码改动,不触碰下列文件。所有改动均按 dev/26.1 现状结构落位,不回迁 port 侧对应文件。 + +| 文件 | 回迁项 | 改动内容 | 对照来源 | +|-------------------------------------------------------------------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------| +| `settings.gradle` | #1 | `include 'module.yukkuri'` + `project(':module.yukkuri').name = 'anvillib-yukkuri-neoforge-26.1'`(dev 侧坐标后缀) | port `569e01d`(坐标后缀改 `-neoforge-26.1`) | +| `module.yukkuri/build.gradle`(新建) | #1 | 按 dev 侧精简形态:`dependencies {}` 块即可,其余由根级 `module.gradle` 聚合(`subprojects { apply from: rootProject.file("module.gradle") }`)提供 | dev 侧 `module.util/build.gradle` 形态;**非** port 侧 270 行独立脚本 | +| `module.yukkuri/gradle.properties`(新建) | #1 | `mod_id=anvillib_yukkuri` / `mod_name=AnvilLib-Yukkuri` / `mod_description=...`;不设 `anvillib.needRunConfig*`(无 datagen/gametest 需求) | dev 侧 `module.util/gradle.properties` | +| `module.yukkuri/src/main/resources/META-INF/neoforge.mods.toml`(新建) | #1 | dev 模板落位;删 `[[mixins]]` 块(无 mixin);`[[dependencies]]` 保留 neoforge / minecraft 两条 | dev 侧 `module.util` 模板;port `569e01d` 内容底稿 | +| `module.main/build.gradle` | #1 | `dependencies` 块 NOT_DEV 双分支各加一行:`jarJar(api("dev.anvilcraft.lib:anvillib-yukkuri-neoforge-26.1:latest.release"))` / `jarJar(implementation project(":anvillib-yukkuri-neoforge-26.1"))` | dev 侧同块既有 19 模块写法;port `569e01d` 对应改动 | +| `module.main/build.gradle` | #8 | `publishMods.curseforge` 块 `minecraftVersions.add(...)` 后加 `clientRequired = true` / `serverRequired = true` 两行 | port `1311782` | +| `.github/modules.json` | #1 | 增加 `{ "module": "yukkuri", "needs": [] }`(yukkuri 无模块间依赖 → Level 0) | port `18b29ca` 的 CI 意图(dev 侧机制为矩阵拓扑,等价实现) | +| `.github/workflows/ci.yml` | #1 | **无需改动**:`generate-matrix.js` 自动派生 `module_id=anvillib-yukkuri`、`mod_id=anvillib_yukkuri`、`GRADLE_PROJECT=anvillib-yukkuri-neoforge-26.1`、`MODULE_DIR=module.yukkuri`;`main` job 的 `needs: [build-l2]` 传递覆盖 Level 0 | 对比 port `18b29ca`(手工加 job + 改 main needs) | +| `.github/workflows/build_and_test.yml` / `publish_maven_central.yml` | #1 | **无需改动**(参数默认值即匹配 yukkuri 命名) | — | +| `README.md` / `README.en.md` | #1 | 各 4 处:模块功能表 + 聚合模块清单 + 两处依赖坐标示例(坐标后缀 `-neoforge-26.1`) | port `569e01d` | +| `module.yukkuri/README.md`(新建) | #1 | port 侧版本 + 聚合坐标改为 dev 侧 `-neoforge-26.1` | port `569e01d` | +| `module.test/gradle.properties` | #7 | 若采用 runData 验证,加 `anvillib.needRunConfig.data=true`(参照 dev 侧 `module.explosion` 等);验证后可保留(module.test 属开发模块) | dev 侧现有模块写法 | + +--- + +## 5. 不做清单与原因 + +> 以下内容 **明确不回迁**。理由均基于两侧 diff 分类结论(详见 `docs/branch-comparison.md` 第 5 章),执行时不得以「顺手」「保险」为由扩大范围。 + +| # | 不回迁内容 | 原因(依据) | +|-----|------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| N1 | `module.codec` / `module.network` / `module.config` / `module.integration` / `module.registrum` / `module.wheel` | dev 侧为超集或已独立演进:port 侧相关提交(`0b56109`/`50aeac7`/`6a0e8a9`、网络库系列、wheel 系列)均有 dev 同主题对应(`3b5dfea`/`2e1b537`/`922d523`、`8d8ac8c` 系列、`0d73ab0`/`1ae3411`),逐文件 diff 无 port 独有内容 | +| N2 | `module.moveable-entity-block`(含 port `1acdcf8` PoseStack 修复) | dev 侧以 26.1 渲染架构**同源重写**(`83477ab`/`b5b27aa`/`2d4cd84`);port 侧修复的「PoseStack 提前 return 未清空」问题在新渲染架构下天然规避,搬运旧修复反而可能破坏新架构 | +| N3 | port 侧各模块独立构建脚本与 `gradle.properties`(含 yukkuri 的 270 行 build.gradle) | 已被 dev 侧 `module.gradle` 聚合 + `gradle/libs.versions.toml` 版本目录取代(`ea4301f`/`16b7bb5`);回迁即倒退,且两套构建体系并存会产生维护分歧 | +| N4 | lang datagen 产物(`zh_cn.json` / `en_us.json` 等已生成 JSON) | dev 侧统一由 datagen 生成(`runData` 产出,块物品自行提供 lang 的工作流),直接提交产物与 dev 工作流冲突;需要语言内容时应以 datagen 代码回迁而非产物回迁(#7 即按此执行) | +| N5 | `module.multiblock/ASYNC_MULTIBLOCK_CHECK_PLAN.md` | 文档已标注「已完成实现」;dev 侧异步检查实现等价且更完善(含未加载区块快照修复 `d888714` #73),无迁移价值 | +| N6 | `18b29ca` 的 CI 片段本身 | CI 体系 dev 侧已整体重构(`aa59e37` 模块化矩阵 + `7f27908` 同步工作流),port 侧手工 job 片段不适用;#1 已按 dev 侧 `modules.json` 机制等价落位(见 3.1.3 阶段 E) | +| N7 | dev 侧特有演进(不回迁方向) | 动态多方块事件 `1e5bfb3`(#69)、未加载区块快照修复 `d888714`(#73)、`@NullMarked` 注解统一 `3bf9b6c`/`f94126f`、`module.gradle` 构建体系 `ea4301f`、`@ApiStatus.Internal` `c72aa30`(#93)——均为 dev 独有演进,port 侧不反向吸收 | +| N8 | port 侧 `module.test` 的 mixin 与 wheel 测试(`ChestBlockMixin`、`wheel/` 包) | dev 侧 module.test 已有 26.1 对应结构(`all/` 包、独立 wheel 相关演进),port 侧这些文件是旧平台形态;#7 只迁 multiblock 测试组 | +| N9 | `module.util` 其余 port 提交(`9d61997`/`c8ee900`/`0efcd3d`/`95bd5ca`) | dev 侧已有等价或超集实现(分维度/玩家名工具、WeightedChanceBlockStates、VoxelShape 裁剪、UtilityClass 注解化等,`7019b38` 移植后继续演进) | +| N10 | port 侧 `module.recipe` 其余内容 | dev 侧为演进主线(`2033fd9` 资源处理器缓存、`0d0649f` 配方代码优化等);仅 #2/#3 两个 bugfix 为 port 独有 | + +> 执行纪律:出现「N 项之外是否遗漏」的疑问时,回到 `git log --oneline dev/26.1..port/1.21.1` 与 +> `git diff port/1.21.1 dev/26.1 -- <模块>` 复核,确认有 port 独有内容再立项,不得凭印象扩大范围。 + +--- + +## 6. 建议执行顺序 + +> 总原则: **先大后小、先依赖后使用、独立项先验证**。每步完成即验收(第 3 章对应小节),全部通过再进入下一步;每步在 dev/26.1 +> 分支以独立提交落地(提交信息建议按 dev 侧现有风格:`feat(yukkuri): ...` / `fix(recipe): ...` 等,具体按仓库约定)。 + +### 第 1 步:yukkuri(#1)——最大项与最大风险项 + +先做 #1 的原因:唯一整模块回迁,涉及流体 API 适配(26.1 最大不确定点)与四处工程联动;尽早暴露风险,后续步骤都在已稳定的聚合构建上叠加。 + +```bash +# 验证命令(在 dev/26.1 分支) +gradlew :module.yukkuri:build # 模块编译 + 打包(验收见 3.1.5-1/2) +gradlew :module.main:build # 聚合验证(验收见 3.1.5-3) +node .github/workflows/generate-matrix.js # 本地核对 CI 矩阵含 yukkuri(需 GITHUB_WORKSPACE/GITHUB_OUTPUT 环境,见 3.1.3-E) +git diff port/1.21.1 dev/26.1 -- module.yukkuri # 只读复核:只剩预期差异 +``` + +### 第 2 步:recipe(#2 → #3)——纯模块内 bugfix + +两个 bugfix 同模块同主题(鱼缸加工),一起做;#2 无 API 风险先落,再落 #3(涉及资源处理器 API)。 + +```bash +git diff port/1.21.1 dev/26.1 -- module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java # #2 落位后核对 +gradlew :module.recipe:compileJava # #2 验收(3.2.3-1) +gradlew :module.recipe:compileJava # #3 验收(3.3.3-1);行为用例见 3.3.3-2/3 +``` + +### 第 3 步:util(#4 → #5)——小修复 + API 补充 + +#4 独立小改先落;#5 与 #6 联动,本步只做代码与编译验证, **不**合并 #6 决策(见 3.6.3)。 + +```bash +gradlew :module.util:compileJava # #4 / #5 验收(3.4.3-1、3.5.3-1) +gradlew :module.main:runClient # #4 运行验收(3.4.3-2) +# #5 行为断言(3.5.3-2)可在 dev 侧 module.util 补一个测试方法或临时 main 验证后移除 +``` + +### 第 4 步:multiblock(#6)——决策先行 + +先与 dev 侧维护者确认 3.6.3 的决策点:确认「迁」→ 落地 16 个重载(#5 生效);确认「不迁」→ 回滚 #5 并跳过本步与 #7 的 BlockState +依赖部分。 + +```bash +gradlew :module.multiblock:compileJava # #6 验收(3.6.4-1) +``` + +### 第 5 步:test(#7)——测试组落位 + +依赖 #6 的便捷重载(若 #6 放弃,本步定义改用 `block` 级重载构造,测试面收窄,需在验收中注明)。 + +```bash +gradlew :module.test:compileJava # #7 验收(3.7.3-1) +gradlew :module.test:runData # datagen 产物核对(3.7.3-2,需开 anvillib.needRunConfig.data=true) +gradlew :module.test:runServer # 成形/解体行为验证(3.7.3-3) +``` + +### 第 6 步:main(#8)——收尾 + +独立两行,作为回迁批次最后一个提交。 + +```bash +gradlew :module.main:build # 配置阶段验收(3.8.3-1) +``` + +### 顺序之外的两条纪律 + +1. 每步提交前先跑一遍该模块 `build`(含 `jar`),避免只验 `compileJava` 漏掉资源处理(mods.toml 替换、jarjar 内嵌)。 +2. #1 与 #7 涉及 `runData` / 运行类验证,如本地环境不便跑客户端,可先以编译 + CI 为准,把运行验收登记为「待 CI/人工补验」并给出复现步骤, + **不得**把未验证项标记为已完成(见第 7 章验收总表)。 + +--- + +## 7. 总体验收标准 + +### 7.1 构建验收(硬性门槛) + +| # | 验收项 | 命令 / 判据 | 覆盖回迁项 | +|----|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|------------| +| B1 | dev/26.1 全量构建 | 在 dev/26.1 分支 `gradlew build`(根级,聚合全部模块含新 yukkuri)**全部成功** | 全部 | +| B2 | 模块独立构建 | 依次 `gradlew :module.yukkuri:build` `:module.recipe:build` `:module.util:build` `:module.multiblock:build` `:module.test:build` `:module.main:build` 均通过 | #1–#8 | +| B3 | 弃用告警检查(仅 #1 选 B 时) | `gradlew :module.yukkuri:compileJava` 日志中 `IFluidHandler` / `FluidStack` 相关 deprecation 警告为 0 | #1 | +| B4 | 聚合内嵌 | `module.main` 产物 jarjar 目录含 `anvillib-yukkuri-neoforge-26.1-*.jar` | #1 | +| B5 | CI 通过 | 回迁 PR 的 GitHub Actions 全绿:`build-l0`(含 yukkuri)、`build-l1/l2`、`main`(含 gametest) | #1、全部 | + +### 7.2 功能验收清单(行为核对) + +| # | 验收项 | 判据 | 执行方式 | +|----|----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------| +| F1 | 油库 API 行为(#1 特有验收) | `VaporizationManager.tick(ServerLevel, VaporizationCauldron)` 在服务器 tick 正常驱动汽化流程:源发现(按 priority 降序)→ 顶层流体抽取 → `IVaporConsumer.receiveVapor`(SIMULATE 不改变状态;执行态按模拟接受量入账)→ 出口密封回压(`isOutletBlocked` / `sealsOutlet`)路径正确;`VAPOR_CONSUMER` 以 `Direction.DOWN` 在出口方块可查到;全程无异常日志 | dev/26.1 运行环境 + 模拟消费者;细节见 3.1.5-4 | +| F2 | 鱼缸加工(#2) | 失败匹配后谓词栈回滚至初始尺寸,后续配方可正常匹配;成功路径仅清理本次新增谓词;`context.getStack()` 无残留 | module.test 回归用例(3.2.3-3) | +| F3 | 缓存同步(#3) | 完全一致场景零写入;同类型差量仅 extract/insert 差额;不同类型全量替换;空栈场景与 port 行为一致 | 计数包装的 ResourceHandler 测试(3.3.3-2/3) | +| F4 | ClientTickRecorder(#4) | 主菜单 tick 不增长;进世界累计;退出世界归零 | runClient 手工验证(3.4.3-2) | +| F5 | BlockStatePredicate(#5) | `with(BlockState)` 属性级精确匹配(含 StringRepresentable / 整型 / 布尔属性) | 行为断言(3.5.3-2) | +| F6 | MultiblockDefinition(#6,若迁) | 16 个重载编译可用;`BlockState` 入参定义与 `BlockStatePredicate` 匹配一致 | 编译 + #7 测试覆盖(3.6.4) | +| F7 | 动态多方块测试组(#7) | SIMPLE(熔炉)/ COMPLICATED(TestControllerBlock)/ WAHT(白床分层)三个定义 datagen 产物正确;进世界成形触发 `onFormed`、解体触发 `onUnformed`(铁锭/铁栏杆弹出);定义 JSON 与 port 语义一致 | runData + runServer(3.7.3-2/3) | +| F8 | CurseForge 环境(#8) | 发布后 CurseForge 元数据含客户端/服务端必需声明 | 发布 dry-run + 一次真实发布核对(3.8.3-2) | + +### 7.3 收尾核对 + +1. **提交完整性**:dev/26.1 分支上 #1–#8 各为独立提交(#5/#6 可合提交,取决于维护者偏好),提交信息含对应 issue + 号(#88/#89/#83/#84/#90 等)。 +2. **范围守卫**:`git log --oneline dev/26.1..port/1.21.1` 中,除 N 表(第 5 章)与 #1–#8 外,不应有「实际回迁但未立项」的内容;如执行中发现新 + port 独有内容,先补充本计划再执行。 +3. **文档同步**:`docs/branch-comparison.md` 的「待验证事项」(V1–V5)在回迁执行中逐项销账;本计划中的「待验证」标注同样逐一关闭,关闭记录可追加到附录 + B。 +4. **遗留登记**:任何因环境限制未完成的运行类验收(F1/F4/F7 的运行部分)登记为遗留项,附复现步骤,不得默认通过。 + +--- + +## 附录 + +### A. 常用只读 git 命令(本计划全部事实均来自以下命令) + +```bash +# 提取 port 侧文件内容 +git show port/1.21.1:module.yukkuri/src/main/java/dev/anvilcraft/lib/v2/yukkuri/Yukkuri.java + +# 查看来源提交的完整 diff +git show 569e01d # #1(yukkuri 引入) +git show 18b29ca # #1(CI 发布顺序,port 侧形态) +git show fab551b # #1(yukkuri 收尾修复) +git show edd71ec # #2 +git show e78d020 # #3 +git show 9874db7 # #4 +git show 5fc6f65 # #5 / #6(动态多方块系统 + BlockStatePredicate 引入) +git show 1311782 # #8 + +# 两侧同路径对比 +git diff port/1.21.1 dev/26.1 -- module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java + +# 某模块 port 独有提交清单(回迁候选扫描) +git log --oneline dev/26.1..port/1.21.1 -- module.recipe + +# dev 侧落点现状 +git show dev/26.1:module.util/src/main/java/dev/anvilcraft/lib/v2/util/ClientTickRecorder.java + +# 临时工作树对比(不切换当前分支) +# git worktree add <临时目录> dev/26.1 # 注意:此为 git 写入操作,仅当执行者确认可创建 worktree 时使用; +# git -C <临时目录> diff port/1.21.1 dev/26.1 -- <路径> +``` + +> 说明:`git worktree add` 属于工作区变更(非 commit/push),是否允许由执行者按仓库纪律判断;纯只读场景直接用 `git show` / +> `git diff` 两分支参数即可。 + +### B. 待验证事项汇总(执行时逐项销账) + +| 编号 | 事项 | 涉及项 | 验证方法 | 状态 | +|------|-------------------------------------------------------------------------------------------------------------|--------|--------------------------------------------------------------------------------------------------------|--------| +| V-Y1 | `Level.getCapability(BlockCapability, BlockPos, Direction)` 在 26.1 的签名与 side 传参顺序 | #1 | `gradlew :module.yukkuri:compileJava` 编译定位 + IDE 反编译 `net.minecraft.world.level.Level` 确认语义 | 未验证 | +| V-Y2 | 选项 B 迁移完整性:14 个 Java 文件 `FluidStack` / `IFluidHandler` 零残留 | #1 | `grep -rn "FluidStack\|IFluidHandler" module.yukkuri/src` 无匹配 + 编译零告警 | 未验证 | +| V-Y3 | yukkuri 与 AnvilCraft 大锅实现的流体对接契约(选 B 后 `FluidResource` 类型约定) | #1 | dev 侧仓库外与 AnvilCraft 维护者确认调用面 | 未验证 | +| V-Y4 | `modules.json` 的 `needs` 是否需补充(当前结论 `[]`) | #1 | yukkuri 编译通过后按实际 import 复核 | 未验证 | +| V-Y5 | mods.toml 删 `[[mixins]]` 块后模组加载正常 | #1 | `gradlew :module.main:runServer` 启动日志无 mixin 报错 | 未验证 | +| V-R1 | `InWorldRecipeContext.pop(...)` 在 dev 侧存在(或等价栈操作 API) | #2 | dev 侧 `InWorldRecipeContext` 源码 + 编译 | 未验证 | +| V-R2 | `ItemStack.isSameItemSameComponents` / `copyWithCount` 在 26.1 可用性 | #3 | `gradlew :module.recipe:compileJava` | 未验证 | +| V-U1 | `ClientPlayerNetworkEvent.LoggingOut` 在 26.1 客户端事件体系可用 | #4 | `gradlew :module.util:compileJava` + runClient 验证 | 未验证 | +| V-U2 | `with(Property, T)` 泛型重载下 `with(BlockState)` 通配符调用的编译通过性 | #5 | `gradlew :module.util:compileJava`;遇错参照 port 侧泛型强转模式 | 未验证 | +| V-U3 | dev 侧 `Util` 是否保留 `cast`(新实现不依赖,仅记录) | #5 | `git grep "class Util" dev/26.1 -- module.util` | 未验证 | +| V-T1 | dev 侧 registrum `DataProviderInitializer` 对 datapack registry(`DEFINITIONS_KEY`)的 add 方法签名 | #7 | dev 侧 `AbstractRegistrum.getDataGenInitializer` / `RegistrumDatapackProvider` 源码 + 编译 | 未验证 | +| V-T2 | dev 侧 `BlockEntry` builder 链式 API 形态(`.blockstate()` / `.item()` / `.model()` / `.build()` 是否保留) | #7 | dev 侧 `module.registrum` `BlockEntry` 源码 + 编译 | 未验证 | +| V-T3 | dev 侧 `MultiblockState` / `IController` 事件化后(`1e5bfb3`)回调体适配 | #7 | dev 侧 multiblock 源码 + `runServer` 行为验证 | 未验证 | +| V-M1 | mod-publish 1.1.0 的 `curseforge.clientRequired/serverRequired` 属性可用性 | #8 | `gradlew :module.main:build` 配置阶段 | 未验证 | +| V-M2 | 真实发布后 CurseForge 元数据含环境声明 | #8 | 一次真实发布核对 | 未验证 | + +### C. 参考资料 + +| 资料 | 位置 | +|---------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 分支对比报告(编号体系、分类依据、待验证 V1–V5) | `docs/branch-comparison.md` | +| 8 个并行子代理模块级调研报告 | `C:/Users/Administrator/.kimi-code/sessions/wd_anvillib_031cb785b7e3/session_6033f19b-f4ef-481a-b18c-d626cc22c314/agents/main/tool-results/AgentSwarm-call_00_nh47UIFAaqo9d96levr46332-594fb004-9f1f-4c9e-b230-43bc1440211d.txt` | +| NeoForge 26.1.2.76 sources jar(流体能力 API 核实依据) | `C:/Users/Administrator/.gradle/caches/modules-2/files-2.1/net.neoforged/neoforge/26.1.2.76/1d1dabe31afb953e46fb278e1cd8f04ab8e0d9a0/neoforge-26.1.2.76-sources.jar` | +| dev 侧关键文件实测基准 | `settings.gradle`、`module.gradle`、根 `build.gradle`、`.github/modules.json`、`.github/workflows/{ci,build_and_test,generate-matrix}.js/yml`(均以 `0b7e45f` 为准) | +| port 侧来源提交基准 | `569e01d` / `18b29ca` / `fab551b` / `edd71ec` / `e78d020` / `9874db7` / `5fc6f65` / `1311782` | + +### D. 回迁提交清单(PR 提交模板) + +> 每个回迁项在 dev/26.1 分支上对应一个独立提交(#5/#6 可合并为一个提交)。下表可直接作为 PR 描述模板,勾选「状态」列后随 PR +> 提交。 + +| 提交 | 回迁项 | 涉及文件(变更面) | 关键验证命令 | 状态 | +|----------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|------| +| commit 1 | #1 yukkuri | `module.yukkuri/**`(18 文件)+ `settings.gradle` + `module.main/build.gradle` + `.github/modules.json` + `README.md` / `README.en.md` | `gradlew :module.yukkuri:build`;`gradlew :module.main:build`;`grep -rn "ResourceLocation" module.yukkuri/src`(0 匹配) | ☐ | +| commit 2 | #2 谓词栈回滚 | `module.recipe/.../InWorldRecipe.java` | `gradlew :module.recipe:compileJava` | ☐ | +| commit 3 | #3 sync 短路/差量 | `module.recipe/.../cache/item/ItemResourceHandlerCacheElement.java` | `gradlew :module.recipe:compileJava` + 写入计数测试 | ☐ | +| commit 4 | #4 ClientTickRecorder | `module.util/.../util/ClientTickRecorder.java` | `gradlew :module.util:compileJava` + runClient 手工验证 | ☐ | +| commit 5 | #5 with(BlockState) | `module.util/.../predicate/BlockStatePredicate.java` | `gradlew :module.util:compileJava` + 行为断言 | ☐ | +| commit 6 | #6 16 重载(若迁) | `module.multiblock/.../dynamic/definition/MultiblockDefinition.java` | `gradlew :module.multiblock:compileJava` | ☐ | +| commit 7 | #7 测试组 | `module.test/.../test/multiblock/**`(6 文件)+ `AnvilLibTest.java` + `module.test/gradle.properties` | `gradlew :module.test:compileJava`;`runData`;`runServer` | ☐ | +| commit 8 | #8 CF 环境两行 | `module.main/build.gradle`(publishMods 块) | `gradlew :module.main:build` | ☐ | +| 收尾 | 全量回归 | 全仓 | `gradlew build`(根级)全绿;CI 全绿;附录 B 待验证清单销账 | ☐ | diff --git a/docs/migration-plan-port-1.21.1.md b/docs/migration-plan-port-1.21.1.md new file mode 100644 index 00000000..ea05a560 --- /dev/null +++ b/docs/migration-plan-port-1.21.1.md @@ -0,0 +1,1397 @@ +# AnvilLib 回移植计划:`dev/26.1` → `port/1.21.1`(功能回移植执行方案) + +> 文档信息 +> +> - 生成日期:2026-08-10 +> - 分析对象:`dev/26.1` @ `0b7e45f`(Minecraft 26.1.2 / NeoForge 26.1.2.76)、`port/1.21.1` @ `bd25229`(Minecraft 1.21.1 / NeoForge 21.1.226 / Java 21) +> - 共同祖先(merge-base):`81c5a9c` +> - 数据来源:8 个并行子代理模块级调研报告(2026-08-10,1395 行,路径见附录 C)+ git 只读命令(`git show` / `git diff` / `git ls-tree`,2026-08-10 本地仓库核对)+ NeoForge 21.1.226 / fancymodloader loader 4.0.42/4.0.43 sources jar 解压核实 + 1.21.1 混淆 jar javap 实测 +> - 关联文档与编号体系:本文与 `docs/migration-plan-dev-26.1.md`(port→dev 方向)**互为反向、互相补充**,两分支整体关系见 `docs/branch-comparison.md`;本文使用独立的 canonical 编号 **#P1–#P19**(「回移植项」),与 `docs/migration-plan-dev-26.1.md` 的 #1–#8 无任何对应关系,正文提及其它文档一律用文件名,避免编号混淆 +> - 结论性质:除标注「待验证」处外,均基于调研报告与本地 git / sources jar / javap 实测得出;所有「待验证」条目均已给出验证方法,执行时必须完成验证后方可标记该项完成 +> - 执行约束:本计划只描述 dev 侧「功能 / 修复 / 演进」的回移植,**不迁移**「26.1 平台适配」本身(`Identifier`、`ItemStackTemplate`、`RecipeMap`、`SavedDataType`、`ValueInput`/`ValueOutput`、`RenderPipeline`、`ResourceHandler`、`DataComponentPredicate`、`neoforgespi.transformation`、`@NullMarked` 等为 26.1 平台特性,port 侧已有 1.21.1 等价物,反向迁移无意义);本文仅为计划,执行时需在 port/1.21.1 分支工作树上以受控提交落地,本文档本身不产生任何 git 写入操作 + +--- + +## 1. 概述与范围 + +### 1.1 回移植背景 + +AnvilLib 的两个长期分支在 merge-base `81c5a9c` 之后完全分叉:`dev/26.1` 是演进主线(沿 1.21.2 → 1.21.11 → 26.1 版本链升级,独有 170 提交),沿路新增了 rpc / sync / explosion / collision / space-select / font / rendering / renderdoc-loader 八个模块,并对既有共享模块(config / integration / network / util / registrum / multiblock / recipe / wheel)做了大量功能演进;`port/1.21.1` 是维护与特性分支(独有 46 提交),冻结在 Minecraft 1.21.1 / NeoForge 21.1.226 / Java 21 平台。 + +`docs/migration-plan-dev-26.1.md` 已把 port 侧独有的少量内容(yukkuri 模块、三个 bugfix、测试组、发布配置,共 #1–#8)回迁到 dev/26.1。与之相反,**dev 侧有大量「功能」是 port 侧完全没有的**——尤其是八个新模块,以及共享模块中的功能性演进(注册表 builder 扩展、配置分组、网络工具方法等)。这些功能大多建立在与平台无关的逻辑之上,或可经「26.1 → 1.21.1」反向 API 替换后落地。 + +本文档即这 19 项(canonical 编号 #P1–#P19)的执行计划:每项给出**目标、步骤(具体到文件与要点)、API 适配点(26.1 → 1.21.1 反向替换清单)、验收方式**,并汇总不可移植清单(第 5 章)、建议执行顺序(第 6 章)与总体验收标准(第 7 章)。 + +### 1.2 范围界定 + +本计划 **包含**: + +- 19 个回移植项(#P1–#P19)的完整执行方案与验收判据; +- 每项与 dev 侧来源提交、port/1.21.1 落点文件的一一对应关系; +- 每项涉及的 26.1 → 1.21.1 反向 API 替换清单(全部以调研报告的 1.21.1 API 核实结论为准)。 + +本计划 **不包含**(即「不做清单」的完整理由见第 5 章): + +- 任何「26.1 平台适配」的反向搬运:`Identifier`、`ItemStackTemplate`、`RecipeMap`、`SavedDataType`、`ValueInput`/`ValueOutput`、`RenderPipeline`/`RenderState`/`SubmitNodeStorage`、`ResourceHandler`、`DataComponentPredicate`、`neoforgespi.transformation` SPI、jspecify `@NullMarked` 等,port 侧均有 1.21.1 等价物,反向迁移无意义; +- `module.moveable-entity-block` 的全部内容(dev 侧零功能增量,见 2.4 节); +- dev 侧构建体系重构中的 26.1 特有内容(`clientRenderDoc`/`clientIrisWorkaround` run 配置、`createModule` 模板中的 `Identifier`、`java_version=25` 等,见 #P19); +- module.test 中依赖 dev 新渲染模块的集成测试(Bloom / CachedBER / Compute / SDF 图层 / GUI 测试,见第 5 章)。 + +**迁移方式**:两侧无共同提交,直接 merge 方向错误且冲突不可控,故采用 **受控回移植**——在 port/1.21.1 分支工作树上按 #P1–#P19 逐个以独立提交落地,每个提交只改本项内容,便于评审与回滚。 + +### 1.3 前置条件与约定 + +| 项 | 约定 | +|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 编号 | 回移植项一律使用 canonical 编号 **#P1–#P19**;引用其它文档时使用文件名(如 `docs/migration-plan-dev-26.1.md`),不得沿用其 #1–#8 编号 | +| 基线 | port/1.21.1 @ `bd25229`(文档描述的全部落点均以该提交为准;执行时如 HEAD 已前进,需先以 `git log` 复核落点文件未变) | +| 提取方式 | 一律用 `git show dev/26.1:<路径>` 提取源码,**不**进行分支切换、不产生 git 写入操作 | +| 工作区 | 回移植在 port/1.21.1 分支工作树执行;只读对比可用 `git diff port/1.21.1 dev/26.1 -- <路径>` | +| 验证基线 | port 侧构建命令:`gradlew :module.xxx:build`、`gradlew :module.xxx:compileJava`、`gradlew :module.xxx:runData`(模块需要 datagen 时);port 侧工具链为 **Java 21**(`module.network/build.gradle:33` 确认),dev 侧代码中 Java 22+ 语法(如 lambda 参数 `_`、`field.accessFlags()`)必须改写 | +| API 事实 | 本计划全部 1.21.1 API 存在性结论来自调研报告(NeoForge 21.1.226 sources jar 解压、fancymodloader loader 4.0.42/4.0.43 sources、1.21.1 混淆 jar javap、port 分支现有代码引用),执行时以编译期核对为最终判据 | +| 待验证 | 所有标注「待验证」的条目均已给出验证方法(附录 B),执行时须完成验证后才能标记该项完成 | + +### 1.4 回移植项一览(结论先行) + +| #P | 内容 | 难度 | 一句话结论 | +|-----|------------------------------------------------------------------------------|-----------|------------------------------------------------------------------------------------------------| +| #P1 | `module.rpc` 整模块(注解式远程调用,25 文件) | 低 | 整模块拷贝 + 3 处机械 API 反替换(Identifier / FMLLoader / accessFlags) | +| #P2 | `module.sync` + processor(同步框架,31 文件) | 中-高 | 约 50/56 文件直接搬;processor 的 26.1 SPI 需 CoreMod(ICoreMod + ITransformer)重写 | +| #P3 | `module.explosion` 整模块(10 文件) | 低 | 6 处 26.1 API 反替换,其余逐字可搬;依赖 port module.config | +| #P4 | `module.collision` 整模块(纯数学,3 文件) | 极低 | 零 MC/NeoForge API 依赖,逐字搬移 | +| #P5 | `module.space-select` 整模块(18 文件) | 中 | 唯一实现重写点是 DistrictRenderer 线框路径(ShapeRenderer → LevelRenderer.renderVoxelShape) | +| #P6 | `module.font` 整模块(25 文件) | 中 | 约 60% 纯 CPU 逻辑直接搬;GPU 上传/绘制层与 GUI 按 1.21.1 改写;依赖 #P7 | +| #P7 | `module.rendering`(113 文件,按功能块分 5 批) | 高(分批)| 纯逻辑 19 项直接搬;后处理 / SDF GUI / CachedBER / Compute 四块执行层按 1.21.1 重写 | +| #P8 | renderdoc-loader(Java Agent,2 文件) | 低 | 源码逐字搬;构建脚本按 port 侧(Java 21、无 jreleaser 别名)改写 | +| #P9 | `module.config`:group 分组 + TranslatableEnum + TOML key 点分隔 | 低 | 三处纯逻辑/配置演进,1.21.1 加载链路原生支持子目录文件名 | +| #P10 | `module.integration`:数据加载客户端/服务端拆分 + meter 递增 | 低 | 纯反射逻辑,可移植;破坏性 API(applyData 改名)需提示下游 | +| #P11 | `module.network`:PacketData 泛型检查修复 + NetworkUtil Included 方法 + PacketProtocol public | 低 | 三处纯逻辑/修饰符改动,无平台依赖 | +| #P12 | `module.util`:OutlineUtil + predicate HolderGetter 重构 + withCount + @EqualsAndHashCode | 中 | 四处功能演进;HolderGetter 重构连带 module.recipe 四个调用方 | +| #P13 | `module.registrum`:14 种新注册表 builder/entry | 低 | 绝大多数仅 Identifier 改名或个别签名微调;VillagerProfession 需按 1.21.1 6 元 record 适配 | +| #P14 | `module.registrum` datagen 增强(RecipeProvider public 化 + dataMap(provider))| 中 | 方法 public 化改「公开包装内调静态方法」;Runner / 模型生成器不迁 | +| #P15 | `module.multiblock`:DynamicMultiblockEvent + 快照复用 + BlockPos 键化 + 懒解析 | 中 | 四项修复/演进均可移植;Config.group 依赖 #P9 | +| #P16 | `module.recipe`:SpawnItem 零数量守卫 + SetBlock nbt 默认值 | 低 | 两处纯逻辑修复(含一个潜在 NPE),与 dev 侧其它改动无关 | +| #P17 | `module.wheel`:环形扇区选择效果(WheelSelectionEffect + fsh + 渲染适配) | 中-高 | 纯 UI 功能可移植;渲染必须落在 port 旧 Tesselator/ShaderInstance 体系,切断 dev 渲染依赖 | +| #P18 | `module.test`:可选集成测试(T2 / T8 / T9 / T10) | 低-中 | 仅带回不依赖 dev 新渲染模块的测试;随 #P1/#P3/#P9/#P17 落地 | +| #P19 | 构建体系:roseauCheck API 兼容检查(+ module.gradle 集中化可选) | 中 | roseau-cli 无 MC 绑定直接搬;集中化需裁剪 26.1 特有内容 | + +--- + +## 2. 判定框架与分类规则 + +### 2.1 三类分类定义 + +调研报告(附录 C)对 dev 侧每个功能点给出了三分类判定。本计划沿用该分类,规则如下: + +**【可直接移植】**:功能逻辑完全不依赖 26.1 专有 API,或仅依赖两侧签名一致的稳定 API(如 `Mth`、JOML、`EventBus` 体系、`PacketDistributor`、codec/stream codec 基础件),可原样拷贝或仅做机械改名(`Identifier`→`ResourceLocation` 等)。判定标准:全部引用的类/方法在 1.21.1 sources / javap 中逐项核实存在,且无行为差异。 + +**【需反向适配】**:功能本质与平台无关(算法、数据结构、业务流程),但实现载体使用 26.1 平台 API;1.21.1 有直接等价物(`RegisterShadersEvent + ShaderInstance` 对应 `RegisterRenderPipelinesEvent + RenderPipeline`、`LevelRenderer.renderVoxelShape` 对应 `ShapeRenderer`、`GatherDataEvent` 对应 `GatherDataEvent.Client` 等)。移植 = 保留功能逻辑 + 按等价物重写执行层。判定标准:1.21.1 等价物存在且功能语义可完整承接;若等价物需整体重写(如 GPU 抽象层),难度上调。 + +**【不可移植】**(仅 26.1 平台):功能本身就是 26.1 平台适配(`Identifier` 改名、`@NullMarked` 风格、`neoforgespi.transformation` SPI、`RenderPipeline` 注册、`ItemStackTemplate` 编解码分支、`SavedDataType` 持久化、`ValueInput`/`ValueOutput` 序列化等),或依赖的 26.1 API 在 1.21.1 无对应物(如 `GpuDevice`/`CommandEncoder` 抽象、`PictureInPictureRenderer` 框架)。**反向迁移无意义**:port 侧已有 1.21.1 等价实现,且迁移会破坏 port 侧现状。第 5 章按模块给出完整清单。 + +> 三类关系的简化表述:**直接移植**搬「功能 + 平台无关逻辑」;**反向适配**搬「功能」但重写「平台载体」;**不可移植**的「功能」本身就是平台,不搬。 + +### 2.2 判定时的参考判据(源自报告) + +- **功能面判定**:一个 dev 侧差异是否构成「功能」,看其是否改变模块对外行为/能力(新增 API、修复 bug、新增注册表入口、性能演进);纯风格、注解风格(`@NullMarked` vs `@MethodsReturnNonnullByDefault`)、反混淆名调整、构建重构均不构成功能。 +- **平台面判定**:引用 26.1 专属类型/机制(`Identifier`、`ItemStackTemplate`、`RecipeMap`、`SavedDataType`、`ValueInput`/`ValueOutput`、`RenderPipeline`/`SubmitNodeStorage`/`GpuDevice`、`GuiGraphicsExtractor`、`neoforgespi.transformation.*`、`DataComponentPredicate`、`ResourceHandler`、jspecify `@NullMarked`、`GatherDataEvent.Client/Server`、`AddClientReloadListenersEvent`、`RegisterRenderPipelinesEvent` 等,完整清单见 1.2 与第 5 章)的任何代码,其「平台部分」一律不可迁移。 +- **困难等级**:低 = 机械改名/拷贝,1 个提交可完成;中 = 涉及执行层重写或跨模块协调;高 = 需按 1.21.1 生态整体重写(后处理、CachedBER、Compute、SPI 适配层)。 +- **已由 port 侧平行 PR 接收的内容不重复移植**:如 multiblock 双半方块修复、`AbstractCacheElement.grow()` 空堆修复、`ICacheInputOutputImpl.grow()` 只记录实际消耗、`ItemCache` 元素范围 `0.5→1.0`(port `6a0e8a9` #79 / `f2b60b6`,对应 dev `922d523` #78 / `1c523ee`),以及 wheel 与活塞模块的基础移植(`18834b5`/`53eaa11`/`9c437b1` 等)——均已核实存在,**不立项**。 + +### 2.3 为什么 moveable-entity-block 被整体排除 + +`module.moveable-entity-block` 在 dev 侧的差异经逐文件核实**全部为 26.1 平台适配,零功能增量**: + +| dev 侧差异 | 性质 | +|---|---| +| `PistonMovingBlockEntityMixin`:`ValueInput`/`ValueOutput` 序列化重构(`b5b27aa`) | 26.1 NBT 序列化体系;port 侧 `loadAdditional(CompoundTag, HolderLookup)`/`saveAdditional` 等价 | +| `PistonHeadRendererMixin` + 新增 `PistonHeadRenderStateMixin` + `IPistonHeadRenderStateExtension`(`83477ab` 后续 26.1 适配) | 注入点改 `extractRenderState`/`submit`,依赖 `BlockEntityRenderState`/`SubmitNodeCollector`(26.1 渲染管线);port 侧注入 `render(...MultiBufferSource)` 功能等价 | +| `PistonBaseBlockMixin` 局部变量名/反混淆名调整 | 26.1 反混淆名(`blockpos3→pos`、`blockstate→newState`),逻辑零变化 | +| 活塞 bug 修复(`922d523`) | port 侧 `6a0e8a9`(#79)已含同一修复,无需迁移 | +| `@ApiStatus.Internal` / `@NullMarked` 注解 | 纯注解风格,非功能 | + +**结论**:该模块在 port/1.21.1 侧的现有实现(`PistonHeadRendererMixin` 注入 `render` + 原版 NBT 序列化)即 dev 侧功能的 1.21.1 等价物,反向迁移只会破坏 port 侧现状。整体排除,不立项。 + +### 2.4 平台适配「不迁移」的通用原则 + +除第 5 章清单外,以下 **机械性平台适配** 在任何一个回移植项中出现时均按「保留 port 侧写法」处理,不作为独立改动: + +- `Identifier` → 保持 port 侧 `ResourceLocation`(`ResourceLocation.fromNamespaceAndPath` 与 26.1 `Identifier.fromNamespaceAndPath` 同名同参); +- `FMLLoader.getCurrent().getLoadingModList()` / `getCurrent().getDist()` → 保持 port 侧 `LoadingModList.get()` / `FMLLoader.getDist()`(1.21.1 的 `FMLLoader` **无 `getCurrent()`**,loader 4.0.42/4.0.43 源码核实); +- `field.accessFlags()` + `AccessFlag`(Java 22 API)→ `Modifier.isPublic/isStatic/isFinal`(port 工具链 Java 21); +- `isClientSide()` 方法调用 → port 侧 `isClientSide` 字段;`ResourceKey::identifier` → port 侧 `ResourceKey::location`; +- `getValue/getKey`(26.1 注册表改名)→ port 侧 `get/getKey`; +- `GatherDataEvent.Client/Server` → port 侧单一 `GatherDataEvent`(21.1.226 核实无子类,构造器 `(ModContainer, DataGenerator, DataGeneratorConfig, ExistingFileHelper)`);`AddClientReloadListenersEvent` → port 侧 `RegisterClientReloadListenersEvent`; +- `@NullMarked`(jspecify)→ 保持 port 侧 `@MethodsReturnNonnullByDefault` + `javax.annotation.ParametersAreNonnullByDefault`; +- lambda 参数 `_`(Java 22+ 语法)→ 改名(如 `ignored`)。 + +--- + +## 3. 回移植项总表 + +> 内容、来源提交、难度、前置依赖与 `docs/migration-plan-dev-26.1.md` 的方向相反(本文为 dev→port);「1.21.1 落点」列为本文新增。来源提交为**代表提交**(同项可能含多个提交,正文给出完整清单)。 + +| #P | 内容 | 来源提交(代表) | 难度 | 前置依赖 | 1.21.1 落点 | +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|--------|------------------------------|-----------------------------------------------------------------------------------------| +| #P1 | module.rpc 整模块(注解式远程调用,25 文件) | `d153e58` #60、`8d57cba` #63、`4910620` #87 | 低 | module.network/util 在 port 侧已就绪 | 新模块 `module.rpc/`(坐标 `anvillib-rpc-neoforge-1.21.1`) | +| #P2 | module.sync + processor(同步框架,31 文件;processor 需 CoreMod 重写) | `3ce2611`、`bf49789`、`8d57cba`、`1063ee9` #65 | 中-高 | 同 #P1 | 新模块 `module.sync/` + `module.sync/processor/`(坐标 `anvillib-sync-neoforge-1.21.1`)| +| #P3 | module.explosion 整模块(10 文件) | `435e23f`、`fba1027` | 低 | module.config | 新模块 `module.explosion/`(坐标 `anvillib-explosion-neoforge-1.21.1`) | +| #P4 | module.collision 整模块(纯数学,3 文件) | `82b4f6a` | 极低 | 无 | 新模块 `module.collision/`(坐标 `anvillib-collision-neoforge-1.21.1`) | +| #P5 | module.space-select 整模块(18 文件) | `1335fc5`、`e75b10f` | 中 | module.network | 新模块 `module.space-select/`(坐标 `anvillib-space-select-neoforge-1.21.1`) | +| #P6 | module.font 整模块(25 文件) | `1d56a4e` #22、`946938f` #56 | 中 | 依赖 module.rendering 的 1.21.1 版(#P7) | 新模块 `module.font/`(坐标 `anvillib-font-neoforge-1.21.1`) | +| #P7 | module.rendering(113 文件,不可整模块;按功能块分批:#P7a 纯逻辑 19 项 / #P7b 后处理三件套 / #P7c SDF GUI / #P7d CachedBER / #P7e Compute) | `7121bf6`、`494274e` #21、`7d07f20` #41、`64bcf21` #33 | 高(分批) | 无 | 新模块 `module.rendering/`(坐标 `anvillib-rendering-neoforge-1.21.1`) | +| #P8 | renderdoc-loader(Java Agent,2 文件) | `2751be4`、`1063ee9` | 低 | 无 | 新模块 `renderdoc-loader/`(坐标 `renderdoc-loader-internal`) | +| #P9 | module.config:group 分组 + TranslatableEnum 枚举翻译 + TOML key 点分隔 | `3d50d76`、`946938f` #56 | 低 | 无 | `module.config` 现有文件原地增强(`Config`/`ConfigManager`/`ConfigRecord`/`ConfigData`/`FormattingUtil`/`util/TranslatableEnum.java` 新增) | +| #P10 | module.integration:数据加载客户端/服务端拆分 + meter 递增 | `dc4d583`、`e8efbd4` | 低 | 无(破坏性 API) | `module.integration` 三个文件(`IntegrationInstance`/`IntegrationManager`/`IntegrationType`) | +| #P11 | module.network:PacketData 泛型检查修复 + NetworkUtil Included 方法 + PacketProtocol public | `20721b1` #52、`5733a9d`、`6220427` | 低 | 无 | `module.network` 三个文件(`PacketData`/`NetworkUtil`/`PacketProtocol`) | +| #P12 | module.util:OutlineUtil + predicate HolderGetter 重构 + withCount + @EqualsAndHashCode | `e4c09fb` #75、`bb09805`、`d84912e`、`c480e9f` #49 | 中 | module.recipe 调用方连带 | `module.util`(`OutlineUtil`/`client/Line`/测试 + 三个 predicate 文件)+ `module.recipe` 四个调用方 | +| #P13 | module.registrum:14 种新注册表 builder/entry(GameEvent/MobEffect/Potion/VillagerType/PoiType/Self/三类 Modifier/Condition/CreativeTab/Attachment/DataComponent/SoundEvent/RecipeSerializer/RecipeType/VillagerProfession) | `9b2d0e3` #31、`9c89bfa` #27、`9627358` #30、`c307a4c` #28、`252922e` #47 | 低 | 无 | `module.registrum` 的 `builders/**`、`util/entry/**` 新增文件 + `AbstractRegistrum` 入口方法 | +| #P14 | module.registrum datagen 增强(RecipeProvider 方法 public 化、dataMap(provider) 重载;GeneratorType 拆分可选) | `58f01d4`、`b4989c0` | 中 | #P13(可选) | `module.registrum` 的 `providers/RegistrumRecipeProvider.java`、`providers/RegistrumDataMapProvider.java`、`builders/Builder.java` | +| #P15 | module.multiblock:DynamicMultiblockEvent + 未加载区块快照复用 + Long→BlockPos 键化 + 定义懒解析 | `1e5bfb3` #69、`d888714` #73、`22091d4`、`1b0a652` | 中 | #P9(Config.group,M7 项) | `module.multiblock` 现有文件(`dynamic/event/` 新建 + `DynamicMultiblockManager`/`MultiblockState`/`MultiblockCheckSnapshot`/两个 packet) | +| #P16 | module.recipe:SpawnItem 零数量守卫 + SetBlock nbt 默认值 | `40b8f84`、`91327c1`、`b8825bd` | 低 | 无 | `module.recipe` 的 `outcome/SpawnItem.java`、`outcome/SetBlock.java` | +| #P17 | module.wheel:环形扇区选择效果(WheelSelectionEffect + fsh + 渲染适配) | `5d0b285`、`46e1b73`、`8a818f0` | 中-高 | 保留 port 旧渲染体系 | `module.wheel` 现有文件(`api/WheelSelectionEffect.java` 新建 + `WheelMenuBuilder`/`WheelMenuModel`/`WheelWidget`/`WheelScreen` + `annular_sector.fsh`) | +| #P18 | module.test:可选集成测试(T2 配置测试 / T8 RPC / T9 爆炸 / T10 wheel 按键) | `a6a24f1`、`d153e58`、`435e23f` | 低-中 | 随 #P1/#P3/#P9/#P17 | `module.test` 新增(`AnvilLibTestConfig.java`、`rpc/**`、`command/TestCommand.java`、`wheel/` 按键扩展) | +| #P19 | 构建体系:roseauCheck API 兼容检查(+ module.gradle 集中化可选) | `ea4301f` #25、`a6a24f1`、`946938f` | 中 | 无 | 根 `roseau.yaml` + `gradle/scripts/roseau.gradle`(可选:根 `module.gradle` + `build.gradle` 调整) | + +### 3.1 编号一致性说明 + +- 本表 #P1–#P19 为本文唯一执行依据,与 `docs/migration-plan-dev-26.1.md` 的 #1–#8 无任何对应关系。 +- #P7 拆为 #P7a–#P7e 五个子项分别执行与验收(第 4 章对应小节),但作为单一模块落地。 +- #P6 依赖 #P7 的 1.21.1 版(build.gradle jarJar 依赖 `anvillib-rendering-neoforge-1.21.1`),但 #P6 自身的 SDF 图集/布局/测量等纯逻辑部分可在 #P7a 完成后先行落地,无需等 #P7b–#P7e。 +- #P15 的 Config.group 子项(M7)依赖 #P9(`@Config(group=…)` 离开 module.config 的 group 支持无法编译);若 #P9 未先行,#P15 中两个模块保持 `@Config(name=…)` 亦可编译,但 group 语义缺失;#P18 各测试项分别依赖 #P1/#P3/#P9/#P17,可随对应项落地,不单独立项执行顺序;#P19 的 roseauCheck 独立于全部代码项,module.gradle 集中化为可选工程决策(port 侧现有独立构建脚本自洽,是否重构由维护者决定)。 + +--- + +## 4. 逐项详细计划 + +> 每节统一包含四部分:**目标**(一句话)、**步骤**(具体到文件与要点)、**API 适配点**(26.1 → 1.21.1 反向替换清单)、**验收方式**。步骤中的 `git show dev/26.1:<路径>` 均为只读操作。所有 1.21.1 API 存在性结论均来自调研报告(核实方式:NeoForge 21.1.226 sources jar 解压、fancymodloader loader 4.0.42/4.0.43 sources、1.21.1 混淆 jar javap、port 分支现有代码引用)。 + +### 4.1 回移植项 #P1:module.rpc 整模块(低) + +#### 4.1.1 目标 + +把 dev 侧注解式远程方法调用框架 `module.rpc`(25 文件)整体回移植到 port/1.21.1,作为新模块 `anvillib-rpc-neoforge-1.21.1`,实现「`@RemoteCallable` 静态方法 + 方法引用解析 + 索引下发 + 双向 play 网络包 + 带返回值 invoke」的完整 RPC 能力。 + +#### 4.1.2 步骤 + +```bash +# 1) 提取 dev 侧模块源码(25 个 Java 文件,整体拷贝) +for f in $(git ls-tree -r --name-only dev/26.1 -- module.rpc | grep '\.java$'); do + git show "dev/26.1:$f" > "$f" +done + +# 2) 构建文件:build.gradle 依赖改 anvillib-network-neoforge-1.21.1 / +# anvillib-util-neoforge-1.21.1(codec 仅为 jarJar 传递依赖,同步改坐标); +# jarJar 分支按 port 侧现有模块模式(NOT_DEV 双分支写法,参照 port module.wheel) +# 3) settings.gradle 注册:include 'module.rpc' + project(':module.rpc').name = 'anvillib-rpc-neoforge-1.21.1' +# 4) 模块元数据:neoforge.mods.toml 按 port 侧模板(mod_id=anvillib_rpc); +# anvillib_rpc.mixins.json 保留(dev 侧为空清单,port 侧同样空);gradle.properties 按 port 模块惯例 +# 5) 按 4.1.3 的三处 API 反替换修改源码 +``` + +#### 4.1.3 API 适配点(已逐项核实) + +| dev 侧(26.1) | port 侧(1.21.1) | 落点 | +|---|---|---| +| `net.minecraft.resources.Identifier` | `net.minecraft.resources.ResourceLocation`(`ResourceLocation.fromNamespaceAndPath` 同名);`IPacket.type(...)` 随之适配(port 侧签名 `type(ResourceLocation)`) | `AnvilLibRpc.of/mod`、`RpcRegistry` 等全模块机械替换 | +| `FMLLoader.getCurrent().getLoadingModList()` | `LoadingModList.get()`(1.21.1 的 `FMLLoader` 无 `getCurrent()`,loader 4.0.43 源码核实;port 侧 NetworkRegistrar 即此写法) | `RpcRegistry.getKeys()` | +| `field.accessFlags()` + `AccessFlag`(Java 22 API) | `Modifier.isPublic/isStatic/isFinal`(port 工具链 Java 21,`module.network/build.gradle:33` 确认) | `RpcMethods.findDeclaredCodec` | + +**已核实的 1.21.1 兼容面**(无需改动):`RegisterConfigurationTasksEvent.register/getListener`、`ICustomConfigurationTask`、`IPayloadContext.reply/enqueueWork/flow/player/finishCurrentTask`、`ModFileScanData.AnnotationData`(1.21.1 与 26.1 的 record 签名完全一致,`clazz()` 均返回 asm `Type`)、`PacketDistributor.sendToPlayer`、`ClientPacketDistributor.sendToServer`、`RegistryFriendlyByteBuf(ByteBuf, RegistryAccess, ConnectionType)`、`ByteBufCodecs.*` 全家桶、`UUIDUtil.STREAM_CODEC`、`ServerTickEvent.Post`/`ServerStoppedEvent`/`ClientTickEvent.Post`/`ClientPlayerNetworkEvent.LoggingOut`。`LambdaResolver`/`RpcMethods` 反射为纯 Java 标准 API;无 mixin 注入(`anvillib_rpc.mixins.json` 为空)。 + +#### 4.1.4 验收方式 + +1. `gradlew :module.rpc:compileJava` 通过(3 处反替换后零编译错误)。 +2. 行为验证(随 #P18 T8 或手工):客户端注册 `@RemoteCallable` 方法并下发索引 → 服务端经 `invoke` 远程调用返回 `CompletableFuture` 结果;`@CallableParam` 自定义编解码器生效;tick 超时路径正确。 +3. `git diff port/1.21.1 dev/26.1 -- module.rpc` 只读复核:只剩「预期差异」(`ResourceLocation`、`LoadingModList.get()`、`Modifier`、构建脚本形态、坐标后缀)。 + +--- + +### 4.2 回移植项 #P2:module.sync + processor(中-高) + +#### 4.2.1 目标 + +把 dev 侧同步框架 `module.sync`(31 文件 = 主模块 25 + processor 子模块 6)整体回移植到 port/1.21.1;其中 processor 的字节码注入能力从 26.1 专属的 `neoforgespi.transformation` SPI 改写为 1.21.1 的 **CoreMod(`ICoreMod` + `ITransformer`)** 方案,注入器逻辑复用。 + +#### 4.2.2 步骤(按功能点子项) + +**子项 2.1:注解 + SyncProxy + SyncManager + SyncRegisterEntry + 自定义注册表(可直接移植,低)** + +```bash +git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/Sync.java # 及 annotation/* 全部 +git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncProxy.java +git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncManager.java +git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncRegisterEntry.java +# init/* 同名落位;AnvilLibSync.of() 的 Identifier → ResourceLocation +``` + +- 已核实 1.21.1 存在:`NewRegistryEvent`、`RegistryBuilder.maxId(int).create()`(返回 `Registry`)、`DeferredRegister.create(ResourceKey, modid)`、`DeferredHolder`、`Registry.entrySet()`、`RegisterEvent.getRegistryKey()`、`FMLCommonSetupEvent`、`Entity::level`/`getUUID`、`BlockEntity::getBlockPos/getLevel`。 +- `SyncProxy.defaultCodec` 的 codec 表(`ByteBufCodecs.TAG/COMPOUND_TAG/VECTOR3F/QUATERNIONF/GAME_PROFILE/GAME_PROFILE_PROPERTIES`、`BlockPos.STREAM_CODEC`、`ComponentSerialization.STREAM_CODEC`、`ItemStack.OPTIONAL_STREAM_CODEC`、`ByteBufCodecs.fromCodecWithRegistries`)全部为 1.20.5+/1.21.1 标准 API(`fromCodecWithRegistries` port 侧 `StreamCodecUtil` 已用)。 +- **删除项**:`ItemStackTemplate.STREAM_CODEC` 分支——`ItemStackTemplate` 是 26.1 ItemStack 重构专有类型,1.21.1 无对应物(javap「找不到类」),1.21.1 的 `ItemStack` 已有 `OPTIONAL_STREAM_CODEC`,删除后功能无损(见第 5 章)。 + +**子项 2.2:SyncConfigManager(需反向适配,中)** + +```bash +git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncConfigManager.java +``` + +- `FMLLoader.getCurrent().getLoadingModList()` → `LoadingModList.get()`(`AnnotationData` 用法两侧兼容,直接可用)。 +- `modFile.getContents()`(`IModFile.getContents()` 返回 `net.neoforged.fml.jarcontents.JarContents`,其 `containsFile/openFile` 为 26.1 新增——loader 11.0.13 源码确认)→ **1.21.1 的 `IModFile` 无 `getContents()`**(loader 4.0.43 `IModFile.java` 方法列表确认)。等价实现:`fileInfo.getFile().getSecureJar().getPath(classPath)`(`cpw.mods.jarhandling.SecureJar.getPath(String, String...)` 返回 `Path`,securejarhandler 3.0.8 sources 确认)+ `Files.newInputStream`,或 `getSecureJar().findFile(classPath)` 返回 `Optional`;注意 1.21.1 的 `JarContents`(cpw)仅 `findFile/getPackages`,无 `openFile`。 + +**子项 2.3:LazySyncManager + LazySync 注解(可直接移植,低)** + +```bash +git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/LazySyncManager.java +git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/LazySync.java +git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SyncDirection.java +``` + +纯 Java(`java.lang.reflect` + Guava `MapMaker` 弱引用 + netty)+ `SyncProxy.defaultCodec` + `SideUtil.send`,无 26.1 专有 API。 + +**子项 2.4:网络 payload 层(可直接移植,低)** + +```bash +git ls-tree -r --name-only dev/26.1 -- module.sync/src/main/java | grep -E 'network/' | while read f; do git show "dev/26.1:$f" > "$f"; done +``` + +`IInsensitiveBiPacket` 在 port/1.21.1 与 dev/26.1 **逐字节一致**(默认方法 `bidirectionalHandler` 相同,port 侧仅 javadoc 缩进差异);`IPacket.type(ResourceLocation)`、`IClientboundPacket`/`IServerboundPacket`、`ctx.reply/finishCurrentTask`、`ByteBufCodecs.map(...)`、`StreamCodec.unit`、`PacketFlow` 全部 1.21.1 标准。 + +**子项 2.5:SideUtil + 入口类(可直接移植,低)** + +```bash +git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SideUtil.java +git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/AnvilLibSync.java +git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/client/AnvilLibSyncClient.java +``` + +已核实存在:`PacketDistributor.sendToAllPlayers/sendToPlayersTrackingChunk(ServerLevel, ChunkPos, Packet)/sendToPlayersTrackingEntity/sendToPlayersInDimension`、`ChunkPos.containing`、`ServerLifecycleHooks.getCurrentServer()`、`ClientPacketDistributor.sendToServer`、`Minecraft.level/getConnection`、`ClientPacketListener.registryAccess()`、`ServerLevel.getEntity(UUID)`、`RegisterConfigurationTasksEvent.getListener()` 返回 `ServerConfigurationPacketListener`(21.1.226 sources 确认)。仅 `Identifier`→`ResourceLocation` 机械改名。 + +**子项 2.6:processor 子模块——CoreMod 重写(需反向适配,高)** + +dev 侧 `processor/src/main/java/.../transform/` 五个类(`SyncClassProcessor`/`SyncTargetIndex`/`SyncBytecodeInjector`/`LazySyncBytecodeInjector`/`LazySyncTargetIndex`)+ `META-INF/services/net.neoforged.neoforgespi.transformation.ClassProcessor`。`SyncClassProcessor` 实现的 `net.neoforged.neoforgespi.transformation.ClassProcessor`(`name()/handlesClass(SelectionContext)/processClass(TransformationContext)` + `ProcessorName`、`ComputeFlags`)是 **26.1 全新 SPI**:loader 4.0.42/4.0.43(1.21.1)jar 与 sources 中 `neoforgespi` 包**无 transformation 子包**(逐 jar 扫描确认),neoforge 21.1.226 sources 中无任何 transformation 文件,loader 11.0.13(26.1 系)含 40 个 transformation 类——该 SPI 属 26.1 平台特性,必须重写适配层。 + +**方案 A(推荐):CoreMod 重写** + +```bash +# 1) SyncClassProcessor 骨架改写为 ICoreMod 实现: +# net.neoforged.neoforgespi.coremod.ICoreMod(getTransformers() 返回 Iterable>, +# loader 4.0.43 源码确认存在) +# 2) services 文件改为注册 net.neoforged.neoforgespi.coremod.ICoreMod +# 3) 新增两个 ITransformer(modlauncher 11.0.5 源码确认,即 1.21.1 所用版本): +# - targets() 取自 SyncTargetIndex / LazySyncTargetIndex 的静态索引; +# - transform(ClassNode, ITransformerVotingContext) 内调用 +# SyncBytecodeInjector / LazySyncBytecodeInjector.inject(ClassNode); +# - ClassWriter.COMPUTE_FRAMES 对应原 COMPUTE_FRAMES 返回语义 +``` + +**方案 B(备选)**:modlauncher `ITransformationService`(services 注册 `cpw.mods.modlauncher.api.ITransformationService`),同样复用注入器。 + +**可复用部分(约 90%)**:`SyncBytecodeInjector`/`LazySyncBytecodeInjector` 的 ASM `InsnList` 生成逻辑(操作 `ClassNode`,与 SPI 无关)、`SyncTargetIndex`/`LazySyncTargetIndex` 的注解扫描(`AnnotationData` 两侧兼容,仅 `FMLLoader.getCurrent()`→`LoadingModList.get()` 一处)。 + +**风险/验证点**:CoreMod transformer 在 game layer 类加载时执行,`AnvilLibSync.LAZY_SYNC_MANAGER` 的 GETSTATIC 引用要求 anvillib_sync 类可解析(V-10);jar-in-jar(`jarJar(api(...))` 携带 processor)场景下 services 文件可见性需实测(V-9)。 + +#### 4.2.3 验收方式 + +1. `gradlew :module.sync:compileJava` 与 processor 编译通过(SPI 适配层零残留:`grep -rn "neoforgespi.transformation" module.sync` 无匹配)。 +2. 行为验证(随 #P18 T8 或手工):`@Sync` 注解字段在两端同步;LazySync 惰性差分同步生效;Configuration 阶段配置表下发正常。 +3. 字节码注入验证:CoreMod 加载后目标类被注入(启动日志 + 反编译核对注入点)。 + +--- + +### 4.3 回移植项 #P3:module.explosion 整模块(低) + +#### 4.3.1 目标 + +把 dev 侧分层球形爆炸执行器模块 `module.explosion`(10 文件)整体回移植到 port/1.21.1,作为新模块 `anvillib-explosion-neoforge-1.21.1`,依赖 port 侧 `anvillib-config-neoforge-1.21.1`。 + +#### 4.3.2 步骤 + +```bash +# 1) 提取 10 个文件(explosion/ 包 + data/ 包 + mixin + resources) +for f in $(git ls-tree -r --name-only dev/26.1 -- module.explosion | grep -E '\.(java|json|toml)$'); do + git show "dev/26.1:$f" > "$f" +done +# 2) build.gradle:依赖 anvillib-config-neoforge-26.1 → anvillib-config-neoforge-1.21.1; +# port 侧无 module.gradle 约定(anvillib.needRunConfig* 不生效),run 配置在模块 build.gradle 内自建, +# 参照 port module.config/build.gradle(含 data run 段) +# 3) settings.gradle 注册 module.explosion → anvillib-explosion-neoforge-1.21.1 +# 4) mixins.json compatibilityLevel: JAVA_25 → JAVA_21(port 工具链 Java 21) +# 5) AnvilLibExplosionConfig 去掉 group = "anvillib"(port @Config 无 group 属性,见 4.3.3); +# zh_cn.json 语言键改写(section 名变化),en_us.json 由 runData 重生成 +``` + +#### 4.3.3 API 适配点 + +| dev 侧(26.1) | port 侧(1.21.1) | 落点 | +|---|---|---| +| `RecipeManager.getRecipes()` 返回 `Collection>` | 同名存在(javap) | `AnvilLibExplosion` 无需改 | +| `Ingredient.items()`(Stream\>) | `Ingredient.getItems()` 返回 `ItemStack[]`(javap) | `AnvilLibExplosion` | +| `ItemStackTemplate`(`count()`/`item().value()`) | `ItemStack`(`getCount()`/`getItem()`) | `AnvilLibExplosion` | +| `BuiltInRegistries.BLOCK.get(TagKey)` | `Registry.getTag(TagKey)` 返回 `Optional>`(javap) | `AnvilLibExplosion` | +| `Identifier` | `ResourceLocation.fromNamespaceAndPath` | `AnvilLibExplosion` | +| `recipe.input()` | 无此方法;改用 `recipe.getIngredients().get(0)`(`AbstractCookingRecipe` 恒为单元素) | `AnvilLibExplosion` | +| `SingleItemRecipe` 的 `result` 字段(dev 经 mixin 访问器) | 1.21.1 `SingleItemRecipe` 为抽象类,`protected final ItemStack result` 存在(javap)——`@Accessor ItemStack getResult()` 原结构可用,仅类型换 `ItemStack` | `mixin/SingleItemRecipeAccessor.java` | +| `entity.hurtServer(level, source, amount)`(26.1 专属) | `Entity.hurt(DamageSource, float)` 返回 boolean(javap) | `ExplosionExecutor` 的 entityProcessor 默认 lambda | +| `OnDatapackSyncEvent`(`getPlayerList().getServer()`)、`ServerStartedEvent` | 同名存在(NF sources) | `AnvilLibExplosion` 无需改 | +| `GatherDataEvent.Client` | 单一 `GatherDataEvent`(`getGenerator()`/`getPackOutput()`/`includeClient()` 存在);`addProvider(event.includeClient(), ...)` 参照 port `module.multiblock` 的 `AnvilLibDatagen` | `data/AnvilLibDatagen.java`、`data/provider/ModLanguageProvider.java` | +| `@Config(group = "anvillib")` | port `@Config` 仅 `name()`/`type()`,**无 `group` 属性**(git show port 核实)→ 去掉 `group`;语言键 section 名变化(`section.anvillib.explosion.common.toml` → `section.anvillib_explosion.common.toml`) | `AnvilLibExplosionConfig.java` | + +**逐字可搬(无需改动)**:`ExplosionSession.java`(`ServerTickEvent.Post`、`NeoForge.EVENT_BUS.register/unregister`、`ServerLevel.getEntities()` 公开返回 `LevelEntityGetter` 且含 `getAll()`、`GameEvent.Context.of(Entity, BlockState)`、`Block.UPDATE_CLIENTS`、`Block.dropResources` 6 参重载、`FluidState.createLegacyBlock()`、`Level.getHeight(Heightmap.Types, int, int)`、Guava `MultimapBuilder`、log4j `TriConsumer`——全部 javap 核实存在)。`ExplosionExecutor` 其余(`damageSources().explosion(Entity, Entity)`、`block.defaultDestroyTime()`、`block.builtInRegistryHolder()`、`Tags.Blocks.GLASS_BLOCKS/GLASS_PANES`、`BlockTags.LEAVES/REPLACEABLE`)均存在。 + +#### 4.3.4 验收方式 + +1. `gradlew :module.explosion:compileJava` 通过;`gradlew :module.explosion:runData` 生成 lang 产物并核对 section 键。 +2. 行为验证(随 #P18 T9):`ServerTickEvent.Post` 驱动的分层球壳破坏按概率半径执行,熔化替换(熔炼配方表)生效,实体处理器(`hurt`)与掉落/熔化路径正确。 +3. `grep -rn "ItemStackTemplate\|hurtServer\|group" module.explosion/src` 复核零残留。 + +--- + +### 4.4 回移植项 #P4:module.collision 整模块(极低) + +#### 4.4.1 目标 + +把 dev 侧纯数学 AABB×三角形 SAT 碰撞检测模块 `module.collision`(3 文件)整体回移植,作为新模块 `anvillib-collision-neoforge-1.21.1`。 + +#### 4.4.2 步骤 + +```bash +# 1) 提取 3 个文件(AnvilLibCollision.java、CollisionTest.java、package-info.java 如有) +for f in $(git ls-tree -r --name-only dev/26.1 -- module.collision | grep '\.java$'); do + git show "dev/26.1:$f" > "$f" +done +# 2) build.gradle 为 dependencies {} 空块(无平台绑定);mods.toml + 空 mixins.json 骨架 +# (模块为「无 @Mod 类的 mod jar」纯工具,port 侧同构) +# 3) settings.gradle 注册 module.collision → anvillib-collision-neoforge-1.21.1 +``` + +#### 4.4.3 API 适配点 + +无。全文件仅引用 `org.joml.Vector3d/Vector3dc/Vector3fc`(joml 随 MC 1.21.1 提供,编译期在 neoForge 依赖里);`CollisionTest.java` 为纯 Java(`System.out`/`System.nanoTime`/LCG 伪随机),自带断言与基准,为手动运行的自测入口(注意其 `static void main()` 无 `String[]` 参数,非标准 JVM 入口,IDE 手动调用)。 + +#### 4.4.4 验收方式 + +1. `gradlew :module.collision:compileJava` 通过。 +2. 手动运行 `AnvilLibCollision` 测试入口(静态 SAT 13 轴 / 三轴独立扫掠 / 真扫掠碰撞三个入口 + `overlapOnAxis` 公开工具)断言全部通过、基准无异常。 +3. `grep -rn "net.minecraft\|net.neoforged" module.collision/src` 确认零 MC/NF import。 + +--- + +### 4.5 回移植项 #P5:module.space-select 整模块(中) + +#### 4.5.1 目标 + +把 dev 侧选区(区域框选/线框渲染/滚轮缩放)模块 `module.space-select`(18 文件)整体回移植,作为新模块 `anvillib-space-select-neoforge-1.21.1`,依赖 port 侧 `anvillib-network-neoforge-1.21.1`。 + +#### 4.5.2 步骤 + +```bash +# 1) 提取 18 个文件(space_select/ 包 + client/ + event/ + network/ + resources) +for f in $(git ls-tree -r --name-only dev/26.1 -- module.space-select | grep -E '\.(java|json|toml)$'); do + git show "dev/26.1:$f" > "$f" +done +# 2) build.gradle:jarJar 依赖 anvillib-network-neoforge-26.1 → anvillib-network-neoforge-1.21.1; +# port 无 module.gradle → 模块无 run 需求可不建 run 配置 +# 3) settings.gradle 注册 module.space-select → anvillib-space-select-neoforge-1.21.1 +# 4) 按 4.5.3 逐文件反替换;DistrictRenderer 渲染路径重写(本项唯一实现重写点) +``` + +#### 4.5.3 API 适配点 + +| dev 侧(26.1) | port 侧(1.21.1) | 落点 | +|---|---|---| +| `Identifier` | `ResourceLocation.fromNamespaceAndPath` | `AnvilLibSpaceSelect`、网络层 | +| `ARGB`(26.1 新类,javap「找不到类」) | 手写 `(a<<24)|(r<<16)|(g<<8)|b` int 打包 | `District.color()` | +| `player.getInventory().getSelectedSlot()`(javap:1.21.1 `Inventory` 无此方法) | 公开字段 `Inventory.selected` | `DistrictManager`、`SpaceSelectItem` | +| `ClientPacketDistributor.sendToServer(payload)`(26.1 新增;NF 21.1 sources 无 `client/network` 目录) | `net.neoforged.neoforge.network.PacketDistributor.sendToServer(payload)` | `ClientDistrictManager` | +| `RenderLevelStageEvent.AfterTranslucentParticles` 子类 + `event.getLevelRenderState()` | 单一 `RenderLevelStageEvent`:`getStage()`(`Stage.AFTER_TRANSLUCENT_BLOCKS`/`AFTER_PARTICLES`)、`getPoseStack()`、`getCamera()`(NF sources) | `DistrictRenderer` | +| `BlockOutlineRenderState`、`LevelRenderState`、`cameraRenderState.pos`、`windowRenderState.appropriateLineWidth` | 无对应物(1.21.1 无 `client/renderer/state/level` 包);相机位置取 `event.getCamera().getPosition()`,线宽用默认 | `DistrictRenderer` | +| `ShapeRenderer`(1.21.1 不存在,1.21.2 才引入) | 公开静态 `LevelRenderer.renderVoxelShape(PoseStack, VertexConsumer, VoxelShape, dx, dy, dz, r, g, b, a, boolean)` 与 `renderLineBox(PoseStack, VertexConsumer, AABB, r, g, b, a)`(javap) | `DistrictRenderer`(dev 的 `BlockOutlineRenderState` 分支渲染改写为直接 `renderVoxelShape(shape, 平移量, r,g,b,a)`;`DEBUG_SHAPES` 分支保留 `renderLineBox` 逐 shape 绘制) | +| `RenderTypes.lines()`(26.1 类) | `RenderType.lines()`(javap) | `DistrictRenderer` | +| `mc.hasControlDown()`/`mc.hasAltDown()`(javap:1.21.1 `Minecraft` 无此方法) | 静态 `Screen.hasControlDown()`/`Screen.hasAltDown()`(javap) | `SpaceSelectScrollHandler` | +| `Direction.getApproximateNearest(x,y,z)` | `Direction.getNearest(double,double,double)`(javap) | `SpaceSelectScrollHandler` | +| `InputEvent.MouseScrollingEvent` + `getScrollDeltaY()` + `setCanceled(true)` | 同名存在(NF sources);`player.getViewVector(1.0F)` 存在 | `SpaceSelectScrollHandler` 无需改 | + +**无需改动**:`AnvilLibSpaceSelectClient`(`@Mod(value=..., dist=Dist.CLIENT)` 为 21.1 标准)、`PlayerCreateDistrictEvent`+Handler(`PlayerEvent(Player)` 构造 + `getEntity()`、`@EventBusSubscriber(modid=...)` 属性名一致)、`BlockPos.MutableBlockPos`(公开无参构造)、`Direction.getStepX`、`Direction.Axis.choose`、`Vec3` 公开字段、`AABB`、`Shapes.create(double×6)`、`Minecraft.hitResult`、`CustomPacketPayload.Type` record(构造参数为 `ResourceLocation`)、`ByteBufCodecs.BOOL`、`BlockPos.STREAM_CODEC`、`RegisterPayloadHandlersEvent.registrar(String)`。 + +#### 4.5.4 验收方式 + +1. `gradlew :module.space-select:compileJava` 通过。 +2. 行为验证:手持选区物品框选区域 → `DistrictManager` 创建区域;`DistrictRenderer` 在 `RenderLevelStageEvent` 各 stage 正确绘制线框(平移量/颜色语义目测确认,V-16);滚轮缩放/移动(`Screen.hasControlDown` 组合键 + `Direction.getNearest`)生效;网络包(`SpaceSelectPayload` 经 port `NetworkRegistrar` 注册)双向收发正常。 +3. `git diff port/1.21.1 dev/26.1 -- module.space-select` 只读复核只剩预期差异。 + +--- + +### 4.6 回移植项 #P6:module.font 整模块(中) + +#### 4.6.1 目标 + +把 dev 侧 SDF 字体模块 `module.font`(25 文件,AWT 字体 → CPU 生成 SDF 图集 → GPU 采样渲染)整体回移植,作为新模块 `anvillib-font-neoforge-1.21.1`;约 60% 纯 CPU 逻辑直接搬,GPU 上传/绘制层与 GUI 屏幕按 1.21.1 API 改写。**依赖 module.rendering 的 1.21.1 版(#P7)**(build.gradle jarJar 依赖 `anvillib-rendering-neoforge-1.21.1`;纯逻辑部分可在 #P7a 完成后先行落地)。 + +#### 4.6.2 步骤 + +```bash +# 1) 提取 25 个文件 +for f in $(git ls-tree -r --name-only dev/26.1 -- module.font | grep -E '\.(java|json|toml|fsh|vsh|glsl)$'); do + git show "dev/26.1:$f" > "$f" +done +# 2) build.gradle:依赖 anvillib-rendering-neoforge-26.1 → anvillib-rendering-neoforge-1.21.1; +# gradle.properties interface_injection=true 保留(1.21.1 支持) +# 3) settings.gradle 注册 module.font → anvillib-font-neoforge-1.21.1 +# 4) 按 4.6.3 分类处理:直接移植 9 项原样搬 + 反向适配 8 项改写 +``` + +#### 4.6.3 分类与 API 适配点 + +**可直接移植(9 项)**: + +| 文件 | 适配点 | +|---|---| +| `sdf/SdfGlyphAtlas.java`、`sdf/SdfGlyphPage.java` | 核心为 AWT(`Font`/`Graphics2D`/`BufferedImage`)+ 并发(虚拟线程、`CompletableFuture`)+ Dead Reckoning EDT 距离场算法,无 MC 渲染 API;`SdfGlyphPage.textureId` 的 `Identifier`→`ResourceLocation`;`Math.clamp`(Java 21 ✓)、`Thread.ofVirtual()`(Java 21 ✓,port `java_version=21`);`GLYPH_EXECUTOR` 虚拟线程保留 | +| `sdf/SdfTextLayout.java`、`ALFont.java` | 纯 CPU(quad 生成 + LRU 缓存);`Identifier` 改名;`Mth.ceil`、`FormattedCharSequence.accept`、`FormattedText.getString` 1.21.1 全有;含 `flatten`/`wrapLines` 工具原样搬 | +| `FontManager.java` | 纯 AWT/Swing(`GraphicsEnvironment`/`UIManager`) | +| `AnvilLibFontConfig.java` | 纯 Gson + `FMLLoader.getCurrent().getGameDir()`——报告子代理称 1.21.1 可用,但与其它子代理「1.21.1 的 `FMLLoader` 无 `getCurrent()`」(loader 4.0.42/4.0.43 源码)结论矛盾,**标为待验证(V-12)**:以编译为准,报错则改 `FMLPaths.GAMEDIR.get()` | +| `AnvilLibFont.java` | `IConfigScreenFactory`(21.1.226 sources 核实存在) | +| `data/AnvilLibFontData.java` | `GatherDataEvent.Client` ❌ → 单一 `GatherDataEvent` + `includeClient()` 判断(21.1.226 构造器 `(ModContainer, DataGenerator, DataGeneratorConfig, ExistingFileHelper)` 核实);`generator.getPackOutput()`、`LanguageProvider`、`Util.makeDescriptionId` 存在 | +| shader 资源(`sdf_text.fsh/vsh`) | GLSL 原样搬,补 1.21.1 `.json` 声明 | + +**需反向适配(8 项)**: + +| 文件 | 适配点 | +|---|---| +| `sdf/SdfAtlasTexture.java` | 上传层:`GpuDevice.createTexture/createCommandEncoder().writeToTexture` + `RenderSystem.getSamplerCache()` → `DynamicTexture`(或 `NativeImage` 直传)+ `TextureManager.register(ResourceLocation, AbstractTexture)` + `RenderSystem.setShaderTexture`;`toNativeImage` 像素拷贝逻辑复用;`SdfTexture` 内部类改为基于 `DynamicTexture`;`PageEntry.version` 版本号缓存机制保留 | +| `ALFPipelines.java` | 26.1 `RenderPipeline` 注册 → 1.21.1 `RegisterShadersEvent` 注册 `ShaderInstance`(port 侧 wheel `LibShaders` 先例);`SDF_TEXT_FORMAT` 用 1.21.1 `VertexFormat` 构造器重建(`new VertexFormat(ImmutableList.of(...), Mode.QUADS)`,1.21.1 无 builder) | +| `sdf/SdfTextRenderer.java` | 绘制提交走 `GuiGraphicsExtractor.submitGuiElementRenderState` → `RegisterShadersEvent` 注册 SDF shader + `GuiGraphics` 的 `BufferSource`/`RenderType` 直接画 quad;**分段样式逻辑**(颜色/粗斜体/下划线/删除线/混淆分段、`obfuscateCodepoint`、`drawDecorations`、居中/换行)纯逻辑原样复用;`drawAtlasPipeline` 改为 `guiGraphics.draw`(自定义 RenderType)或直接 `BufferBuilder` 提交 | +| `sdf/state/SdfTextRenderState.java` | `buildVertices` 的 UV/坐标计算复用;26.1 状态对象基类替换为 1.21.1 等价载体 | +| `extension/GuiGraphicsExtractorExtension.java` + `mixin/GuiGraphicsExtractorMixin.java` + `interface_injections.json` | 目标类 `GuiGraphicsExtractor` 1.21.1 不存在 → 注入 `net/minecraft/client/gui/GuiGraphics`(interface injection 机制 1.21.1 可用,port 侧已有先例);`anvillib$text` 系列 11 个默认方法签名保留(`FormattedText`/`FormattedCharSequence`/`Component` 1.21.1 全有);`ARGB`(26.1 新类)→ `FastColor.ARGB32` 或直接 int 运算(`textWithBackdrop` 中 `ARGB.multiply` 需手写) | +| `screen/FontConfigScreen.java`、`screen/FontTestScreen.java`、`screen/widget/Dropdown.java` | 26.1 `Screen.extractRenderState`/widget `extractWidgetRenderState`/`MouseButtonEvent` → 1.21.1 `Screen.render(GuiGraphics, int, int, float)`、`AbstractWidget.renderWidget(GuiGraphics)`、`mouseClicked/mouseReleased/mouseDragged/mouseScrolled`;布局/滚动/选项逻辑(`Dropdown` 的 expanded/scrollOffset/shielding 状态机)纯逻辑复用;`Button.builder().size().pos()`、`Component.translatable` 存在;绘制调用改 `guiGraphics.fill/drawString/drawCenteredString` | + +#### 4.6.4 验收方式 + +1. `gradlew :module.font:compileJava` 通过(依赖 `:module.rendering` 编译产物或已发布的 `-1.21.1` 快照)。 +2. `gradlew :module.font:runData` 生成 `en_us.json` 并核对。 +3. 行为验证(运行 `runClient`):`FontConfigScreen`/`FontTestScreen` 打开正常;SDF 文字渲染(含粗斜体/下划线/删除线/混淆分段)与居中/换行行为正确;`anvillib$text` 系列扩展在 `GuiGraphics` 上可用;字体配置(`FontManager` AWT 字体枚举)生效。 + +--- + +### 4.7 回移植项 #P7:module.rendering(高 · 按功能块分批) + +#### 4.7.0 总体说明与拆批依据 + +`module.rendering`(约 113 文件)在 port/1.21.1 上完全不存在(`git diff --name-status` 全部为 `A`)。其功能层深度构建在 26.1 全新渲染架构之上(`GpuDevice/CommandEncoder/RenderPass/GpuBuffer`、`RenderPipeline/RenderState`、`SubmitNodeStorage/FeatureRenderDispatcher/GameRenderState`、`GuiGraphicsExtractor`),**不能整模块移植**;1.21.1 等价物为 `RegisterShadersEvent + ShaderInstance`、`RenderTarget`、`VertexBuffer`、`GuiGraphics`、`MultiBufferSource`。按功能块拆为 **#P7a–#P7e** 五批,每批独立步骤与验收,作为单一模块(`anvillib-rendering-neoforge-1.21.1`)落地。 + +**已核实的平台基线**:port 侧 `java_version=21`(`Math.clamp`、虚拟线程、`List.getFirst` 均可用);port 侧已有 `RegisterShadersEvent + ShaderInstance` 用法(`module.wheel/.../init/LibShaders.java`);`IConfigScreenFactory` ✓;`interface_injections.json` 机制 ✓(module.moveable-entity-block、module.recipe 已用);NeoForge 21.1.226 有 `RenderFrameEvent`、`RenderLevelStageEvent`、`RegisterClientReloadListenersEvent`、`GatherDataEvent`(无 `Client` 子类),**无** `RegisterRenderPipelinesEvent`、`RegisterPictureInPictureRenderersEvent`、`AddClientReloadListenersEvent`、`ConfigureMainRenderTargetEvent`(后两者名称/形态已变化,均经 21.1.226 sources jar 核实)。 + +**模块落位**(#P7a–#P7e 共用):`settings.gradle` 注册 `module.rendering` → `anvillib-rendering-neoforge-1.21.1`;mods.toml 按 port 侧模板对齐(`${loader_version_range}` 等模板变量由 port 构建脚本注入,V-7);mixins.json 按 1.21.1 实际 mixin 清单重建;accesstransformer.cfg 整体重写(dev AT 目标 `GuiRenderer$Draw`、`GpuDevice`、`RenderType.state` 等绝大多数 1.21.1 不存在,仅 `BufferBuilder`(`vertices`/`format`/`mode`)与 `RenderType.name/outline` 等少数有意义)。 + +#### 4.7.1 子项 #P7a:纯逻辑与资源(19 项,可直接移植,低) + +**目标**:把与平台无关的 19 项纯逻辑/资源先落地,形成模块骨架。 + +**步骤**:从 dev 侧提取以下文件原样落位(`foundation/buffers/layout/**` 11 文件 + `Std140LayoutRulesTest.java`、`sdf/Sdf2d.java`、`sdf/SdfParameters.java`、`sdf/SdfRenderType.java`、`sdf/SdfPassType.java`(剥离 `BufferObject` 的 `upload(CommandEncoder,...)` 依赖,见下)、`foundation/buffers/object/BufferObject.java`(反适配)、`foundation/compound/DirtyTracked.java`、`event/MainTargetResizeEvent.java` + `mixins/GameRendererMixin.java`、`event/RegisterComputePipelinesEvent.java`、`integration/mixins/ALRIntegrationCompatMixinPlugin.java`、`mixins/MinecraftMixin.java`、`mixins/RenderTypeMixin.java` + `extension/ALRRenderTypeExtension.java`、`ALRSharedMath.java`、`util/Timer.java`、`foundation/buffers/EmptyVertexConsumer.java`、`EmptyBufferSource.java`、`EmptyOutlineBufferSource.java`、`TransformingVertexConsumerWrapper.java`、`foundation/ALRMeshSorting.java`、`extension/blaze3d/MemoryBarrierFlag.java`、`extension/blaze3d/ALRComputeCapabilities.java`、`foundation/buffers/GpuBufferConstants.java`、shader 资源(7 个 shader + util.glsl))。 + +**API 适配点**: + +| 项 | 适配点 | +|---|---| +| `Sdf2d` | 9 种 SDF 距离函数,仅用 `Mth.length/cos/sin/clamp/sign`(1.21.1 全有),原样搬 | +| `SdfParameters`/`SdfRenderType`/`SdfPassType` | 数据模型 + std140 布局定义,仅依赖 `Mth`/JOML/`BufferLayout`;剥离 `BufferObject` 基类中 `upload(CommandEncoder, GpuBufferSlice)` 与 `DynamicUniformStorage` 依赖 | +| `BufferObject` 基类 | `implements DynamicUniformStorage.DynamicUniform`(26.1 新类)→ 去掉接口与 `createDynamicStorage`;`upload(CommandEncoder, GpuBufferSlice)` → 改为「写入用户提供的 `ByteBuffer`」;各 UBO 子类(bloom/glitch/blur 参数类)改动极小 | +| `Timer` | `getDeltaTracker()`(26.1)→ `Minecraft.getInstance().getTimer().getGameTimeDeltaPartialTick(paused)`,一行 | +| `EmptyVertexConsumer`/`TransformingVertexConsumerWrapper` | `VertexConsumer` 接口方法集(`addVertex/setColor/setUv/setUv1/setUv2/setNormal/setLineWidth`)两版本一致 | +| `EmptyBufferSource`/`EmptyOutlineBufferSource` | `MultiBufferSource.BufferSource` 1.21.1 存在;构造器参数 `(BufferAllocator, RenderType)` 需核对(dev 侧 `super(null, null)` 需小调,V-3) | +| `ALRMeshSorting` | `VertexSorting`/`CompactVectorArray`/`MeshData.sortQuads`/`SortState.buildSortedIndexBuffer` 1.21.1 均存在(1.20.2+ 稳定 API,LevelRenderer 使用;存在性判定见 V-1) | +| `MinecraftMixin` | `Minecraft.(GameConfig)` 与 `updateLevelInEngines(ClientLevel, boolean)` 1.21.1 均存在(注入点签名一致) | +| `RenderTypeMixin` + `ALRRenderTypeExtension`(bloom 标记字段) | 1.21.1 `RenderType` 存在;interface injection 机制可用(port 侧已有 `interface_injections.json` 先例) | +| `MainTargetResizeEvent` + `GameRendererMixin` | `Event`/`IModBusEvent`/`ModLoader.postEvent` 1.21.1 存在;`GameRenderer.resize(int,int)` 存在(长期稳定方法) | +| `RegisterComputePipelinesEvent` | 纯 `Event`/`IModBusEvent`,1.21.1 存在;随 #P7e 的 compute 结构一起迁移 | +| `ALRIntegrationCompatMixinPlugin` | `IMixinConfigPlugin` 机制版本无关 | +| `MemoryBarrierFlag`/`ALRComputeCapabilities`/`GpuBufferConstants` | 纯 GL 常量,仅依赖 LWJGL | +| `Mth.roundToward` | 1.20.2+ 引入,1.21.1 `net.minecraft.util.Mth` 存在——**待验证(V-1)**:本机 gradle 缓存无 1.21.1 client jar 可反查,移植首个编译时以 1.21.1 client sources 复核 | + +**验收**:`gradlew :module.rendering:compileJava` 通过;`Std140LayoutRulesTest` 作为 JVM 测试随 `gradlew :module.rendering:test` 运行通过(纯逻辑验证,符合仓库 TDD 规范)。 + +#### 4.7.2 子项 #P7b:后处理三件套(Bloom / GaussianBlur / Glitch,需反向适配,高) + +**目标**:把「全屏后处理 + 高斯金字塔」三件套的算法与 GLSL 保留,执行层从 26.1 `GpuDevice/CommandEncoder/RenderPass` 体系重写到 1.21.1 `RenderTarget + ShaderInstance` 体系。 + +**来源提交**:`7121bf6`、`b5f5420`(#17/#20)、`6bb3269`(#38 resize 修复)、`0e8290d`(#92 稳定化)、`64bcf21`(#33 GaussianBlur)、`eccdc6d`(#43 Glitch)、`6b5a544`(#44)。 + +**步骤与适配点**: + +```bash +# dev 侧文件(落点新包 dev.anvilcraft.lib.v2.rendering.bloom / blur / glitch): +# bloom/BloomPostEffect.java、BloomParametersUbo.java、BloomPipelineParametersUbo.java、TransformsUbo.java、 +# BloomRenderCallback.java、foundation/BloomSubmitNodeStorage.java、ALRPostEffects.java、 +# blur/GaussianBlur.java、blur/BlurParametersUbo.java、glitch/GlitchPostEffect.java、glitch/GlitchParametersUbo.java、 +# shaders(apply_bloom/down_sample/up_sample/blit 及 glitch/blur 相关) +``` + +- **保留**:5 级降采样/升采样流程、全部 GLSL、UBO 参数类的布局定义(`BlurParametersUbo`/`GlitchParametersUbo` 直接复用)。 +- **重写**: + - `RenderTarget`(`MainTarget`/`TextureTarget` 存在)+ 自定义 `ShaderInstance`(`RegisterShadersEvent` 注册,port 侧 wheel 已有先例)+ 全屏四边形(`BufferUploader`/`Tesselator`)逐 pass 绘制;UBO 上传改为 `ShaderInstance` uniform 或自管 GL 缓冲; + - `RenderSystem.outputColorTextureOverride`(26.1 字段)改为临时切换 framebuffer 目标;bloom 重绘收集改为 `MultiBufferSource.BufferSource` 重画一遍(丢弃 `SubmitNodeStorage` 路径——`BloomSubmitNodeStorage`/`CompoundSubmitNodeStorage` 本身不可移植,见第 5 章); + - 尺寸变化监听用 `GameRendererMixin`(#P7a 已落位); + - `GlitchPostEffect` 的 `DeltaTracker` → `Minecraft.getTimer().getGameTimeDeltaPartialTick`;`process(GpuTextureView)` 改为「输入 RenderTarget 颜色纹理 → 输出 RenderTarget」。 +- **挂接**:`RenderLevelStageEvent.AfterTranslucentParticles/AfterLevel`(均存在)。 + +**验收**:`gradlew :module.rendering:compileJava` 通过;运行验证 Bloom/Blur/Glitch 后处理效果正确(含窗口 resize 后尺寸跟随、帧稳定);glitch 的时间驱动动画正常。 + +#### 4.7.3 子项 #P7c:SDF GUI 图形(需反向适配,中) + +**目标**:把 `Sdf2d`(#P7a 已落位)之上的 `SdfGraphics` 绘制编排从 26.1 GUI 提交体系重写到 1.21.1 `GuiGraphics + BufferSource + 自定义 RenderType`。 + +**来源提交**:`494274e`(#21)、`27a8c79`、`05a1f6c`(#39)、`e8ee3f9`(#40)、`d7a8f5d`(#38 AMD 修复)、`6b5a544`(#44)、`6d5a97b`(#45)。 + +**步骤与适配点**: + +- `SdfGraphics._draw` 改为「用 `GuiGraphics` 的 `BufferSource` + 自定义 `RenderType`(绑定 SDF shader)提交 4 顶点 quad,参数经 uniform 上传」;`graphics.pose()` 的 `Matrix3x2f` 换成 `PoseStack` 2D 变换;命中测试 `collide` 原样保留;旋转/居中数学全部复用。 +- `MAX_SDFS=256` 的 UBO 数组在 1.21.1 若走 uniform 需改为按需绑定或保留自管 GL UBO(`GL46.glBindBufferRange` 可用,LWJGL 全量存在)。 +- `mixins/GuiRendererMixin.java`(SDF 注入部分)**不迁移**:目标类 `GuiRenderer`/`GuiElementRenderState` 1.21.1 不存在(见第 5 章)。 +- shader 资源(`sdf_graphics.fsh/vsh`)GLSL 复用 + 补 `.json` 声明。 + +**验收**:`gradlew :module.rendering:compileJava` 通过;运行验证 SDF 图形(圆/矩形/圆角等 9 种距离函数)绘制正确、命中测试准确。 + +#### 4.7.4 子项 #P7d:CachedBER(缓存型 BER 渲染管线,需反向适配,高——难度最高的一项) + +**目标**:把「BER 渲染结果按 chunk 缓存为 GPU 顶点缓冲、视锥剔除、半透明排序、bloom 双画」的核心功能保留,执行层从 26.1 `GpuBuffer/RenderPass/SubmitNodeStorage` 重写到 1.21.1 `VertexBuffer + RenderSystem.draw + RenderType.shader()` 老路径。 + +**来源提交**:`2751be4`(#26)、`cff1802`(#29)、`d121047`(#68)、`48bfe55`、`fba1027`(#76)、`084c503`(#85)、`0e8290d`(#92)。 + +**步骤与适配点**: + +- **保留**:`CachedBlockEntityRenderingPipeline` 的 chunk 表、`RebuildTask` 编译/上传队列、`blockRemoved/update/forcedUpdate` 失效逻辑;`ALRMeshSorting` 原样搬(#P7a 已落位)。 +- **重写**: + - `GpuBuffer` → `VertexBuffer`(`com.mojang.blaze3d.vertex.VertexBuffer`,`upload(MeshData)`/`bind()`); + - 渲染走 `RenderSystem.draw` + `RenderType.shader()` 老路径(port 侧 wheel 已证明 ShaderInstance 路径可用); + - `CachedBlockEntityRenderer` 接口改为单阶段 `render(...)`(1.21.1 的 BER 调度是 `BlockEntityRenderDispatcher.render(be, partialTick, poseStack, bufferSource, light, overlay, camera, frustum)` 单阶段,无 extract/submit 两阶段); + - `FullyBufferedBufferSource`/`VertexBufferHost` 随管线重写。 +- **挂接**:`RenderLevelStageEvent.AfterOpaqueBlocks/AfterTranslucentFeatures` + `RenderFrameEvent.Pre`。 +- **Iris 集成**(`integration/IrisSupport.java` + 两个集成 mixin):`net.irisshaders.iris.api.v0.IrisApi`(`isShaderPackInUse`)在 1.21.1 的 Iris 1.7.x 中存在(长期 API);`net.irisshaders.iris.vertices.ImmediateState` 与 `skipExtension`/`isRenderingLevel` 字段需按 1.21.1 Iris 版本核实(dev compileOnly 依赖为 Iris 1.10.9+26.1,1.21.1 对应 1.7.x,**待验证 V-2**);`oculus` 分支 1.21.1 不存在,`ModList.get().isLoaded("oculus")` 判断可保留但恒假;build.gradle 中 Modrinth 依赖坐标同步替换(**待验证 V-6**:坐标需移植时查询)。 + +**验收**:`gradlew :module.rendering:compileJava` 通过;运行验证:缓存 BER 渲染正确(含视锥剔除与半透明排序)、方块移除/更新后失效重建正确、bloom 双画效果正常、Iris 开启时回退路径正常(无 Iris 时可跳过)。 + +#### 4.7.5 子项 #P7e:GPU 计算管线扩展(Compute,需反向适配,高) + +**目标**:把 GL 4.3 compute 管线扩展保留全部 API 形状(`ALRComputePipeline.builder()` 等)与 binding 模型,后端从「26.1 `GpuDevice` mixin 扩展」重写为独立自管 GL 工具层。 + +**来源提交**:`7d07f20`(#41)、`0e8290d`(#92)。 + +**步骤与适配点**: + +| 部分 | 适配点 | +|---|---| +| `extension/blaze3d/MemoryBarrierFlag.java`、`ALRComputeCapabilities.java`、`foundation/buffers/GpuBufferConstants.java` | 【直接搬】纯 GL 常量,仅依赖 LWJGL(#P7a 已落位) | +| `ALRComputePipeline`/`ALRComputePass`/bindings | 【直接搬】纯描述结构;`ShaderDefines` 是 26.1 类 → 改为简单 `Map` defines(1.21.1 的 `ShaderInstance` 用 `Supplier` 方式,无 ShaderDefines,**待验证 V-8**) | +| `ALRComputeShaderManager` | 【直接搬 + 小适配】`SimplePreparableReloadListener` 机制 1.21.1 存在,经 `RegisterClientReloadListenersEvent` 注册;`ShaderManager.createPreprocessor` 为 26.1 静态方法 → 1.21.1 的 `ShaderInstance` 内部有自己的 preprocessor 逻辑,改为自实现或简化 | +| `GlComputePassBackend`/`ALRComputePassBackend`/扩展接口与全部 6 个 blaze3d mixins(`GpuDevice/CommandEncoder/GlDevice/GlCommandEncoder/DirectStateAccess/GlDebugLabel`) | 【重写】mixin 目标 1.21.1 不存在;改为独立类持有 program/buffer 状态机;底层 GL 能力(`glDispatchCompute`、`glBindImageTexture`、`glBindBufferRange`(SSBO/atomic counter)、`glMemoryBarrier`、`glObjectLabel`)在 1.21.1 的 LWJGL 中完整可用;program 编译/链接逻辑(`GL46.glCreateShader(ARBComputeShader.GL_COMPUTE_SHADER)` 等)逐行复用 | +| `event/RegisterComputePipelinesEvent.java` | 机制保留(#P7a 已落位;纯 `Event`/`IModBusEvent`) | + +**验收**:`gradlew :module.rendering:compileJava` 通过;运行验证:compute pipeline 注册、program 编译链接、dispatch 与内存屏障执行正确(可用 #P18 之外的临时测试或 module.test 现有用例核对,若随迁则登记)。 + +#### 4.7.6 #P7 收尾(资源与元数据) + +- shader 文件原样搬 + 为每个管线补 1.21.1 的 `.json` 声明(1.21.1 的 `ShaderInstance` 加载约定 `shaders/core/xxx.json` 声明 uniform + `.vsh/.fsh`); +- mixins.json 按 1.21.1 实际 mixin 清单重建;AT 文件重写(见 4.7.0);mods.toml 按 port 模板;`AnvilLibRendering` 挂接点中 `AddClientReloadListenersEvent` → `RegisterClientReloadListenersEvent`。 + +**验收**:`gradlew :module.rendering:build`(含 jar/sourcesJar)通过;`gradlew :module.rendering:runData`(如有 datagen 需求)通过。 + +--- + +### 4.8 回移植项 #P8:renderdoc-loader(低) + +#### 4.8.1 目标 + +把 dev 侧 RenderDoc 调试用 Java Agent 模块 `renderdoc-loader`(2 文件)回移植,作为 `renderdoc-loader-internal`,实现 `premain`/`agentmain` 加载 RenderDoc DLL。 + +#### 4.8.2 步骤 + +```bash +# 1) 提取 2 个 Java 文件(RenderDocAgent.java、package-info.java),源码逐字搬移 +git show dev/26.1:renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/RenderDocAgent.java +git show dev/26.1:renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/package-info.java +# 2) jar manifest 的 Premain-Class/Agent-Class 属性照抄 +# 3) build.gradle 按 4.8.3 改写 +``` + +#### 4.8.3 API 适配点 + +| dev 侧(26.1) | port 侧(1.21.1) | +|---|---| +| `JavaLanguageVersion.of(25)` | `JavaLanguageVersion.of(21)`(port 工具链) | +| `alias libs.plugins.jreleaser`(port `gradle/libs.versions.toml` 无 jreleaser 别名,仅 modDevGradle/lombok/machete) | 改为 `id 'org.jreleaser' version '1.23.0'`(或省略——本模块不发布中央仓库) | +| `alias libs.plugins.lombok` | port toml 有该别名,可直接用 | +| dev 侧 `module.gradle` 的 `clientRenderDoc`/`clientRenderDocIrisWorkaround` run 配置引用该 agent jar | port 无 module.gradle;如需 RenderDoc 调试需在目标模块 build.gradle 手工接线(`-javaagent:` + `-Dneoforge.rendernurse.renderdoc.library=...`) | + +功能代码零依赖:仅 `java.lang.instrument` + jetbrains annotations compileOnly,`System.load` 读取 `renderdoc.library.path`(或 `neoforge.rendernurse.renderdoc.library`)系统属性。 + +#### 4.8.4 验收方式 + +1. `gradlew :renderdoc-loader:compileJava` 通过,jar 产物 manifest 含 `Premain-Class`/`Agent-Class`。 +2. 冒烟:`java -javaagent:renderdoc-loader.jar -Drenderdoc.library.path= -version`(或任一 JVM 启动)确认 agent 加载不抛异常(无 RenderDoc DLL 时仅跳过加载)。 + +--- + +### 4.9 回移植项 #P9:module.config——group 分组 + TranslatableEnum + TOML key 点分隔(低) + +#### 4.9.1 目标 + +把 dev 侧 module.config 的三项功能演进回移植:`@Config(group=…)` 配置分组(子目录文件)、`TranslatableEnum` 枚举翻译 key 生成、TOML 语言 key 点分隔化。 + +#### 4.9.2 步骤 + +```bash +# 1) group 分组(3d50d76):Config.java(+group() 字段)、ConfigManager.java(group 传递)、 +# ConfigRecord.java(+group 记录组件,getFileName() 返回 group/modId-type.toml) +# 2) TranslatableEnum(946938f #56):util/TranslatableEnum.java 新文件 + ConfigData.java +# (+ENUM_STRING 常量、enumValueAdd()、枚举字段检测分支、@Slf4j) +# 3) TOML key 点分隔(946938f #56):FormattingUtil.java(+toPointSplitName,并用 Arrays.stream 重构)、 +# ConfigData.java(TOML_STRING/TOML_TITLE_STRING 的 key 改用 FormattingUtil.toPointSplitName(name)) +# 4) (可选)ConfigManager 模组容器获取逻辑修复(c0a619b):ModList.get().getModContainerById(modId) +# → Optional.of(ModList.get()).flatMap(...)——1.21.1 中 ModList.get() 不返回 null,移植后行为等价无副作用, +# 该改动本身有争议(Optional.of(null) 仍抛 NPE),建议评估后决定是否保留 +``` + +#### 4.9.3 API 适配点 + +| 项 | 核实结论 | +|---|---| +| 配置子目录文件名 | 1.21.1 的 `ConfigTracker.openConfig` 用 `basePath.resolve(config.getFileName())` 拼接路径(`ConfigTracker.java:138`),`setupConfigFile` 中 `Files.createDirectories(file.getParent())` 自动建目录(`ConfigTracker.java:250-251`)——**原生支持子目录**,无平台差异 | +| `TranslatableEnum` | 21.1.226 存在 `net.neoforged.neoforge.common.TranslatableEnum`(含 `getTranslatedName()` 默认方法);AnvilLib 扩展接口(override `getTranslatedName` + 新增 `getTranslationKey`)可在 21.1 父接口上原样编译;`LanguageProvider`、`Component.translatableWithFallback` 均存在 | +| `enumValueAdd` | 用 `field.get(null)` 反射取枚举常量(`IllegalAccessException` 有日志,符合仓库规范);`getTranslationKey()` 基于 `getCanonicalName()` 生成,与平台无关 | +| TOML 点分隔 | 纯 Java 字符串处理(`String.split` 正则 `[^A-Za-z0-9]` 切分过滤空串 + `Collectors.joining`),1.21.1 行为一致 | + +**破坏性提示**:TOML key 点分隔化会改变语言文件 key(如 `section.` 的 name 改为点分隔形式),使用方现有语言文件需同步——移植 commit 中必须说明;`section.anvillib.explosion.common.toml` 类键名变化会影响 #P3 的 lang 产物(若 #P3 先落地,可在 #P9 完成后补回 `group` 语义并重跑 datagen)。 + +#### 4.9.4 验收方式 + +1. `gradlew :module.config:compileJava` 通过。 +2. `gradlew :module.config:runData`(或调用模块的 runData)生成 lang 产物,核对 `section.<点分隔名>.common.toml` 键名与枚举翻译 key(`enum.`)正确。 +3. 运行验证:`@Config(group="xxx")` 的模块配置生成于 `/xxx/-common.toml` 子目录。 + +--- + +### 4.10 回移植项 #P10:module.integration——数据加载拆分 + meter 递增(低 · 破坏性 API) + +#### 4.10.1 目标 + +把 dev 侧 module.integration 的两项功能演进回移植:数据加载按客户端/服务端拆分(`applyData` → `applyClientData`/`applyServerData`)、集成实例计数进度条递增。 + +#### 4.10.2 步骤 + +```bash +# 1) 数据加载拆分(dc4d583): +# IntegrationInstance.java:dataLoader 字段拆为 clientDataLoader + serverDataLoader, +# findVirtual 查找方法名改为 applyClientData / applyServerData(各自 try/catch,失败置 null), +# invokeData() 拆为 invokeClientData() / invokeServerData(),toString() 同步更新; +# IntegrationManager.java:loadData 拆为 loadClientData / loadServerData 及对应 loadAll*, +# load 中 containsType 判断逻辑不变; +# IntegrationType.java:DATA → CLIENT_DATA + SERVER_DATA +# 2) meter 递增(e8efbd4):IntegrationManager.compileContent() 循环内 meter.increment()(一行) +``` + +#### 4.10.3 API 适配点 + +- 纯 `java.lang.invoke.MethodHandle` 反射 + 枚举逻辑,无 26.1 API;`Integration` 注解、`IntegrationHook` 两侧一致,不受影响。 +- 已 grep 确认仓库内无其他模块使用 `IntegrationType.DATA`/`applyData`,破坏面仅限模块自身——但 **API 破坏性**:旧 `applyData` 方法名不再被识别,下游使用方(如 AnvilCraft 主模组)需同步改名,移植 commit 必须说明。 +- `StartupNotificationManager`/`ProgressMeter` 1.21.1 存在(port 侧原代码已用 `StartupNotificationManager.popBar(meter)`)。 + +#### 4.10.4 验收方式 + +1. `gradlew :module.integration:compileJava` 通过。 +2. 行为验证:注册同时含 `applyClientData`/`applyServerData` 的集成实现,启动时两侧各自加载、进度条随实例递增;`@ApiStatus.Internal` 类(如附带迁移 `c72aa30` 时)不对外暴露。 + +--- + +### 4.11 回移植项 #P11:module.network——泛型检查修复 + Included 方法 + public(低) + +#### 4.11.1 目标 + +把 dev 侧 module.network 的三处小演进回移植:`PacketData` 泛型类型参数精确匹配修复、`NetworkUtil` 条件筛选发送工具、`PacketProtocol` 枚举 public 化。 + +#### 4.11.2 步骤 + +```bash +# 1) PacketData 泛型检查修复(20721b1 #52): +# register/PacketData.java 新增 isMatchingTypeArgument(Type, int, Class); +# 字段判断从 declaringClass.isAssignableFrom(Type.class) 反转为 Type.class.isAssignableFrom(fieldType), +# 并追加类型实参校验:Type 检查第 0 参、StreamCodec 检查第 1 参 +# 2) NetworkUtil Included 方法(5733a9d): +# util/NetworkUtil.java 新增 sendToAllPlayersIncluded(Predicate, ...) 与 +# sendToAllPlayersInDimensionIncluded(ServerLevel, Predicate, ...); +# included == null 时默认全通过;两方法与现有 sendToAllPlayersExcluded / sendToAllPlayersInDimension 对称; +# 顺带将 player.equals(excluded) 改为 Objects.equals 防 NPE +# 3) PacketProtocol public(6220427):enum → public enum +``` + +#### 4.11.3 API 适配点 + +- 纯 `java.lang.reflect.ParameterizedType` 反射逻辑与纯 Java,无平台依赖。 +- 已核实:21.1.226 存在 `PacketDistributor.sendToPlayer(ServerPlayer, CustomPacketPayload, CustomPacketPayload...)`(`PacketDistributor.java:50`);`ServerLifecycleHooks.getCurrentServer()`、`ServerLevel.players()`、`ServerPlayerList.getPlayers()` 均为 port 侧已在用 API。 + +#### 4.11.4 验收方式 + +1. `gradlew :module.network:compileJava` 通过。 +2. 行为验证:同一类中多个 `Type`/`StreamCodec` 静态字段时注册解析正确(旧逻辑无法区分,修复后可区分);`sendToAllPlayersIncluded` 过滤发送正确(含 `included == null` 全通过路径)。 + +--- + +### 4.12 回移植项 #P12:module.util——OutlineUtil + HolderGetter 重构 + withCount + @EqualsAndHashCode(中) + +#### 4.12.1 目标 + +把 dev 侧 module.util 的四项功能演进回移植:AABB 轮廓直接提取(OutlineUtil + Line + 测试)、predicate 系列 HolderGetter 参数化重构、`ItemIngredientPredicate.withCount`、`BlockStatePredicate` @EqualsAndHashCode;HolderGetter 重构连带 module.recipe 四个调用方。 + +#### 4.12.2 步骤 + +```bash +# 1) OutlineUtil(e4c09fb #75): +# util/OutlineUtil.java(434 行)整文件复制;util/client/Line.java(48 行)按 4.12.3 反适配; +# src/test/.../ShapeUtilJoinTimingTest.java(232 行)复制; +# build.gradle 集成:注册 runShapeUtilJoinTimingTest(JavaExec)+ check 依赖 + +# test { failOnNoDiscoveredTests = false } + test sourceSets 配置(按 port 侧旧式 build.gradle 结构) +# 2) HolderGetter 重构(bb09805): +# predicate/BlockStatePredicate.java、predicate/ItemIngredientPredicate.java、predicate/ItemPredicate.java: +# Builder.of(TagKey) → of(HolderGetter, TagKey); +# 连带 module.recipe:recipe/builder/InWorldRecipeBuilder.java、recipe/predicate/block/HasBlock.java、 +# HasBlockIngredient.java、recipe/predicate/item/HasItem.java 四处调用方同步改 +# 3) withCount(d84912e):ItemIngredientPredicate 新增 withCount(int) +# 4) @EqualsAndHashCode(c480e9f #49):BlockStatePredicate 加 lombok 注解 +``` + +#### 4.12.3 API 适配点 + +| 项 | 适配点 | +|---|---| +| `OutlineUtil` | 仅依赖 `net.minecraft.world.phys.AABB/Vec3`(1.21.1 存在,mappings 确认)+ Java 标准库(TreeSet/BitSet/CompletableFuture),**原样复制** | +| `Line.java` | `vertex.setLineWidth(...)` 在 1.21.1 **不存在**(javap 1.21.1 `VertexConsumer`:仅 addVertex/setColor/setNormal 等,该 API 是 1.21.2 渲染管线改造后引入)→ 删除 `.setLineWidth(thickness)` 链式调用(thickness 字段可保留但无效,或改用 `RenderSystem.lineWidth()` 在渲染层设置——1.21.1 线宽是全局状态,无法逐顶点设置);`PoseStack.Pose` 参数签名与 `pose.pose()`/`pose.last()` 兼容 | +| `ShapeUtilJoinTimingTest` | 测试依赖 port 侧**已有**的 `ShapeUtil.threadedJoin`(两分支 ShapeUtil 方法集一致)、`Shapes/BooleanOp/VoxelShape`(1.21.1 均有);测试无渲染依赖(只构造 Line 不渲染) | +| HolderGetter 重构 | 1.21.1 `HolderGetter` 有 `getOrThrow(TagKey)`(default 方法)与 `get(TagKey)`(返回 Optional)——`BlockStatePredicate` 用 `blocks.getOrThrow(tag)`,另两个用 `items.get(tag).map(Function.identity())`;`Builder.of(HolderGetter, TagKey)` 与 `items.get(tag).map(...)` 均可行 | +| **不可照搬项** | dev 侧同时删除了 `Builder.with(BlockState)` 方法——**不能照搬**:port 侧 `module.multiblock/.../MultiblockDefinition.java` 大量使用 `.with(state)`(已 grep 确认 8 处以上),dev 侧是配合 26.1 multiblock 重构删的;port 侧保留 `with(BlockState)` | +| `ResourceKey::location` | dev 侧 `ResourceKey::identifier` 为 26.1 改名,反向迁移时保留 port 写法 | +| `withCount` | `public ItemIngredientPredicate withCount(int count) { return new ItemIngredientPredicate(this.items, count, this.components, this.subPredicates); }`(port 侧 4 参构造器) | +| `@EqualsAndHashCode` | lombok 注解,无平台依赖;注意 dev 侧**未**对 `statesCache` 做 `@EqualsAndHashCode.Exclude`,缓存字段参与 equals(dev 侧现状,照搬并留意该语义) | + +#### 4.12.4 验收方式 + +1. `gradlew :module.util:compileJava` 与 `gradlew :module.recipe:compileJava` 通过(连带调用方编译验证)。 +2. `gradlew :module.util:runShapeUtilJoinTimingTest` 运行测试通过。 +3. 行为验证:`OutlineUtil` 提取 AABB 轮廓正确;`BlockStatePredicate.builder().of(holderLookup, tag)` 与 `.with(state)`(保留项)均正常;`withCount` 返回新实例不改原实例;equals/hashCode 包含属性与缓存字段(语义与 dev 一致)。 + +--- + +### 4.13 回移植项 #P13:module.registrum——14 种新注册表 builder/entry(低) + +#### 4.13.1 目标 + +把 dev 侧 module.registrum 的「注册表类型扩展」回移植:14 种新 builder/entry 与 `AbstractRegistrum` 对应入口方法(17 个注册入口,其中 3 个 Modifier 同类),以及两个零碎修复(item 默认 lang、OneTimeEventReceiver 空值防御)。 + +#### 4.13.2 步骤 + +```bash +# 1) 提取 builders/ 与 util/entry/ 新增文件 + AbstractRegistrum 入口方法 +for f in $(git ls-tree -r --name-only dev/26.1 -- module.registrum | grep -E '(builders|util/entry)/.*\.java$'); do + git show "dev/26.1:$f" > "$f" +done +# 2) 按 4.13.3 分类处理:直接移植 10 项原样搬 + 反向适配 9 项微调 +# 3) 零碎修复:BlockBuilder.item() 默认 lang(e3ba2cd,1 行:return item().defaultLang().build())、 +# util/OneTimeEventReceiver 空值防御(23f5686:pairs == null return;unregister 判空 bus) +``` + +#### 4.13.3 分类与 API 适配点 + +**可直接移植(10 项)**——已核实 1.21.1 API 全部存在: + +| 注册类型 | 核实结论 | +|---|---| +| GameEventBuilder/GameEventEntry + `gameEvent()` | 1.21.1 `GameEvent` 同为 record `GameEvent(int notificationRadius)`(mc-1.21.1 源码),`Registries.GAME_EVENT` 存在,零差异 | +| MobEffectBuilder/MobEffectEntry + `mobEffect()` | `Registries.MOB_EFFECT` 存在;builder 仅包 NonNullSupplier + DeferredHolder | +| PotionBuilder/PotionEntry + `potion()` | 1.21.1 存在 `Potion(@Nullable String name, MobEffectInstance... effects)` 构造;`Registries.POTION` 存在 | +| VillagerTypeBuilder + `villager()` / PoiTypeBuilder + `poi()` | `VillagerType::new`(String 构造)与 `Registries.VILLAGER_TYPE` 存在;`PoiType(Set, int, int)` 构造存在(内部经 `PoiStateSet` 包装),`Registries.POINT_OF_INTEREST_TYPE` 存在 | +| SelfBuilder/SelfEntry | 纯泛型包装(ResourceKey + NonNullSupplier + DeferredHolder),无平台 API | +| 三类 Modifier(Biome/GlobalLoot/Structure)+ entries | 21.1.226:`NeoForgeRegistries.Keys.BIOME_MODIFIER_SERIALIZERS/GLOBAL_LOOT_MODIFIER_SERIALIZERS/STRUCTURE_MODIFIER_SERIALIZERS` 均存在;`BiomeModifier`/`StructureModifier`/`IGlobalLootModifier` 均存在;builder 仅包 MapCodec | +| ConditionBuilder/ConditionEntry + `condition()` | 21.1.226:`NeoForgeRegistries.Keys.CONDITION_CODECS` 与 `ICondition` 存在 | + +**需反向适配(9 项)**: + +| 注册类型 | 适配点 | +|---|---| +| CreativeTabBuilder + `creativeTab()`(9c89bfa #27) | `CreativeModeTab.Builder` 具备 dev 用到的**全部**方法(title/icon/displayItems/alignedRight/hideTitle/noScrollBar/backgroundTexture/withSearchBar(int)/withScrollBarSpriteLocation/withTabsImage/withLabelColor/withTabFactory/withTabsBefore\|After);`Item.getDefaultInstance()`、`Util.makeDescriptionId` 存在;仅 `Identifier`→`ResourceLocation`(4 处参数与 2 处 `getResourceKey().identifier()`) | +| AttachmentBuilder/AttachmentEntry + `attachment()`(9627358 #30) | 21.1.226 `AttachmentType.Builder` 有 `serialize(Codec)`/`serialize(Codec, Predicate)`/`copyOnDeath`/`copyHandler`/`sync(...)` 系列;**差异**:dev 的 `serialize(MapCodec)` 在 21.1 是 `serialize(Codec)`(26.1 才改 MapCodec)→ 参数类型改回 Codec | +| DataComponentBuilder/DataComponentEntry + `dataComponent()`(9b2d0e3) | 1.21.1 `DataComponentType.Builder` 有 `persistent(Codec)`/`networkSynchronized(StreamCodec)`/`cacheEncoding()`,**无 `ignoreSwapAnimation()`**(1.21.2+ 才加)→ 删除该方法(其余原样) | +| SoundEventBuilder/SoundEventEntry + `soundEvent()`(c307a4c #28) | `SoundEvent.createVariableRangeEvent(ResourceLocation)`/`createFixedRangeEvent(ResourceLocation, float)` 存在;`Registries.SOUND_EVENT` 存在;仅 `Identifier`→`ResourceLocation` | +| RecipeSerializerBuilder/RecipeTypeBuilder/RecipeEntry + `recipe()`(c307a4c、9627358) | `Registries.RECIPE_SERIALIZER` 与 `Registries.RECIPE_TYPE` 1.21.1 **均存在**;但 `RecipeSerializer` 在 1.21.1 是**接口**(含 `MapCodec codec()` + `StreamCodec streamCodec()`),**不能 `new RecipeSerializer<>(...)`**(26.1 才改成类)→ `RecipeSerializerBuilder.createEntry()` 改为匿名类实现 `codec()`/`streamCodec()`;`RecipeTypeBuilder` 的匿名 `RecipeType{toString}` 写法原样可用 | +| VillagerProfessionBuilder + `profession()`(9b2d0e3) | 1.21.1 `VillagerProfession` 是 **6 元 record**:`(String name, Predicate> heldJobSite, Predicate> acquirableJobSite, ImmutableSet requestedItems, ImmutableSet secondaryPoi, @Nullable SoundEvent workSound)`——**没有 tradeSetsByLevel 参数**(`TradeSet` 是 26.1 专有);dev 的 `Component.translatable(...)` 名字参数 → 1.21.1 第一参数改传 `modid + ":" + name` 字符串;交易注册改走 1.21.1 机制(21.1.226 存在 `VillagerTradesEvent`,或引导用户用 `VillagerTrades.TRADES`) | +| (可选)`dataComponentPredicate()` 入口 | **不可移植**(见第 5 章):1.21.1 无 `DataComponentPredicate`(1.21.5+ 引入),等价物为 `ItemSubPredicate` + `Registries.ITEM_SUB_PREDICATE_TYPE`;按框架判定不迁移,不提供该入口 | + +#### 4.13.4 验收方式 + +1. `gradlew :module.registrum:compileJava` 通过(17 个入口全部编译可用)。 +2. 行为验证(可在 module.test 或调用方验证):GameEvent/MobEffect/Potion/VillagerType/PoiType 注册后可在 `BuiltInRegistries` 查到;`creativeTab()` 生成标签页;`recipe()` 注册的 RecipeSerializer 可被配方加载;`attachment()` 的附件类型可附加与同步。 +3. 破坏性核对:`AttachmentBuilder.serialize` 的 Codec 参数与 `DataComponentBuilder` 无 `ignoreSwapAnimation` 的签名差异在 javadoc 中注明。 + +--- + +### 4.14 回移植项 #P14:module.registrum datagen 增强(中) + +#### 4.14.1 目标 + +把 dev 侧 module.registrum 的 datagen 增强回移植(**不包含** 26.1 新模型生成器与 Runner 模式):`RegistrumRecipeProvider` 的 vanilla 方法 public 化、`Builder.dataMap(..., HolderLookup.Provider)` 重载;`GeneratorType` 拆分列为可选单独提交。 + +#### 4.14.2 步骤 + +```bash +# 1) RecipeProvider 方法 public 化(58f01d4 及 ea4468a/d4191a0/e07aaa5/c1b10b8/b8825bd): +# cherry-pick dev 版 RegistrumRecipeProvider 的公开方法清单到 port 版文件(port 版已有 +# cooking/smelting/stonecutting/storage 等主体,实际增量是 vanilla 方法 public 化 + +# shelf/colorItemWithDye 等);port 版原地增强,不新建 Runner +# 2) dataMap(provider) 重载(58f01d4):builders/Builder.java 新增带 HolderLookup.Provider 的 dataMap 重载 +# (port 已有 dataMap(type, val) 与 dataMap(type, factory)); +# providers/RegistrumDataMapProvider.java 按 4.14.3 捕获 provider +# 3) (可选)GeneratorType/ProviderType 拆分(58f01d4):单独提交,见 4.14.3 +``` + +#### 4.14.3 API 适配点 + +| 项 | 适配点 | +|---|---| +| RecipeProvider 方法 public 化 | 1.21.1 这些方法均为 **`protected static` 且首参为 `RecipeOutput`**(如 `oneToOneConversionRecipe(RecipeOutput, ...)`、`trimSmithing(RecipeOutput, Item, ResourceLocation)` 在 1.21.1 存在)——1.21.1 落法是「公开包装方法内调用 `RecipeProvider.xxx(this, ...)`」而非 override(26.1 才可 override);**需剔除**:`shaped(RecipeCategory, ItemStackTemplate)`/`shapeless(RecipeCategory, ItemStackTemplate)`(ItemStackTemplate 26.1 专有,1.21.1 用 ItemLike 重载);`generateForEnabledBlockFamilies(FeatureFlagSet)` 的 26.1 签名(1.21.1 为 `(RecipeOutput, FeatureFlagSet)`,port 已 override 旧签名);`safeKey(...)` + `save(this, ResourceKey)` 需改为 `ResourceLocation`(已核实 1.21.1 `RecipeBuilder.save(RecipeOutput, ResourceLocation)` 存在、**无 ResourceKey 重载**) | +| `dataMap(provider)` | dev 侧靠 26.1 `DataMapProvider.gather(HolderLookup.Provider)` 重载捕获 provider;1.21.1 的 `gather()` 无参数(port 已用 `@SuppressWarnings("removal")` 标记)→ 等价方式:在 `RegistrumDataMapProvider` 里 override `run(CachedOutput, HolderLookup.Provider)` 缓存 provider 再调 super(datagen 线程内安全),或直接使用构造器传入的 lookup future;`DataMapType` 在 21.1.226 存在(数据映射 1.21.1 引入) | +| GeneratorType 拆分(可选) | `GeneratorType` 接口本身无平台依赖,但拆分动机是支撑 26.1 新模型生成器(MODEL/BLOCKSTATE/ITEM_MODEL generator 装配)与 `RegistrumRecipeRunner`,**这两个消费方在 1.21.1 均不存在**;若同步迁移需连带改 `AbstractRegistrum.genData/setDataGenerator/addDataGenerator`、`RegistrumDataProvider`(新增 `subGenerators` 与 `putSubProvider`)、`Builder.setData/addMiscData` 签名(ProviderType→GeneratorType),并同步放弃「sides 过滤」(dev 注释掉 `includeServer/includeClient` 过滤是行为变更,1.21.1 需自行决策是否保留)——**建议低优先级、单列一次提交** | +| RegistrumRecipeRunner / 模型生成器五件套 | **不迁移**:`RecipeProvider.Runner` 1.21.1 不存在(26.1 才引入 runner 模式);模型生成器依赖 26.1 新模型 datagen(`net.minecraft.client.data.models.*`、`net.minecraft.client.renderer.block.dispatch.*`、NeoForge 26.1 `template.*`,且 `RegistrumBlockModelGenerator` 用 `ObfuscationReflectionHelper` 反射改写 26.1 字段)——port 保留旧体系 `RegistrumBlockstateProvider`/`RegistrumItemModelProvider`(功能等价) | + +#### 4.14.4 验收方式 + +1. `gradlew :module.registrum:compileJava` 通过;`gradlew :module.registrum:runData` 生成产物核对(配方/数据映射/provider 正常输出)。 +2. 行为验证:公开包装方法可从外部模块调用生成配方(如 `shelf`/`colorItemWithDye`);`dataMap(type, holderLookup, factory)` 在 datagen 时正确捕获 `HolderLookup.Provider`。 +3. 若做 GeneratorType 拆分:`gradlew :module.registrum:compileJava` + `runData` 回归核对(sides 过滤决策记录在 commit 说明中)。 + +--- + +### 4.15 回移植项 #P15:module.multiblock——事件化 + 快照复用 + BlockPos 键化 + 懒解析(中) + +#### 4.15.1 目标 + +把 dev 侧 module.multiblock 的四项功能演进/修复回移植:`DynamicMultiblockEvent` 成型/解散可取消事件、未加载区块快照复用、控制器位置 Long→BlockPos 键化、定义懒解析(ResourceKey + registryAccess)。**注意**:dev 侧大部分 multiblock 代码源自 1.21.1 移植(`7019b38`),真正增量即上述四项(M1–M4,M5/M6 可选注解项)。 + +#### 4.15.2 步骤 + +```bash +# M1 DynamicMultiblockEvent(1e5bfb3 #69): +# dynamic/event/DynamicMultiblockEvent.java 新建(原样复制); +# DynamicMultiblockManager.updateFormed、MultiblockFormPacket、MultiblockUnformPacket 各加事件触发点: +# 服务端 updateFormed 中取消 → 回滚 setFormed;客户端两个 packet handler 中取消 → 同步回滚 +# M2 未加载区块快照复用(d888714 #73): +# DynamicMultiblockManager.buildSnapshot 加 if (!level.isLoaded(pos)) 分支:复用 old.entries() +# 或填 (null, null, predicate);MultiblockCheckSnapshot.Entry.blockState 可空 + Entry.test() 空态返回 true; +# MultiblockState 新增 snapshot 字段(构造时初始化 new MultiblockCheckSnapshot(this.controllerPos, Map.of())); +# checkMultiblockFormed 中 mstate.setSnapshot(snapshot) 先存快照 +# M3 Long→BlockPos 键化(22091d4): +# DynamicMultiblockManager:Map→Map、pendingChecks 同理(约 20 处 asLong()/immutable() 调整); +# MultiblockCheckSnapshot record 字段 controllerPosLong → BlockPos controllerPos; +# 注意 getAt/add/removeAt/containsAt 与异步回调处的 pos.immutable() 防哈希突变(BlockPos 可变) +# M4 定义懒解析(1b0a652): +# MultiblockState 构造器改收 ResourceKey,持 @Nullable Holder.Reference, +# getDefinition(HolderLookup.Provider) 懒解析;DEFINITION_KEY_CODEC / DEFINITION_KEY_STREAM_CODEC 外提; +# MultiblockFormPacket / MultiblockUnformPacket / DynamicMultiblockManager.add/onPlace(dev 用 holder.key())同步; +# STREAM_CODEC:ByteBufCodecs.holder(...) → ResourceKey.streamCodec(...)(两者 1.21.1 都有) +# M5(可选)ApiStatus.Internal(c72aa30 #93):multiblock 6 个类逐行加注 +# M6(可选)package-info @NullMarked(3bf9b6c):multiblock 10 个包(jspecify 在 1.21.1 构建链可用, +# port module.test 已用 org.jspecify.annotations.NullMarked,且 gradle 缓存存在 org.jspecify/jspecify) +``` + +#### 4.15.3 API 适配点 + +| 项 | 适配点 | +|---|---| +| 事件体系 | `net.neoforged.bus.api.Event`、`ICancellableEvent`、`NeoForge.EVENT_BUS.post()` 全套 21.1.226 自带(EventBus 体系 1.20.5 起未变) | +| M1 平台写法 | dev `this.state.getDefinitionKey().identifier()` → port `location()`;dev `level.isClientSide()` → port `isClientSide` 字段 | +| M2 | `Level.isLoaded(BlockPos)` 是 1.21.1 标准 vanilla API;`Map.of()`/`LinkedHashMap` 纯 JDK | +| M4 | `ResourceKey.streamCodec(ResourceKey)`(port `module.codec/StreamCodecUtil.java:264` 已在用)、`HolderLookup.Provider.lookup(...).getOrThrow(...)`、`Level.registryAccess()`(port `SetBlock.java:81` 已用)、`Holder.Reference`——全部 1.21.1 存在 | +| Java 21 陷阱 | dev 使用了 `computeIfAbsent(level, _ -> ...)`——`_` 作为 lambda 参数名需 Java 22+,port 工具链是 Java 21,**必须改名**(如 `ignored`) | +| M7 Config.group(跨模块) | `@Config(group=…)` 离开 module.config 的 group 支持无法编译——**依赖 #P9**;#P9 落地后两个模块各改 1 行注解(`AnvilLibMultiblockConfig`/`AnvilLibRecipeConfig`),config 文件路径变化(`group/xxx-common.toml`);若 #P9 未先行,两个模块维持 `@Config(name=…)` 即可编译 | + +#### 4.15.4 验收方式 + +1. `gradlew :module.multiblock:compileJava` 与 `gradlew :module.recipe:compileJava` 通过(连带验证)。 +2. 行为验证(运行 `runServer` 或测试环境):成形/解散事件可取消(取消后回滚);未加载区块处快照复用不触发强加载;`Map` 键化后 getAt/add/removeAt/containsAt 正确(异步回调防哈希突变);定义经 `HolderLookup.Provider` 懒解析正确。 +3. `grep -rn "asLong()\|controllerPosLong" module.multiblock/src` 复核零残留(预期差异除外)。 + +--- + +### 4.16 回移植项 #P16:module.recipe——SpawnItem 守卫 + SetBlock nbt 默认值(低) + +#### 4.16.1 目标 + +把 dev 侧 module.recipe 的两处纯逻辑修复回移植:`SpawnItem.accept()` 零数量守卫、`SetBlock` nbt 编解码默认值 null→空 `CompoundTag`(修复潜在 NPE)。 + +#### 4.16.2 步骤 + +```bash +# R1 SpawnItem 零数量守卫(40b8f84): +# outcome/SpawnItem.java 的 accept():int count = context.getInt(...); if (count == 0) return; +# (port 当前 SpawnItem.java:107 直接 copyWithCount(...) 无守卫,随机 count 掷 0 时仍会生成空堆物品实体) +# R2(可选)SpawnItem Builder 必填校验(91327c1):Builder.item 字段 ItemStack.EMPTY → @Nullable, +# build() 里 Objects.requireNonNull(1.21.1 用 @Nullable ItemStack 等价实现即可,不必引入 ItemStackTemplate) +# R3 SetBlock nbt 默认值(b8825bd): +# outcome/SetBlock.java 的 optionalFieldOf("nbt", null) → optionalFieldOf("nbt", new CompoundTag()) +# (port SetBlock.java:100 仍为 null,而 accept() 直接 entity.loadWithComponents(this.nbt, ...)—— +# JSON 缺 nbt 字段时 1.21.1 的 loadWithComponents(null, ...) 会 NPE;改默认值即可,无需动 loadWithComponents 调用) +# R4(可选)InWorldRecipe.matches() 谓词栈清理简化(0d0649f):行为等价重构,风险收益比低,可缓做 +``` + +#### 4.16.3 API 适配点 + +- 纯逻辑改动,无 API 适配;port 用 `this.item.copyWithCount(count)`,与 `ItemStackTemplate` 无关。 +- **已由 port 平行 PR 接收、无需重复移植**:双半方块修复、`AbstractCacheElement.grow()` 空堆修复、`ICacheInputOutputImpl.grow()` 只记录实际消耗、`ItemCache` 方块实体元素范围 `0.5→1.0`(port `6a0e8a9` #79 / `f2b60b6`,对应 dev `922d523` #78 / `1c523ee`)。 + +#### 4.16.4 验收方式 + +1. `gradlew :module.recipe:compileJava` 通过。 +2. 行为验证:随机 count 掷 0 时 `SpawnItem` 不生成物品实体;`SetBlock` 在 JSON 缺 `nbt` 字段时正常放置(无 NPE)。 + +--- + +### 4.17 回移植项 #P17:module.wheel——环形扇区选择效果(中-高) + +#### 4.17.1 目标 + +把 dev 侧 module.wheel 唯一的真功能——环形扇区选择效果(`WheelSelectionEffect`,DOT/ANNULAR_SECTOR 两种形态 + 可配置颜色 + 删除固定 `selectionEffectRadius` 参数)回移植;**渲染必须落在 port 侧既有 Tesselator / `ShaderInstance` / `LibShaders` 体系上,不带入任何 RenderState/UBO 管线**。 + +#### 4.17.2 步骤 + +```bash +# W1 WheelSelectionEffect(5d0b285、46e1b73、8a818f0 累计): +# api/WheelSelectionEffect.java 新建(纯逻辑部分原样搬); +# api/WheelMenuBuilder.java(链式 selectionEffect()/selectionEffectColor())、 +# api/WheelMenuModel.java(of(...) 新重载)、 +# gui/component/WheelWidget.java(renderSelectionEffect)、 +# gui/screen/WheelScreen.java 按 4.17.3 适配 +# W2 annular_sector.fsh(5d0b285):assets/anvillib/shaders/core/annular_sector.fsh 落位, +# 片段逻辑(径向+角度 AA、atan 扇区裁剪)GLSL 150 通用; +# 26.1 的 layout(std140) uniform AnnularSectorUniform(UBO)改为仿 port 现存 ring.json/selection.json 注册方式 +# + safeGetUniform("Center") 逐个 set(port 侧 WheelWidget.renderRing 即此模式) +# W3 环/选择点渲染(46e1b73、494274e 上游): +# WheelWidget.renderRing/renderSelectionEffect/renderProgressAnimation 功能(环、选中点随进度缩放、 +# getRendererSize 动态尺寸)移植;port 侧保留 renderRingFallback/renderDisc 式多边形回退与旧 shader 双路径 +# W7(可选)@ApiStatus.Internal(c72aa30):AnvilLibWheel、WheelWidget 等 +``` + +#### 4.17.3 API 适配点 + +| 项 | 适配点 | +|---|---| +| 纯逻辑(可直接搬) | `normalizePositiveAngle()` 角度归一化、扇区范围 `(angleEnd-angleStart)/2`、`WheelMenuModel.of(...)` 新重载、builder 链式 `selectionEffect()`/`selectionEffectColor()`——纯 Java | +| 渲染实现(重写) | dev 依赖 `SdfGraphics`(module.rendering)、`GuiGraphicsExtractor`、`Matrix3x2fStack`、`AnnularSectorRenderState`(`RenderPipeline`+`GpuBufferSlice`)、`LibDynamicUniforms`(`DynamicUniformStorage`/`Std140Builder`)——全部 26.1 → 1.21.1 落点为 port 侧 `WheelWidget` 原 Tesselator / `ShaderInstance`+`LibShaders` 体系(现仍存在),按 port 旧式重写扇区绘制 | +| 输入/渲染签名 | dev `WheelScreen`(`KeyEvent`/`MouseButtonEvent`/`extractRenderState`)、`WheelEntryRenderer`(`GuiGraphicsExtractor`+`Matrix3x2fStack`)→ 1.21.1 对应 `keyPressed(int,int,int)`/`mouseClicked(double,double,int)`/`GuiGraphics`/`PoseStack`——port 侧现存代码即等价物,不搬 dev 写法 | +| 模块依赖 | dev 侧 module.wheel 的 build.gradle 已 `jarJar` 依赖 `anvillib-rendering-26.1`(dev 侧 wheel 的新增模块依赖)——**回移植时应切断**,不引入 rendering 依赖 | + +**不迁移(仅 26.1 平台)**:`gui/render/state/{AnnularSector,Ring,Selection}RenderState.java`、`client/init/LibRenders.java`、`client/init/LibDynamicUniforms.java`(三者直接实现 `dev.anvilcraft.lib.v2.rendering.state.LibQuadGuiElementRenderState`、引用 `RenderPipeline`/`GpuBufferSlice`/`DynamicUniformStorage`;port 无 module.rendering 目录),dev 侧删除的 `LibShaders.java` port 保留。 + +#### 4.17.4 验收方式 + +1. `gradlew :module.wheel:compileJava` 通过。 +2. 行为验证(运行 `runClient`):环形菜单打开后显示扇区选择效果(DOT 点状 / ANNULAR_SECTOR 环形扇区两种形态);选中项随进度缩放动画正确;颜色配置(`selectionEffectColor`)生效;滚轮/按键交互无回归(port 旧渲染路径仍正常)。 + +--- + +### 4.18 回移植项 #P18:module.test——可选集成测试(低-中) + +#### 4.18.1 目标 + +把 dev 侧 module.test 中**不依赖 dev 新渲染模块**的四组集成测试回移植:T2 配置类测试、T8 RPC 测试、T9 爆炸测试、T10 wheel 按键扩展。其余测试(Bloom/CachedBER/Compute/SDF 图层/GUI 测试)依赖 module.rendering 与 26.1 渲染管线,不迁移(见第 5 章)。 + +#### 4.18.2 步骤 + +```bash +# T2 配置类测试(a6a24f1):AnvilLibTestConfig.java + ConfigManager.register +# ——port module.config 已有 ConfigManager/Config/Comment/CollapsibleObject/ConfigData; +# 缺 TranslatableEnum → 随 #P9 移植后保留 TestEnum 翻译,否则去掉 TestEnum 翻译部分 +# T8 RPC 测试(d153e58、2754d18):rpc/{RpcClientTest,RpcIntegrationTest,TestRpcMethods,RpcTestCommands,RpcTestScheduler} +# ——依赖 module.rpc(#P1);@EventBusSubscriber(Dist.CLIENT) 无 modid 为 26.1 语法,1.21.1 需补 modid +# T9 爆炸测试(435e23f):command/TestCommand.java ——依赖 module.explosion(#P3),命令骨架为 1.21.1 通用 API +# T10 wheel 测试扩展(随 W1):wheel/{WheelTestClientHandler,WheelTestKeys,WheelDemoMenus} +# ——KeyMapping.Category + event.registerCategory() 为 26.1 新 API;port 1.21.1 为 +# String category 构造 + RegisterKeyMappingsEvent;KeyMapping.matches(KeyEvent) 换 matches(key, scanCode) +# T13(可选)构建形式:module.test 的 runtimeOnly dev.dubhe:anvilcraft-neoforge-26.1.2 +# 换 1.21.1 版坐标或删除(dev 平台调试依赖) +``` + +#### 4.18.3 API 适配点 + +- T8 测试代码所用 MC/NF API 1.21.1 全部存在(`ClientPlayerNetworkEvent.LoggingIn`、`RegisterCommandsEvent`、`ServerTickEvent`、`PlayerEvent.PlayerLoggedInEvent`、`StreamCodec`/`ByteBufCodecs`);依赖 `RPC`/`RpcTarget`/`RemoteCallable`/`CallableParam`/`IRemoteCallableValidator`(#P1)。 +- T9 爆炸功能逻辑(分 tick 破坏、概率半径)与平台无关;`ExplosionExecutor` 属 #P3。 +- T10 按键 API 反向替换见上表。 + +#### 4.18.4 验收方式 + +1. `gradlew :module.test:compileJava` 通过。 +2. 运行验证(`runClient`/`runServer` 或 gameTest):T2 配置加载与枚举翻译正确;T8 RPC 远程调用往返成功;T9 爆炸命令执行分 tick 破坏正确;T10 wheel 环形按键切换与 demo 菜单正常。 + +--- + +### 4.19 回移植项 #P19:构建体系——roseauCheck(中) + +#### 4.19.1 目标 + +把 dev 侧构建体系中最具迁移价值的两项回移植:**roseauCheck API 兼容检查**(核心项,可直接搬);**module.gradle 集中化**(可选工程决策,需裁剪 26.1 特有内容)。 + +#### 4.19.2 步骤 + +```bash +# 1) roseauCheck(a6a24f1、946938f):根 roseau.yaml + gradle/scripts/roseau.gradle 拷贝, +# 根 build.gradle 接入 apply from + 任务注册 +# 2)(可选)module.gradle 集中化(ea4301f #25):根 module.gradle + build.gradle 的 createModule 脚手架 + +# 各模块 build.gradle 精简为 dependencies 块 + gradle.properties 的 anvillib.needRunConfig* 属性—— +# port 侧现有独立构建脚本自洽,是否重构由维护者决定;若做,按 4.19.3 裁剪 +``` + +#### 4.19.3 API 适配点(若做集中化,必须裁剪的 26.1 特有内容) + +| dev 侧内容 | port 处理 | +|---|---| +| `clientIrisWorkaround`/`clientRenderDoc` run 配置 | 剔除(port 无 module.gradle 的 RenderDoc 接线,见 #P8) | +| `evaluationDependsOn(':renderdoc-loader-internal')` | 按需保留/剔除(若 #P8 落地为独立模块可保留) | +| `createModule` 模板生成 `Identifier` 代码 | port 应生成 `ResourceLocation` | +| `java_version=25` | port 为 21 | +| parchment 配置已移除 | port **需要**(保留 port 现有 parchment 配置) | +| roseau 配置 | 通用(`io.github.alien-tools:roseau-cli:0.6.0` 无 MC 绑定,直接搬) | + +**不做**:`changeModsToml` 依赖注入任务(port 无 `.github/modules.json`,需补建该清单或裁剪任务);版本目录插件化与 `jarJar api→implementation`(低价值工程改动,由维护者决定)。 + +#### 4.19.4 验收方式 + +1. `gradlew roseauCheck`(或对应任务)在 port/1.21.1 基线上运行通过,输出 API 差分报告(可对 1.21.1 基线跑差分)。 +2. 若做集中化:全量 `gradlew build` 通过(全部模块聚合构建无回归)。 + +--- + +## 5. 不可移植清单(明确不迁移的平台适配项) + +> 以下内容**明确不迁移**。判定依据见第 2 章分类规则:这些「功能」本身就是 26.1 平台适配,port 侧已有 1.21.1 等价物,反向迁移无意义且会破坏 port 侧现状。执行时不得以「顺手」「保险」为由扩大范围。按模块分节,每项一句话理由。 + +### 5.1 module.rendering(26.1 渲染架构本体) + +| 不迁移内容 | 理由(一句话) | +|---|---| +| `ALRPipelines.java`(`RenderPipeline.builder()`、`UniformType.UNIFORM_BUFFER`)与 `RegisterRenderPipelinesEvent` 注册 | `RenderPipeline` 体系与 `RegisterRenderPipelinesEvent` 均为 26.1 专属(21.1.226 事件列表核实无此事件);1.21.1 等价物是 `RegisterShadersEvent`(port 侧已用),其 vertex format 声明以 1.21.1 `VertexFormat` 构造器在 #P7 中重建 | +| `mixins/GuiRendererMixin.java`(SDF 注入部分) | 目标类 `GuiRenderer`/`GuiElementRenderState` 1.21.1 不存在(26.1 GUI 渲染状态体系);SDF 绘制经 #P7c 改走 `GuiGraphics` | +| `extension/GuiGraphicsExtractorExtension.java` + `GuiGraphicsExtractorMixin` + `ItemStackRenderStateMixin` + `ItemStackRenderStateInternals` | 目标类 `GuiGraphicsExtractor`/`ItemStackRenderState` 为 26.1 对象;其功能(半透明物品/方块/结构预览)并入 #P7c 的 GUI 增强,按 1.21.1 `GuiGraphics + MultiBufferSource` 重写 | +| `foundation/compound/CompoundSubmitNodeStorage`、`CompoundSubmitNodeCollection`、`foundation/BloomSubmitNodeStorage` | 父类 `SubmitNodeStorage`/`SubmitNodeCollection` 与 `SubmitNodeCollector`/`FeatureRenderDispatcher` 体系为 26.1 渲染架构核心,1.21.1 不存在(port 分支全库无 `SubmitNode` 引用);bloom 双画在 #P7b 的 BufferSource 重画方案中直接实现 | +| `extension/blaze3d/compute/**` 的 6 个 blaze3d mixin(`GpuDevice`/`CommandEncoder`/`GlDevice`/`GlCommandEncoder`/`DirectStateAccess`/`GlDebugLabel`) | mixin 目标类 1.21.1 不存在(无 GPU 抽象层);compute 能力经 #P7e 以自管 GL 工具层重建 | +| `state/LibGuiElementRenderState`、`LibQuadGuiElementRenderState` 及 `gui/state/` 系列状态类 | 26.1 GUI 渲染状态体系(`GuiElementRenderState`/`PictureInPictureRenderState` 接口),1.21.1 无对应物 | +| `foundation/fakeworld` 中的 `cardinalLighting()` 相关实现 | `CardinalLighting` 是 26.1 新接口方法,1.21.1 的 `BlockAndTintGetter` 无此方法(1.21.1 用 `getShade(Direction, boolean)` 体系);`FakeDisplayLevel` 抽象方法集按 1.21.1 补齐(#P7 相关但不单独立项,随 fakeworld 反向适配处理) | + +### 5.2 module.font + +| 不迁移内容 | 理由 | +|---|---| +| 无独立不可移植项 | font 模块「需反向适配」项(GPU 上传/绘制层、GUI 屏幕)均在 #P6 中给出 1.21.1 改写方案,无仅 26.1 平台的内容 | + +### 5.3 module.rpc / module.sync + +| 不迁移内容 | 理由 | +|---|---| +| `ItemStackTemplate.STREAM_CODEC` 编解码分支(SyncProxy.defaultCodec) | `ItemStackTemplate` 是 26.1 ItemStack 重构专有类型,1.21.1 无对应物(javap「找不到类」);1.21.1 的 `ItemStack` 已有 `OPTIONAL_STREAM_CODEC`,删除后功能无损 | +| `neoforgespi.transformation.ClassProcessor` SPI(processor 子模块的注册面) | 该 SPI 属 26.1 平台特性(loader 4.0.42/4.0.43 无 transformation 子包,loader 11.0.13 才有 40 个类);processor 功能经 #P2 子项 2.6 以 CoreMod(`ICoreMod` + `ITransformer`)重写 | + +### 5.4 module.explosion / module.collision / module.space-select / renderdoc-loader + +| 不迁移内容 | 理由 | +|---|---| +| 无独立不可移植项 | 四模块功能依赖的 26.1 API 面很窄(`Identifier`、`ItemStackTemplate`、`hurtServer`、`Ingredient.items()`、`Registry.get(TagKey)`、`Inventory.getSelectedSlot()`、`ClientPacketDistributor`、`Minecraft.hasControlDown`、`Direction.getApproximateNearest`、`ARGB`、`GatherDataEvent.Client`、渲染状态体系),每一项在 1.21.1 都有直接等价物(javap 逐一核实),已并入 #P3/#P5/#P8 的适配点 | + +### 5.5 module.config / module.integration / module.network + +| 不迁移内容 | 理由 | +|---|---| +| package-info 全面切换 `@NullMarked`(jspecify) | jspecify 是 26.1 MC 的新依赖(21.1.226 sources jar 中无 `org/jspecify`);1.21.1 标准写法是 `@MethodsReturnNonnullByDefault` + `javax.annotation.ParametersAreNonnullByDefault`(port 现状),纯风格迁移无功能价值 | +| `IPacket.type(ResourceLocation→Identifier)` 改名 | `Identifier` 是 26.1 MC 的类名变更;1.21.1 为 `net.minecraft.resources.ResourceLocation`(port 现状) | +| `FMLLoader.getCurrent()` 系用法 | 1.21.1 的 `FMLLoader` 无 `getCurrent()`(loader 4.0.42 sources 核实),port 侧 `FMLLoader.getDist()`/`LoadingModList.get()` 写法正确,不随 dev 改动 | +| BIDIRECTIONAL 包注册的 4 参数 handler | 21.1.226 `PayloadRegistrar` 三个 bidirectional 方法均为 3 参数(26.1 才引入双 handler 签名);port 侧 3 参调用保持 | + +### 5.6 module.util + +| 不迁移内容 | 理由 | +|---|---| +| 新版 `UnlimitedItemStack`(util 包,`fd18fe9` #55,445 行) | 实现 `ItemInstance`/`MutableDataComponentHolder`,方法签名依赖 `ItemStackTemplate`/`DataComponentGetter`/`TooltipDisplay`/`Item.CODEC_WITH_BOUND_COMPONENTS`/`Item.STREAM_CODEC`——1.21.1 mappings 核实全部不存在;其功能(不限数量 ItemStack)由 port 侧已有的 `stack/UnlimitedItemStack`(NBT 序列化版)承担 | +| 旧 `stack/UnlimitedItemStack` 的 `@Deprecated(forRemoval)`、`typeHolder()` 改名、去 `INBTSerializable` | 均为 26.1 序列化体系适配(`b1484bc` #86 的构造器优化已在 port 侧存在);port 侧无替代新类,不应加弃用标记 | +| `ClientTickRecorder` 修复(`6b4d6bd` #61,删除 level 判空与 LoggingOut 归零) | dev 的修复针对 26.1 事件触发时机变化(主菜单 tick 行为);1.21.1 的 `ClientTickEvent.Pre` 行为与 port 现状一致,照搬会破坏计时器正确性(反向移植引入 bug) | +| `ChanceItemStack`/`ChanceBlockState`/`WeightedChanceBlockStates`/`NbtPredicate` 的 26.1 版 | `ItemStackTemplate` 化、`ContextMap/ContextKeySet` LootParams 构造、`TagValueOutput`+`ProblemReporter`——均为 26.1 专属;port 侧 ItemStack 版/LootParams 旧构造器实现完整可用 | +| package-info `@NullMarked` 迁移 | 同 5.5,注解风格迁移无功能价值 | + +### 5.7 module.registrum + +| 不迁移内容 | 理由 | +|---|---| +| `DataComponentPredicateBuilder`/`DataComponentPredicateEntry` + `dataComponentPredicate()` 入口(`252922e` #47) | `DataComponentPredicate` 与 `Registries.DATA_COMPONENT_PREDICATE_TYPE` 1.21.1 不存在(1.21.5+ 才引入);1.21.1 等价物是 `ItemSubPredicate` + `Registries.ITEM_SUB_PREDICATE_TYPE`(`d6fb290` 即 dev 侧从 ItemSubPredicate 换成 DataComponentPredicate 的迁移记录)——按框架判定不迁移 | +| generators/ 模型生成器五件套(`RegistrumModelProvider`/`RegistrumBlockModelGenerator`/`RegistrumItemModelGenerator`/`RegistrumLegacyBlockModelBuilder`/`PropertyDispatchWrap`) | 全部依赖 26.1 新模型 datagen(`net.minecraft.client.data.models.*`、`net.minecraft.client.renderer.block.dispatch.*`、NeoForge 26.1 `template.*`),且 `RegistrumBlockModelGenerator` 用 `ObfuscationReflectionHelper` 反射改写 26.1 字段;1.21.1 仍是旧体系 `net.neoforged.neoforge.client.model.generators.*`,port 现有 `RegistrumBlockstateProvider`/`RegistrumItemModelProvider` 功能等价 | +| `RegistrumRecipeRunner` 与 `generators/RegistrumRecipeProvider` 的 Runner 形态 | `RecipeProvider.Runner` 1.21.1 不存在(26.1 才引入 runner 模式);port 沿用现有「extends RecipeProvider implements RecipeOutput」模式(#P14 只做方法增强) | +| 平台适配 diff(Identifier/jspecify/`FMLLoader.getCurrent()`/`EntitySpawnReason`/`BlockTintSource`/渲染器双泛型/loot 基类更换/TagsProvider 去 ExistingFileHelper/`GatherDataEvent.Client`/`Registration` static 化等) | 均为 26.1 平台适配(见报告 §4 清单),port 侧现状即 1.21.1 正确写法 | +| `RegistrumDataProvider` 「忽略 sides 过滤,全部生成」行为变更 | 与 26.1 `GatherDataEvent.Client` 拆分相关,1.21.1 仍是一个 `GatherDataEvent`;建议保留 port 现有过滤(#P14 中不迁移此行为变更) | + +### 5.8 module.multiblock / module.recipe + +| 不迁移内容 | 理由 | +|---|---| +| 持久化重写:`SavedData.load/save(NBT)` → `SavedDataType` + `CODEC` | `SavedDataType` 是 26.1 新 API;1.21.1 用 `SavedData.Factory`(port 现有实现保留);dev 新 CODEC 只持久化 `controller_pos`+`definition`(丢弃 `formed`)属平台重构副作用,不回移植 | +| `BreakBlockEvent` | 26.1 把 `BlockEvent.BreakEvent` 拆为 `net.neoforged.neoforge.event.level.block.BreakBlockEvent`;1.21.1 用原类(port 现状正确) | +| `MultiblockDefinition` Builder 12 个重载删除 | 因 dev 的 `module.util/BlockStatePredicate` 移除了 `with(BlockState)` 与 `of(TagKey)`(26.1 标签/方块状态 Holder 化);port 的 `BlockStatePredicate` 两者俱在,重载保留(见 #P12「不可照搬项」) | +| `RecipeMapMixin` + `IRecipeMapExtension` | `RecipeMap` 是 1.21.2+ 新类;1.21.1 `RecipeManager` 仍是 `byName`/`byType` 字段,port `RecipeManagerMixin` 已含 dev 拆分前的完整实现 | +| `ItemResourceHandlerCache(Element)` + `VanillaContainerWrapper` | `net.neoforged.neoforge.transfer.ResourceHandler/ItemResource` 是 26.1 transfer API;1.21.1 等价物是 `IItemHandler`(21.1.226 自带)+ `InvWrapper`,port 的 `IItemHandlerCache`/`ItemHandlerCacheElement` 保留 | +| `LibDataComponentPredicates` → `LibItemSubPredicates` | `DataComponentPredicate.Type` 注册表是 26.1 特性;1.21.1 用 `ItemSubPredicate.Type`(`BuiltInRegistries.ITEM_SUB_PREDICATE_TYPE`),And/Or/Not 组合谓词两侧等价,port 保留 | +| `ValueInput`/`ValueOutput`/`ProblemReporter`/`TagValueInput`(BlockCache/SetBlock 的 NBT 加载错误上报) | 26.1 专属;1.21.1 用 `loadWithComponents(CompoundTag, Provider)` | +| Recipe 接口新方法(`placementInfo()`/`recipeBookCategory()`/`showNotification()`/`group()`、Serializer 脱接口化)、`serverLevel.recipeAccess()`、`RecipeUnlockedTrigger` 包迁移、`ContextMap/ContextKeySet` LootParams、`ItemStackTemplate`(icon/SpawnItem/Builder/SaveComponentToTag) | 26.1 Recipe/注册表体系变化,1.21.1 用 `canCraftInDimensions/getResultItem/RecipeSerializer` 等旧接口,port 现状即正确 | +| `ResourceEventListener` 注释掉 `RecipesUpdatedEvent` 客户端处理器 | 26.1 客户端配方管理重构;port 保留该处理器是正确行为 | +| `IRecipeManagerExtension.anvillib$getRegistries` @Deprecated | 26.1 适配 | +| `ASYNC_MULTIBLOCK_CHECK_PLAN.md` 删除 | 文档,不回移植(port 现状可用) | +| 语言文件管理差异(dev 删除 generated en_us.json 跟踪、zh_cn.json) | 资源管理差异,port 现状可用,不回移植 | + +### 5.9 module.moveable-entity-block(整体排除) + +| 不迁移内容 | 理由 | +|---|---| +| 模块全部(F1–F5) | dev 侧零功能增量,全部为 26.1 平台适配(ValueIO 序列化重构、RenderState 渲染管线重构、反混淆名调整);活塞 bug 修复已在 port(`6a0e8a9` #79);注解为纯风格——详见 2.3 节 | + +### 5.10 module.wheel + +| 不迁移内容 | 理由 | +|---|---| +| `gui/render/state/{AnnularSector,Ring,Selection}RenderState.java`、`client/init/LibRenders.java`、`client/init/LibDynamicUniforms.java` | 三者直接实现 `dev.anvilcraft.lib.v2.rendering.state.LibQuadGuiElementRenderState`(module.rendering)、引用 `RenderPipeline`/`GpuBufferSlice`/`DynamicUniformStorage`;port 无 module.rendering 目录,1.21.1 无对应物(见 #P17) | +| 输入/渲染签名 26.1 化(`WheelScreen` 的 `KeyEvent`/`MouseButtonEvent`/`extractRenderState`、`WheelEntryRenderer` 的 `GuiGraphicsExtractor`+`Matrix3x2fStack`) | 1.21.1 对应 `keyPressed(int,int,int)`/`mouseClicked(double,double,int)`/`GuiGraphics`/`PoseStack`,port 侧现存代码即等价物 | +| dev 侧删除的 `LibShaders.java` | port 保留(环形扇区渲染落点依赖它) | + +### 5.11 module.test + +| 不迁移内容 | 理由 | +|---|---| +| T1 测试组织重构(`all/` 包 + `TestLangGenerator`) | 依赖 dev 侧 registrum 演进 API(entry 包 + `PropertyDispatchWrap`),datagen 输出含 26.1 MC 类;port 保留现有结构 | +| T3 Bloom 测试(`TestBloomBlock/Tile/TESR`) | 依赖 `ALRPostEffects`/`BloomPostEffect`/`CompoundSubmitNodeStorage`(module.rendering)+ 26.1 渲染管线 + `SyncProxy`;等于重写 | +| T4 缓存型 BER 测试(`TestCachedRendering*`) | 依赖 `CachedBlockEntityRenderingPipeline` 等 + `BlockModelRenderState`/`BlockDisplayContext`(26.1 渲染) | +| T5 Compute 测试(`ComputeSupport`/`TestPipelines` + 4 个 .csh) | 依赖 `GpuDevice`/`CommandEncoder`/`GpuFence`/`GpuSampler`/`GpuTexture`/`TextureFormat`(26.1 GPU 抽象,1.21.1 无) | +| T6 SDF 图层测试(`SdfGraphicsLayer`) | `SdfGraphics`(26.1 管线)+ `implements GuiLayer` 为 26.1 NeoForge 接口(21.1.226 的 `RegisterGuiLayersEvent` 接受 `LayeredDraw.Layer`,非 `GuiLayer`) | +| T7 GUI 渲染测试(`GuiTestScreen`) | `FakeDisplayLevel`/`GuiRenderExtras`(module.rendering)+ `ALRCommandEncoderExtension`/`GpuSampler`(26.1 GPU) | +| T11 `MinecraftMixin` | 注入 `Minecraft.` 初始化 `ALRComputeCapabilities`,依赖 module.rendering 26.1 | +| T12 生成资源(bloom/cber 的模型、loot、原版模型覆盖) | datagen 产物,随 T3/T4 无意义;lang 中 `anvillib.configuration.*` 段随 T2/#P9 可再生成 | + +### 5.12 构建体系(module.gradle 中的 26.1 特有内容) + +| 不迁移内容 | 理由 | +|---|---| +| `clientIrisWorkaround`/`clientRenderDoc` run 配置、`evaluationDependsOn(':renderdoc-loader-internal')` | 26.1 模块化构建的 RenderDoc/Iris 调试接线;port 无 module.gradle 约定,RenderDoc 调试需手工接线(见 #P8) | +| `createModule` 模板中的 `Identifier` 生成、`java_version=25`、parchment 移除 | 26.1 平台内容;port 应生成 `ResourceLocation`、Java 21、保留 parchment | +| `changeModsToml` 依赖注入任务 | port 无 `.github/modules.json`(git ls-tree 核实),需补建清单或裁剪任务 | +| `gradle.properties` 的 `anvillib.needRunConfig*` 属性体系 | 26.1 聚合构建约定;port 各模块自建 run 配置(见 #P3) | + +--- + +## 6. 执行顺序与依赖关系 + +> 总原则:**先依赖后使用、纯逻辑先行、独立项先验证、高风险项尽早暴露**。每步完成即验收(第 4 章对应小节),全部通过再进入下一步;每步在 port/1.21.1 分支工作树以独立提交落地(提交信息建议按仓库现有风格:`feat(rpc): ...` / `fix(recipe): ...` 等)。 + +### 执行顺序总览 + +``` +#P1/#P2 ──→(#P7a)──→ #P6 ──→ #P3/#P4/#P5/#P8 ──→ #P9 ──→ #P11/#P12/#P13/#P16 ──→ #P15 ──→ #P17 ──→ #P14 ──→ #P18 ──→ #P19 +``` + +### 第 1 步:#P1 → #P2(新模块骨架与最大 SPI 风险) + +- #P1(RPC)先行:依赖面最窄(port 侧 network/util 已就绪)、3 处机械反替换,作为「新模块落地模板」验证 `settings.gradle` 注册、jarJar 坐标 `-1.21.1`、mods.toml 模板在 port 侧的整套流程;#P2(sync + processor)紧随其后,processor 的 CoreMod 重写(子项 2.6)是**最大的 SPI 不确定点**(services 文件可见性、GETSTATIC 类解析),尽早暴露。 + +### 第 2 步:#P7a(rendering 纯逻辑批)→ #P6(font) + +- #P7a 先落地 rendering 模块骨架与 19 项纯逻辑(Buffer 布局、Sdf2d、事件、辅助类、shader 资源),难度低、无渲染平台风险,同时为 #P6 提供 jarJar 依赖坐标;#P6 在 #P7a 完成后即可开工(SDF 图集/布局/测量等纯逻辑不依赖 #P7b–#P7e),其 GPU 上传/绘制层可先以「DynamicTexture + GuiGraphics」独立路径落地,不受后处理进度阻塞。 + +### 第 3 步:#P3/#P4/#P5/#P8(其余新模块,可并行) + +- #P4(collision)极低难度独立模块,可随时插入;#P3(explosion)依赖 port module.config(已就绪),与 #P5(space-select,依赖 port module.network)互不阻塞;#P8(renderdoc-loader)独立。四者均可并行执行,每个独立提交。 + +### 第 4 步:#P9(config 三项演进)——必须早于 #P15 + +- #P9 是 #P15 的 M7 子项(Config.group)的代码级前置:`@Config(group=…)` 离开 module.config 的 group 支持无法编译;与 #P3 的交互——#P3 若先落地按「去掉 group」处理(port @Config 无 group 属性),#P9 完成后可选补回 `group = "anvillib"` 并重跑 datagen(lang section 键名以 #P9 的点分隔规则为准)。 + +### 第 5 步:#P11/#P12/#P13/#P16(共享模块小演进,可并行) + +- 四项均低-中难度、模块内改动:#P11(network)、#P12(util + recipe 调用方连带)、#P13(registrum builder 全家桶)、#P16(recipe 两处修复)。#P12 的 HolderGetter 重构与 #P16 互不干扰;#P12 注意保留 port 的 `Builder.with(BlockState)`(不可照搬 dev 删除)。 + +### 第 6 步:#P15(multiblock 四项)——依赖 #P9 已落地 + +- M1–M4 建议一并做(M3 与 M4 构造路径耦合、M2 与 M3 快照 record 字段耦合);M7 的模块注解改动依赖 #P9。 + +### 第 7 步:#P17(wheel 环形扇区效果) + +- 依赖 port 旧渲染体系(Tesselator/ShaderInstance/LibShaders),与 #P7 的新渲染模块无依赖(已切断 dev 侧 rendering jarJar 依赖);独立执行。 + +### 第 8 步:#P14(registrum datagen 增强) + +- 独立于 #P13(可不依赖其 entry API);GeneratorType 拆分列为可选单独提交(消费方 1.21.1 不存在,价值低)。 + +### 第 9 步:#P18(module.test 可选测试) + +- T2 随 #P9(TranslatableEnum)、T8 随 #P1、T9 随 #P3、T10 随 #P17;各测试项随对应功能落地,作为功能项的集成验收补充。 + +### 第 10 步:#P19(构建体系)——收尾 + +- roseauCheck 独立可随时执行,安排在最后作为全量 API 差分验收;module.gradle 集中化为可选工程决策(port 侧现有独立构建脚本自洽,是否重构由维护者决定,**不属于本计划硬性范围**)。 + +### 顺序之外的两条纪律 + +1. 每步提交前先跑一遍该模块 `build`(含 `jar`),避免只验 `compileJava` 漏掉资源处理(mods.toml 替换、jarjar 内嵌)。 +2. 涉及 `runData`/运行类验证的项(#P3/#P6/#P7/#P9/#P14/#P15/#P17/#P18),如本地环境不便跑客户端,可先以编译 + CI 为准,把运行验收登记为「待 CI/人工补验」并给出复现步骤,**不得**把未验证项标记为已完成(见第 7 章)。 + +--- + +## 7. 总体验收标准 + +### 7.1 构建验收(硬性门槛) + +| #P 涉及 | 验收项 | 命令 / 判据 | +|---|---|---| +| 全部 | port/1.21.1 全量构建 | 在 port/1.21.1 分支 `gradlew build`(根级,聚合全部模块含新增 8 个)**全部成功** | +| 全部 | 模块独立构建 | 依次 `gradlew :module.rpc:build` `:module.sync:build` `:module.explosion:build` `:module.collision:build` `:module.space-select:build` `:module.font:build` `:module.rendering:build` `:module.test:build` `:module.main:build` 均通过 | +| #P1–#P18 | 依赖完整性 | 各新模块 jar 的 `META-INF/jarjar/` 内含 `anvillib-*-neoforge-1.21.1-*.jar`(如 module.main 聚合产物);无 `-26.1` 坐标残留 | +| #P2 | SPI 重写零残留 | `grep -rn "neoforgespi.transformation" module.sync` 无匹配;CoreMod services 注册生效 | +| #P19 | roseauCheck | `gradlew roseauCheck`(或对应任务)在 1.21.1 基线上运行通过,输出 API 差分报告 | + +### 7.2 功能验收清单(行为核对) + +| 覆盖项 | 验收项 | 判据 | 执行方式 | +|---|---|---|---| +| #P1/#P18 | RPC 远程调用 | 客户端注册 `@RemoteCallable` 方法并下发索引 → 服务端经 `invoke` 远程调用返回 `CompletableFuture` 结果;`@CallableParam` 自定义编解码器生效;tick 超时路径正确 | module.test T8 / 手工 | +| #P2 | sync 同步 | `@Sync` 注解字段两端同步;LazySync 惰性差分同步生效;Configuration 阶段配置表下发正常;processor 字节码注入目标类生效(启动日志 + 反编译核对) | module.test / 手工 | +| #P3/#P18 | 爆炸执行器 | 分层球壳破坏按概率半径执行;熔化替换(熔炼/高炉配方表)生效;实体 `hurt` 与掉落/熔化路径正确 | module.test T9 / 手工 | +| #P4 | 碰撞检测 | 静态 SAT 13 轴 / 三轴独立扫掠 / 真扫掠碰撞三个入口断言通过;`overlapOnAxis` 工具正确 | 手动运行测试入口 | +| #P5 | 选区渲染 | 框选创建区域;`DistrictRenderer` 各 stage 线框正确(含平移量/颜色语义);滚轮缩放/移动生效;网络包双向收发正常 | 运行 runClient | +| #P6 | SDF 文字 | SDF 图集生成正确;文字分段样式(粗斜体/下划线/删除线/混淆)与居中/换行正确;`anvillib$text` 扩展可用;配置/测试界面正常 | 运行 runClient | +| #P7b | 后处理三件套 | Bloom 5 级金字塔效果正确(含 resize 跟随、帧稳定);Blur/Glitch 效果正确;glitch 时间驱动动画正常 | 运行 runClient | +| #P7c | SDF GUI | 9 种距离函数图形绘制正确;命中测试准确 | 运行 runClient | +| #P7d | CachedBER | 缓存 BER 渲染正确(视锥剔除/半透明排序);失效重建正确;bloom 双画正常;Iris 开启时回退正常 | 运行 runClient(无 Iris 可跳过最后一项) | +| #P7e | Compute | pipeline 注册、program 编译链接、dispatch 与内存屏障执行正确 | 运行 runClient | +| #P9 | 配置分组/枚举翻译 | `@Config(group)` 生成 `//-common.toml`;枚举翻译 key(`enum.`)正确;TOML section 键为点分隔 | runData + 运行 | +| #P10 | 集成数据加载 | 客户端/服务端数据加载器各自生效;进度条随实例递增 | 运行验证 | +| #P11 | 网络工具 | 多静态字段注册解析正确;`sendToAllPlayersIncluded` 过滤发送正确 | 编译 + 行为用例 | +| #P12 | util 演进 | OutlineUtil 轮廓提取正确;HolderGetter 谓词匹配正确;`withCount` 不改原实例;`with(BlockState)`(保留项)正常 | 编译 + 行为用例 | +| #P13 | 注册表入口 | 17 个入口注册后可在 `BuiltInRegistries`/`NeoForgeRegistries` 查到;CreativeTab 生成标签页;RecipeSerializer 可加载配方 | 编译 + 行为用例 | +| #P14 | datagen 增强 | 公开包装方法可生成配方(shelf/colorItemWithDye);`dataMap(type, provider, factory)` 捕获 HolderLookup 正确 | runData | +| #P15 | multiblock 四项 | 成形/解散事件可取消(取消后回滚);未加载区块快照复用不触发强加载;BlockPos 键化正确;定义懒解析正确 | 运行 runServer | +| #P16 | recipe 修复 | 随机 count 掷 0 不生成物品实体;JSON 缺 `nbt` 字段正常放置(无 NPE) | 行为用例 | +| #P17/#P18 | 环形扇区选择效果 | 菜单显示 DOT/ANNULAR_SECTOR 两种形态;选中点随进度缩放;颜色配置生效;旧渲染路径无回归 | 运行 runClient + T10 | + +### 7.3 收尾核对 + +1. **提交完整性**:port/1.21.1 分支上 #P1–#P19 各为独立提交,提交信息含来源 issue/PR 号(#60/#63/#87/#65/#22/#56/#21/#41/#33/#25/#27/#28/#30/#31/#47/#52/#69/#73/#75/#49 等)。 +2. **范围守卫**:`git log --oneline port/1.21.1..dev/26.1` 中,除第 5 章「不可移植清单」与 #P1–#P19 外,不应有「实际回移植但未立项」的内容;如执行中发现新功能,先补充本计划再执行。 +3. **文档同步**:附录 B 的「待验证事项」在回移植执行中逐项销账;本计划中的「待验证」标注同样逐一关闭,关闭记录可追加到附录 B。 +4. **遗留登记**:任何因环境限制未完成的运行类验收登记为遗留项,附复现步骤,不得默认通过。 +5. **与姊妹文档一致性**:本计划不覆盖 `docs/migration-plan-dev-26.1.md` 已处理的 port→dev 方向内容(yukkuri 等 #1–#8);若两侧计划对同一文件的修改意图冲突(如 #P12 保留 `with(BlockState)` vs dev 侧删除),以「port 侧保留现状」为准,并在 commit 说明中标注。 + +--- + +## 附录 + +### A. 常用只读 git 命令(本计划全部事实均来自以下命令) + +```bash +# 提取 dev 侧文件内容(全部为只读操作) +git show dev/26.1:module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/AnvilLibRpc.java +git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/AnvilLibSync.java +# 查看来源提交的完整 diff(代表性提交) +git show d153e58 # #P1(RPC 主体) +git show 1063ee9 # #P2(processor 新增) +git show 435e23f # #P3(explosion 初建) +git show 82b4f6a # #P4(collision) +git show 1335fc5 # #P5(space-select) +git show 1d56a4e # #P6(font) +git show 7121bf6 # #P7(rendering Bloom 系) +git show 7d07f20 # #P7(rendering Compute/Buffer 布局) +git show 494274e # #P7(SDF GUI) +git show 2751be4 # #P8(renderdoc-loader 初建) +git show 3d50d76 # #P9(Config.group) +git show 946938f # #P9 / #P6 / #P19(TranslatableEnum + TOML 点分隔 + roseau) +git show dc4d583 # #P10(integration 数据加载拆分) +git show 20721b1 # #P11(PacketData 泛型检查) +git show e4c09fb # #P12(OutlineUtil) +git show bb09805 # #P12(predicate HolderGetter 重构) +git show 9b2d0e3 # #P13(注册表更多种类) +git show 58f01d4 # #P14(datagen 重构) +git show 1e5bfb3 # #P15(DynamicMultiblockEvent) +git show d888714 # #P15(未加载区块快照) +git show 22091d4 # #P15(Long→BlockPos 键化) +git show 1b0a652 # #P15(定义懒解析) +git show 40b8f84 # #P16(SpawnItem 零数量守卫) +git show 5d0b285 # #P17(环形扇区选择效果) +git show a6a24f1 # #P18 / #P19(roseauCheck + T2 配置测试) +git show ea4301f # #P19(module.gradle 集中化) +# 两侧同路径对比 +git diff port/1.21.1 dev/26.1 -- module.network/src/main/java/dev/anvilcraft/lib/v2/network/util/NetworkUtil.java +# 某模块 dev 独有提交清单(回移植候选扫描) +git log --oneline port/1.21.1..dev/26.1 -- module.registrum +# dev 侧落点现状 +git show dev/26.1:module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/event/DynamicMultiblockEvent.java +# 模块文件清单(新增/删除核对) +git diff --name-status port/1.21.1 dev/26.1 -- module.rpc +git ls-tree -r --name-only dev/26.1 -- module.space-select +``` + +### B. 待验证事项汇总(执行时逐项销账) + +| 编号 | 事项 | 涉及项 | 验证方法 | 状态 | +|---|---|---|---|---| +| V-1 | `Mth.roundToward`/`VertexSorting`/`CompactVectorArray`/`MeshData.sortQuads` 在 1.21.1 的存在性(报告依据「1.20.2+ 稳定 API」判断;本机 gradle 缓存无 1.21.1 client jar 可反查) | #P7a | 移植首个编译时以 1.21.1 client sources 复核 | 未验证 | +| V-2 | Iris 1.7.x 的 `ImmediateState` 字段(`skipExtension`/`isRenderingLevel`)与 `IrisApi` 方法签名(dev compileOnly 依赖为 Iris 1.10.9+26.1) | #P7d | 按 1.21.1 Iris 版本(1.7.x)sources 复核 | 未验证 | +| V-3 | `MultiBufferSource.BufferSource` 构造器参数(dev 侧 `super(null, null)` 需小调) | #P7a | 1.21.1 sources + 编译 | 未验证 | +| V-4 | `LevelRenderer.renderVoxelShape`/`renderLineBox` 的颜色语义(RGBA 0-1 float)与 dev `ARGB` int 色的转换 | #P5 | 实现后目测确认(report §6) | 未验证 | +| V-5 | port 侧各模块 build.gradle 的 run 配置自建模式(port 无 module.gradle,`anvillib.needRunConfig*` 不生效) | #P3/#P5/#P8 | 参照 port `module.config/build.gradle`(含 data run 段) | 未验证 | +| V-6 | Iris 1.21.1 对应版本(1.7.x)的 Modrinth 依赖坐标 | #P7d | 移植时查询 Modrinth | 未验证 | +| V-7 | mods.toml 模板变量(`${loader_version_range}` 等)在 port 侧注入方式 | #P7 | 按 port 现有模块模板对齐 | 未验证 | +| V-8 | `ShaderDefines`(26.1 类)改为 `Map` defines 后 `ALRComputePipeline`/`ALRComputePass` 编译与运行 | #P7e | 编译 + 运行验证 | 未验证 | +| V-9 | CoreMod transformer 在 jar-in-jar(`jarJar(api(...))` 携带 processor)场景下 services 文件可见性 | #P2 | 打包后实测启动 | 未验证 | +| V-10 | CoreMod transformer 中 `AnvilLibSync.LAZY_SYNC_MANAGER` GETSTATIC 引用的类可解析性(game layer 类加载时机) | #P2 | 启动日志 + 反编译核对注入点 | 未验证 | +| V-11 | `getSecureJar().getPath(classPath)` 在 1.21.1 securejarhandler 3.0.8 的签名(返回 `Path`) | #P2 | securejarhandler 3.0.8 sources + 编译 | 未验证 | +| V-12 | `AnvilLibFontConfig` 中 `FMLLoader.getCurrent().getGameDir()` 在 1.21.1 可用性(`getCurrent()` 的例外调用点) | #P6 | 编译;报错则改 `FMLPaths.GAMEDIR.get()` | 未验证 | +| V-13 | jspecify 依赖在 port 构建链的可用性(#P15 M6 可选注解项) | #P15 | port `module.test` 已用 `org.jspecify.annotations.NullMarked` 且 gradle 缓存存在 `org.jspecify/jspecify`——以编译为准 | 未验证 | +| V-14 | `VillagerProfession` 6 元 record 与 `Potion(@Nullable String, MobEffectInstance...)` 构造在 1.21.1 的确认 | #P13 | 1.21.1 源码(报告已核实,落位时以编译复核) | 未验证 | +| V-15 | `RecipeSerializer` 匿名类实现(`codec()`/`streamCodec()`)在 1.21.1 的编译通过性 | #P13 | `gradlew :module.registrum:compileJava` | 未验证 | +| V-16 | `District.color()` 手写 int 打包(`(a<<24)|(r<<16)|(g<<8)|b`)在渲染端的颜色语义 | #P5 | 渲染目测 + 与 dev 行为对比 | 未验证 | +| V-17 | `GatherDataEvent`(单一类)的 `includeClient()`/`getPackOutput()` 在 21.1.226 的签名 | #P3/#P6/#P14 | 21.1.226 sources(报告已核实)+ 编译 | 未验证 | +| V-18 | 根构建脚本(`build.gradle`/`settings.gradle`)模块注册方式与 port 侧现有模块(module.wheel 等)对齐 | #P1–#P8 | 参照 port 现有模块 | 未验证 | + +### C. 参考资料 + +| 资料 | 位置 | +|---|---| +| **8 个并行子代理模块级调研报告(本文全部事实依据,1395 行)** | `C:/Users/Administrator/.kimi-code/sessions/wd_anvillib_031cb785b7e3/session_6033f19b-f4ef-481a-b18c-d626cc22c314/agents/main/tool-results/AgentSwarm-call_00_ET_e0hBtNS4BowwxHQBCso99671-b31c451d-23ff-46b7-8aa0-0ba516956d9d.txt` | +| 反向迁移计划(port→dev,编号 #1–#8,编号体系与本计划无关) | `docs/migration-plan-dev-26.1.md` | +| 分支对比报告(两分支整体关系与分类) | `docs/branch-comparison.md` | +| dev 侧关键文件实测基准 | `module.rpc/`、`module.sync/`、`module.explosion/`、`module.collision/`、`module.space-select/`、`module.font/`、`module.rendering/`、`renderdoc-loader/`(均以 `0b7e45f` 为准) | +| port 侧现状基准 | port/1.21.1 @ `bd25229`;`module.config`(ConfigManager/Config/Comment/ConfigData)、`module.network`(NetworkRegistrar/IPacket/IServerboundPacket/NetworkUtil)、`module.util`(ShapeUtil/UnlimitedItemStack)、`module.wheel`(LibShaders/ring.json/selection.json)、`module.multiblock`(AnvilLibDatagen) | +| NeoForge 21.1.226 sources jar(事件/网络/配置 API 核实依据) | 本地 gradle 缓存(`net.neoforged:neoforge:21.1.226-sources.jar`)与 `module.config/build/moddev/artifacts/` 下 patched 源码 | +| 1.21.1 混淆 jar / official mappings(javap 核实依据) | `neoformruntime/artifacts/minecraft_1.21.1_client.jar`、`minecraft_1.21.1_client_mappings.txt` | + +### D. 回移植提交清单(PR 提交模板) + +> 每个回移植项在 port/1.21.1 分支上对应一个独立提交。下表可直接作为 PR 描述模板,勾选「状态」列后随 PR 提交。 + +| 提交 | 回移植项 | 涉及文件(变更面) | 关键验证命令 | 状态 | +|---|---|---|---|---| +| commit 1 | #P1 rpc | `module.rpc/**`(25 文件)+ `settings.gradle` | `gradlew :module.rpc:compileJava`;`grep -rn "Identifier\|accessFlags" module.rpc/src`(0 匹配) | ☐ | +| commit 2 | #P2 sync | `module.sync/**`(31 文件)+ processor CoreMod 重写 + `settings.gradle` | `gradlew :module.sync:compileJava`;`grep -rn "neoforgespi.transformation" module.sync`(0 匹配) | ☐ | +| commit 3 | #P3 explosion | `module.explosion/**`(10 文件)+ `settings.gradle` | `gradlew :module.explosion:compileJava` + `runData` | ☐ | +| commit 4 | #P4 collision | `module.collision/**`(3 文件)+ `settings.gradle` | `gradlew :module.collision:compileJava` + 手动测试 | ☐ | +| commit 5 | #P5 space-select | `module.space-select/**`(18 文件)+ `settings.gradle` | `gradlew :module.space-select:compileJava` + runClient | ☐ | +| commit 6 | #P7a rendering 纯逻辑 | `module.rendering/**`(19 项纯逻辑 + 骨架)+ `settings.gradle` | `gradlew :module.rendering:compileJava` + `:module.rendering:test` | ☐ | +| commit 7 | #P6 font | `module.font/**`(25 文件)+ `settings.gradle` | `gradlew :module.font:compileJava` + `runData` + runClient | ☐ | +| commit 8 | #P7b–#P7e rendering 执行层 | `module.rendering/**`(后处理/SDF GUI/CachedBER/Compute 四批) | 各批 `compileJava` + 运行验证(7.2 清单) | ☐ | +| commit 9 | #P8 renderdoc-loader | `renderdoc-loader/**`(2 文件) | `gradlew :renderdoc-loader:compileJava` + jar manifest 核对 | ☐ | +| commit 10 | #P9 config | `module.config/**`(Config/ConfigManager/ConfigRecord/ConfigData/FormattingUtil/TranslatableEnum) | `gradlew :module.config:compileJava` + `runData` | ☐ | +| commit 11 | #P10 integration | `module.integration/**`(3 文件) | `gradlew :module.integration:compileJava` + 行为验证 | ☐ | +| commit 12 | #P11 network | `module.network/**`(3 文件) | `gradlew :module.network:compileJava` | ☐ | +| commit 13 | #P12 util | `module.util/**`(4 处演进)+ `module.recipe/**`(4 个调用方) | `gradlew :module.util:compileJava` + `:module.recipe:compileJava` + `runShapeUtilJoinTimingTest` | ☐ | +| commit 14 | #P13 registrum | `module.registrum/**`(builders/util.entry 新增 + AbstractRegistrum) | `gradlew :module.registrum:compileJava` | ☐ | +| commit 15 | #P16 recipe | `module.recipe/**`(SpawnItem/SetBlock) | `gradlew :module.recipe:compileJava` + 行为用例 | ☐ | +| commit 16 | #P15 multiblock | `module.multiblock/**`(M1–M4 + M7 注解) | `gradlew :module.multiblock:compileJava` + runServer | ☐ | +| commit 17 | #P17 wheel | `module.wheel/**`(WheelSelectionEffect + fsh + 渲染适配) | `gradlew :module.wheel:compileJava` + runClient | ☐ | +| commit 18 | #P14 registrum datagen | `module.registrum/**`(RecipeProvider/DataMapProvider/Builder) | `gradlew :module.registrum:compileJava` + `runData` | ☐ | +| commit 19 | #P18 test | `module.test/**`(T2/T8/T9/T10) | `gradlew :module.test:compileJava` + 运行验证 | ☐ | +| commit 20 | #P19 构建 | 根 `roseau.yaml` + `gradle/scripts/roseau.gradle`(+ 可选 module.gradle) | `gradlew roseauCheck`;`gradlew build` | ☐ | +| 收尾 | 全量回归 | 全仓 | `gradlew build`(根级)全绿;附录 B 待验证清单销账;与 `docs/migration-plan-dev-26.1.md` 无冲突 | ☐ | diff --git a/docs/minecraft-neoforge-version-differences.md b/docs/minecraft-neoforge-version-differences.md new file mode 100644 index 00000000..8dece3cc --- /dev/null +++ b/docs/minecraft-neoforge-version-differences.md @@ -0,0 +1,1140 @@ +# Minecraft / NeoForge 版本差异适配文档:1.21.1 → 26.1 + +> 文档信息 +> +> - 生成日期:2026-08-10 +> - 分析对象:`port/1.21.1` @ `fab551b`(Minecraft 1.21.1 / NeoForge 21.1.226)↔ `dev/26.1` @ `0b7e45f`(Minecraft 26.1.2 / + NeoForge 26.1.2.76),merge-base `81c5a9c` +> - 数据来源:`git diff port/1.21.1 dev/26.1` 逐模块 diff 实测 + dev/26.1 侧源码抽查 + NeoForge 26.1.2.76 sources jar 实测( + `net.neoforged.neoforge.transfer.*`、`net.neoforged.neoforge.fluids.capability.*`、 + `net.neoforged.neoforge.capabilities.*`) +> - 关联文档:本文是 **版本差异适配文档**,专注「差异中属于 Minecraft/NeoForge API 版本差异的必要适配」;两侧分支关系与回迁项判定见 + `docs/branch-comparison.md` +> - 结论性质:除标注「待验证」处外,均基于实测提交、文件内容与 26.1.2.76 sources jar 源码得出 + +--- + +## 1. 概述与结论 + +AnvilLib 的 `dev/26.1` 分支沿 1.21.2 → 1.21.11 → 26.1 的版本链逐级升级(`6f2685a` 正式切入 26.1),而 `port/1.21.1` 冻结在 +1.21.1 平台。对共享模块执行 `git diff port/1.21.1 dev/26.1` 后,把差异中「Minecraft/NeoForge API 版本差异」的部分单独抽出,即得到本文整理的 +13 个差异面。这些差异是 **单向必要适配**:dev/26.1 侧已是新 API 形态,任何从 port/1.21.1 回迁的代码都必须按此清单改写,否则无法在 +26.1 编译。 + +### 1.1 差异面总览 + +| # | 差异面 | 一句话概括 | +|----|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 2 | 标识符与基础类型 | `ResourceLocation` → `Identifier`(含 CODEC/STREAM_CODEC、buffer 读写方法),`ResourceKey.location()` → `identifier()`,官方包 `critereon` → `criterion` | +| 3 | 物品栈体系 | 可变 `ItemStack` 让位给不可变 `ItemStackTemplate`(`typeHolder()`/`withCount(n).create()`/`components()`),`Inventory` 公开字段移除 | +| 4 | 谓词体系 | `ItemSubPredicate`/`DataComponentPredicate` 谓词参数让位给 `net.minecraft.advancements.criterion.DataComponentMatchers`,`matches(ItemStack)` → `matches(DataComponentGetter)` | +| 5 | 配方体系 | `RecipeSerializer` 变 final 类(`new RecipeSerializer<>(CODEC, STREAM_CODEC)`),`RecipeManager` 内部改 `RecipeMap`,`save` 改收 `ResourceKey`,`recipeAccess()` 取代 `getRecipeManager()` | +| 6 | 网络 | `PayloadRegistrar` 双向注册 3 参 → 4 参(client/server handler 拆分),`FMLLoader.getCurrent()` 实例化访问,`IPacket.type()` 返回 `Identifier` | +| 7 | 注册表与延迟注册 | `Registry.get` → `getOptional`/`getValue`,tag 解析需 `HolderGetter`,`getTag` → `getTagOrEmpty` | +| 8 | 渲染管线 | `extractRenderState`/`submit` 两段式 RenderState 架构,`GuiGraphics` → `GuiGraphicsExtractor`,`PoseStack` → `Matrix3x2fStack`,`RenderPipeline`+UBO 取代 `ShaderInstance`/shader json | +| 9 | 事件与加载器 | `FMLLoader.getDist()` → `getCurrent().getDist()`,`GatherDataEvent` 按端拆分,`BreakBlockEvent`,`KeyEvent`/`MouseButtonEvent` 输入事件 | +| 10 | NBT 与持久化 | `CompoundTag`+`HolderLookup` → `ValueInput`/`ValueOutput`,`SavedData.Factory` → `SavedDataType`+Codec | +| 11 | 能力(capability) | 物品 `IItemHandler` → `ResourceHandler`(无 `setResource`),流体 `IFluidHandler` 弃用 → `ResourceHandler`,`BlockCapability.createSided(Identifier, ...)` | +| 12 | 注解体系 | javax/neoforge 空注解 → jspecify `@NullMarked` + `org.jspecify.annotations.Nullable` | +| 13 | 数据生成与构建 | `REGISTRUM.addDataGenerator` 取代 `GatherDataEvent` 注册,根级 `module.gradle` 聚合,坐标 `-neoforge-26.1`,`anvillib.needRunConfig` | + +### 1.2 回迁代码的共性适配清单 + +以下适配项与具体业务无关, **任何从 port/1.21.1 回迁的代码几乎必然触及**,是回迁前的统一预处理清单: + +1. `ResourceLocation` → `Identifier`:import 与全部用法(含 `ResourceLocation.CODEC/STREAM_CODEC`、`fromNamespaceAndPath`、 + `writeResourceLocation/readResourceLocation`)。 +2. 空注解体系切换:`@MethodsReturnNonnullByDefault`/`@ParametersAreNonnullByDefault`/`javax.annotation.Nullable` → 包级 + `@NullMarked` + `org.jspecify.annotations.Nullable`。 +3. `FMLLoader`/`LoadingModList` 静态访问 → `FMLLoader.getCurrent()` 实例访问(`getDist()`、`getLoadingModList()`)。 +4. 构建文件不搬运:port 侧各模块独立 `build.gradle`(约 270 行)不回迁,按 dev 侧 `module.gradle` 聚合体系(依赖坐标 + `-neoforge-26.1`)落位。 +5. `@Mod` 构造器去掉 `ModContainer` 参数(`(IEventBus bus, ModContainer container)` → `(IEventBus modBus)`)。 +6. 渲染相关代码(若涉及 GUI/方块实体渲染):`render(GuiGraphics, ...)` → `extractRenderState(GuiGraphicsExtractor, ...)`、 + `PoseStack` → `Matrix3x2fStack`、shader 注册改 `RenderPipeline`。 + +### 1.3 判定规则与依据 + +- 本文只收录「属于 Minecraft/NeoForge API 版本差异」的部分(dev 侧已是新 API 的必要适配),不收录 dev + 侧业务演进(事件系统、构建重构、新模块等),也不收录 port 侧独有 bugfix 回迁(见 `docs/branch-comparison.md` 的 #1–#8)。 +- 每个条目给出 dev/26.1 侧真实文件路径,可通过 `git show dev/26.1:<路径>` 复核。 +- 涉及 26.1.2.76 官方 API 的表述,优先以本地 sources jar 源码为准。 + +--- + +## 2. 标识符与基础类型 + +### 2.1 `ResourceLocation` → `Identifier` + +**变化**:Minecraft 1.21.5+ 官方映射将 `net.minecraft.resources.ResourceLocation` 重命名为 +`net.minecraft.resources.Identifier`,是本仓库 diff 中出现频率最高的机械替换项,具体包括: + +| 旧(1.21.1) | 新(26.1) | +|------------------------------------------------------------|----------------------------------------------------| +| `net.minecraft.resources.ResourceLocation` | `net.minecraft.resources.Identifier` | +| `ResourceLocation.fromNamespaceAndPath(ns, path)` | `Identifier.fromNamespaceAndPath(ns, path)` | +| `ResourceLocation.CODEC` / `ResourceLocation.STREAM_CODEC` | `Identifier.CODEC` / `Identifier.STREAM_CODEC` | +| `buf.writeResourceLocation(RL)` / `readResourceLocation()` | `buf.writeIdentifier(id)` / `buf.readIdentifier()` | + +dev/26.1 侧实际形态(`InWorldRecipe.java` 的 `STREAM_CODEC`): + +```java +buf.writeIdentifier(Objects.requireNonNull(recipe.trigger(). + +getId())); +// ... +Identifier location = buf.readIdentifier(); +``` + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java` +- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/CodecUtil.java` +- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/StreamCodecUtil.java` +- `module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IPacket.java` +- `module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTest.java` + +**迁移注意**:这是纯机械替换,但注意 26.1 下 `Identifier` 不再接受字符串「直接构造」的旧写法(1.21.1 的 +`new ResourceLocation(String)` 已移除),统一走 `Identifier.fromNamespaceAndPath` / `Identifier.parse`。 + +### 2.2 `ResourceKey.location()` → `ResourceKey.identifier()` + +**变化**:`ResourceKey` 的资源位置访问方法随 1.21.5 改名:`key.location()` → `key.identifier()`。dev/26.1 侧实际形态( +`MultiblockFormPacket.java`): + +```java +IController controller = ControllerRecord.get(state.getBlock(), this.state.getDefinitionKey().identifier()); +``` + +**涉及文件**: + +- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/network/MultiblockFormPacket.java` +- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/network/MultiblockUnformPacket.java` +- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/DynamicMultiblockManager.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/mixin/RecipeMapMixin.java` + +**迁移注意**:`ResourceKey.create(Registries.X, Identifier)` 的构造方式不变,只有 `location()` 访问方法改名;grep 时以 +`\.location()` 结尾且接收方为 `ResourceKey` 的调用点为搜索目标。 + +### 2.3 官方包改名:`critereon` → `criterion` + +**变化**:`net.minecraft.advancements.critereon` 包整体更名为 `net.minecraft.advancements.criterion`(26.1 官方改名),影响该包下全部约 +20 个 predicate 类的 import(`module.codec/StreamCodecUtil.java` 全部 import 变更即为此事)。两个具体影响点: + +- `RecipeUnlockedTrigger`:`net.minecraft.advancements.critereon.RecipeUnlockedTrigger` → + `net.minecraft.advancements.criterion.RecipeUnlockedTrigger`(见 5.4)。 +- AT 文件目标描述符同步改名:`module.codec/src/main/resources/META-INF/accesstransformer.cfg` 中 + `net.minecraft.advancements.critereon.LocationPredicate$PositionPredicate` → + `net.minecraft.advancements.criterion.LocationPredicate$PositionPredicate`。 + +**涉及文件**: + +- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/StreamCodecUtil.java` +- `module.codec/src/main/resources/META-INF/accesstransformer.cfg` + +**迁移注意**:回迁代码中所有 `critereon` 包 import 必须改为 `criterion`,否则编译失败;此条与 4.1 的 +`DataComponentMatchers`(新包名下的新类型)配套出现。 + +--- + +## 3. 物品栈体系 + +### 3.1 `ItemStack` → 不可变 `ItemStackTemplate` + +**变化**:26.1 中用于「配方产出、缓存、序列化」等场景的物品栈主力形态从可变的 `ItemStack` 改为不可变的 `ItemStackTemplate` +,核心差异: + +| 旧(1.21.1) | 新(26.1) | +|------------------------------------------------|--------------------------------------------------------------------------------------------| +| `ItemStack stack = new ItemStack(item, count)` | `ItemStackTemplate`(record 式不可变,字段为 `typeHolder()` + `count()` + `components()`) | +| `stack.copy()` / `stack.setCount(n)` | `template.withCount(n).create()`(`create()` 产出 `ItemStack`) | +| `ItemStack.CODEC` / `STREAM_CODEC` | `ItemStackTemplate.CODEC` / `ItemStackTemplate.STREAM_CODEC` | +| `stack.getItem()` | `template.typeHolder().value()` | +| `stack.getComponents()` | `template.components()` | + +dev/26.1 侧实际形态(`InWorldRecipe.java`,icon 字段与序列化): + +```java +private final @Unmodifiable ItemStackTemplate icon; + +// ... +public ItemStack getResultItem() { + return this.icon.create(); +} +// ... +ItemStackTemplate.CODEC. + +fieldOf("icon"). + +orElseGet(() ->new + +ItemStackTemplate(Items.ANVIL)). + +forGetter(InWorldRecipe::icon), +// ... +ItemStackTemplate.STREAM_CODEC. + +encode(buf, recipe.icon()); +``` + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/SpawnItem.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java` +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ChanceItemStack.java` +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/stack/UnlimitedItemStack.java`(旧版, + `@Deprecated(forRemoval = true)`) + +**迁移注意**:模板化后「先改数量再使用」必须走 `withCount(n).create()` 链式调用;`create()` 每次产出新 `ItemStack` +,不要再假设可以原地修改共享模板。 + +### 3.2 `ItemStack.ITEM_NON_AIR_CODEC` → `Item.CODEC` + +**变化**:26.1 移除了 `ItemStack.ITEM_NON_AIR_CODEC`,需要「只编解码物品类型、不带数量」的场景改用 `Item.CODEC`(非空校验)或 +`Item.CODEC_WITH_BOUND_COMPONENTS`(带组件)。dev/26.1 侧实际形态(`SpawnItem.java`,配合 `typeHolder()` 使用): + +```java +// 旧:ItemStack.ITEM_NON_AIR_CODEC → 新:Item.CODEC / Item.CODEC_WITH_BOUND_COMPONENTS(配合 typeHolder()) +``` + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/SpawnItem.java` +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ChanceItemStack.java` +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/stack/UnlimitedItemStack.java` + +**迁移注意**:`Item.CODEC` 与 `Item.CODEC_WITH_BOUND_COMPONENTS` 的语义差异在于组件是否参与编解码,按业务场景选择,不要一律替换为 +`Item.CODEC`。 + +### 3.3 `Inventory` 公开字段移除 + +**变化**:`net.minecraft.world.entity.player.Inventory` 的 `items`/`armor`/`offhand` 公开字段在 26.1 移除,遍历玩家背包改为走 +`getContainerSize()` + `getItem(i)` 循环。dev/26.1 侧 `InventoryUtil.getItems` 即按此改写。 + +**涉及文件**: + +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/InventoryUtil.java` + +**迁移注意**:若回迁代码直接引用 `inventory.items` 等字段,改写成按槽位循环;注意 `Inventory` 的槽位区间(主手 0–35、盔甲 +36–39、副手 40)在 26.1 未变。 + +### 3.4 相关:`LootParams` 第二参数改为 `ContextMap` + +**变化**:`LootParams(Level, Map)` 构造中第二参数在 26.1 改为 `ContextMap`: +`new LootParams(level, Map.of(), ...)` → `new LootParams(level, ContextMap.EMPTY, ...)`(或 +`new ContextMap.Builder().create(new ContextKeySet.Builder().build())`)。出现在 `ChanceBlockState`/ +`WeightedChanceBlockStates`/`ChanceItemStack`/`InWorldRecipeContext` 的 loot table 上下文构造中。 + +**涉及文件**: + +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ChanceBlockState.java` +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/WeightedChanceBlockStates.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipeContext.java` + +**迁移注意**:`ContextMap.EMPTY` 只在业务不依赖自定义参数时可用;需要向 loot context 传参的场景须改用 `ContextKeySet`/ +`ContextMap.Builder`。 + +--- + +## 4. 谓词体系 + +### 4.1 `ItemSubPredicate`/`DataComponentPredicate` → `DataComponentMatchers` + +**变化**:1.21.1 的「物品子谓词」体系在 26.1 中拆为两条线: + +1. **谓词参数类型**(`ItemPredicate` 构造时传入的组件匹配器):`ItemSubPredicate` 与早期 `DataComponentPredicate` 参数 → + `net.minecraft.advancements.criterion.DataComponentMatchers`(含 `CODEC.codec()`、`ANY` 常量、 + `Builder.components().exact(DataComponentExactPredicate.builder()...)` 链)。 +2. **谓词实现载体**:`net.minecraft.core.component.predicates.DataComponentPredicate` 保留并成为「可注册的组件谓词」接口,AnvilLib + 的 `AndPredicate`/`NotPredicate`/`OrPredicate` 直接实现它。 + +dev/26.1 侧实际形态: + +```java +// module.recipe/predicate/item/HasItem.java + +import net.minecraft.advancements.criterion.DataComponentMatchers; +// ItemPredicate 以 DataComponentMatchers 作为参数字段 +``` + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItem.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItemIngredient.java` +- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/StreamCodecUtil.java`(`ITEM_PREDICATE` 字段: + `DataComponentPredicate.STREAM_CODEC` → `DataComponentMatchers.STREAM_CODEC`) +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java` + +**迁移注意**:这是「类型归属」迁移——1.21.1 的 `DataComponentPredicate` import(`net.minecraft.advancements.critereon` 或旧 +`component` 包)需要按用途分流:匹配器参数走 `DataComponentMatchers`,谓词实现走 +`net.minecraft.core.component.predicates.DataComponentPredicate`,两者不可混用。 + +### 4.2 `matches(ItemStack)` → `matches(DataComponentGetter)` + +**变化**:组件谓词的匹配入口从「整个物品栈」收窄为「组件读取器」:`matches(ItemStack)` → `matches(DataComponentGetter)` +。dev/26.1 侧实际形态(`AndPredicate.java`): + +```java + +@Override +public boolean matches(DataComponentGetter getter) { + return this.subPredicates().stream().allMatch(it -> it.getValue().matches(getter)); +} +``` + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/advancement/predicate/item/AndPredicate.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/advancement/predicate/item/OrPredicate.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/advancement/predicate/item/NotPredicate.java` + +**迁移注意**:调用方需要「先取组件再匹配」——`stack.getComponents()`(`ItemStack` 实现了 `DataComponentGetter` +)传入;谓词内不再直接读取栈的数量/物品类型信息。 + +### 4.3 `ItemPredicate.withSubPredicate` 删除 + +**变化**:`ItemPredicate` 的 `subPredicates` 字段与 `Builder.withSubPredicate(...)` 在 26.1 一并删除(1.21.1 的 +`ItemSubPredicate` 无对应替代),dev 侧全部调用方已清理(grep 无残留)。`module.util` 的 `IItemStackPredicate` 同样删除了 +`subPredicates()`。 + +**涉及文件**: + +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java` +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/IItemStackPredicate.java` + +**迁移注意**:不要尝试保留 `withSubPredicate` 兼容层;如需组合谓词,走 `AndPredicate`/`OrPredicate`/`NotPredicate`(实现 +`DataComponentPredicate`)的组合方式。 + +### 4.4 注册表 `ITEM_SUB_PREDICATE_TYPE` → `DATA_COMPONENT_PREDICATE_TYPE` + +**变化**:1.21.5+ 引入 `DATA_COMPONENT_PREDICATE_TYPE` 注册表取代 `ITEM_SUB_PREDICATE_TYPE`;注册条目类型由 +`ItemSubPredicate.Type` 改为 `DataComponentPredicate.TypeBase`(匿名子类),codec/streamCodec 签名随之变化。dev/26.1 侧 +`LibItemSubPredicates.java` 已被 `LibDataComponentPredicates.java` 取代。 + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/init/LibDataComponentPredicates.java` + +**迁移注意**:1.21.1 侧 `LibItemSubPredicates` 不迁移;新注册走 `DATA_COMPONENT_PREDICATE_TYPE`,谓词类型自身实现 +`DataComponentPredicate`(见 4.2)。 + +--- + +## 5. 配方体系 + +### 5.1 `RecipeSerializer` 变 final 类,注册改 `new RecipeSerializer<>(CODEC, STREAM_CODEC)` + +**变化**:1.21.1 中可被继承实现 `Serializer` 的 `RecipeSerializer` 接口在 26.1 成为 final +类,自定义配方的序列化器不再「implements」,而是直接实例化:`new RecipeSerializer<>(CODEC, STREAM_CODEC)`。CODEC/STREAM_CODEC +从接口方法变为构造参数,须以 `public static final` 字段形式挂在自定义配方类上。dev/26.1 侧实际形态(`LibRecipeTypes.java`): + +```java +public static final DeferredHolder, RecipeSerializer> IN_WORLD_RECIPE_SERIALIZER = + RECIPE_SERIALIZERS.register( + "in_world_recipe", + () -> new RecipeSerializer<>(InWorldRecipe.Serializer.CODEC, InWorldRecipe.Serializer.STREAM_CODEC) + ); +``` + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/init/recipe/LibRecipeTypes.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java`(`Serializer` 内 `CODEC` 改为 + `public static final`) + +**迁移注意**:回迁自定义配方时把「序列化器类」改为「序列化器常量」,`RecipeType` 的注册方式( +`DeferredRegister>` + `new RecipeType<>() {}`)不变。 + +### 5.2 `RecipeManager` 内部结构 `RecipeMap` + +**变化**:1.21.2+ 起 `RecipeManager` 内部按 key 索引的 +`byName: Map>` 与 `byType: Multimap<...>` 重构为 `net.minecraft.world.item.crafting.RecipeMap`(按 `ResourceKey>` +索引),对 `RecipeManager` 的注入/访问逻辑迁移到 `RecipeMapMixin`。dev/26.1 侧实际形态(`RecipeMapMixin.java`): + +```java + +@Mixin(RecipeMap.class) +public class RecipeMapMixin implements IRecipeMapExtension { + @Mutable + @Final + @Shadow + private Map>, RecipeHolder> byKey; // 遍历 key 时经 key.identifier() 取 Identifier +} +``` + +同时 `RecipeHolder.id()` 返回 `ResourceKey`,取 `Identifier` 需 `.id().identifier()`(如 `InWorldRecipeManager` 中 +`recipe.id().identifier()`)。 + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/mixin/RecipeMapMixin.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/injection/IRecipeMapExtension.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/mixin/RecipeManagerMixin.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipeManager.java` + +**迁移注意**:注入目标从 `RecipeManager` 换到 `RecipeMap` 后,`@Shadow` 字段类型与索引 key 类型(`ResourceKey>` +而非 `Identifier`)必须同步;`anvillib$getRegistries` 在 dev 侧已 `@Deprecated`,新代码不要依赖。 + +### 5.3 `RecipeBuilder.save(RecipeOutput, ResourceKey>)` + +**变化**:26.1 中 `RecipeBuilder.save(...)` 与进度触发均从「资源位置」改为「配方 key」: + +| 旧(1.21.1) | 新(26.1) | +|-------------------------------------------------------|------------------------------------------------------------------------------| +| `builder.save(recipeOutput, ResourceLocation)` | `builder.save(recipeOutput, ResourceKey>)`,配套新增 `defaultId()` | +| `RecipeUnlockedTrigger.unlocked(ResourceLocation)` | `RecipeUnlockedTrigger.unlocked(ResourceKey>)` | +| `AdvancementRewards.Builder.recipe(ResourceLocation)` | `AdvancementRewards.Builder.recipe(ResourceKey>)` | + +dev/26.1 侧实际形态(`InWorldRecipeBuilder.java`): + +```java +public ResourceKey> defaultId() { ...} + +public void save(RecipeOutput recipeOutput, ResourceKey> key) { ...} +// 为兼容保留 save(RecipeOutput, Identifier) 重载 +``` + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java` + +**迁移注意**:dev 侧保留了 `save(RecipeOutput, Identifier)` 重载作兼容,回迁代码可沿用旧签名;但进度解锁路径必须传 +`ResourceKey`。 + +### 5.4 `RecipeUnlockedTrigger` 包移动 + +**变化**:`RecipeUnlockedTrigger` 随 `critereon` → `criterion` 官方包改名(见 2.3)移动: +`net.minecraft.advancements.critereon.RecipeUnlockedTrigger` → +`net.minecraft.advancements.criterion.RecipeUnlockedTrigger`。 + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java` + +**迁移注意**:纯 import 替换,无 API 形态变化;与 5.3 的 `unlocked(ResourceKey)` 参数变化配套出现。 + +### 5.5 `Level.getRecipeManager()` → `serverLevel.recipeAccess()` + +**变化**:26.1 中获取配方访问入口改为 `ServerLevel#recipeAccess()`(`RecipeAccess`),`Level.getRecipeManager()` +在配方相关访问路径中不再使用。dev/26.1 侧实际形态(`ItemEntityEventListener.java`): + +```java +InWorldRecipeManager manager = serverLevel.recipeAccess().anvillib$getInWorldRecipeManager(); +``` + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/event/listener/ItemEntityEventListener.java` + +**迁移注意**:仅在持有 `ServerLevel` 时可用;`ClientLevel` 侧没有 `recipeAccess()`,客户端若需访问配方走其他入口(datapack +同步后的客户端缓存)。另注意 `Recipe` 接口在 26.1 的签名变化:`assemble(RecipeInput, HolderLookup.Provider)` → +`assemble(RecipeInput)`;`canCraftInDimensions(...)` 等旧方法被 `showNotification()` / `group()` / `placementInfo()`( +`PlacementInfo` 字段,须显式提供)/ `recipeBookCategory()` 取代(`module.recipe/.../InWorldRecipe.java`);客户端 +`RecipesUpdatedEvent` 在 26.1 已移除(`module.recipe/.../event/listener/ResourceEventListener.java` 仅保留 +`ServerStartedEvent`/`OnDatapackSyncEvent` 监听)。 + +--- + +## 6. 网络 + +### 6.1 `PayloadRegistrar` 双向注册 3 参 → 4 参 + +**变化**:NeoForge 26.1 的 `PayloadRegistrar` 双向(BIDIRECTIONAL)注册方法从 3 参数变为 4 参数,client/server 侧 handler +拆开传入:`playBidirectional(type, codec, handler)` → `playBidirectional(type, codec, handler, handler)`(其余 +`configurationBidirectional`/`commonBidirectional` 同理)。dev/26.1 侧实际形态(`NetworkRegistrar.java`): + +```java +case BIDIRECTIONAL -> + registrar. + +playBidirectional(data.type(),data. + +streamCodec(),data. + +handler(),data. + +handler()); +``` + +**涉及文件**: + +- `module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/NetworkRegistrar.java` + +**迁移注意**:AnvilLib 的 `IPacket` 体系单 handler 即可满足双向语义,4 参写法是同一 handler 传两次;若回迁代码自定义双向包,需按 +client/server 分别提供。 + +### 6.2 `LoadingModList.get()` → `FMLLoader.getCurrent().getLoadingModList()` + +**变化**:`LoadingModList.get()` 静态访问在 26.1 不可用,改为 `FMLLoader.getCurrent()` 实例的 `getLoadingModList()` +。dev/26.1 侧实际形态(`NetworkRegistrar.java`,用于扫描 `@Network` 注解包): + +```java +IModFileInfo fileInfo = FMLLoader.getCurrent().getLoadingModList().getModFileById(modId); +``` + +**涉及文件**: + +- `module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/NetworkRegistrar.java` +- `module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java`(同款改造,另配套 + `@SuppressWarnings("UnstableApiUsage")`) + +**迁移注意**:`FMLLoader.getCurrent()` 返回的实例在加载期外仍可调用,但部分方法标注 `UnstableApiUsage`,需保留 dev 侧的抑制注解。 + +### 6.3 `IPacket.type()` 返回 `Identifier` + +**变化**:`CustomPacketPayload.Type` 的构造参数从 `ResourceLocation` 改为 `Identifier`(网络包类型标识随 2.1 +改名)。dev/26.1 侧实际形态(`IPacket.java`): + +```java +import net.minecraft.resources.Identifier; + +static Type type(Identifier id) { ...} +``` + +**涉及文件**: + +- `module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IPacket.java` + +**迁移注意**:`CustomPacketPayload` 接口在 26.1 本身要求 `type()` 返回 `Type`,随 2.1 机械替换即可,网络注册侧( +`NetworkRegistrar`)无需额外适配。 + +--- + +## 7. 注册表与延迟注册 + +### 7.1 `Registry.get(RL)` → `getOptional` / `getValue` + +**变化**:`Registry.get(ResourceLocation)` 在 26.1 改名/移除,dev 侧按使用场景二选一: + +| 旧(1.21.1) | 新(26.1) | 场景 | +|----------------------------------|----------------------------------------------------------------------------|---------------| +| `BuiltInRegistries.ITEM.get(id)` | `BuiltInRegistries.ITEM.getOptional(id).orElse(Items.AIR)` | 有默认值兜底 | +| `ENTITY_TYPE.get(id)` | `ENTITY_TYPE.getValue(id)`(或 `containsKey` 预检 + `getOptional` 双保险) | 直接取值/判空 | + +dev/26.1 侧实际形态(`InWorldRecipe.java` STREAM_CODEC 解码侧): + +```java +IRecipeTrigger trigger = Objects.requireNonNull(LibRegistries.TRIGGER_REGISTRY.getValue(buf.readIdentifier())); +``` + +**涉及文件**: + +- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/CodecUtil.java`(`ITEM`/`BLOCK`/`ENTITY` 字段) +- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/StreamCodecUtil.java`(`ENTITY` 字段) +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java`(`getValue`) + +**迁移注意**:`getOptional` 返回 `Optional`(26.1 移除 `get` 后无「返回 null」语义的旧入口),所有 `get(id)` 调用点必须按「兜底 +orElse / 显式判空」改写,不能只换方法名。 + +### 7.2 tag 解析需 `HolderGetter` + +**变化**:26.1 延迟注册体系要求 tag 解析持有 `HolderGetter`(不能静态访问 `BuiltInRegistries`),相关 `Builder.of(...)` +入口从「tag key」升级为「`HolderGetter` + tag key」: + +```java +// 旧:BlockStatePredicate.Builder.of(TagKey) +// 新(dev/26.1 BlockStatePredicate.java): +public Builder of(HolderGetter blocks, TagKey tag) { ...} +``` + +`module.recipe` 侧 `HasBlock`/`HasBlockIngredient`/`HasItem`/`HasItemIngredient` 的 builder 层 `hasItem`/`hasBlock` 系列 +12 个方法签名同步增加 `HolderGetter` 参数。 + +**涉及文件**: + +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasBlock.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItem.java` +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java` + +**迁移注意**:`HolderGetter` 来源一般是 `level.registryAccess()`(服务器/数据包环境)或 `registry` 自身;datapack 环境不能再走 +`BuiltInRegistries` 静态路径。 + +### 7.3 `Registry.getTag` → `getTagOrEmpty` + +**变化**:`Registry.getTag(TagKey)`(返回 `Optional`)在 26.1 改名/移除,dev 侧改用 `getTagOrEmpty(TagKey)`(返回 `HolderSet` +可空语义)。`ItemCache.java` 的 tag 命中判断与 `Entity.getType().is(TagKey)` → `Entity.is(TagKey)` 同批适配。 + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/ItemCache.java` + +**迁移注意**:`getTagOrEmpty` 不再返回 `Optional`,判空逻辑从 `if (opt.isPresent())` 改为对 `HolderSet` 本身判空。 + +--- + +## 8. 渲染管线 + +### 8.1 RenderState 架构:`extractRenderState` / `submit` 两段式 + +**变化**:1.21.2+ 引入的 RenderState 架构把「渲染器每帧提交」拆为两段:`extractRenderState(...)` +在帧开始阶段从实体/方块实体提取渲染状态(不可变快照), +`submit(RenderState, PoseStack, SubmitNodeCollector, CameraRenderState)` 在渲染提交阶段消费快照。旧 `render(...)` +注入点全部改写。dev/26.1 侧实际形态(`PistonHeadRendererMixin.java`): + +```java +// 注入 extractRenderState(在 createMovingBlock 调用点,预生成内部方块实体的 RenderState) +private void extractRenderState( + PistonMovingBlockEntity blockEntity, PistonHeadRenderState state, + float partialTicks, Vec3 cameraPosition, ModelFeatureRenderer$CrumblingOverlay breakProgress +) { ...} +// 注入 submit(TAIL,负责平移并提交渲染):@Inject(method = "submit(", at = @At("TAIL")) +``` + +新类型:`net.minecraft.client.renderer.blockentity.state.PistonHeadRenderState`、`BlockEntityRenderState`、 +`MovingBlockRenderState`、`SubmitNodeCollector`、`state.level.CameraRenderState`、 +`feature.ModelFeatureRenderer$CrumblingOverlay`。位移数据从 `blockEntity.getXOff(partialTick)` 改为 +`state.xOffset/yOffset/zOffset`。 + +**涉及文件**: + +- `module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/mixin/PistonHeadRendererMixin.java` +- +`module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/injection/IPistonHeadRenderStateExtension.java` +(扩展 `PistonHeadRenderState`,`anvillib$setExtraState`/`anvillib$getExtraState`) +- `module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/mixin/PistonHeadRenderStateMixin.java` +- `module.moveable-entity-block/src/main/resources/interface_injections.json` + +**迁移注意**:port 侧 `1acdcf8` 的「PoseStack 提前 return 未清空」bug 在新架构下天然规避(`submit` 中 `return` 位于 +`pushPose()` 之前、`popPose()` 无条件执行);回迁 mixin 时 `@Local` 变量名也随反混淆名变化(如 `blockpos3` → `pos`、 +`blockstate` → `newState`)。 + +### 8.2 `BlockEntityRenderer` 泛型化为 `` + +**变化**:`BlockEntityRenderer` → `BlockEntityRenderer`, +`getBlockEntityRenderDispatcher().getRenderer()` 相应提供双泛型重载。dev/26.1 侧实际形态: + +```java +abstract class PistonHeadRendererMixin implements BlockEntityRenderer { + // renderer.extractRenderState(blockEntity1, renderState, partialTicks, cameraPosition, breakProgress) +} +``` + +**涉及文件**: + +- `module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/mixin/PistonHeadRendererMixin.java` + +**迁移注意**:`extractRenderState` 需要在 RenderState 已创建后调用(dev 侧在 `createMovingBlock` 调用点注入保证时序),不要自行 +new RenderState。 + +### 8.3 `GuiGraphics` → `GuiGraphicsExtractor` + +**变化**:GUI 渲染提取阶段的操作入口从 `GuiGraphics` 改为 `GuiGraphicsExtractor`(`net.minecraft.client.gui`),文本绘制 +`drawCenteredString(...)` → `centeredText(...)`。dev/26.1 侧实际形态: + +```java +// WheelScreen.java:guiGraphics.centeredText(font, pageInfo, width / 2, height - 22, 0xFFFFFFFF) +``` + +**涉及文件**: + +- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java` +- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java` +- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelEntryRenderer.java` + +**迁移注意**:`GuiGraphicsExtractor` 只能收集「绘制描述」不直接操作 GL;跨层传递时携带 `peekScissorStack()`(dev 侧 +`submitGuiElementRenderState(...)` 的配套调用)。 + +### 8.4 `PoseStack` → `Matrix3x2fStack` + +**变化**:GUI/2D 渲染的矩阵栈在 26.1 改为 `org.joml.Matrix3x2fStack`( **无 z 分量**):`pushPose/popPose` → +`pushMatrix/popMatrix`,`translate/scale` 调用去掉 z 参数。dev/26.1 侧 `WheelWidget.java` 全文按此改写(SDF +图形、环形选择特效的绘制路径)。 + +**涉及文件**: + +- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java` + +**迁移注意**:带 z 的 3D 变换(如方块实体渲染)仍用 `PoseStack`,只有 GUI 2D 路径换 `Matrix3x2fStack`;回迁时按调用上下文区分,不要全局替换。 + +### 8.5 `RenderPipeline` + UBO + `DynamicUniformStorage` 取代 `ShaderInstance`/`RegisterShadersEvent`/shader json + +**变化**:26.1 移除 `ShaderInstance` 加载与 `RegisterShadersEvent`,改由 `RenderPipeline` 声明式管线 + uniform block(UBO)+ +`DynamicUniformStorage` 管理 shader 参数: + +| 旧(1.21.1) | 新(26.1) | +|--------------------------------------------------------------|--------------------------------------------------------------------------------------------| +| `RegisterShadersEvent` + `ShaderInstance` + `safeGetUniform` | `com.mojang.blaze3d.pipeline.RenderPipeline.builder()` 声明管线 | +| shader json(`assets/.../shaders/core/*.json`) | 管线注册资源位置(`withLocation(...)`),shader 源文件保留 `.fsh`/`.vsh` | +| `RenderSystem.setShader(...)` | `GuiGraphicsExtractor.submitGuiElementRenderState(...)` 提交渲染状态 | +| 顶点 uniform 逐帧 set | `UniformType.UNIFORM_BUFFER`(`layout(std140)` uniform block)+ `DynamicUniformStorage` | + +dev/26.1 侧实际形态(`LibRenders.java` + `LibDynamicUniforms.java`): + +```java +public static final RenderPipeline RING_PIPELINE = RenderPipeline.builder(SNIPPET_COMMON) + .withLocation(AnvilLibWheel.of("pipeline/ring")) + .withVertexShader("core/position_color") + .withFragmentShader(AnvilLibWheel.of("core/ring")) + .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, VertexFormat.Mode.QUADS) + .withUniform("RingUniform", UniformType.UNIFORM_BUFFER) + .build(); + +public record RingUniform(Vector2fc center, float innerDiameter, float outerDiameter, float antiAliasingRadius) + implements DynamicUniformStorage.DynamicUniform { ... +} + +private final DynamicUniformStorage ringUbo = new DynamicUniformStorage<>(...); +``` + +配套的 `Std140Builder`/`Std140SizeCalculator`(`com.mojang.blaze3d.buffers`)负责 UBO 内存布局; +`ConfigureMainRenderTargetEvent` 初始化 UBO、`RenderGuiEvent.Post` 的 `endFrame` 重置 UBO。 + +**涉及文件**: + +- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/LibRenders.java` +- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/LibDynamicUniforms.java` +- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java` +- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/AnvilLibWheel.java` + +**迁移注意**:旧 `LibShaders.java` 与 shader json 不迁移(dev 侧已删除);自定义 shader 需按 `std140` 布局改写 uniform +声明并登记到 `DynamicUniformStorage`。 + +### 8.6 `AbstractWidget.render` → `extractRenderState` + +**变化**:`AbstractWidget.render(GuiGraphics, int, int, float)` → +`extractRenderState(GuiGraphicsExtractor, int, int, float)`,`renderWidget` → `extractWidgetRenderState`;`Screen.render` +同步拆为 `extractRenderState` + 新增 `extractBackground` 覆写。AT 文件目标方法名同步改名。dev/26.1 侧实际形态( +`WheelWidget.java` + `WheelScreen.java`): + +```java +// WheelWidget.java:511 +public void extractRenderState(GuiGraphicsExtractor guiGraphics, int mouseX, int mouseY, float partialTick) { + // ... + this.extractWidgetRenderState(guiGraphics, mouseX, mouseY, partialTick); +} + +// WheelScreen.java:91 +public void extractBackground(GuiGraphicsExtractor guiGraphics, int mouseX, int mouseY, float partialTick) { ...} +``` + +**涉及文件**: + +- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java` +- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java` +- `module.wheel/src/main/resources/META-INF/accesstransformer.cfg`( + `public-f ... extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V`) + +**迁移注意**:自定义控件回迁时覆写方法名与 AT 目标必须同步改为 `extractRenderState`;绘制逻辑放在提取阶段,GL 提交由 +`submit` 阶段(控件内部或 `submitGuiElementRenderState`)完成。 + +--- + +## 9. 事件与加载器 + +### 9.1 `FMLLoader.getDist()` → `FMLLoader.getCurrent().getDist()` + +**变化**:`FMLLoader.getDist()` 静态方法在 26.1 移除,统一走 `FMLLoader.getCurrent().getDist()`;`FMLEnvironment.dist` +字段同样改为 `FMLEnvironment.getDist()`。dev/26.1 侧实际形态(`ConfigManager.java`): + +```java +if(FMLLoader.getCurrent(). + +getDist(). + +isClient())manager. + +registerScreen(container); +``` + +**涉及文件**: + +- `module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigManager.java` +- `module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java` +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/DistExecutor.java` +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/Util.java` + +**迁移注意**:除加载器静态访问外,dev 侧还删除了部分 `@OnlyIn(Dist.CLIENT)` 标注(`ConfigManager`),以 `getDist()` +运行时判断替代;回迁时不要引入新的 `@OnlyIn` 依赖客户端侧代码。`ModList.get().getModContainerById(...)` 在 dev 侧改为 +`Optional.of(ModList.get()).flatMap(...)` 链式规避空值(`module.config/.../ConfigManager.java`)。 + +### 9.2 `GatherDataEvent` → `GatherDataEvent.Client` / `GatherDataEvent.Server` + +**变化**:NeoForge 26.1 把 `GatherDataEvent` 按物理端拆分为 `GatherDataEvent.Client` 与 `GatherDataEvent.Server`, +`@SubscribeEvent` 方法签名对应拆分。dev/26.1 侧实际形态(`module.recipe` 的 `AnvilLibDatagen.java`): + +```java + +@SubscribeEvent +public static void gatherData(GatherDataEvent.Client event) { ...} + +@SubscribeEvent +public static void gatherData(GatherDataEvent.Server event) { ...} +``` + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/gen/AnvilLibDatagen.java` +- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/data/AnvilLibDatagen.java`(仅 + `GatherDataEvent.Client`,lang provider 属于客户端) + +**迁移注意**:只有服务端数据的 provider(如配方、loot table)挂 `Server`,语言/模型等挂 `Client`;注意这是「事件签名拆分」,与 +13.1 的 `REGISTRUM.addDataGenerator` 是两条可并存的注册通道。 + +### 9.3 `BlockEvent.BreakEvent` → `BreakBlockEvent` + +**变化**:NeoForge 26.1 方块事件重构:`net.neoforged.neoforge.event.level.BlockEvent.BreakEvent` → +`net.neoforged.neoforge.event.level.block.BreakBlockEvent`(独立顶级类)。dev/26.1 侧实际形态(`BlockEventListener.java`): + +```java +import net.neoforged.neoforge.event.level.block.BreakBlockEvent; +``` + +**涉及文件**: + +- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/event/BlockEventListener.java` + +**迁移注意**:import 与 `@SubscribeEvent` 参数类型同步替换;事件字段(`getPlayer()`/`getPos()` 等)在 26.1 未变。 + +### 9.4 输入事件 `KeyEvent` / `MouseButtonEvent` + +**变化**:26.1 客户端输入事件从「原语参数」改为事件对象:`InputEvent.Key.matches(int, int)` → `matches(KeyEvent)`( +`event.getKeyEvent()` 取原始键事件);`Screen.keyPressed(int, int, int)` → `keyPressed(KeyEvent)`; +`mouseClicked(int, int, int)` → `mouseClicked(MouseButtonEvent, boolean isDoubleClick)`。dev/26.1 侧实际形态( +`WheelTestClientHandler.java` + `WheelScreen.java`): + +```java +WheelTestKeys.HOLD_KEY.matches(event.getKeyEvent()); +// Screen:keyPressed(KeyEvent event) / mouseClicked(MouseButtonEvent event, boolean isDoubleClick) +``` + +`KeyMapping` 的 category 参数也从 `String` 改为 `new KeyMapping.Category(Identifier)`。 + +**涉及文件**: + +- `module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelTestClientHandler.java` +- `module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelTestKeys.java` +- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java` + +**迁移注意**:`KeyEvent`/`MouseButtonEvent` 均为 `net.minecraft.client.input` 包新类型;`InputEvent.Key` 的旧 +`matches(int,int)` 重载已移除,按键绑定判断统一传事件对象。 + +--- + +## 10. NBT 与持久化 + +### 10.1 `CompoundTag` + `HolderLookup` → `ValueInput` / `ValueOutput` + +**变化**:26.1 实体/方块实体的 NBT 读写入口从「`CompoundTag` + `HolderLookup.Provider`」改为流式读写器 `ValueInput`/ +`ValueOutput`(`net.minecraft.world.level.storage`): + +| 旧(1.21.1) | 新(26.1) | +|--------------------------------------------------------------------------------------|------------------------------------------------------| +| `loadAdditional(CompoundTag, HolderLookup.Provider)` | `loadAdditional(ValueInput)` | +| `saveAdditional(CompoundTag, HolderLookup.Provider)` | `saveAdditional(ValueOutput)` | +| `Codec.decode(registries.createSerializationContext(NbtOps.INSTANCE), tag)` + `Pair` | `valueInput.read("id", codec)`(返回 `Optional`) | +| `input.contains(name)` / `getCompound(name)` | `input.child(name)`(返回 `Optional`) | +| `tag.getInt(name)` | `valueInput.getIntOr(name, def)` | +| `blockEntity.loadWithComponents(tag, registries)` | `blockEntity.loadWithComponents(valueInput)`(单参) | + +dev/26.1 侧实际形态(`PistonMovingBlockEntityMixin.java`): + +```java +private void loadAdditional(ValueInput input, CallbackInfo ci) { + Optional child = input.child(anvillib$MOVEABLE_BLOCK_ENTITY); + if (child.isEmpty()) return; + Optional> entityType = child.get().read("id", anvillib$TYPE_CODEC); + if (entityType.isEmpty()) return; + // 坐标经 getIntOr("x"/"y"/"z", 0) 读取,随后 create 内部方块实体并 loadWithComponents + this.anvillib$blockEntity.loadWithComponents(child.get()); +} + +private void saveAdditional(ValueOutput output, CallbackInfo ci) { + ValueOutput child = output.child(anvillib$MOVEABLE_BLOCK_ENTITY); + this.anvillib$blockEntity.saveWithFullMetadata(child); +} +``` + +**涉及文件**: + +- `module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/mixin/PistonMovingBlockEntityMixin.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/BlockCache.java`( + `TagValueInput.create(ProblemReporter.ScopedCollector, registryAccess, nbt)` + `entity.problemPath()`) +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/NbtPredicate.java`( + `entity.saveWithoutId(TagValueOutput)`) + +**迁移注意**:`ValueInput`/`ValueOutput` 的 `child()` 返回 `Optional`,读写路径必须显式判空; +`ProblemReporter.ScopedCollector` 用于 `ValueInput` 构造时的错误收集(`entity.problemPath()` 取路径),回迁 NBT 代码时不要遗漏。 + +### 10.2 `SavedData.Factory` → `SavedDataType` + Codec + +**变化**:26.1 的 `SavedData` 注册从「`SavedData.Factory` + `computeIfAbsent(name, Factory)` + 手写 +`load/save(CompoundTag, HolderLookup)`」改为「`SavedDataType` + Codec 序列化」。dev/26.1 侧实际形态( +`DynamicMultiblockManager.java`): + +```java +public static final Codec CODEC = RecordCodecBuilder.create(inst -> inst.group( + MultiblockState.CODEC.codec() // ... +).apply(inst, DynamicMultiblockManager::new)); +public static final SavedDataType TYPE = new SavedDataType<>(/* key / typeToken / CODEC / 默认工厂 */); +``` + +**涉及文件**: + +- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/DynamicMultiblockManager.java` +- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockState.java`(`CODEC`、 + `DEFINITION_KEY_CODEC`) + +**迁移注意**:`MultiblockState` 的 `CODEC`/`STREAM_CODEC` 同时服务持久化与网络(`ResourceKey.streamCodec(...)` 只传 key,收端 +`getDefinition(level.registryAccess())` 延迟解析),回迁时注意「持久化 Codec 与网络 StreamCodec 分离」的 26.1 写法。 + +--- + +## 11. 能力(capability) + +### 11.1 物品 `IItemHandler` → `ResourceHandler` + +**变化**:NeoForge 26.1 用统一资源处理器体系取代物品能力接口:`net.neoforged.neoforge.items.IItemHandler` → +`net.neoforged.neoforge.transfer.ResourceHandler`(物品场景 T = `net.neoforged.neoforge.transfer.item.ItemResource` +)。方法对照: + +| 旧 `IItemHandler` | 新 `ResourceHandler` | +|---------------------------------------|---------------------------------------------------------------------| +| `getSlots()` | `size()` | +| `getStackInSlot(int)` | `getResource(int)` | +| `insertItem(int, ItemStack, boolean)` | `insert(int, T, int amount, TransactionContext)`(返回插入数量) | +| `extractItem(int, int, boolean)` | `extract(int, T, int amount, TransactionContext)`(返回提取数量) | +| `setStackInSlot` / `setResource` | **不存在**,替换语义用 Transaction 内 `extract` + `insert` 组合实现 | + +dev/26.1 侧实际形态(`ItemResourceHandlerCache.java`,26.1.2.76 sources jar 中 `ResourceHandler` 源码确认无 +`setResource`): + +```java +ResourceHandler getInput(); + +ResourceHandler getOutput(); +// 写路径:Transaction.openRoot() 内 extract + insert +``` + +`IItemHandlerCache`/`ItemHandlerCacheElement` 同步重命名为 `ItemResourceHandlerCache`/`ItemResourceHandlerCacheElement` +;实体/方块实体的物品访问改为 `instanceof Container` + `VanillaContainerWrapper.of(...)` 包装。 + +**涉及文件**: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/ItemResourceHandlerCache.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/ItemResourceHandlerCacheElement.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/ItemCache.java` + +**迁移注意**:`insert`/`extract` 返回「实际数量」而非栈对象,且必须在 `Transaction` 内执行(`Transaction.openRoot()` +或外层事务上下文);「替换槽位内容」一律改写为事务内先 `extract` 后 `insert`(`ResourceHandler` 无 `setResource`,见 +`docs/branch-comparison.md` #3 的适配说明)。 + +### 11.2 流体能力:`IFluidHandler` 弃用 → `ResourceHandler` + +**变化**(基于 NeoForge 26.1.2.76 sources jar 实测): + +1. 旧 API 仍存在但已弃用:`net.neoforged.neoforge.fluids.capability.IFluidHandler` 与 + `net.neoforged.neoforge.fluids.FluidStack` 可编译,但 `IFluidHandler` 标注 + `@Deprecated(since = "1.21.9", forRemoval = true)`(`getTanks`/`getFluidInTank`/`fill`/`drain` 均带弃用标注),并提供迁移包装器 + `IFluidHandler.of(ResourceHandler)`。 +2. 新 API:`net.neoforged.neoforge.transfer.ResourceHandler`,方法为 `getResource`/`extract`/`insert`/ + `isValid`/`getCapacityAsInt`/`size`, **无 `setResource`**;`FluidResource`( + `net.neoforged.neoforge.transfer.fluid.FluidResource`)为不可变类,实现 `DataComponentHolderResource`,静态字段 + `CODEC`/`OPTIONAL_CODEC`/`STREAM_CODEC`/`EMPTY`。 +3. 配套类:`FluidUtil`、`BucketResourceHandler`、`CauldronWrapper`、`FluidStacksResourceHandler` 等(`transfer/fluid` 包)。 + +```java +// 新 API 形态(26.1.2.76 sources jar) +public final class FluidResource implements DataComponentHolderResource { + public static final FluidResource EMPTY = ...; + public static final Codec CODEC = ...; + public static final Codec OPTIONAL_CODEC = ...; + public static final StreamCodec STREAM_CODEC = ...; +} +``` + +| 迁移选项 | 做法 | 代价 | 建议 | +|--------------|----------------------------------------------------------------------|---------------------------------------------|----------| +| A 最小移植 | 继续使用已弃用的 `IFluidHandler`/`FluidStack` | 可编译但有弃用告警;`forRemoval` 后必须再迁 | 仅兜底 | +| B 适配新 API | 改用 `ResourceHandler`,Transaction 内 insert/extract | 与 11.1 物品能力迁移方向一致,一步到位 | **推荐** | + +**涉及文件**:`module.yukkuri`(port 侧待迁模块,dev/26.1 无此模块);参考 dev 侧物品能力形态: + +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/ItemResourceHandlerCache.java` +- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/ItemResourceHandlerCacheElement.java` + +**迁移注意**:流体槽位语义与物品一致(`size()`/`getResource(int)`);桶灌装/倾倒等操作走 `FluidUtil`/`BucketResourceHandler` +而不是手写 `fill`/`drain` 兼容层。 + +### 11.3 `BlockCapability.createSided` 改用 `Identifier` + +**变化**:`BlockCapability.createSided(ResourceLocation, Class)` → `createSided(Identifier, Class)`(26.1.2.76 +sources jar 实测签名 +`public static BlockCapability createSided(Identifier name, Class typeClass)`),仅标识符类型随 +2.1 改名,方法本体与 `BlockCapabilityCache` 等配套不变。 + +**涉及文件**:capability 定义侧为 port `module.yukkuri`(待迁);dev/26.1 侧无 `createSided` 调用点(物品能力已走 +`ResourceHandler`)。 + +**迁移注意**:capability 对象定义处替换 `Identifier` 即可;NeoForge 内置 capability 常量在 26.1 仍可用,但类型已整体迁移—— +`Capabilities.ItemHandler.BLOCK` 现在声明为 `BlockCapability, @Nullable Direction>`( +`Capabilities.Fluid.BLOCK` 同理为 `ResourceHandler`,26.1.2.76 sources jar 实测),查询方类型必须跟随。 + +### 11.4 `Level.getCapability` 签名核对 + +**变化**:26.1 下 `Level#getCapability(BlockCapability, BlockPos, Object)` 签名保持不变(26.1.2.76 sources jar +`BlockCapability.java` javadoc 明文 `level.getCapability(Capabilities.ItemHandler.BLOCK, pos, side)`),因此 1.21.1 的 +`level.getCapability(cap, pos, direction)` 调用形态无需改动,需要核对的是 capability 对象本身的类型参数与创建方式(见 +11.3)。 + +**涉及文件**:`module.yukkuri`(待迁,port 侧流体能力查询点)。 + +**迁移注意**:回迁时逐个核对 capability 查询点的泛型参数是否已换为 `ResourceHandler`(若选选项 B);签名本身无变化。 +**待验证**:port 侧 `module.yukkuri` 中 `level.getCapability` 的全部调用点清单,验证方法—— +`git grep -n "getCapability" port/1.21.1 -- module.yukkuri` 收集调用点,逐点对照 26.1.2.76 sources jar 的 +`BlockCapability.java`/`EntityCapability.java` 泛型签名。 + +--- + +## 12. 注解体系 + +### 12.1 javax/neoforge 注解 → jspecify `@NullMarked` / `@Nullable` + +**变化**:dev/26.1 全仓库空注解体系统一迁移到 jspecify(`f94126f`): + +| 旧 | 新 | +|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------| +| `net.minecraft.MethodsReturnNonnullByDefault` + `javax.annotation.ParametersAreNonnullByDefault` | `org.jspecify.annotations.NullMarked`(包级 `package-info.java`) | +| `javax.annotation.Nullable` | `org.jspecify.annotations.Nullable` | +| `javax.annotation.Nonnull`(jetbrains `@NotNull` 等) | `@NullMarked` 覆盖,删除显式标注 | + +dev/26.1 侧实际形态(各模块 `package-info.java`): + +```java +@NullMarked +package dev.anvilcraft.lib.v2.util; +``` + +**涉及文件**(代表性,实际覆盖全部 19 个模块): + +- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/package-info.java` +- +`module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/injection/IPistonMovingBlockEntityExtension.java` +- `module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/package-info.java` +- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/package-info.java` + +**迁移注意**:回迁代码若保留 `javax.annotation.Nullable` import 会编译失败(26.1 不再依赖 javax.annotation);`@NullMarked` +语义下非标注成员默认非空,可空字段/参数必须显式 `@Nullable`(dev 侧 `BlockStatePredicate.statesCache` 即加注 `@Nullable`)。 + +--- + +## 13. 数据生成与构建 + +### 13.1 `REGISTRUM.addDataGenerator` 模式替代 `GatherDataEvent` + `genInit.add` + +**变化**:dev/26.1 侧 Registrum 提供声明式数据生成注册通道,取代「`AnvilLibTestDatagen` 类 + `GatherDataEvent` + +`REGISTRUM.getDataGenInitializer().add(KEY, ...)`」的旧模式。dev/26.1 侧实际形态(`AnvilLibTest.java` + +`TestLangGenerator.java`): + +```java +public void setupDataGeneration() { + REGISTRUM.addDataGenerator(ProviderType.LANG, TestLangGenerator::accept); +} +// TestLangGenerator.accept(RegistrumLangProvider provider):provider.add(...) + ConfigData.readConfigClass(...) +``` + +**涉及文件**: + +- `module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTest.java` +- `module.test/src/main/java/dev/anvilcraft/lib/v2/test/data/TestLangGenerator.java` +- `module.test/src/main/java/dev/anvilcraft/lib/v2/test/all/TestBlocks.java`(`setupRegistration()` 模式, + `defaultCreativeTab(RegistryEntry)`) + +**迁移注意**:port 侧 `AnvilLibTestDatagen`(`GatherDataEvent` 版)不迁移;multiblock definition 的 datagen 注册点(port 侧 +`genInit.add(DEFINITIONS_KEY, ...)`)在 dev/26.1 Registrum 的保留情况 **待验证**——验证方法:grep dev/26.1 侧 +`module.registrum` 的 `addDataGenerator` 与 `getDataGenInitializer` 公开 API,确认 `DEFINITIONS_KEY` 类型的 provider +是否仍可注册。 + +### 13.2 根级 `module.gradle` 聚合构建 + +**变化**:dev/26.1 将各模块独立 `build.gradle`(约 270 行,含 plugins/neoForge runs/publishing/jreleaser/machete/lombok +全量配置)统一为根级 `module.gradle` 聚合(`ea4301f`)+ `createModule` 任务(`16b7bb5`),各模块 `build.gradle` 缩减为依赖块 + +可选 publishMods 块(如 `module.main/build.gradle` 49 行)。 + +**涉及文件**: + +- `module.gradle`(仓库根,聚合构建核心) +- `module.recipe/build.gradle`、`module.util/build.gradle`、`module.network/build.gradle`(各 2–11 行依赖块) +- `module.main/build.gradle`(依赖块 + `publishMods` 块保留在本文件) + +**迁移注意**:port 侧独立构建脚本一律不回迁;新增模块按 dev 侧 `module.gradle` 结构注册(参考 `createModule` +任务约定),不要在模块内重建旧式构建配置。 + +### 13.3 依赖坐标 `-neoforge-26.1` + +**变化**:模块间依赖坐标后缀从 `-neoforge-1.21.1` 改为 `-neoforge-26.1`,并区分发布/本地两种形态: + +```gradle +if (System.getenv("NOT_DEV") == 'true') { + jarJar(api("dev.anvilcraft.lib:anvillib-util-neoforge-26.1:latest.release")) +} else { + jarJar(implementation project(":anvillib-util-neoforge-26.1")) +} +``` + +**涉及文件**: + +- `module.registrum/build.gradle`、`module.recipe/build.gradle`、`module.main/build.gradle` 等全部模块构建文件 + +**迁移注意**:本地用 `jarJar(implementation ...)`、发布用 `jarJar(api ...)` 是 dev 侧既定形态, **不要**把 port 侧 +`jarJar(api ...)` 的本地分支写法搬回(见 `docs/branch-comparison.md` #8 注意事项)。 + +### 13.4 `anvillib.needRunConfig` 属性 + +**变化**:dev 侧 `gradle.properties` 新增模块化 run config 开关 `anvillib.needRunConfig=true` 与细分项 +`anvillib.needRunConfig.data=true`(module.main/module.multiblock 等),由根级 `module.gradle` 消费,控制是否为模块生成运行配置。 + +**涉及文件**: + +- `module.main/gradle.properties` +- `module.multiblock/gradle.properties` +- `module.test/gradle.properties` + +**迁移注意**:该属性属 dev 侧构建机制,port 侧没有;回迁模块若需要 datagen/客户端运行配置,按 dev 侧现有模块的 +`gradle.properties` 写法声明,不要自建 run config 块。 + +--- + +## 附录 + +### A. 核实命令 + +```bash +# 两侧 diff(本文全部差异面的原始来源) +git diff port/1.21.1 dev/26.1 -- module.recipe +# 查看 dev/26.1 侧文件的实际代码形态 +git show dev/26.1:module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java +# 收集 port 侧能力查询调用点(11.4 待验证项) +git grep -n "getCapability" port/1.21.1 -- module.yukkuri +``` + +NeoForge 26.1.2.76 sources jar 本地路径: + +``` +C:/Users/Administrator/.gradle/caches/modules-2/files-2.1/net.neoforged/neoforge/26.1.2.76/ + 1d1dabe31afb953e46fb278e1cd8f04ab8e0d9a0/neoforge-26.1.2.76-sources.jar +``` + +### B. 参考资料 + +| 资料 | 位置 | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 分支对比报告(回迁项 #1–#8、模块差异分类) | `docs/branch-comparison.md` | +| 8 个并行子代理模块级调研报告(codec/util/network/recipe/registrum/multiblock/moveable-entity-block/main/config/integration/wheel/test 的 API 变化点明细) | `C:/Users/Administrator/.kimi-code/sessions/wd_anvillib_031cb785b7e3/session_6033f19b-f4ef-481a-b18c-d626cc22c314/agents/main/tool-results/AgentSwarm-call_00_nh47UIFAaqo9d96levr46332-594fb004-9f1f-4c9e-b230-43bc1440211d.txt` | +| NeoForge 26.1.2.76 sources jar(`ResourceHandler`/`FluidResource`/`IFluidHandler`/`BlockCapability` 源码) | 见附录 A | + +### C. 待验证事项 + +| # | 事项 | 验证方法 | +|----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| V1 | `Level.getCapability` 在 port 侧 `module.yukkuri` 的全部调用点清单与泛型适配(11.4) | `git grep -n "getCapability" port/1.21.1 -- module.yukkuri` 收集调用点,逐点对照 26.1.2.76 sources jar 的 `BlockCapability.java`/`EntityCapability.java` 泛型签名 | +| V2 | multiblock definition 的 datagen 注册(`genInit.add(DEFINITIONS_KEY, ...)`)在 dev/26.1 Registrum 是否保留(13.1) | grep dev/26.1 侧 `module.registrum` 的 `addDataGenerator`/`getDataGenInitializer` 公开 API,确认 `DEFINITIONS_KEY` 类型 provider 的注册通道 | +| V3 | `FriendlyByteBuf.writeVec3/readVec3` 在 26.1 是否移除(`module.codec` VEC3 字段已手写 3×float 替代,double → float 精度降级) | 在 26.1 环境编译 port 侧该字段写法;若需 double 精度自实现 | +| V4 | `MapCodec.encodeStart(...)` 在 26.1 是否原生提供(port 侧 `CodecUtil.encodeStart` 被 dev 删除的原因) | 26.1 环境检查 `MapCodec` 接口方法集 | +| V5 | `Ingredient.CODEC_NONEMPTY` 与 `NonNullList.of(E, E...)` 在 26.1 是否移除(`CodecUtil.createIngredientListCodec` 已改 `Ingredient.CODEC` + `NonNullList.copyOf(List.of(...))`) | 26.1 环境编译旧写法,或对照 MC 26.1.2 源码 | + +--- + +*本文档仅整理「Minecraft/NeoForge API 版本差异」这一差异类别;两侧分支的业务差异、回迁项判定与执行建议见 +`docs/branch-comparison.md`。* \ No newline at end of file From 56cade3a32c75334edb47a84c5b43c7fdc7e614a Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 10 Aug 2026 22:18:12 +0800 Subject: [PATCH 02/38] =?UTF-8?q?fix(network):=20=E5=B0=86=20PacketProtoco?= =?UTF-8?q?l=20=E6=9E=9A=E4=B8=BE=E6=94=B9=E4=B8=BA=20public=20=E4=BB=A5?= =?UTF-8?q?=E4=BE=BF=E5=A4=96=E9=83=A8=E6=A8=A1=E5=9D=97=E6=B3=A8=E8=A7=A3?= =?UTF-8?q?=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回移植 dev/26.1 6220427 的 public 化改动,作为 module.rpc 等外部模块 使用 @Network(protocol = PacketProtocol.PLAY) 注解的前置条件。 --- .../dev/anvilcraft/lib/v2/network/register/PacketProtocol.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketProtocol.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketProtocol.java index 930d7566..158887c5 100644 --- a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketProtocol.java +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketProtocol.java @@ -1,5 +1,5 @@ package dev.anvilcraft.lib.v2.network.register; -enum PacketProtocol { +public enum PacketProtocol { CONFIGURATION, PLAY, COMMON } From 26b11e181ad58c6dbcb10c202c6fb9aff15062a6 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 10 Aug 2026 22:23:21 +0800 Subject: [PATCH 03/38] =?UTF-8?q?feat(rpc):=20=E5=9B=9E=E7=A7=BB=E6=A4=8D?= =?UTF-8?q?=E6=B3=A8=E8=A7=A3=E5=BC=8F=E8=BF=9C=E7=A8=8B=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=20module.rpc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 dev/26.1 的 module.rpc(d153e58 #60、8d57cba #63、4910620 #87)整体回移植, 作为新模块 anvillib-rpc-neoforge-1.21.1,实现 @RemoteCallable 静态方法 + 方法引用解析 + 索引下发 + 双向 play 网络包 + 带返回值 invoke 的完整 RPC 能力。 API 适配(26.1 → 1.21.1): - Identifier → ResourceLocation - FMLLoader.getCurrent().getLoadingModList() → LoadingModList.get() - field.accessFlags()/AccessFlag(Java 22)→ Modifier.isPublic/isStatic/isFinal - ClientPacketDistributor(26.1 新增)→ PacketDistributor.sendToServer - ByteBufCodecs.LONG_ARRAY(26.1 新增)→ VarInt/VarLong 手写等价编解码 - Java 22 lambda 参数 _ 改名为 ignored - 补 compileOnly org.jspecify:jspecify:1.0.0(@NullMarked 注解依赖) 构建:build.gradle 按 port 侧模板,jarJar 依赖 anvillib-network-neoforge-1.21.1。 --- module.rpc/build.gradle | 277 +++++++ module.rpc/gradle.properties | 4 + .../anvilcraft/lib/v2/rpc/AnvilLibRpc.java | 65 ++ .../lib/v2/rpc/AnvilLibRpcNetwork.java | 23 + .../anvilcraft/lib/v2/rpc/CallableParam.java | 45 ++ .../lib/v2/rpc/ClientRpcTargets.java | 48 ++ .../lib/v2/rpc/IRemoteCallableValidator.java | 30 + .../anvilcraft/lib/v2/rpc/IRpcPayload.java | 30 + .../anvilcraft/lib/v2/rpc/LambdaResolver.java | 45 ++ .../java/dev/anvilcraft/lib/v2/rpc/RPC.java | 674 ++++++++++++++++++ .../anvilcraft/lib/v2/rpc/RemoteCallable.java | 30 + .../anvilcraft/lib/v2/rpc/RpcFunctionRef.java | 149 ++++ .../anvilcraft/lib/v2/rpc/RpcMethodRef.java | 155 ++++ .../dev/anvilcraft/lib/v2/rpc/RpcMethods.java | 372 ++++++++++ .../dev/anvilcraft/lib/v2/rpc/RpcPayload.java | 103 +++ .../lib/v2/rpc/RpcPendingCalls.java | 109 +++ .../anvilcraft/lib/v2/rpc/RpcRegistry.java | 163 +++++ .../lib/v2/rpc/RpcRequestPayload.java | 109 +++ .../lib/v2/rpc/RpcResponsePayload.java | 113 +++ .../dev/anvilcraft/lib/v2/rpc/RpcTarget.java | 81 +++ .../lib/v2/rpc/client/AnvilLibRpcClient.java | 55 ++ .../lib/v2/rpc/client/package-info.java | 4 + .../config/RpcConfigurationFinishPayload.java | 34 + .../rpc/config/RpcConfigurationPayload.java | 47 ++ .../v2/rpc/config/RpcConfigurationTask.java | 26 + .../lib/v2/rpc/config/package-info.java | 7 + .../anvilcraft/lib/v2/rpc/package-info.java | 7 + .../resources/META-INF/neoforge.mods.toml | 86 +++ .../main/resources/anvillib_rpc.mixins.json | 14 + module.rpc/src/main/resources/icon.png | Bin 0 -> 1089 bytes settings.gradle | 2 + 31 files changed, 2907 insertions(+) create mode 100644 module.rpc/build.gradle create mode 100644 module.rpc/gradle.properties create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/AnvilLibRpc.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/AnvilLibRpcNetwork.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/CallableParam.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/ClientRpcTargets.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/IRemoteCallableValidator.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/IRpcPayload.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/LambdaResolver.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RPC.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RemoteCallable.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcFunctionRef.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcMethodRef.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcMethods.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcPayload.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcPendingCalls.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcRegistry.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcRequestPayload.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcResponsePayload.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcTarget.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/client/AnvilLibRpcClient.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/client/package-info.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/RpcConfigurationFinishPayload.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/RpcConfigurationPayload.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/RpcConfigurationTask.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/package-info.java create mode 100644 module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/package-info.java create mode 100644 module.rpc/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 module.rpc/src/main/resources/anvillib_rpc.mixins.json create mode 100644 module.rpc/src/main/resources/icon.png diff --git a/module.rpc/build.gradle b/module.rpc/build.gradle new file mode 100644 index 00000000..a8457d91 --- /dev/null +++ b/module.rpc/build.gradle @@ -0,0 +1,277 @@ +plugins { + id 'java-library' + id 'eclipse' + id 'idea' + id 'maven-publish' + alias libs.plugins.modDevGradle + alias libs.plugins.lombok + alias libs.plugins.machete + id 'org.jreleaser' version '1.23.0' + id "me.modmuss50.mod-publish-plugin" version "1.1.0" +} + +String buildType = 'snapshot' +String buildNumber +if (System.getenv("CI_BUILD") == 'false') buildNumber = null +else { + if (System.getenv("PR_BUILD") != 'false') buildType = 'pr' + buildNumber = System.getenv("GITHUB_RUN_NUMBER") +} +version = "${mod_version}" + (buildNumber != null ? "+${buildType}.${buildNumber}" : "") +group = mod_group_id + +base { + archivesName = "${project.mod_id}-neoforge-${libs.versions.minecraft.get()}" +} + +repositories { + maven { url = "https://server.cjsah.net:1002/maven/" } + mavenLocal() + mavenCentral() +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +neoForge { + // Specify the version of NeoForge to use. + version = libs.versions.neoForge.get() + + parchment { + mappingsVersion = project.parchment_mappings_version + minecraftVersion = project.parchment_minecraft_version + } + + // This line is optional. Access Transformers are automatically detected + // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') + + interfaceInjectionData { + // from 'src/main/resources/interface_injections.json' + // publish file('src/main/resources/interface_injections.json') + } + + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + client { + client() + + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + server { + server() + programArgument '--nogui' + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + // This run config launches GameTestServer and runs all registered gametests, then exits. + // By default, the server will crash when no gametests are provided. + // The gametest system is also enabled by default for other run configs under the /test command. + gameTestServer { + type = "gameTestServer" + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + data { + data() + + // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it + // gameDirectory = project.file('run-data') + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } + + // applies to all the run configs above + configureEach { + // Recommended logging data for a userdev environment + // The markers can be added/remove as needed separated by commas. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + systemProperty 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + logLevel = org.slf4j.event.Level.DEBUG + } + } + + mods { + // define mod <-> source bindings + // these are used to tell the game which sources are for which mod + // mostly optional in a single mod project + // but multi mod projects should define one per mod + "${mod_id}" { + sourceSet(sourceSets.main) + } + } +} + +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } + + +dependencies { + compileOnly "org.jspecify:jspecify:1.0.0" + + if (System.getenv("NOT_DEV") == 'true') { + jarJar(api("dev.anvilcraft.lib:anvillib-network-neoforge-1.21.1:latest.release")) + } else { + jarJar(implementation project(":anvillib-network-neoforge-1.21.1")) + } +} + +// This block of code expands all declared replace properties in the specified resource targets. +// A missing property will result in an error. Properties are expanded using ${} Groovy notation. +// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. +// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html +tasks.withType(ProcessResources).configureEach { + var replaceProperties = [minecraft_version : libs.versions.minecraft.get(), + minecraft_version_range: minecraft_version_range, + neo_version : libs.versions.neoForge.get(), + neo_version_range : neo_version_range, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : version, + mod_authors : mod_authors, + mod_description : mod_description] + inputs.properties replaceProperties + + filesMatching(['META-INF/neoforge.mods.toml']) { + expand replaceProperties + } +} + +tasks.register('sourcesJar', Jar) { + from delombok.outputs.files + dependsOn delombok + archiveClassifier = "sources" +} + +tasks.register('javadocJar', Jar) { + from javadoc.destinationDir + dependsOn javadoc + archiveClassifier = "javadoc" +} + +javadoc { + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') +} + +jar { + from(rootProject.file("LICENSE")) + from(rootProject.file("ASSETS_LICENSE")) + from(rootProject.file("README.md")) + from(rootProject.file("README.en.md")) +} + +artifacts { + archives tasks.jar + archives tasks.sourcesJar + archives tasks.javadocJar +} + +def this_name = base.archivesName.get() +def this_description = mod_description + +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + + artifact sourcesJar + artifact javadocJar + + versionMapping { + allVariants { + fromResolutionResult() + } + } + + pom { + name = this_name + description = this_description + url = 'https://github.com/Anvil-Dev/AnvilLib' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/license/mit' + } + } + developers { + developer { + id = 'Anvil-Dev' + name = 'AnvilCraft Dev' + email = 'admin@anvilcraft.dev' + } + } + scm { + url = 'https://github.com/Anvil-Dev/AnvilLib' + connection = 'https://github.com/Anvil-Dev/AnvilLib.git' + developerConnection = 'https://github.com/Anvil-Dev/AnvilLib.git' + } + } + } + } + repositories { + // Add repositories to publish to here. + def MAVEN_URL = System.getenv("MAVEN_URL") + if (MAVEN_URL != null) { + maven { + url MAVEN_URL + credentials { + username System.getenv("MAVEN_USERNAME") + password System.getenv("MAVEN_PASSWORD") + } + } + } + mavenLocal() + maven { + name = "staging" + url = layout.buildDirectory.dir("staging-deploy") + } + } +} + +jreleaser { + signing { + active = 'ALWAYS' + armored = true + } + + deploy { + maven { + mavenCentral { + sonatype { + active = 'ALWAYS' + url = 'https://central.sonatype.com/api/v1/publisher' + stagingRepository('build/staging-deploy') + } + } + } + } +} + +// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} + +machete { + // disable machete locally for faster builds + enabled = false +} + +lombok { + version = "1.18.34" +} diff --git a/module.rpc/gradle.properties b/module.rpc/gradle.properties new file mode 100644 index 00000000..9af9cb71 --- /dev/null +++ b/module.rpc/gradle.properties @@ -0,0 +1,4 @@ +## Mod Properties +mod_id=anvillib_rpc +mod_name=AnvilLib-RPC +mod_description=RPC based on Minecraft Network System diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/AnvilLibRpc.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/AnvilLibRpc.java new file mode 100644 index 00000000..1faf0d1c --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/AnvilLibRpc.java @@ -0,0 +1,65 @@ +package dev.anvilcraft.lib.v2.rpc; + +import dev.anvilcraft.lib.v2.rpc.config.RpcConfigurationTask; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.server.ServerStoppedEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.neoforged.neoforge.network.event.RegisterConfigurationTasksEvent; +import org.jetbrains.annotations.ApiStatus; + +@Mod(AnvilLibRpc.MOD_ID) +@ApiStatus.Internal +public class AnvilLibRpc { + public static final String MAIN_ID = "anvillib"; + public static final String MOD_ID = "anvillib_rpc"; + + /** + * 服务端 / 通用侧的权威索引表:本地扫描填充,从不被服务端下发覆盖。 + */ + public static final RpcRegistry REGISTRY = new RpcRegistry(true); + + /** + * 服务端侧待响应的 {@link RPC#invoke} 调用登记表。 + */ + public static final RpcPendingCalls PENDING = new RpcPendingCalls(); + + public AnvilLibRpc(IEventBus modEventBus, ModContainer modContainer) { + modEventBus.addListener(this::onRegisterConfigurationTasks); + NeoForge.EVENT_BUS.addListener(this::onServerTick); + NeoForge.EVENT_BUS.addListener(this::onServerStopped); + } + + public static ResourceLocation of(String path) { + return ResourceLocation.fromNamespaceAndPath(AnvilLibRpc.MAIN_ID, path); + } + + public static ResourceLocation mod(String path) { + return ResourceLocation.fromNamespaceAndPath(AnvilLibRpc.MOD_ID, path); + } + + @SubscribeEvent + public void onRegisterConfigurationTasks(RegisterConfigurationTasksEvent event) { + event.register(new RpcConfigurationTask()); + } + + /** + * 驱动服务端侧 {@link RPC#invoke} 调用的超时检查。 + */ + @SubscribeEvent + public void onServerTick(ServerTickEvent.Post event) { + AnvilLibRpc.PENDING.tick(); + } + + /** + * 服务器停止时清理服务端侧未完成的 {@link RPC#invoke} 调用。 + */ + @SubscribeEvent + public void onServerStopped(ServerStoppedEvent event) { + AnvilLibRpc.PENDING.clear(); + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/AnvilLibRpcNetwork.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/AnvilLibRpcNetwork.java new file mode 100644 index 00000000..9dfcbd8e --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/AnvilLibRpcNetwork.java @@ -0,0 +1,23 @@ +package dev.anvilcraft.lib.v2.rpc; + +import dev.anvilcraft.lib.v2.network.register.NetworkRegistrar; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import org.jetbrains.annotations.ApiStatus; + +/** + * 注册 RPC 网络包。 + */ +@EventBusSubscriber(modid = AnvilLibRpc.MOD_ID) +@ApiStatus.Internal +public class AnvilLibRpcNetwork { + public static final String NETWORK_VERSION = "1"; + + @SubscribeEvent + public static void register(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar(NETWORK_VERSION); + NetworkRegistrar.register(registrar, AnvilLibRpc.MOD_ID); + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/CallableParam.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/CallableParam.java new file mode 100644 index 00000000..986c21f1 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/CallableParam.java @@ -0,0 +1,45 @@ +package dev.anvilcraft.lib.v2.rpc; + +import net.minecraft.network.codec.StreamCodec; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 为 {@link RemoteCallable} 方法的某个参数指定 {@link StreamCodec};当标注在方法上时,为该方法的 + * 返回值指定 {@link StreamCodec}(用于 {@link RPC#invoke} 的有返回值调用)。 + * + *

当(参数或返回值)类型不在 + * {@link net.minecraft.network.codec.ByteBufCodecs ByteBufCodecs} 默认支持的类型之列时, + * 需用本注解指向一个 {@code public static final StreamCodec} 字段,用于该值的网络编解码。

+ * + *

示例

+ *
{@code
+ * @CallableParam(clazz = MyCodecs.class, field = "RESULT") // 指定返回值编解码器
+ * @RemoteCallable
+ * public static Result compute(
+ *     @CallableParam(clazz = MyCodecs.class, field = "BLOCK_POS") BlockPos pos // 指定参数编解码器
+ * ) { ... }
+ * }
+ * + * @see RemoteCallable + */ +@Target({ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CallableParam { + /** + * 声明 {@link StreamCodec} 字段的类。 + * + * @return 持有编解码器字段的类 + */ + Class clazz(); + + /** + * {@link StreamCodec} 字段的名称。该字段须为 {@code static}。 + * + * @return 编解码器字段名 + */ + String field(); +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/ClientRpcTargets.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/ClientRpcTargets.java new file mode 100644 index 00000000..d55f7dfa --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/ClientRpcTargets.java @@ -0,0 +1,48 @@ +package dev.anvilcraft.lib.v2.rpc; + +import dev.anvilcraft.lib.v2.rpc.client.AnvilLibRpcClient; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.neoforged.neoforge.network.PacketDistributor; + +import java.util.Objects; + +/** + * {@link RpcTarget#server()} 的客户端侧实现。 + * + *

独立成类以便其引用的客户端专用类({@link Minecraft} 等)仅在客户端被加载, + * 专用服务器不会在链接期触及。

+ */ +final class ClientRpcTargets { + private ClientRpcTargets() { + } + + static RpcTarget server() { + return new RpcTarget() { + @Override + public void send(CustomPacketPayload payload) { + PacketDistributor.sendToServer(payload); + } + + @Override + public RpcRegistry registry() { + return AnvilLibRpcClient.REGISTRY; + } + + @Override + public RegistryAccess registryAccess() { + ClientPacketListener connection = Objects.requireNonNull( + Minecraft.getInstance().getConnection(), "Not connected to a server" + ); + return connection.registryAccess(); + } + + @Override + public RpcPendingCalls pending() { + return AnvilLibRpcClient.PENDING; + } + }; + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/IRemoteCallableValidator.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/IRemoteCallableValidator.java new file mode 100644 index 00000000..9da5c23a --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/IRemoteCallableValidator.java @@ -0,0 +1,30 @@ +package dev.anvilcraft.lib.v2.rpc; + +import net.neoforged.neoforge.network.handling.IPayloadContext; + +import java.lang.reflect.Method; + +/** + * {@link RemoteCallable} 方法的接收端校验器:在目标方法执行之前判定本次远程调用是否被允许。 + * + *

校验在接收端(实际执行方法的一侧)进行,可据 {@link IPayloadContext}(发起方玩家、连接、方向等) + * 与实参决定是否放行,用于实现「仅 OP 可调用」「目标坐标须在范围内」等安全策略。

+ * + *

实现类须提供一个无参构造器(可为非 public),框架会反射实例化并缓存。 + * 不指定校验器时(注解 {@code validator} 取默认值),等价于始终放行。

+ * + * @see RemoteCallable#validator() + */ +@FunctionalInterface +public interface IRemoteCallableValidator { + /** + * 判定一次远程调用是否被允许执行。 + * + * @param ctx 网络包上下文(含发起方玩家、方向等) + * @param method 即将执行的目标方法 + * @param args 已解码的实参 + * @return {@code true} 放行并执行;{@code false} 拒绝({@link RPC#call} 静默丢弃, + * {@link RPC#invoke} 使调用方 future 以异常失败) + */ + boolean validate(IPayloadContext ctx, Method method, Object[] args); +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/IRpcPayload.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/IRpcPayload.java new file mode 100644 index 00000000..7cf47a5b --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/IRpcPayload.java @@ -0,0 +1,30 @@ +package dev.anvilcraft.lib.v2.rpc; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import org.jetbrains.annotations.ApiStatus; + +import java.lang.reflect.Method; + +@ApiStatus.Internal +public interface IRpcPayload { + static byte[] encodeParams(RpcRegistry registry, Method method, Object[] args, RegistryFriendlyByteBuf buf) { + buf.writeVarInt(registry.index(method)); + StreamCodec[] codecs = RpcMethods.codecs(method); + for (int i = 0; i < codecs.length; i++) { + codecs[i].encode(buf, args[i]); + } + byte[] data = new byte[buf.readableBytes()]; + buf.readBytes(data); + return data; + } + + static Object[] decodeParams(Method method, RegistryFriendlyByteBuf buf) { + StreamCodec[] codecs = RpcMethods.codecs(method); + Object[] args = new Object[codecs.length]; + for (int i = 0; i < codecs.length; i++) { + args[i] = codecs[i].decode(buf); + } + return args; + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/LambdaResolver.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/LambdaResolver.java new file mode 100644 index 00000000..5f4be526 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/LambdaResolver.java @@ -0,0 +1,45 @@ +package dev.anvilcraft.lib.v2.rpc; + +import java.io.Serializable; +import java.lang.invoke.SerializedLambda; +import java.lang.reflect.Method; + +/** + * 从可序列化的方法引用中还原出其指向的目标方法。 + * + *

{@link RPC#call} 接收的方法引用(如 {@code SomeClass::someStaticMethod})会被编译为实现了 + * {@link Serializable} 的 lambda 实例。JVM 为这类实例自动生成 {@code writeReplace} 方法,返回携带实现类、 + * 方法名与方法描述符的 {@link SerializedLambda},据此即可定位目标方法。

+ */ +final class LambdaResolver { + private LambdaResolver() { + } + + /** + * 解析方法引用所指向的、经 {@link RemoteCallable} 校验的静态方法。 + * + * @param methodRef 指向静态方法的可序列化方法引用 + * @return 已校验通过的目标方法 + */ + static Method resolve(Serializable methodRef) { + SerializedLambda lambda = serializedLambda(methodRef); + String className = lambda.getImplClass().replace('/', '.'); + return RpcMethods.resolve(className, lambda.getImplMethodName(), lambda.getImplMethodSignature()); + } + + private static SerializedLambda serializedLambda(Serializable methodRef) { + try { + Method writeReplace = methodRef.getClass().getDeclaredMethod("writeReplace"); + writeReplace.setAccessible(true); + Object replacement = writeReplace.invoke(methodRef); + if (replacement instanceof SerializedLambda lambda) { + return lambda; + } + throw new IllegalArgumentException( + "RPC target must be a method reference to a static method, got: " + methodRef.getClass().getName() + ); + } catch (ReflectiveOperationException e) { + throw new IllegalArgumentException("Cannot extract method reference metadata", e); + } + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RPC.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RPC.java new file mode 100644 index 00000000..e4e6ffb7 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RPC.java @@ -0,0 +1,674 @@ +package dev.anvilcraft.lib.v2.rpc; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.concurrent.CompletableFuture; + +/** + * 远程过程调用入口。 + * + *

将一个静态方法标注为 {@link RemoteCallable},即可通过 + * {@link #call(RpcTarget, RpcMethodRef.R1, Object) RPC.call(target, Type::method, args...)} + * 在目标端执行该方法。

+ * + *

方法参数默认支持 {@link net.minecraft.network.codec.ByteBufCodecs ByteBufCodecs} 中提供的类型 + * (如 {@code int}、{@code String}、{@code byte[]} 等),这些参数无需额外标注;其余类型需通过 + * {@link CallableParam} 指定对应的 {@link net.minecraft.network.codec.StreamCodec StreamCodec}。

+ * + *

示例

+ *
{@code
+ * public final class Greetings {
+ *     @RemoteCallable
+ *     public static void hello(String name, int times) {
+ *         for (int i = 0; i < times; i++) System.out.println("Hello " + name);
+ *     }
+ * }
+ *
+ * // 服务端令某个客户端执行 Greetings.hello("world", 3)
+ * RPC.call(RpcTarget.player(serverPlayer), Greetings::hello, "world", 3);
+ * }
+ */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class RPC { + /** + * 调用无参方法。 + * + * @param target 目标端 + * @param methodRef 指向 {@link RemoteCallable} 静态方法的方法引用 + */ + public static void call(RpcTarget target, RpcMethodRef.R0 methodRef) { + dispatch(target, methodRef); + } + + /** + * 调用单参方法。 + * + * @param target 目标端 + * @param methodRef 指向 {@link RemoteCallable} 静态方法的方法引用 + * @param a 第 1 个参数 + * @param 第 1 个参数类型 + */ + public static void call(RpcTarget target, RpcMethodRef.R1 methodRef, A a) { + dispatch(target, methodRef, a); + } + + /** + * 调用双参方法。 + */ + public static void call(RpcTarget target, RpcMethodRef.R2 methodRef, A a, B b) { + dispatch(target, methodRef, a, b); + } + + /** + * 调用三参方法。 + */ + public static void call(RpcTarget target, RpcMethodRef.R3 methodRef, A a, B b, C c) { + dispatch(target, methodRef, a, b, c); + } + + /** + * 调用四参方法。 + */ + public static void call(RpcTarget target, RpcMethodRef.R4 methodRef, A a, B b, C c, D d) { + dispatch(target, methodRef, a, b, c, d); + } + + /** + * 调用五参方法。 + */ + public static void call(RpcTarget target, RpcMethodRef.R5 methodRef, A a, B b, C c, D d, E e) { + dispatch(target, methodRef, a, b, c, d, e); + } + + /** + * 调用六参方法。 + */ + public static void call( + RpcTarget target, + RpcMethodRef.R6 methodRef, + A a, + B b, + C c, + D d, + E e, + F f + ) { + dispatch(target, methodRef, a, b, c, d, e, f); + } + + /** + * 调用七参方法。 + */ + public static void call( + RpcTarget target, + RpcMethodRef.R7 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g + ) { + dispatch(target, methodRef, a, b, c, d, e, f, g); + } + + /** + * 调用八参方法。 + */ + public static void call( + RpcTarget target, + RpcMethodRef.R8 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h + ) { + dispatch(target, methodRef, a, b, c, d, e, f, g, h); + } + + /** + * 调用九参方法。 + */ + public static void call( + RpcTarget target, + RpcMethodRef.R9 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i + ) { + dispatch(target, methodRef, a, b, c, d, e, f, g, h, i); + } + + /** + * 调用十参方法。 + */ + public static void call( + RpcTarget target, + RpcMethodRef.R10 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j + ) { + dispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j); + } + + /** + * 调用十一参方法。 + */ + public static void call( + RpcTarget target, + RpcMethodRef.R11 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j, + K k + ) { + dispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j, k); + } + + /** + * 调用十二参方法。 + */ + public static void call( + RpcTarget target, + RpcMethodRef.R12 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j, + K k, + L l + ) { + dispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j, k, l); + } + + /** + * 调用十三参方法。 + */ + public static void call( + RpcTarget target, + RpcMethodRef.R13 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j, + K k, + L l, + M m + ) { + dispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j, k, l, m); + } + + /** + * 调用十四参方法。 + */ + public static void call( + RpcTarget target, + RpcMethodRef.R14 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j, + K k, + L l, + M m, + N n + ) { + dispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j, k, l, m, n); + } + + /** + * 调用十五参方法。 + */ + public static void call( + RpcTarget target, + RpcMethodRef.R15 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j, + K k, + L l, + M m, + N n, + O o + ) { + dispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o); + } + + /** + * 调用十六参方法。 + */ + public static void call( + RpcTarget target, + RpcMethodRef.R16 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j, + K k, + L l, + M m, + N n, + O o, + P p + ) { + dispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p); + } + + /** + * 逃生口:按类与方法名调用 {@link RemoteCallable} 方法,参数个数不限。 + * + *

当参数个数超过 {@link #call} 提供的 16 个档次,或无法使用方法引用时使用。代价是失去对实参的 + * 编译期类型检查:调用者需自行保证 {@code args} 的类型与个数同目标方法一致。若该方法名存在重载, + * 将因歧义而抛出异常,此时应改用方法引用形式的 {@link #call}。

+ * + * @param target 目标端 + * @param clazz 目标方法所属类 + * @param methodName 目标方法名(须唯一且为 {@link RemoteCallable} 静态方法) + * @param args 实参 + */ + public static void callByName(RpcTarget target, Class clazz, String methodName, Object... args) { + Method method = RpcMethods.resolveByName(clazz, methodName); + sendChecked(target, method, args); + } + + /** + * 发起无参、有返回值的远程调用。 + * + * @param target 目标端 + * @param methodRef 指向 {@link RemoteCallable} 静态方法的方法引用 + * @param 返回类型 + * @return 在收到对端响应时兑现的 future + */ + public static CompletableFuture invoke(RpcTarget target, RpcFunctionRef.F0 methodRef) { + return invokeDispatch(target, methodRef); + } + + /** + * 发起单参、有返回值的远程调用。 + */ + public static CompletableFuture invoke(RpcTarget target, RpcFunctionRef.F1 methodRef, A a) { + return invokeDispatch(target, methodRef, a); + } + + /** + * 发起双参、有返回值的远程调用。 + */ + public static CompletableFuture invoke(RpcTarget target, RpcFunctionRef.F2 methodRef, A a, B b) { + return invokeDispatch(target, methodRef, a, b); + } + + /** + * 发起三参、有返回值的远程调用。 + */ + public static CompletableFuture invoke(RpcTarget target, RpcFunctionRef.F3 methodRef, A a, B b, C c) { + return invokeDispatch(target, methodRef, a, b, c); + } + + /** + * 发起四参、有返回值的远程调用。 + */ + public static CompletableFuture invoke( + RpcTarget target, + RpcFunctionRef.F4 methodRef, + A a, + B b, + C c, + D d + ) { + return invokeDispatch(target, methodRef, a, b, c, d); + } + + /** + * 发起五参、有返回值的远程调用。 + */ + public static CompletableFuture invoke( + RpcTarget target, + RpcFunctionRef.F5 methodRef, + A a, + B b, + C c, + D d, + E e + ) { + return invokeDispatch(target, methodRef, a, b, c, d, e); + } + + /** + * 发起六参、有返回值的远程调用。 + */ + public static CompletableFuture invoke( + RpcTarget target, + RpcFunctionRef.F6 methodRef, + A a, + B b, + C c, + D d, + E e, + F f + ) { + return invokeDispatch(target, methodRef, a, b, c, d, e, f); + } + + /** + * 发起七参、有返回值的远程调用。 + */ + public static CompletableFuture invoke( + RpcTarget target, + RpcFunctionRef.F7 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g + ) { + return invokeDispatch(target, methodRef, a, b, c, d, e, f, g); + } + + /** + * 发起八参、有返回值的远程调用。 + */ + public static CompletableFuture invoke( + RpcTarget target, + RpcFunctionRef.F8 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h + ) { + return invokeDispatch(target, methodRef, a, b, c, d, e, f, g, h); + } + + /** + * 发起九参、有返回值的远程调用。 + */ + public static CompletableFuture invoke( + RpcTarget target, + RpcFunctionRef.F9 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i + ) { + return invokeDispatch(target, methodRef, a, b, c, d, e, f, g, h, i); + } + + /** + * 发起十参、有返回值的远程调用。 + */ + public static CompletableFuture invoke( + RpcTarget target, + RpcFunctionRef.F10 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j + ) { + return invokeDispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j); + } + + /** + * 发起十一参、有返回值的远程调用。 + */ + public static CompletableFuture invoke( + RpcTarget target, + RpcFunctionRef.F11 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j, + K k + ) { + return invokeDispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j, k); + } + + /** + * 发起十二参、有返回值的远程调用。 + */ + public static CompletableFuture invoke( + RpcTarget target, + RpcFunctionRef.F12 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j, + K k, + L l + ) { + return invokeDispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j, k, l); + } + + /** + * 发起十三参、有返回值的远程调用。 + */ + public static CompletableFuture invoke( + RpcTarget target, + RpcFunctionRef.F13 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j, + K k, + L l, + M m + ) { + return invokeDispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j, k, l, m); + } + + /** + * 发起十四参、有返回值的远程调用。 + */ + public static CompletableFuture invoke( + RpcTarget target, + RpcFunctionRef.F14 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j, + K k, + L l, + M m, + N n + ) { + return invokeDispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j, k, l, m, n); + } + + /** + * 发起十五参、有返回值的远程调用。 + */ + public static CompletableFuture invoke( + RpcTarget target, + RpcFunctionRef.F15 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j, + K k, + L l, + M m, + N n, + O o + ) { + return invokeDispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o); + } + + /** + * 发起十六参、有返回值的远程调用。 + */ + public static CompletableFuture invoke( + RpcTarget target, + RpcFunctionRef.F16 methodRef, + A a, + B b, + C c, + D d, + E e, + F f, + G g, + H h, + I i, + J j, + K k, + L l, + M m, + N n, + O o, + P p + ) { + return invokeDispatch(target, methodRef, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p); + } + + /** + * 逃生口:按类与方法名发起有返回值的远程调用,参数个数不限。 + * + *

与 {@link #callByName} 同理,失去对实参的编译期类型检查;返回类型由调用者通过 {@code } 指定, + * 运行时若与方法实际返回类型不符将导致解码或转型失败。

+ * + * @param target 目标端 + * @param clazz 目标方法所属类 + * @param methodName 目标方法名(须唯一且为 {@link RemoteCallable} 静态方法) + * @param args 实参 + * @param 返回类型 + * @return 在收到对端响应时兑现的 future + */ + public static CompletableFuture invokeByName(RpcTarget target, Class clazz, String methodName, Object... args) { + Method method = RpcMethods.resolveByName(clazz, methodName); + return invokeChecked(target, method, args); + } + + private static void dispatch(RpcTarget target, Serializable methodRef, Object... args) { + Method method = LambdaResolver.resolve(methodRef); + sendChecked(target, method, args); + } + + private static void sendChecked(RpcTarget target, Method method, Object[] args) { + if (method.getParameterCount() != args.length) { + throw new IllegalArgumentException("RPC method " + method + " expects " + method.getParameterCount() + " arguments, got " + args.length); + } + target.send(RpcPayload.encode(target.registry(), target.registryAccess(), method, args)); + } + + private static CompletableFuture invokeDispatch(RpcTarget target, Serializable methodRef, Object... args) { + return invokeChecked(target, LambdaResolver.resolve(methodRef), args); + } + + private static CompletableFuture invokeChecked(RpcTarget target, Method method, Object[] args) { + if (method.getReturnType() == void.class) { + throw new IllegalArgumentException("RPC method " + method + " returns void; use RPC.call instead"); + } + if (method.getParameterCount() != args.length) { + throw new IllegalArgumentException("RPC method " + method + " expects " + method.getParameterCount() + " arguments, got " + args.length); + } + CompletableFuture future = new CompletableFuture<>(); + int callId = target.pending().register(method, future); + target.send(RpcRequestPayload.encode(target.registry(), target.registryAccess(), callId, method, args)); + @SuppressWarnings("unchecked") CompletableFuture typed = (CompletableFuture) future; + return typed; + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RemoteCallable.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RemoteCallable.java new file mode 100644 index 00000000..148dc39c --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RemoteCallable.java @@ -0,0 +1,30 @@ +package dev.anvilcraft.lib.v2.rpc; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 标记一个静态方法为可远程调用方法。 + * + *

仅被该注解标注的 {@code static} 方法才能作为 {@link RPC#call} 的目标, + * 接收端在执行前会校验目标方法确实带有此注解,以防止任意方法被远程触发。

+ * + * @see RPC#call + * @see CallableParam + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RemoteCallable { + /** + * 接收端校验器:在方法执行前判定本次调用是否被允许。 + * + *

默认值 {@link IRemoteCallableValidator} 本身为「始终放行」哨兵;指定其它实现类时, + * 该类须有无参构造器,框架会反射实例化并缓存。校验不通过时,{@link RPC#call} 静默丢弃, + * {@link RPC#invoke} 使调用方 future 以异常失败。

+ * + * @return 校验器类型 + */ + Class validator() default IRemoteCallableValidator.class; +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcFunctionRef.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcFunctionRef.java new file mode 100644 index 00000000..c891a5f6 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcFunctionRef.java @@ -0,0 +1,149 @@ +package dev.anvilcraft.lib.v2.rpc; + +import java.io.Serializable; + +/** + * 指向有返回值的 {@link RemoteCallable} 静态方法的可序列化方法引用接口集合,按参数个数区分。 + * + *

与 {@link RpcMethodRef} 对应,但方法返回类型为 {@code R},供 {@link RPC#invoke} 使用。

+ */ +public final class RpcFunctionRef { + private RpcFunctionRef() { + } + + /** + * 无参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F0 extends Serializable { + R invoke(); + } + + /** + * 单参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F1 extends Serializable { + R invoke(A a); + } + + /** + * 双参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F2 extends Serializable { + R invoke(A a, B b); + } + + /** + * 三参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F3 extends Serializable { + R invoke(A a, B b, C c); + } + + /** + * 四参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F4 extends Serializable { + R invoke(A a, B b, C c, D d); + } + + /** + * 五参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F5 extends Serializable { + R invoke(A a, B b, C c, D d, E e); + } + + /** + * 六参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F6 extends Serializable { + R invoke(A a, B b, C c, D d, E e, F f); + } + + /** + * 七参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F7 extends Serializable { + R invoke(A a, B b, C c, D d, E e, F f, G g); + } + + /** + * 八参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F8 extends Serializable { + R invoke(A a, B b, C c, D d, E e, F f, G g, H h); + } + + /** + * 九参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F9 extends Serializable { + R invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i); + } + + /** + * 十参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F10 extends Serializable { + R invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j); + } + + /** + * 十一参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F11 extends Serializable { + R invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k); + } + + /** + * 十二参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F12 extends Serializable { + R invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l); + } + + /** + * 十三参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F13 extends Serializable { + R invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m); + } + + /** + * 十四参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F14 extends Serializable { + R invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, N n); + } + + /** + * 十五参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F15 extends Serializable { + R invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, N n, O o); + } + + /** + * 十六参数、返回 {@code R} 的方法引用。 + */ + @FunctionalInterface + public interface F16 extends Serializable { + R invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, N n, O o, P p); + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcMethodRef.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcMethodRef.java new file mode 100644 index 00000000..c0a56ef0 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcMethodRef.java @@ -0,0 +1,155 @@ +package dev.anvilcraft.lib.v2.rpc; + +import java.io.Serializable; + +/** + * 指向 {@link RemoteCallable} 静态方法的可序列化方法引用接口集合,按参数个数区分。 + * + *

这些接口继承 {@link Serializable},使得 {@code Foo::bar} 形式的方法引用能被编译为可序列化 lambda, + * 从而可通过 {@link LambdaResolver} 还原出目标方法。

+ */ +public final class RpcMethodRef { + private RpcMethodRef() { + } + + /** + * 无参数方法引用。 + */ + @FunctionalInterface + public interface R0 extends Serializable { + void invoke(); + } + + /** + * 单参数方法引用。 + * + * @param 第 1 个参数类型 + */ + @FunctionalInterface + public interface R1 extends Serializable { + void invoke(A a); + } + + /** + * 双参数方法引用。 + * + * @param 第 1 个参数类型 + * @param 第 2 个参数类型 + */ + @FunctionalInterface + public interface R2 extends Serializable { + void invoke(A a, B b); + } + + /** + * 三参数方法引用。 + */ + @FunctionalInterface + public interface R3 extends Serializable { + void invoke(A a, B b, C c); + } + + /** + * 四参数方法引用。 + */ + @FunctionalInterface + public interface R4 extends Serializable { + void invoke(A a, B b, C c, D d); + } + + /** + * 五参数方法引用。 + */ + @FunctionalInterface + public interface R5 extends Serializable { + void invoke(A a, B b, C c, D d, E e); + } + + /** + * 六参数方法引用。 + */ + @FunctionalInterface + public interface R6 extends Serializable { + void invoke(A a, B b, C c, D d, E e, F f); + } + + /** + * 七参数方法引用。 + */ + @FunctionalInterface + public interface R7 extends Serializable { + void invoke(A a, B b, C c, D d, E e, F f, G g); + } + + /** + * 八参数方法引用。 + */ + @FunctionalInterface + public interface R8 extends Serializable { + void invoke(A a, B b, C c, D d, E e, F f, G g, H h); + } + + /** + * 九参数方法引用。 + */ + @FunctionalInterface + public interface R9 extends Serializable { + void invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i); + } + + /** + * 十参数方法引用。 + */ + @FunctionalInterface + public interface R10 extends Serializable { + void invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j); + } + + /** + * 十一参数方法引用。 + */ + @FunctionalInterface + public interface R11 extends Serializable { + void invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k); + } + + /** + * 十二参数方法引用。 + */ + @FunctionalInterface + public interface R12 extends Serializable { + void invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l); + } + + /** + * 十三参数方法引用。 + */ + @FunctionalInterface + public interface R13 extends Serializable { + void invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m); + } + + /** + * 十四参数方法引用。 + */ + @FunctionalInterface + public interface R14 extends Serializable { + void invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, N n); + } + + /** + * 十五参数方法引用。 + */ + @FunctionalInterface + public interface R15 extends Serializable { + void invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, N n, O o); + } + + /** + * 十六参数方法引用。 + */ + @FunctionalInterface + public interface R16 extends Serializable { + void invoke(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, N n, O o, P p); + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcMethods.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcMethods.java new file mode 100644 index 00000000..2bd58b54 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcMethods.java @@ -0,0 +1,372 @@ +package dev.anvilcraft.lib.v2.rpc; + +import io.netty.buffer.ByteBuf; +import net.minecraft.core.UUIDUtil; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.VarInt; +import net.minecraft.network.VarLong; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * RPC 方法解析与参数编解码器解析的内部工具。 + * + *

负责:

+ *
    + *
  • 根据类名、方法名、方法描述符定位 {@link RemoteCallable} 静态方法(并做安全校验);
  • + *
  • 为方法的每个参数解析 {@link StreamCodec}:优先使用 {@link CallableParam} 指定的编解码器, + * 否则回退到 {@link ByteBufCodecs} 中的默认编解码器。
  • + *
+ */ +final class RpcMethods { + /** + * {@link ByteBufCodecs} 中按参数类型提供的默认编解码器。使用这些类型的参数无需 {@link CallableParam}。 + */ + private static final Map, StreamCodec> DEFAULTS = new HashMap<>(); + /** + * 方法解析缓存,键为 {@code className#methodName + descriptor}。 + */ + private static final Map METHOD_CACHE = new ConcurrentHashMap<>(); + /** + * 参数编解码器解析缓存。 + */ + private static final Map[]> CODEC_CACHE = new ConcurrentHashMap<>(); + /** + * 返回值编解码器解析缓存。 + */ + private static final Map> RETURN_CODEC_CACHE = new ConcurrentHashMap<>(); + /** + * 方法校验器实例缓存。{@code Optional.empty()} 表示该方法无校验器(始终放行)。 + */ + private static final Map> VALIDATOR_CACHE = new ConcurrentHashMap<>(); + + static { + // int / long 默认使用 VarInt / VarLong,与原版网络包惯例一致 + register(boolean.class, Boolean.class, ByteBufCodecs.BOOL); + register(byte.class, Byte.class, ByteBufCodecs.BYTE); + register(short.class, Short.class, ByteBufCodecs.SHORT); + register(int.class, Integer.class, ByteBufCodecs.VAR_INT); + register(long.class, Long.class, ByteBufCodecs.VAR_LONG); + register(float.class, Float.class, ByteBufCodecs.FLOAT); + register(double.class, Double.class, ByteBufCodecs.DOUBLE); + DEFAULTS.put(String.class, ByteBufCodecs.STRING_UTF8); + DEFAULTS.put(byte[].class, ByteBufCodecs.BYTE_ARRAY); + // 1.21.1 的 ByteBufCodecs 无 LONG_ARRAY(26.1 新增),手写等价编解码:VarInt 长度 + VarLong 元素 + DEFAULTS.put(long[].class, StreamCodec.of( + (ByteBuf buf, long[] arr) -> { + VarInt.write(buf, arr.length); + for (long v : arr) VarLong.write(buf, v); + }, + buf -> { + long[] arr = new long[VarInt.read(buf)]; + for (int i = 0; i < arr.length; i++) arr[i] = VarLong.read(buf); + return arr; + } + )); + DEFAULTS.put(UUID.class, UUIDUtil.STREAM_CODEC); + DEFAULTS.put(Tag.class, ByteBufCodecs.TAG); + DEFAULTS.put(CompoundTag.class, ByteBufCodecs.COMPOUND_TAG); + } + + private RpcMethods() { + } + + private static void register(Class primitive, Class boxed, StreamCodec codec) { + DEFAULTS.put(primitive, codec); + DEFAULTS.put(boxed, codec); + } + + /** + * 解析并校验目标方法。 + * + *

该方法是接收端的安全闸口:仅当目标方法是 {@code static} 且标注了 {@link RemoteCallable} 时才会返回, + * 否则抛出异常拒绝调用。

+ * + * @param className 方法所属类的全限定名 + * @param methodName 方法名 + * @param descriptor 方法描述符(JVM 字节码格式),用于区分重载 + * @return 已校验通过、可访问的目标方法 + */ + static Method resolve(String className, String methodName, String descriptor) { + return METHOD_CACHE.computeIfAbsent(className + "#" + methodName + descriptor, ignored -> doResolve(className, methodName, descriptor)); + } + + private static Method doResolve(String className, String methodName, String descriptor) { + Class clazz = loadClass(className); + for (Method method : clazz.getDeclaredMethods()) { + if (!method.getName().equals(methodName)) continue; + if (!methodDescriptor(method).equals(descriptor)) continue; + validate(method); + method.setAccessible(true); + return method; + } + throw new IllegalStateException("Cannot find method " + methodName + descriptor + " in " + className); + } + + /** + * 按类与方法名解析唯一的 {@link RemoteCallable} 方法(用于无方法引用的逃生口调用)。 + * + *

若该名称存在多个重载,视为歧义并抛出异常——此时应改用方法引用形式以借助参数类型消歧。

+ * + * @param clazz 方法所属类 + * @param methodName 方法名 + * @return 已校验通过、可访问的目标方法 + */ + static Method resolveByName(Class clazz, String methodName) { + Method found = null; + for (Method method : clazz.getDeclaredMethods()) { + if (!method.getName().equals(methodName)) continue; + if (!Modifier.isStatic(method.getModifiers())) continue; + if (!method.isAnnotationPresent(RemoteCallable.class)) continue; + if (found != null) { + throw new IllegalStateException("Ambiguous @RemoteCallable method " + clazz.getName() + "#" + methodName + "; use a method reference (RPC.call) to disambiguate overloads"); + } + method.setAccessible(true); + found = method; + } + if (found == null) { + throw new IllegalStateException("Cannot find @RemoteCallable static method " + clazz.getName() + "#" + methodName); + } + return found; + } + + private static Class loadClass(String className) { + try { + return Class.forName(className, true, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Cannot find RPC target class: " + className, e); + } + } + + private static void validate(Method method) { + if (!Modifier.isStatic(method.getModifiers())) { + throw new IllegalStateException("@RemoteCallable method must be static: " + method); + } + if (!method.isAnnotationPresent(RemoteCallable.class)) { + throw new IllegalStateException("Method is not @RemoteCallable: " + method); + } + } + + /** + * 解析方法各参数的编解码器,顺序与参数声明顺序一致。 + */ + static StreamCodec[] codecs(Method method) { + return CODEC_CACHE.computeIfAbsent(method, RpcMethods::resolveCodecs); + } + + @SuppressWarnings( + { + "unchecked", + "rawtypes" + } + ) + private static StreamCodec[] resolveCodecs(Method method) { + Parameter[] parameters = method.getParameters(); + StreamCodec[] codecs = new StreamCodec[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + codecs[i] = resolveCodec(parameters[i]); + } + return (StreamCodec[]) codecs; + } + + private static StreamCodec resolveCodec(Parameter parameter) { + // 1. 优先使用 @CallableParam 显式指定的编解码器 + CallableParam annotation = parameter.getAnnotation(CallableParam.class); + if (annotation != null) { + return readCodecField(annotation.clazz(), annotation.field()); + } + // 2. 其次按类型解析(默认编解码器 / 类型自身声明的字段) + StreamCodec codec = codecForType(parameter.getType()); + if (codec != null) { + return codec; + } + throw new IllegalStateException("No StreamCodec for parameter type " + parameter.getType() + .getName() + "; annotate the parameter with @CallableParam, or declare a public static final StreamCodec field in " + parameter.getType() + .getName()); + } + + /** + * 解析方法返回值的编解码器(用于有返回值的 {@link RPC#invoke} 调用)。 + * + *

解析顺序:方法上的 {@link CallableParam} 指定的字段 → 返回类型的默认编解码器 → + * 返回类型自身声明的 {@code public static final StreamCodec} 字段。

+ * + * @param method 目标方法(返回类型不得为 {@code void}) + * @return 返回值编解码器 + */ + static StreamCodec returnCodec(Method method) { + return RETURN_CODEC_CACHE.computeIfAbsent(method, RpcMethods::resolveReturnCodec); + } + + @SuppressWarnings("unchecked") + private static StreamCodec resolveReturnCodec(Method method) { + Class returnType = method.getReturnType(); + if (returnType == void.class) { + throw new IllegalStateException("@RemoteCallable method has no return value: " + method + "; use RPC.call instead"); + } + StreamCodec codec; + CallableParam annotation = method.getAnnotation(CallableParam.class); + if (annotation != null) { + codec = readCodecField(annotation.clazz(), annotation.field()); + } else { + codec = codecForType(returnType); + } + if (codec == null) { + throw new IllegalStateException("No StreamCodec for return type " + returnType.getName() + "; annotate the method with @CallableParam, or declare a public static final StreamCodec field in " + returnType.getName()); + } + return (StreamCodec) codec; + } + + /** + * 按类型解析编解码器:先查 {@link ByteBufCodecs} 默认编解码器,再回退到该类型自身声明的 + * {@code public static final StreamCodec} 字段。 + * + * @param type 目标类型 + * @return 匹配的编解码器;若都没有则返回 {@code null} + */ + private static @Nullable StreamCodec codecForType(Class type) { + StreamCodec codec = DEFAULTS.get(type); + if (codec != null) { + return codec; + } + return findDeclaredCodec(type); + } + + /** + * 在给定类型中查找可用于编码该类型自身的 {@code public static final StreamCodec} 字段。 + * + *

字段的负载类型实参须与参数类型兼容;若存在多个匹配字段则视为歧义并抛出异常。

+ * + * @param type 参数类型 + * @return 匹配的编解码器;若无匹配字段返回 {@code null} + */ + private static @Nullable StreamCodec findDeclaredCodec(Class type) { + StreamCodec found = null; + String foundName = null; + for (Field field : type.getDeclaredFields()) { + int mods = field.getModifiers(); + if (!Modifier.isPublic(mods) || !Modifier.isStatic(mods) || !Modifier.isFinal(mods)) { + continue; + } + if (!StreamCodec.class.isAssignableFrom(field.getType())) continue; + if (!isCodecForPayload(field.getGenericType(), type)) continue; + if (found != null) { + throw new IllegalStateException("Ambiguous StreamCodec fields in " + type.getName() + ": " + foundName + " and " + field.getName() + "; use @CallableParam to disambiguate"); + } + try { + field.setAccessible(true); + found = (StreamCodec) field.get(null); + foundName = field.getName(); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Cannot read StreamCodec field " + type.getName() + "." + field.getName(), e); + } + } + return found; + } + + /** + * 判断 {@code StreamCodec} 的负载类型实参 {@code P} 是否可承载 {@code payloadType}。 + */ + private static boolean isCodecForPayload(Type genericType, Class payloadType) { + if (!(genericType instanceof ParameterizedType pt)) { + // 原始类型无法校验负载类型,保守地接受 + return true; + } + Type[] args = pt.getActualTypeArguments(); + if (args.length < 2) return true; + Type payloadArg = args[1]; + if (payloadArg instanceof Class payloadClass) { + return payloadClass.isAssignableFrom(payloadType); + } + if (payloadArg instanceof ParameterizedType payloadPt && payloadPt.getRawType() instanceof Class raw) { + return raw.isAssignableFrom(payloadType); + } + // 通配符 / 类型变量等无法静态判定,保守接受 + return true; + } + + private static StreamCodec readCodecField(Class clazz, String fieldName) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + Object value = field.get(null); + if (value instanceof StreamCodec codec) { + return codec; + } + throw new IllegalStateException("Field " + clazz.getName() + "." + fieldName + " is not a StreamCodec"); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new IllegalStateException("Cannot read StreamCodec from " + clazz.getName() + "." + fieldName, e); + } + } + + /** + * 运行目标方法的接收端校验器。无校验器(默认哨兵)时始终放行。 + * + * @param method 目标方法 + * @param ctx 网络包上下文 + * @param args 已解码的实参 + * @return 是否允许执行 + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + static boolean validate(Method method, IPayloadContext ctx, Object[] args) { + IRemoteCallableValidator validator = VALIDATOR_CACHE.computeIfAbsent(method, RpcMethods::resolveValidator).orElse(null); + return validator == null || validator.validate(ctx, method, args); + } + + private static Optional resolveValidator(Method method) { + RemoteCallable annotation = method.getAnnotation(RemoteCallable.class); + Class type = annotation.validator(); + // 默认哨兵:注解未指定校验器,等价于始终放行 + if (type == IRemoteCallableValidator.class) { + return Optional.empty(); + } + try { + var constructor = type.getDeclaredConstructor(); + constructor.setAccessible(true); + return Optional.of(constructor.newInstance()); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Cannot instantiate RPC validator " + type.getName() + " (needs a no-arg constructor)", e); + } + } + + /** + * 计算方法的 JVM 字节码描述符,例如 {@code (Ljava/lang/String;I)V}。 + */ + static String methodDescriptor(Method method) { + StringBuilder builder = new StringBuilder("("); + for (Class parameterType : method.getParameterTypes()) { + builder.append(typeDescriptor(parameterType)); + } + return builder.append(')').append(typeDescriptor(method.getReturnType())).toString(); + } + + private static String typeDescriptor(Class type) { + if (type == void.class) return "V"; + if (type == boolean.class) return "Z"; + if (type == byte.class) return "B"; + if (type == char.class) return "C"; + if (type == short.class) return "S"; + if (type == int.class) return "I"; + if (type == long.class) return "J"; + if (type == float.class) return "F"; + if (type == double.class) return "D"; + if (type.isArray()) return "[" + typeDescriptor(type.getComponentType()); + return "L" + type.getName().replace('.', '/') + ";"; + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcPayload.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcPayload.java new file mode 100644 index 00000000..0e7d952c --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcPayload.java @@ -0,0 +1,103 @@ +package dev.anvilcraft.lib.v2.rpc; + +import dev.anvilcraft.lib.v2.network.packet.IInsensitiveBiPacket; +import dev.anvilcraft.lib.v2.network.packet.IPacket; +import dev.anvilcraft.lib.v2.rpc.client.AnvilLibRpcClient; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.connection.ConnectionType; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.ApiStatus; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * 承载一次远程调用的网络包。 + * + *

线上仅传输一段不透明字节({@code 索引 + 编码后的实参})。仿照 + * {@link dev.anvilcraft.lib.v2.network.packet 网络包} 中 SyncPayload 的做法,真正依赖「当前侧」的 + * 编解码在已知方向的位置进行:

+ *
    + *
  • 编码在发送侧({@link RpcTarget} 已知发送方向,故已知使用哪个 {@link RpcRegistry});
  • + *
  • 解码在处理侧({@link IPayloadContext#flow()} 已知接收方向)——而非 {@link StreamCodec} + * 的解码回调中(其运行于网络线程,无法可靠判断当前侧)。
  • + *
+ * + * @see RPC#call + */ +@ApiStatus.Internal +public class RpcPayload implements IRpcPayload, IInsensitiveBiPacket { + public static final Type TYPE = IPacket.type(AnvilLibRpc.mod("rpc")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.BYTE_ARRAY, + RpcPayload::data, + RpcPayload::new + ); + + private final byte[] data; + + RpcPayload(byte[] data) { + this.data = data; + } + + /** + * 在发送侧将方法索引与实参编码为字节。 + * + * @param registry 发送侧索引表(服务端为权威表,客户端为已采纳表) + * @param registryAccess 发送侧注册表访问器,用于构造可承载注册表对象的缓冲区 + * @param method 目标方法 + * @param args 实参 + * @return 编码后的网络包 + */ + static RpcPayload encode(RpcRegistry registry, RegistryAccess registryAccess, Method method, Object[] args) { + RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf(Unpooled.buffer(), registryAccess, ConnectionType.NEOFORGE); + return new RpcPayload(IRpcPayload.encodeParams(registry, method, args, buf)); + } + + byte[] data() { + return this.data; + } + + @Override + public void bidirectionalHandler(IPayloadContext ctx) { + ctx.enqueueWork(() -> this.handle(ctx)); + } + + @Override + public void handleOnBothSide(Player player) { + // 实际处理在 handle(ctx) 中完成(需要 ctx.flow() 与 ctx.player()) + } + + private void handle(IPayloadContext ctx) { + // 接收 clientbound 表示本侧为客户端,使用已采纳的客户端表;serverbound 则本侧为服务端,使用权威表 + RpcRegistry registry = ctx.flow().isClientbound() ? AnvilLibRpcClient.REGISTRY : AnvilLibRpc.REGISTRY; + RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf( + Unpooled.wrappedBuffer(this.data), ctx.player().registryAccess(), ConnectionType.NEOFORGE + ); + Method method = registry.byIndex(buf.readVarInt()); + Object[] args = IRpcPayload.decodeParams(method, buf); + if (!RpcMethods.validate(method, ctx, args)) { + // 校验未通过:静默丢弃该单向调用 + return; + } + try { + method.invoke(null, args); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Cannot invoke RPC method " + method, e); + } catch (InvocationTargetException e) { + throw new RuntimeException("RPC method " + method + " threw an exception", e.getCause()); + } + } + + @Override + public CustomPacketPayload.Type type() { + return RpcPayload.TYPE; + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcPendingCalls.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcPendingCalls.java new file mode 100644 index 00000000..899e4e33 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcPendingCalls.java @@ -0,0 +1,109 @@ +package dev.anvilcraft.lib.v2.rpc; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 一侧(caller)待响应的 {@link RPC#invoke} 调用登记表。 + * + *

每次 invoke 分配一个自增 {@code callId},登记 {@code callId -> (目标方法, future, 登记时的 tick)}; + * 当对端返回携带相同 {@code callId} 的响应时,按其完成对应的 {@link CompletableFuture}。目标方法用于在解码 + * 响应时确定返回值编解码器。

+ * + *

超时

+ *

由所属侧的 tick 事件驱动 {@link #tick()}:超过 {@link #TIMEOUT_TICKS} 个 tick 仍未收到响应的调用, + * 其 future 以 {@link TimeoutException} 失败并从表中移除,避免泄漏。

+ * + *

双实例

+ *

与 {@link RpcRegistry} 同理,服务端 / 客户端各持一个实例({@link AnvilLibRpc#PENDING} / + * {@code AnvilLibRpcClient.PENDING}),各由自身的 tick 事件驱动。否则在单人 / 局域网(同 JVM)下两侧的 + * {@code callId} 会相互覆盖。响应处理时按 {@code ctx.flow()} 选择实例。

+ */ +@ApiStatus.Internal +public final class RpcPendingCalls { + /** + * 调用超时的 tick 数:超过该时长仍无响应即判定超时。 + */ + public static final int TIMEOUT_TICKS = 100; + + private final AtomicInteger nextId = new AtomicInteger(); + private final Map pending = new ConcurrentHashMap<>(); + /** + * 自增的 tick 计数器,作为登记项的时间戳基准。 + */ + private final AtomicInteger currentTick = new AtomicInteger(0); + + /** + * 构造一个空的登记表。 + */ + public RpcPendingCalls() { + } + + /** + * 登记一次待响应调用。 + * + * @param method 目标方法 + * @param future 调用完成时要兑现的 future + * @return 分配的 callId + */ + int register(Method method, CompletableFuture future) { + int id = nextId.getAndIncrement(); + pending.put(id, new Pending(method, future, currentTick.get())); + return id; + } + + /** + * 取出(并移除)指定 callId 的登记项。 + * + * @param callId 调用 id + * @return 登记项;若不存在(如超时已移除)返回 {@code null} + */ + @Nullable Pending remove(int callId) { + return pending.remove(callId); + } + + /** + * 由所属侧的 tick 事件每 tick 调用一次:推进计时并使超时调用失败。 + */ + @ApiStatus.Internal + public void tick() { + int now = currentTick.incrementAndGet(); + Iterator> it = pending.entrySet().iterator(); + while (it.hasNext()) { + Pending entry = it.next().getValue(); + if (now - entry.registeredTick() < TIMEOUT_TICKS) continue; + it.remove(); + entry.future() + .completeExceptionally(new TimeoutException("RPC call timed out after " + TIMEOUT_TICKS + " ticks: " + entry.method())); + } + } + + /** + * 清空登记表,并使所有未完成的调用以异常失败。 + * + *

在所属侧的连接终止时调用(服务端 {@code ServerStoppedEvent}、客户端 + * {@code ClientPlayerNetworkEvent.LoggingOut}),避免遗留的 future 永久挂起或残留到下一次会话。

+ */ + @ApiStatus.Internal + public void clear() { + Iterator> it = pending.entrySet().iterator(); + while (it.hasNext()) { + Pending entry = it.next().getValue(); + it.remove(); + entry.future().completeExceptionally(new CancellationException("RPC connection closed before response")); + } + currentTick.set(0); + } + + record Pending(Method method, CompletableFuture future, int registeredTick) { + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcRegistry.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcRegistry.java new file mode 100644 index 00000000..ad15a860 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcRegistry.java @@ -0,0 +1,163 @@ +package dev.anvilcraft.lib.v2.rpc; + +import lombok.extern.slf4j.Slf4j; +import net.neoforged.fml.loading.LoadingModList; +import net.neoforged.fml.loading.moddiscovery.ModFileInfo; +import net.neoforged.neoforgespi.language.ModFileScanData; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.lang.annotation.ElementType; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * {@link RemoteCallable} 方法的索引表。索引以「规范键」{@code 全限定类名 + '#' + 方法名 + 方法描述符} + * 为单位分配,网络包仅传输整数索引。 + * + *

双实例与一致性

+ *

本类按服务端 / 客户端各维护一个实例:

+ *
    + *
  • 权威实例({@link AnvilLibRpc#REGISTRY}):由本地扫描填充,从不被 + * {@link #adopt(Map)} 覆盖。服务端在 Configuration 阶段用 {@link #snapshot()} 将其映射下发给客户端。
  • + *
  • 客户端实例({@code AnvilLibRpcClient.REGISTRY}):仅通过 {@link #adopt(Map)} 采纳服务端 + * 下发的映射。
  • + *
+ * + *

由此即便客户端断开某服务器后再开局域网,权威实例仍是本地扫描结果(未被污染),不会把上一个服务器的 + * 映射当作自己的下发出去。编解码时按方向选择实例(发送方向决定编码端、{@code ctx.flow()} 决定解码端), + * 双向 RPC 的索引始终一致。

+ */ +@Slf4j +@ApiStatus.Internal +public final class RpcRegistry { + private static final String ANNOTATION_DESCRIPTOR = "L" + RemoteCallable.class.getName().replace('.', '/') + ";"; + + /** + * 是否为权威实例:权威实例在未初始化时执行本地扫描,非权威(客户端)实例则要求先经 + * {@link #adopt(Map)} 采纳服务端映射。 + */ + private final boolean authoritative; + private @Nullable Map indexByKey; + private @Nullable Map keyByIndex; + + /** + * @param authoritative {@code true} 为权威(服务端)实例,按需本地扫描;{@code false} 为客户端实例,仅采纳下发 + */ + public RpcRegistry(boolean authoritative) { + this.authoritative = authoritative; + } + + private static String canonicalKey(Method method) { + return method.getDeclaringClass().getName() + "#" + method.getName() + RpcMethods.methodDescriptor(method); + } + + private static Method resolve(String key) { + int hash = key.indexOf('#'); + int paren = key.indexOf('(', hash); + String className = key.substring(0, hash); + String methodName = key.substring(hash + 1, paren); + String descriptor = key.substring(paren); + return RpcMethods.resolve(className, methodName, descriptor); + } + + /** + * 返回方法对应的整数索引。 + * + * @param method 已注册的 {@link RemoteCallable} 方法 + * @return 该方法的索引 + */ + public synchronized int index(Method method) { + ensureLoaded(); + String key = canonicalKey(method); + assert indexByKey != null; + Integer index = indexByKey.get(key); + if (index == null) { + throw new IllegalStateException("Method is not a registered @RemoteCallable: " + key); + } + return index; + } + + /** + * 返回索引对应的方法。 + * + * @param index 方法索引 + * @return 对应的 {@link RemoteCallable} 方法 + */ + public synchronized Method byIndex(int index) { + ensureLoaded(); + assert keyByIndex != null; + String key = keyByIndex.get(index); + if (key == null) { + throw new IllegalStateException("Unknown RPC method index: " + index + " (registered: " + keyByIndex.size() + ")"); + } + return resolve(key); + } + + /** + * 返回当前 {@code 索引 -> 规范键} 映射的快照,用于在 Configuration 阶段下发给客户端。 + * + * @return 索引到规范键的映射 + */ + public synchronized Map snapshot() { + ensureLoaded(); + assert keyByIndex != null; + return new HashMap<>(keyByIndex); + } + + /** + * 用给定(服务端权威)映射覆盖本实例的索引表。 + * + * @param map 服务端下发的 {@code 索引 -> 规范键} 映射 + */ + public synchronized void adopt(Map map) { + Map byIndex = new HashMap<>(map); + Map byKey = new HashMap<>(); + byIndex.forEach((index, key) -> byKey.put(key, index)); + this.keyByIndex = byIndex; + this.indexByKey = byKey; + log.debug("Adopted {} authoritative @RemoteCallable index mapping(s)", byIndex.size()); + } + + private void ensureLoaded() { + if (indexByKey != null) return; + if (!authoritative) { + throw new IllegalStateException("RPC index mapping has not been received from the server yet"); + } + scan(); + } + + private void scan() { + List keys = getKeys(); + // 排序仅为本地索引分配的确定性与日志可读性;跨端一致性由服务端下发映射保证 + keys.sort(null); + + Map byIndex = new HashMap<>(); + Map byKey = new HashMap<>(); + for (int i = 0; i < keys.size(); i++) { + byIndex.put(i, keys.get(i)); + byKey.put(keys.get(i), i); + log.debug("Registered @RemoteCallable [{}] {}", i, keys.get(i)); + } + this.keyByIndex = byIndex; + this.indexByKey = byKey; + log.info("Scan complete - {} @RemoteCallable method(s) registered.", keys.size()); + } + + @SuppressWarnings("UnstableApiUsage") + private static List getKeys() { + List keys = new ArrayList<>(); + for (ModFileInfo fileInfo : LoadingModList.get().getModFiles()) { + for (ModFileScanData.AnnotationData annotation : fileInfo.getFile().getScanResult().getAnnotations()) { + if (!annotation.annotationType().getDescriptor().equals(ANNOTATION_DESCRIPTOR)) continue; + if (annotation.targetType() != ElementType.METHOD) continue; + // memberName 形如 "methodName(Ljava/lang/String;I)V" + keys.add(annotation.clazz().getClassName() + "#" + annotation.memberName()); + } + } + return keys; + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcRequestPayload.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcRequestPayload.java new file mode 100644 index 00000000..a87df604 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcRequestPayload.java @@ -0,0 +1,109 @@ +package dev.anvilcraft.lib.v2.rpc; + +import dev.anvilcraft.lib.v2.network.packet.IInsensitiveBiPacket; +import dev.anvilcraft.lib.v2.network.packet.IPacket; +import dev.anvilcraft.lib.v2.rpc.client.AnvilLibRpcClient; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.connection.ConnectionType; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.ApiStatus; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * 有返回值远程调用的请求包:携带 {@code callId}、方法索引与实参。接收端执行方法后,回复一个携带相同 + * {@code callId} 的 {@link RpcResponsePayload}。 + * + * @see RPC#invoke + */ +@ApiStatus.Internal +public class RpcRequestPayload implements IRpcPayload, IInsensitiveBiPacket { + public static final Type TYPE = IPacket.type(AnvilLibRpc.mod("rpc_request")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.BYTE_ARRAY, + RpcRequestPayload::data, + RpcRequestPayload::new + ); + + private final byte[] data; + + RpcRequestPayload(byte[] data) { + this.data = data; + } + + /** + * 在发送侧编码请求。 + * + * @param registry 发送侧索引表 + * @param registryAccess 发送侧注册表访问器 + * @param callId 调用 id + * @param method 目标方法 + * @param args 实参 + * @return 编码后的请求包 + */ + static RpcRequestPayload encode( + RpcRegistry registry, RegistryAccess registryAccess, int callId, Method method, Object[] args + ) { + RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf(Unpooled.buffer(), registryAccess, ConnectionType.NEOFORGE); + buf.writeVarInt(callId); + byte[] data = IRpcPayload.encodeParams(registry, method, args, buf); + return new RpcRequestPayload(data); + } + + byte[] data() { + return this.data; + } + + @Override + public void bidirectionalHandler(IPayloadContext ctx) { + ctx.enqueueWork(() -> this.handle(ctx)); + } + + @Override + public void handleOnBothSide(Player player) { + // 实际处理在 handle(ctx) 中完成 + } + + private void handle(IPayloadContext ctx) { + // 收到 clientbound 请求表示本侧为客户端,使用已采纳的客户端索引表;否则为服务端权威表 + boolean onClient = ctx.flow().isClientbound(); + RpcRegistry registry = onClient ? AnvilLibRpcClient.REGISTRY : AnvilLibRpc.REGISTRY; + RegistryAccess registryAccess = ctx.player().registryAccess(); + RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf( + Unpooled.wrappedBuffer(this.data), registryAccess, ConnectionType.NEOFORGE + ); + int callId = buf.readVarInt(); + Method method = registry.byIndex(buf.readVarInt()); + Object[] args = IRpcPayload.decodeParams(method, buf); + if (!RpcMethods.validate(method, ctx, args)) { + ctx.reply(RpcResponsePayload.failure(callId, "RPC call rejected by validator: " + method)); + return; + } + + Object result; + try { + result = method.invoke(null, args); + } catch (IllegalAccessException e) { + ctx.reply(RpcResponsePayload.failure(callId, "Cannot invoke RPC method " + method)); + throw new IllegalStateException("Cannot invoke RPC method " + method, e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + ctx.reply(RpcResponsePayload.failure(callId, String.valueOf(cause))); + throw new RuntimeException("RPC method " + method + " threw an exception", cause); + } + ctx.reply(RpcResponsePayload.success(registry, registryAccess, callId, method, result)); + } + + @Override + public CustomPacketPayload.Type type() { + return RpcRequestPayload.TYPE; + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcResponsePayload.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcResponsePayload.java new file mode 100644 index 00000000..a9400137 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcResponsePayload.java @@ -0,0 +1,113 @@ +package dev.anvilcraft.lib.v2.rpc; + +import dev.anvilcraft.lib.v2.network.packet.IInsensitiveBiPacket; +import dev.anvilcraft.lib.v2.network.packet.IPacket; +import dev.anvilcraft.lib.v2.rpc.client.AnvilLibRpcClient; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.connection.ConnectionType; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.ApiStatus; + +import java.lang.reflect.Method; +import java.util.concurrent.CompletionException; + +/** + * 有返回值远程调用的响应包:携带请求的 {@code callId}、成功标志,以及编码后的返回值或错误信息。 + * + *

caller 侧据 {@code callId} 在 {@link RpcPendingCalls} 中找到对应 future 与目标方法,用方法的返回值 + * 编解码器({@link RpcMethods#returnCodec})解码返回值并完成 future。

+ * + * @see RPC#invoke + */ +@ApiStatus.Internal +public class RpcResponsePayload implements IInsensitiveBiPacket { + public static final Type TYPE = IPacket.type(AnvilLibRpc.mod("rpc_response")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.BYTE_ARRAY, + RpcResponsePayload::data, + RpcResponsePayload::new + ); + + private final byte[] data; + + RpcResponsePayload(byte[] data) { + this.data = data; + } + + /** + * 编码一个成功响应。 + */ + static RpcResponsePayload success(RpcRegistry registry, RegistryAccess registryAccess, int callId, Method method, Object result) { + RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf(Unpooled.buffer(), registryAccess, ConnectionType.NEOFORGE); + buf.writeVarInt(callId); + buf.writeBoolean(true); + RpcMethods.returnCodec(method).encode(buf, result); + return read(buf); + } + + /** + * 编码一个失败响应,仅携带错误信息文本。 + */ + static RpcResponsePayload failure(int callId, String message) { + RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf(Unpooled.buffer(), RegistryAccess.EMPTY, ConnectionType.NEOFORGE); + buf.writeVarInt(callId); + buf.writeBoolean(false); + buf.writeUtf(message); + return read(buf); + } + + private static RpcResponsePayload read(RegistryFriendlyByteBuf buf) { + byte[] data = new byte[buf.readableBytes()]; + buf.readBytes(data); + return new RpcResponsePayload(data); + } + + byte[] data() { + return this.data; + } + + @Override + public void bidirectionalHandler(IPayloadContext ctx) { + ctx.enqueueWork(() -> this.handle(ctx)); + } + + @Override + public void handleOnBothSide(Player player) { + // 实际处理在 handle(ctx) 中完成 + } + + private void handle(IPayloadContext ctx) { + // 收到 clientbound 响应表示本侧为发起 invoke 的客户端 + RpcPendingCalls pending = ctx.flow().isClientbound() ? AnvilLibRpcClient.PENDING : AnvilLibRpc.PENDING; + RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf( + Unpooled.wrappedBuffer(this.data), + ctx.player().registryAccess(), + ConnectionType.NEOFORGE + ); + int callId = buf.readVarInt(); + RpcPendingCalls.Pending entry = pending.remove(callId); + if (entry == null) { + // 调用方未登记该 callId(可能已超时移除),忽略 + return; + } + boolean ok = buf.readBoolean(); + if (!ok) { + entry.future().completeExceptionally(new CompletionException("Remote RPC failed: " + buf.readUtf(), null)); + return; + } + Object result = RpcMethods.returnCodec(entry.method()).decode(buf); + entry.future().complete(result); + } + + @Override + public CustomPacketPayload.Type type() { + return RpcResponsePayload.TYPE; + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcTarget.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcTarget.java new file mode 100644 index 00000000..1f3aa2ee --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcTarget.java @@ -0,0 +1,81 @@ +package dev.anvilcraft.lib.v2.rpc; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.neoforge.network.PacketDistributor; + +/** + * 一次远程调用的目标端,决定 {@link RPC#call} 产生的网络包发往何处,并提供对应发送方向的编码上下文。 + * + * @see #player(ServerPlayer) + * @see #server() + */ +public interface RpcTarget { + /** + * 目标端为指定客户端玩家(服务端 -> 客户端)。 + * + * @param player 接收远程调用的服务端玩家 + * @return 指向该玩家的目标端 + */ + static RpcTarget player(ServerPlayer player) { + return new RpcTarget() { + @Override + public void send(CustomPacketPayload payload) { + PacketDistributor.sendToPlayer(player, payload); + } + + @Override + public RpcRegistry registry() { + return AnvilLibRpc.REGISTRY; + } + + @Override + public RegistryAccess registryAccess() { + return player.registryAccess(); + } + + @Override + public RpcPendingCalls pending() { + return AnvilLibRpc.PENDING; + } + }; + } + + /** + * 目标端为服务端(客户端 -> 服务端)。仅可在客户端调用。 + * + * @return 指向服务端的目标端 + */ + static RpcTarget server() { + return ClientRpcTargets.server(); + } + + /** + * 将网络包发送至该目标端。 + * + * @param payload 远程调用网络包 + */ + void send(CustomPacketPayload payload); + + /** + * 编码本次调用应使用的索引表(由发送方向决定:服务端发送用权威表,客户端发送用已采纳表)。 + * + * @return 发送侧索引表 + */ + RpcRegistry registry(); + + /** + * 编码所用的注册表访问器,用于构造可承载注册表对象的缓冲区。 + * + * @return 发送侧注册表访问器 + */ + RegistryAccess registryAccess(); + + /** + * 发起方(本侧)待响应调用登记表,用于 {@link RPC#invoke} 关联响应。 + * + * @return 发送侧的 pending 表 + */ + RpcPendingCalls pending(); +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/client/AnvilLibRpcClient.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/client/AnvilLibRpcClient.java new file mode 100644 index 00000000..44127fdd --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/client/AnvilLibRpcClient.java @@ -0,0 +1,55 @@ +package dev.anvilcraft.lib.v2.rpc.client; + +import dev.anvilcraft.lib.v2.rpc.AnvilLibRpc; +import dev.anvilcraft.lib.v2.rpc.RpcPendingCalls; +import dev.anvilcraft.lib.v2.rpc.RpcRegistry; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.common.NeoForge; +import org.jetbrains.annotations.ApiStatus; + +/** + * 客户端侧入口,持有仅采纳服务端下发映射的客户端索引表。 + * + *

与权威实例 {@link AnvilLibRpc#REGISTRY} 分离,避免客户端连接某服务器后采纳的映射污染本地权威映射—— + * 从而保证客户端断开后开启局域网时,集成服务端仍下发其本地扫描得到的正确映射。

+ */ +@Mod(value = AnvilLibRpc.MOD_ID, dist = Dist.CLIENT) +@ApiStatus.Internal +public class AnvilLibRpcClient { + /** + * 客户端索引表:仅通过 {@link RpcRegistry#adopt} 采纳服务端下发的映射。 + */ + public static final RpcRegistry REGISTRY = new RpcRegistry(false); + + /** + * 客户端侧待响应的 {@link dev.anvilcraft.lib.v2.rpc.RPC#invoke} 调用登记表。 + */ + public static final RpcPendingCalls PENDING = new RpcPendingCalls(); + + public AnvilLibRpcClient(IEventBus modEventBus, ModContainer modContainer) { + NeoForge.EVENT_BUS.addListener(this::onClientTick); + NeoForge.EVENT_BUS.addListener(this::onLoggingOut); + } + + /** + * 驱动客户端侧 {@link dev.anvilcraft.lib.v2.rpc.RPC#invoke} 调用的超时检查。 + */ + @SubscribeEvent + public void onClientTick(ClientTickEvent.Post event) { + AnvilLibRpcClient.PENDING.tick(); + } + + /** + * 客户端登出时清理客户端侧未完成的 {@link dev.anvilcraft.lib.v2.rpc.RPC#invoke} 调用。 + */ + @SubscribeEvent + public void onLoggingOut(ClientPlayerNetworkEvent.LoggingOut event) { + AnvilLibRpcClient.PENDING.clear(); + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/client/package-info.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/client/package-info.java new file mode 100644 index 00000000..8a6ca921 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/client/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.rpc.client; + +import org.jspecify.annotations.NullMarked; diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/RpcConfigurationFinishPayload.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/RpcConfigurationFinishPayload.java new file mode 100644 index 00000000..d6d31005 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/RpcConfigurationFinishPayload.java @@ -0,0 +1,34 @@ +package dev.anvilcraft.lib.v2.rpc.config; + +import dev.anvilcraft.lib.v2.network.packet.IPacket; +import dev.anvilcraft.lib.v2.network.packet.IServerboundPacket; +import dev.anvilcraft.lib.v2.rpc.AnvilLibRpc; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.ApiStatus; + +/** + * 客户端在采纳服务端 RPC 索引映射后回复的结束包,用于标记 {@link RpcConfigurationTask} 完成。 + */ +@ApiStatus.Internal +public class RpcConfigurationFinishPayload implements IServerboundPacket { + public static final RpcConfigurationFinishPayload INSTANCE = new RpcConfigurationFinishPayload(); + public static final Type TYPE = IPacket.type(AnvilLibRpc.mod("rpc_config_finish")); + public static final StreamCodec STREAM_CODEC = StreamCodec.unit(INSTANCE); + + @Override + public void serverHandler(IPayloadContext ctx) { + ctx.finishCurrentTask(RpcConfigurationTask.TYPE); + } + + @Override + public void handleOnServer(Player player) { + } + + @Override + public Type type() { + return RpcConfigurationFinishPayload.TYPE; + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/RpcConfigurationPayload.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/RpcConfigurationPayload.java new file mode 100644 index 00000000..9a101d52 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/RpcConfigurationPayload.java @@ -0,0 +1,47 @@ +package dev.anvilcraft.lib.v2.rpc.config; + +import dev.anvilcraft.lib.v2.network.packet.IClientboundPacket; +import dev.anvilcraft.lib.v2.network.packet.IPacket; +import dev.anvilcraft.lib.v2.rpc.AnvilLibRpc; +import dev.anvilcraft.lib.v2.rpc.client.AnvilLibRpcClient; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.ApiStatus; + +import java.util.HashMap; +import java.util.Map; + +/** + * Configuration 阶段由服务端下发给客户端的 RPC 索引映射包。 + * + *

客户端收到后用其覆盖本地索引表({@link dev.anvilcraft.lib.v2.rpc.RpcRegistry#adopt(Map)}),从而与服务端共享同一套 + * {@code 索引 -> 规范键} 映射,保证双向 RPC 的索引一致;随后回复 + * {@link RpcConfigurationFinishPayload} 以结束该配置任务。

+ */ +@ApiStatus.Internal +public record RpcConfigurationPayload(Map indexMap) implements IClientboundPacket { + public static final Type TYPE = IPacket.type(AnvilLibRpc.mod("rpc_config")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.map(HashMap::new, ByteBufCodecs.VAR_INT, ByteBufCodecs.STRING_UTF8), + RpcConfigurationPayload::indexMap, + RpcConfigurationPayload::new + ); + + @Override + public void clientHandler(IPayloadContext ctx) { + ctx.enqueueWork(() -> AnvilLibRpcClient.REGISTRY.adopt(this.indexMap())); + ctx.reply(RpcConfigurationFinishPayload.INSTANCE); + } + + @Override + public void handleOnClient(Player player) { + } + + @Override + public Type type() { + return RpcConfigurationPayload.TYPE; + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/RpcConfigurationTask.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/RpcConfigurationTask.java new file mode 100644 index 00000000..f234807e --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/RpcConfigurationTask.java @@ -0,0 +1,26 @@ +package dev.anvilcraft.lib.v2.rpc.config; + +import dev.anvilcraft.lib.v2.rpc.AnvilLibRpc; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.neoforged.neoforge.network.configuration.ICustomConfigurationTask; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Consumer; + +/** + * 服务端配置任务:在 Configuration 阶段向客户端下发权威的 RPC 索引映射。 + */ +@ApiStatus.Internal +public record RpcConfigurationTask() implements ICustomConfigurationTask { + public static final Type TYPE = new Type(AnvilLibRpc.mod("rpc_config")); + + @Override + public void run(Consumer sender) { + sender.accept(new RpcConfigurationPayload(AnvilLibRpc.REGISTRY.snapshot())); + } + + @Override + public Type type() { + return RpcConfigurationTask.TYPE; + } +} diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/package-info.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/package-info.java new file mode 100644 index 00000000..4f5e749f --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/package-info.java @@ -0,0 +1,7 @@ +@NullMarked +@Network(protocol = PacketProtocol.CONFIGURATION) +package dev.anvilcraft.lib.v2.rpc.config; + +import dev.anvilcraft.lib.v2.network.register.Network; +import dev.anvilcraft.lib.v2.network.register.PacketProtocol; +import org.jspecify.annotations.NullMarked; diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/package-info.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/package-info.java new file mode 100644 index 00000000..e535f4d1 --- /dev/null +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/package-info.java @@ -0,0 +1,7 @@ +@NullMarked +@Network(protocol = PacketProtocol.PLAY) +package dev.anvilcraft.lib.v2.rpc; + +import dev.anvilcraft.lib.v2.network.register.Network; +import dev.anvilcraft.lib.v2.network.register.PacketProtocol; +import org.jspecify.annotations.NullMarked; diff --git a/module.rpc/src/main/resources/META-INF/neoforge.mods.toml b/module.rpc/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..70445b6c --- /dev/null +++ b/module.rpc/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,86 @@ +# This is an example mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader = "javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. +loaderVersion = "${loader_version_range}" #mandatory +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license = "${mod_license}" +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory +# The modid of the mod +modId = "${mod_id}" #mandatory +# The version number of the mod +version = "${mod_version}" #mandatory +# A display name for the mod +displayName = "${mod_name}" #mandatory +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional +# A URL for the "homepage" for this mod, displayed in the mod UI +displayURL = "https://github.com/Anvil-Dev/AnvilLib" +# A file name (in the root of the mod JAR) containing a logo for display +logoFile = "icon.png" #optional +# A text field displayed in the mod UI +#credits="" #optional +# A text field displayed in the mod UI +authors = "${mod_authors}" #optional +# Display Test controls the display for your mod in the server connection screen +# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. +# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. +# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. +# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. +# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. +#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) + +# The description text for the mod (multi line!) (#mandatory) +description = '''${mod_description}''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +[[mixins]] +config = "${mod_id}.mixins.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies."${mod_id}"]] #optional +# the modid of the dependency +modId = "neoforge" #mandatory +# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). +# 'required' requires the mod to exist, 'optional' does not +# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning +type = "required" #mandatory +# Optional field describing why the dependency is required or why it is incompatible +# reason="..." +# The version range of the dependency +versionRange = "${neo_version_range}" #mandatory +# An ordering relationship for the dependency. +# BEFORE - This mod is loaded BEFORE the dependency +# AFTER - This mod is loaded AFTER the dependency +ordering = "NONE" +# Side this dependency is applied on - BOTH, CLIENT, or SERVER +side = "BOTH" +# Here's another dependency +[[dependencies."${mod_id}"]] +modId = "minecraft" +type = "required" +# This version range declares a minimum of the current minecraft version up to but not including the next major version +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features."${mod_id}"] +#openGLVersion="[3.2,)" diff --git a/module.rpc/src/main/resources/anvillib_rpc.mixins.json b/module.rpc/src/main/resources/anvillib_rpc.mixins.json new file mode 100644 index 00000000..a7fbf534 --- /dev/null +++ b/module.rpc/src/main/resources/anvillib_rpc.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.anvilcraft.lib.v2.rpc.mixin", + "compatibilityLevel": "JAVA_8", + "refmap": "anvillib.refmap.json", + "mixins": [ + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/module.rpc/src/main/resources/icon.png b/module.rpc/src/main/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..15304bd54d5d97d8482049d5f16a7544748ce05a GIT binary patch literal 1089 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabRA=0U}gyL32}YB&qp?9cb}fRhL)ab z>**&8P1h&;i5V@+5z~{9lu=Mp{cG+rNOuP4jFK;J~t_xRpI&Nf4%?X zZmY?-7zkw=;bC zsjT>Uk~ZUn{GF#RHtMc+sGWYc$A>9-;igj6hzoLO7wEVa*QxB)zM=ff)bhV&`;h}b zD&$S4O;$O;Sf&`=w_-tNu&sr4nCukimfSM|=84-o^=H0sFgVR)oOSu;mLIn4lUH@E zPWkvN(C^lx3(G6wS;EsJIMe%PuP~i06!s*lLHg)A!3fTUY`(JJ860vaMb2DUI{zB$ z25}G0l9!&RrHovbwaBi{I35yXA*NOEX}{&%4{Sd|jrPVzaJhpb&(qb Date: Mon, 10 Aug 2026 22:51:24 +0800 Subject: [PATCH 04/38] =?UTF-8?q?feat(sync):=20=E5=9B=9E=E7=A7=BB=E6=A4=8D?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=A1=86=E6=9E=B6=20module.sync=20+=20proces?= =?UTF-8?q?sor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 dev/26.1 的 module.sync(3ce2611、bf49789、8d57cba、1063ee9 #65)整体回移植, 作为新模块 anvillib-sync-neoforge-1.21.1 + 内嵌 anvillib-sync-processor-neoforge-1.21.1, 实现 @Sync 注解字段双向同步、LazySync 惰性差分同步与 Configuration 阶段配置表下发。 API 适配(26.1 → 1.21.1): - Identifier → ResourceLocation - FMLLoader.getCurrent().getLoadingModList() → LoadingModList.get() - ClientPacketDistributor(26.1 新增)→ PacketDistributor.sendToServer - ByteBufCodecs.LONG/LONG_ARRAY(26.1 新增)→ VAR_LONG / VarInt+VarLong 手写 - ItemStackTemplate.STREAM_CODEC 分支删除(1.21.1 ItemStack.OPTIONAL_STREAM_CODEC 等价) - IModFile.getContents()(26.1 新增)→ getSecureJar().getPath + Files.newInputStream - ServerPlayer.level()(26.1 改名)→ serverLevel() - ChunkPos.containing(BlockPos)(26.1 新增)→ new ChunkPos(blockPos) - ClientLevel.getEntities().get(UUID)(1.21.1 为 protected)→ entitiesForRendering() 遍历 - Java 22 lambda 参数 _ 改名为 ignored processor 字节码注入从 26.1 专属 neoforgespi.transformation SPI 重写为 1.21.1 CoreMod(ICoreMod + ITransformer,services 注册 net.neoforged.neoforgespi.coremod.ICoreMod),注入器(SyncBytecodeInjector / LazySyncBytecodeInjector)纯 ASM 逻辑原样复用。 构建:主模块 jarJar 依赖 processor/codec/network/util(-1.21.1 坐标); processor 子项目 Java 21、jreleaser 按 port 侧直接坐标写法。 --- module.sync/build.gradle | 283 +++++++++++++++++ module.sync/gradle.properties | 4 + module.sync/processor/build.gradle | 159 ++++++++++ module.sync/processor/gradle.properties | 4 + .../transform/LazySyncBytecodeInjector.java | 141 +++++++++ .../sync/transform/LazySyncTargetIndex.java | 59 ++++ .../sync/transform/SyncBytecodeInjector.java | 241 ++++++++++++++ .../v2/sync/transform/SyncClassProcessor.java | 77 +++++ .../v2/sync/transform/SyncTargetIndex.java | 68 ++++ .../lib/v2/sync/transform/package-info.java | 4 + ...net.neoforged.neoforgespi.coremod.ICoreMod | 1 + .../anvilcraft/lib/v2/sync/AnvilLibSync.java | 91 ++++++ .../lib/v2/sync/annotation/LazySync.java | 31 ++ .../lib/v2/sync/annotation/Sync.java | 11 + .../lib/v2/sync/annotation/package-info.java | 4 + .../v2/sync/client/AnvilLibSyncClient.java | 38 +++ .../lib/v2/sync/init/AnvilLibSyncEntries.java | 56 ++++ .../v2/sync/init/AnvilLibSyncRegistries.java | 27 ++ .../lib/v2/sync/init/package-info.java | 4 + .../v2/sync/management/LazySyncManager.java | 295 ++++++++++++++++++ .../v2/sync/management/SyncConfigManager.java | 129 ++++++++ .../lib/v2/sync/management/SyncManager.java | 66 ++++ .../lib/v2/sync/management/SyncProxy.java | 244 +++++++++++++++ .../v2/sync/management/SyncRegisterEntry.java | 62 ++++ .../lib/v2/sync/management/package-info.java | 4 + .../v2/sync/network/AnvilLibSyncNetwork.java | 21 ++ .../lib/v2/sync/network/package-info.java | 4 + .../sync/network/payload/LazySyncPayload.java | 141 +++++++++ .../SyncConfigurationFinishPayload.java | 31 ++ .../payload/SyncConfigurationPayload.java | 42 +++ .../v2/sync/network/payload/SyncPayload.java | 99 ++++++ .../v2/sync/network/payload/package-info.java | 7 + .../anvilcraft/lib/v2/sync/package-info.java | 4 + .../anvilcraft/lib/v2/sync/util/SideUtil.java | 165 ++++++++++ .../lib/v2/sync/util/SyncDirection.java | 17 + .../lib/v2/sync/util/package-info.java | 4 + .../resources/META-INF/neoforge.mods.toml | 86 +++++ .../main/resources/anvillib_sync.mixins.json | 14 + module.sync/src/main/resources/icon.png | Bin 0 -> 1089 bytes settings.gradle | 4 + 40 files changed, 2742 insertions(+) create mode 100644 module.sync/build.gradle create mode 100644 module.sync/gradle.properties create mode 100644 module.sync/processor/build.gradle create mode 100644 module.sync/processor/gradle.properties create mode 100644 module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/LazySyncBytecodeInjector.java create mode 100644 module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/LazySyncTargetIndex.java create mode 100644 module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/SyncBytecodeInjector.java create mode 100644 module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/SyncClassProcessor.java create mode 100644 module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/SyncTargetIndex.java create mode 100644 module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/package-info.java create mode 100644 module.sync/processor/src/main/resources/META-INF/services/net.neoforged.neoforgespi.coremod.ICoreMod create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/AnvilLibSync.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/LazySync.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/Sync.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/package-info.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/client/AnvilLibSyncClient.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/AnvilLibSyncEntries.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/AnvilLibSyncRegistries.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/package-info.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/LazySyncManager.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncConfigManager.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncManager.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncProxy.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncRegisterEntry.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/package-info.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/AnvilLibSyncNetwork.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/package-info.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/LazySyncPayload.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/SyncConfigurationFinishPayload.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/SyncConfigurationPayload.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/SyncPayload.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/package-info.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/package-info.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SideUtil.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SyncDirection.java create mode 100644 module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/package-info.java create mode 100644 module.sync/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 module.sync/src/main/resources/anvillib_sync.mixins.json create mode 100644 module.sync/src/main/resources/icon.png diff --git a/module.sync/build.gradle b/module.sync/build.gradle new file mode 100644 index 00000000..561b5876 --- /dev/null +++ b/module.sync/build.gradle @@ -0,0 +1,283 @@ +plugins { + id 'java-library' + id 'eclipse' + id 'idea' + id 'maven-publish' + alias libs.plugins.modDevGradle + alias libs.plugins.lombok + alias libs.plugins.machete + id 'org.jreleaser' version '1.23.0' + id "me.modmuss50.mod-publish-plugin" version "1.1.0" +} + +String buildType = 'snapshot' +String buildNumber +if (System.getenv("CI_BUILD") == 'false') buildNumber = null +else { + if (System.getenv("PR_BUILD") != 'false') buildType = 'pr' + buildNumber = System.getenv("GITHUB_RUN_NUMBER") +} +version = "${mod_version}" + (buildNumber != null ? "+${buildType}.${buildNumber}" : "") +group = mod_group_id + +base { + archivesName = "${project.mod_id}-neoforge-${libs.versions.minecraft.get()}" +} + +repositories { + maven { url = "https://server.cjsah.net:1002/maven/" } + mavenLocal() + mavenCentral() +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +neoForge { + // Specify the version of NeoForge to use. + version = libs.versions.neoForge.get() + + parchment { + mappingsVersion = project.parchment_mappings_version + minecraftVersion = project.parchment_minecraft_version + } + + // This line is optional. Access Transformers are automatically detected + // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') + + interfaceInjectionData { + // from 'src/main/resources/interface_injections.json' + // publish file('src/main/resources/interface_injections.json') + } + + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + client { + client() + + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + server { + server() + programArgument '--nogui' + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + // This run config launches GameTestServer and runs all registered gametests, then exits. + // By default, the server will crash when no gametests are provided. + // The gametest system is also enabled by default for other run configs under the /test command. + gameTestServer { + type = "gameTestServer" + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + data { + data() + + // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it + // gameDirectory = project.file('run-data') + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } + + // applies to all the run configs above + configureEach { + // Recommended logging data for a userdev environment + // The markers can be added/remove as needed separated by commas. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + systemProperty 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + logLevel = org.slf4j.event.Level.DEBUG + } + } + + mods { + // define mod <-> source bindings + // these are used to tell the game which sources are for which mod + // mostly optional in a single mod project + // but multi mod projects should define one per mod + "${mod_id}" { + sourceSet(sourceSets.main) + } + } +} + +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } + + +dependencies { + compileOnly "org.jspecify:jspecify:1.0.0" + + if (System.getenv("NOT_DEV") == 'true') { + jarJar(api("dev.anvilcraft.lib:anvillib-sync-processor-neoforge-1.21.1:latest.release")) + jarJar(api("dev.anvilcraft.lib:anvillib-codec-neoforge-1.21.1:latest.release")) + jarJar(api("dev.anvilcraft.lib:anvillib-network-neoforge-1.21.1:latest.release")) + jarJar(api("dev.anvilcraft.lib:anvillib-util-neoforge-1.21.1:latest.release")) + } else { + jarJar(implementation project(":anvillib-sync-neoforge-1.21.1:anvillib-sync-processor-neoforge-1.21.1")) + jarJar(implementation project(":anvillib-codec-neoforge-1.21.1")) + jarJar(implementation project(":anvillib-network-neoforge-1.21.1")) + jarJar(implementation project(":anvillib-util-neoforge-1.21.1")) + } +} + +// This block of code expands all declared replace properties in the specified resource targets. +// A missing property will result in an error. Properties are expanded using ${} Groovy notation. +// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. +// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html +tasks.withType(ProcessResources).configureEach { + var replaceProperties = [minecraft_version : libs.versions.minecraft.get(), + minecraft_version_range: minecraft_version_range, + neo_version : libs.versions.neoForge.get(), + neo_version_range : neo_version_range, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : version, + mod_authors : mod_authors, + mod_description : mod_description] + inputs.properties replaceProperties + + filesMatching(['META-INF/neoforge.mods.toml']) { + expand replaceProperties + } +} + +tasks.register('sourcesJar', Jar) { + from delombok.outputs.files + dependsOn delombok + archiveClassifier = "sources" +} + +tasks.register('javadocJar', Jar) { + from javadoc.destinationDir + dependsOn javadoc + archiveClassifier = "javadoc" +} + +javadoc { + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') +} + +jar { + from(rootProject.file("LICENSE")) + from(rootProject.file("ASSETS_LICENSE")) + from(rootProject.file("README.md")) + from(rootProject.file("README.en.md")) +} + +artifacts { + archives tasks.jar + archives tasks.sourcesJar + archives tasks.javadocJar +} + +def this_name = base.archivesName.get() +def this_description = mod_description + +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + + artifact sourcesJar + artifact javadocJar + + versionMapping { + allVariants { + fromResolutionResult() + } + } + + pom { + name = this_name + description = this_description + url = 'https://github.com/Anvil-Dev/AnvilLib' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/license/mit' + } + } + developers { + developer { + id = 'Anvil-Dev' + name = 'AnvilCraft Dev' + email = 'admin@anvilcraft.dev' + } + } + scm { + url = 'https://github.com/Anvil-Dev/AnvilLib' + connection = 'https://github.com/Anvil-Dev/AnvilLib.git' + developerConnection = 'https://github.com/Anvil-Dev/AnvilLib.git' + } + } + } + } + repositories { + // Add repositories to publish to here. + def MAVEN_URL = System.getenv("MAVEN_URL") + if (MAVEN_URL != null) { + maven { + url MAVEN_URL + credentials { + username System.getenv("MAVEN_USERNAME") + password System.getenv("MAVEN_PASSWORD") + } + } + } + mavenLocal() + maven { + name = "staging" + url = layout.buildDirectory.dir("staging-deploy") + } + } +} + +jreleaser { + signing { + active = 'ALWAYS' + armored = true + } + + deploy { + maven { + mavenCentral { + sonatype { + active = 'ALWAYS' + url = 'https://central.sonatype.com/api/v1/publisher' + stagingRepository('build/staging-deploy') + } + } + } + } +} + +// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} + +machete { + // disable machete locally for faster builds + enabled = false +} + +lombok { + version = "1.18.34" +} diff --git a/module.sync/gradle.properties b/module.sync/gradle.properties new file mode 100644 index 00000000..d3c2b041 --- /dev/null +++ b/module.sync/gradle.properties @@ -0,0 +1,4 @@ +## Mod Properties +mod_id=anvillib_sync +mod_name=AnvilLib-Sync +mod_description=A library for syncing data between the server and client in AnvilLib. diff --git a/module.sync/processor/build.gradle b/module.sync/processor/build.gradle new file mode 100644 index 00000000..8bf87f17 --- /dev/null +++ b/module.sync/processor/build.gradle @@ -0,0 +1,159 @@ +plugins { + id 'java' + id 'maven-publish' + alias libs.plugins.lombok + alias libs.plugins.modDevGradle + id 'org.jreleaser' version '1.23.0' +} + +String buildType = 'snapshot' +String buildNumber +if (System.getenv("CI_BUILD") == 'false') buildNumber = null else { + if (System.getenv("PR_BUILD") != 'false') buildType = 'pr' + buildNumber = System.getenv("GITHUB_RUN_NUMBER") +} +version = "${mod_version}" + (buildNumber != null ? "+${buildType}.${buildNumber}" : "") +group = mod_group_id + +base { + archivesName = "${project.mod_id}-neoforge-${libs.versions.minecraft.get()}" +} + +repositories { + mavenCentral() +} + +neoForge { + version = libs.versions.neoForge.get() +} + +dependencies { + compileOnly "org.jspecify:jspecify:1.0.0" +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +tasks.register('sourcesJar', Jar) { + from delombok.outputs.files + dependsOn delombok + archiveClassifier = "sources" +} + +tasks.register('javadocJar', Jar) { + from javadoc.destinationDir + dependsOn javadoc + archiveClassifier = "javadoc" +} + +javadoc { + // options.encoding = 'UTF-8' + options { + encoding = 'UTF-8' + addStringOption 'Xdoclint:none', '-quiet' + addStringOption 'tag', 'apiNote:a:API Note:' + addStringOption 'tag', 'implSpec:a:Implementation Requirement:' + addStringOption 'tag', 'implNote:a:Implementation Note:' + } +} + +jar { + manifest { + attributes( + 'Can-Redefine-Classes': 'false', + 'Can-Retransform-Classes': 'false', + 'Can-Set-Native-Method-Prefix': 'false' + ) + } +} + +artifacts { + archives tasks.jar + archives tasks.sourcesJar + archives tasks.javadocJar +} + +def this_name = base.archivesName.get() +def this_description = mod_description + +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + + artifact sourcesJar + artifact javadocJar + + pom { + name = this_name + description = this_description + url = 'https://github.com/Anvil-Dev/AnvilLib' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/license/mit' + } + } + developers { + developer { + id = 'Anvil-Dev' + name = 'AnvilCraft Dev' + email = 'admin@anvilcraft.dev' + } + } + scm { + url = 'https://github.com/Anvil-Dev/AnvilLib' + connection = 'https://github.com/Anvil-Dev/AnvilLib.git' + developerConnection = 'https://github.com/Anvil-Dev/AnvilLib.git' + } + } + } + } + repositories { + // Add repositories to publish to here. + def MAVEN_URL = System.getenv("MAVEN_URL") + if (MAVEN_URL != null) { + maven { + url MAVEN_URL + credentials { + username System.getenv("MAVEN_USERNAME") + password System.getenv("MAVEN_PASSWORD") + } + } + } + mavenLocal() + maven { + name = "staging" + url = layout.buildDirectory.dir("staging-deploy") + } + } +} + +jreleaser { + signing { + active = 'ALWAYS' + armored = true + } + + deploy { + maven { + mavenCentral { + sonatype { + active = 'ALWAYS' + url = 'https://central.sonatype.com/api/v1/publisher' + stagingRepository('build/staging-deploy') + } + } + } + } +} + +lombok { + version = "1.18.34" +} diff --git a/module.sync/processor/gradle.properties b/module.sync/processor/gradle.properties new file mode 100644 index 00000000..4b8c4db1 --- /dev/null +++ b/module.sync/processor/gradle.properties @@ -0,0 +1,4 @@ +## Mod Properties +mod_id=anvillib_sync_processor +mod_name=AnvilLib-Sync-Processor +mod_description=AnvilLib Sync Processor NeoForge 1.21.1 diff --git a/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/LazySyncBytecodeInjector.java b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/LazySyncBytecodeInjector.java new file mode 100644 index 00000000..c0f5fb7f --- /dev/null +++ b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/LazySyncBytecodeInjector.java @@ -0,0 +1,141 @@ +package dev.anvilcraft.lib.v2.sync.transform; + +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.ApiStatus; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** + * 为含 {@link dev.anvilcraft.lib.v2.sync.annotation.LazySync} 字段的类注入跟踪登记调用: + *
    + *
  • 含实例 {@code @LazySync} 字段 → 在每个 {@code } 末尾注入 + * {@code AnvilLibSync.LAZY_SYNC_MANAGER.track(this)};
  • + *
  • 含静态 {@code @LazySync} 字段 → 在 {@code }(不存在则创建)末尾注入 + * {@code AnvilLibSync.LAZY_SYNC_MANAGER.trackStatic(Owner.class)}。
  • + *
+ * + *

登记后由 {@link dev.anvilcraft.lib.v2.sync.management.LazySyncManager} + * 每 tick 扫描差分,故此处无需注入任何字段写入拦截。

+ */ +@Slf4j +@ApiStatus.Internal +public class LazySyncBytecodeInjector { + private static final String LAZY_SYNC_DESC = "Ldev/anvilcraft/lib/v2/sync/annotation/LazySync;"; + private static final String ANVIL_LIB_SYNC_INTERNAL = "dev/anvilcraft/lib/v2/sync/AnvilLibSync"; + private static final String MANAGER_FIELD_NAME = "LAZY_SYNC_MANAGER"; + private static final String MANAGER_DESC = "Ldev/anvilcraft/lib/v2/sync/management/LazySyncManager;"; + private static final String MANAGER_INTERNAL = "dev/anvilcraft/lib/v2/sync/management/LazySyncManager"; + private static final String TRACK_NAME = "track"; + private static final String TRACK_DESC = "(Ljava/lang/Object;)V"; + private static final String TRACK_STATIC_NAME = "trackStatic"; + private static final String TRACK_STATIC_DESC = "(Ljava/lang/Class;)V"; + + private LazySyncBytecodeInjector() { + } + + /** + * 扫描类中带 {@code @LazySync} 的字段并注入对应的跟踪登记调用。 + * + * @return 是否对类进行了修改 + */ + public static boolean inject(ClassNode classNode) { + boolean hasInstance = false; + boolean hasStatic = false; + for (FieldNode field : classNode.fields) { + if (!hasLazySync(field)) continue; + if ((field.access & Opcodes.ACC_STATIC) != 0) { + hasStatic = true; + } else { + hasInstance = true; + } + } + + if (!hasInstance && !hasStatic) { + return false; + } + + if (hasInstance) { + for (MethodNode method : classNode.methods) { + if ("".equals(method.name)) { + injectBeforeReturn(method, LazySyncBytecodeInjector::buildInstanceInstructions); + } + } + } + if (hasStatic) { + MethodNode clinit = findOrCreateClinit(classNode); + injectBeforeReturn(clinit, () -> buildStaticInstructions(classNode.name)); + } + + log.debug("Injected LazySync tracking into {} (instance={}, static={})", classNode.name, hasInstance, hasStatic); + return true; + } + + private static boolean hasLazySync(FieldNode field) { + if (field.visibleAnnotations == null) return false; + for (AnnotationNode annotation : field.visibleAnnotations) { + if (LAZY_SYNC_DESC.equals(annotation.desc)) return true; + } + return false; + } + + private static MethodNode findOrCreateClinit(ClassNode classNode) { + for (MethodNode method : classNode.methods) { + if ("".equals(method.name)) { + return method; + } + } + MethodNode clinit = new MethodNode(Opcodes.ACC_STATIC, "", "()V", null, null); + clinit.instructions.add(new InsnNode(Opcodes.RETURN)); + classNode.methods.add(clinit); + return clinit; + } + + private static void injectBeforeReturn(MethodNode method, Supplier snippet) { + List returns = new ArrayList<>(); + for (AbstractInsnNode insn : method.instructions) { + if (insn.getOpcode() == Opcodes.RETURN) { + returns.add(insn); + } + } + for (AbstractInsnNode ret : returns) { + method.instructions.insertBefore(ret, snippet.get()); + } + } + + /** + * {@code AnvilLibSync.LAZY_SYNC_MANAGER.track(this);} + */ + private static InsnList buildInstanceInstructions() { + InsnList list = new InsnList(); + list.add(new FieldInsnNode(Opcodes.GETSTATIC, ANVIL_LIB_SYNC_INTERNAL, MANAGER_FIELD_NAME, MANAGER_DESC)); + list.add(new VarInsnNode(Opcodes.ALOAD, 0)); + list.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, MANAGER_INTERNAL, TRACK_NAME, TRACK_DESC, false)); + return list; + } + + /** + * {@code AnvilLibSync.LAZY_SYNC_MANAGER.trackStatic(Owner.class);} + */ + private static InsnList buildStaticInstructions(String owner) { + InsnList list = new InsnList(); + list.add(new FieldInsnNode(Opcodes.GETSTATIC, ANVIL_LIB_SYNC_INTERNAL, MANAGER_FIELD_NAME, MANAGER_DESC)); + list.add(new LdcInsnNode(Type.getObjectType(owner))); + list.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, MANAGER_INTERNAL, TRACK_STATIC_NAME, TRACK_STATIC_DESC, false)); + return list; + } +} diff --git a/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/LazySyncTargetIndex.java b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/LazySyncTargetIndex.java new file mode 100644 index 00000000..3bb63378 --- /dev/null +++ b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/LazySyncTargetIndex.java @@ -0,0 +1,59 @@ +package dev.anvilcraft.lib.v2.sync.transform; + +import lombok.extern.slf4j.Slf4j; +import net.neoforged.fml.loading.LoadingModList; +import net.neoforged.fml.loading.moddiscovery.ModFileInfo; +import net.neoforged.neoforgespi.language.ModFileScanData; +import org.jetbrains.annotations.ApiStatus; + +import java.lang.annotation.ElementType; +import java.util.HashSet; +import java.util.Set; + +/** + * 在类加载早期扫描所有使用 {@link dev.anvilcraft.lib.v2.sync.annotation.LazySync} + * 注解的字段,记录其所属类的内部名。 + * + *

{@link SyncClassProcessor} 依此判断哪些类需要注入实例/静态跟踪调用。 + * 字段级的方向、编解码器等信息在运行时由 + * {@link dev.anvilcraft.lib.v2.sync.management.LazySyncManager} + * 通过反射读取,故此处仅需记录类的集合。

+ */ +@Slf4j +@ApiStatus.Internal +public class LazySyncTargetIndex { + public static final String LAZY_SYNC_DESCRIPTOR = "Ldev/anvilcraft/lib/v2/sync/annotation/LazySync;"; + private static final Set TARGETS = new HashSet<>(); + + static { + LazySyncTargetIndex.onLoad(); + } + + private LazySyncTargetIndex() { + } + + @SuppressWarnings("UnstableApiUsage") + private static void onLoad() { + int count = 0; + for (ModFileInfo fileInfo : LoadingModList.get().getModFiles()) { + for (ModFileScanData.AnnotationData annotation : fileInfo.getFile().getScanResult().getAnnotations()) { + if (!annotation.annotationType().getDescriptor().equals(LAZY_SYNC_DESCRIPTOR)) continue; + if (annotation.targetType() != ElementType.FIELD) continue; + String internalName = annotation.clazz().getInternalName(); + if (TARGETS.add(internalName)) { + log.info("Registered @LazySync target: {}", internalName); + } + count++; + } + } + log.info("Scan complete – {} @LazySync field(s) across {} class(es).", count, TARGETS.size()); + } + + public static boolean contains(String internalName) { + return TARGETS.contains(internalName); + } + + public static Set allTargets() { + return TARGETS; + } +} diff --git a/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/SyncBytecodeInjector.java b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/SyncBytecodeInjector.java new file mode 100644 index 00000000..dd223e1b --- /dev/null +++ b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/SyncBytecodeInjector.java @@ -0,0 +1,241 @@ +package dev.anvilcraft.lib.v2.sync.transform; + +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.ApiStatus; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** + * Injects SyncProxy#setParent(...) calls at the end of and methods + * for all final/static-final SyncProxy fields found in the target class. + *

+ * Instance fields → proxy.setParent(this) in every + * Static fields → proxy.setParent(Owner.class) in (created if absent) + */ +@Slf4j +@ApiStatus.Internal +public class SyncBytecodeInjector { + private static final String SYNC_PROXY_DESC = "Ldev/anvilcraft/lib/v2/sync/management/SyncProxy;"; + private static final String SYNC_PROXY_INTERNAL = "dev/anvilcraft/lib/v2/sync/management/SyncProxy"; + private static final String SET_PARENT_NAME = "setParent"; + private static final String SET_PARENT_DESC = "(Ljava/lang/Object;)V"; + private static final String SET_FIELD_NAME_NAME = "setFieldName"; + private static final String SET_FIELD_NAME_DESC = "(Ljava/lang/String;)V"; + private static final String SET_DIRECTION_NAME = "setDirection"; + private static final String SET_DIRECTION_DESC = "(Ldev/anvilcraft/lib/v2/sync/util/SyncDirection;)V"; + private static final String SYNC_DIRECTION_INTERNAL = "dev/anvilcraft/lib/v2/sync/util/SyncDirection"; + private static final String SYNC_DIRECTION_DESC = "Ldev/anvilcraft/lib/v2/sync/util/SyncDirection;"; + + private SyncBytecodeInjector() { + } + + /** + * Entry point. Scans the class for qualifying SyncProxy fields and injects + * setParent calls in the appropriate constructor/static initializer. + */ + public static boolean inject(ClassNode classNode) { + String owner = classNode.name; + + List instanceFields = new ArrayList<>(); + List staticFields = new ArrayList<>(); + + for (FieldNode field : classNode.fields) { + if (!field.desc.equals(SYNC_PROXY_DESC)) continue; + if ((field.access & Opcodes.ACC_FINAL) == 0) { + log.warn("SyncProxy field {}.{} is not final, skipping", owner, field.name); + continue; + } + + if ((field.access & Opcodes.ACC_STATIC) != 0) { + staticFields.add(field); + } else { + instanceFields.add(field); + } + } + + if (instanceFields.isEmpty() && staticFields.isEmpty()) { + log.info("No qualifying SyncProxy fields found in {}, skipping injection", owner); + return false; + } + + if (!instanceFields.isEmpty()) { + injectIntoInit(classNode, owner, instanceFields); + } + if (!staticFields.isEmpty()) { + injectIntoClinit(classNode, owner, staticFields); + } + + log.debug( + "Injected setParent into {} ({} instance, {} static field(s))", + owner, + instanceFields.size(), + staticFields.size() + ); + return true; + } + + // ----------------------------------------------------------------------- + // Instance constructor injection + // ----------------------------------------------------------------------- + + private static void injectIntoInit(ClassNode classNode, String owner, List fields) { + String direction = SyncTargetIndex.getDirection(classNode.name); + for (MethodNode method : classNode.methods) { + if ("".equals(method.name)) { + injectBeforeReturn(method, () -> buildInstanceInstructions(owner, fields, direction)); + } + } + } + + // ----------------------------------------------------------------------- + // Static initializer injection + // ----------------------------------------------------------------------- + + private static void injectIntoClinit(ClassNode classNode, String owner, List fields) { + String direction = SyncTargetIndex.getDirection(classNode.name); + MethodNode clinit = findOrCreateClinit(classNode); + injectBeforeReturn(clinit, () -> buildStaticInstructions(owner, fields, direction)); + } + + private static MethodNode findOrCreateClinit(ClassNode classNode) { + for (MethodNode method : classNode.methods) { + if ("".equals(method.name)) { + return method; + } + } + // No yet – create a minimal one and append it + MethodNode clinit = new MethodNode(Opcodes.ACC_STATIC, "", "()V", null, null); + clinit.instructions.add(new InsnNode(Opcodes.RETURN)); + classNode.methods.add(clinit); + return clinit; + } + + // ----------------------------------------------------------------------- + // Generic "insert before every RETURN" helper + // ----------------------------------------------------------------------- + + private static void injectBeforeReturn(MethodNode method, Supplier snippet) { + // Collect RETURN nodes first to avoid ConcurrentModificationException + List returns = new ArrayList<>(); + for (AbstractInsnNode insn : method.instructions) { + if (insn.getOpcode() == Opcodes.RETURN) { + returns.add(insn); + } + } + for (AbstractInsnNode ret : returns) { + method.instructions.insertBefore(ret, snippet.get()); + } + } + + // ----------------------------------------------------------------------- + // Bytecode snippets + // ----------------------------------------------------------------------- + + /** + * For each field: + *

+     * if (this.field != null) this.field.setParent(this);
+     * 
+ */ + private static InsnList buildInstanceInstructions(String owner, List fields, String direction) { + InsnList list = new InsnList(); + String directionName = toDirectionName(direction); + for (FieldNode field : fields) { + LabelNode skip = new LabelNode(); + + // null check: if (this.field == null) goto skip + list.add(new VarInsnNode(Opcodes.ALOAD, 0)); + list.add(new FieldInsnNode(Opcodes.GETFIELD, owner, field.name, SYNC_PROXY_DESC)); + list.add(new JumpInsnNode(Opcodes.IFNULL, skip)); + + // this.field.setParent(this) + list.add(new VarInsnNode(Opcodes.ALOAD, 0)); + list.add(new FieldInsnNode(Opcodes.GETFIELD, owner, field.name, SYNC_PROXY_DESC)); + list.add(new VarInsnNode(Opcodes.ALOAD, 0)); // "this" as the parent Object + list.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, SYNC_PROXY_INTERNAL, SET_PARENT_NAME, SET_PARENT_DESC, false)); + + // this.field.setFieldName("fieldName") + list.add(new VarInsnNode(Opcodes.ALOAD, 0)); + list.add(new FieldInsnNode(Opcodes.GETFIELD, owner, field.name, SYNC_PROXY_DESC)); + list.add(new LdcInsnNode(field.name)); + list.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, SYNC_PROXY_INTERNAL, SET_FIELD_NAME_NAME, SET_FIELD_NAME_DESC, false)); + + // this.field.setDirection(direction) + list.add(new VarInsnNode(Opcodes.ALOAD, 0)); + list.add(new FieldInsnNode(Opcodes.GETFIELD, owner, field.name, SYNC_PROXY_DESC)); + list.add(new FieldInsnNode(Opcodes.GETSTATIC, SYNC_DIRECTION_INTERNAL, directionName, SYNC_DIRECTION_DESC)); + list.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, SYNC_PROXY_INTERNAL, SET_DIRECTION_NAME, SET_DIRECTION_DESC, false)); + list.add(skip); + } + return list; + } + + /** + * For each field: + *
+     * if (Owner.field != null) Owner.field.setParent(Owner.class);
+     * 
+ */ + private static InsnList buildStaticInstructions(String owner, List fields, String direction) { + InsnList list = new InsnList(); + String directionName = toDirectionName(direction); + for (FieldNode field : fields) { + LabelNode skip = new LabelNode(); + + // null check: if (Owner.field == null) goto skip + list.add(new FieldInsnNode(Opcodes.GETSTATIC, owner, field.name, SYNC_PROXY_DESC)); + list.add(new JumpInsnNode(Opcodes.IFNULL, skip)); + + // Owner.field.setParent(Owner.class) + list.add(new FieldInsnNode(Opcodes.GETSTATIC, owner, field.name, SYNC_PROXY_DESC)); + list.add(new LdcInsnNode(Type.getObjectType(owner))); + list.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, SYNC_PROXY_INTERNAL, SET_PARENT_NAME, SET_PARENT_DESC, false)); + + // Owner.field.setFieldName("fieldName") + list.add(new FieldInsnNode(Opcodes.GETSTATIC, owner, field.name, SYNC_PROXY_DESC)); + list.add(new LdcInsnNode(field.name)); + list.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, SYNC_PROXY_INTERNAL, SET_FIELD_NAME_NAME, SET_FIELD_NAME_DESC, false)); + + // Owner.field.setDirection(direction) + list.add(new FieldInsnNode(Opcodes.GETSTATIC, owner, field.name, SYNC_PROXY_DESC)); + list.add(new FieldInsnNode(Opcodes.GETSTATIC, SYNC_DIRECTION_INTERNAL, directionName, SYNC_DIRECTION_DESC)); + list.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, SYNC_PROXY_INTERNAL, SET_DIRECTION_NAME, SET_DIRECTION_DESC, false)); + + list.add(skip); + } + return list; + } + + private static String toDirectionName(String direction) { + if (direction.isBlank()) { + return "BOTH"; + } + String value = direction; + int dot = value.lastIndexOf('.'); + if (dot >= 0 && dot + 1 < value.length()) { + value = value.substring(dot + 1); + } + value = value.toUpperCase(); + if (!"C2S".equals(value) && !"S2C".equals(value) && !"BOTH".equals(value)) { + return "BOTH"; + } + return value; + } +} + diff --git a/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/SyncClassProcessor.java b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/SyncClassProcessor.java new file mode 100644 index 00000000..ee7d098e --- /dev/null +++ b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/SyncClassProcessor.java @@ -0,0 +1,77 @@ +package dev.anvilcraft.lib.v2.sync.transform; + +import cpw.mods.modlauncher.api.ITransformer; +import cpw.mods.modlauncher.api.ITransformerVotingContext; +import cpw.mods.modlauncher.api.TargetType; +import cpw.mods.modlauncher.api.TransformerVoteResult; +import lombok.extern.slf4j.Slf4j; +import net.neoforged.neoforgespi.coremod.ICoreMod; +import org.jetbrains.annotations.ApiStatus; +import org.objectweb.asm.tree.ClassNode; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 以 CoreMod({@link ICoreMod} + {@link ITransformer}<{@link ClassNode}>)方式实现 + * 原 26.1 的 {@code neoforgespi.transformation.ClassProcessor} SPI 字节码注入能力。 + * + *

1.21.1 的 fancymodloader loader 4.0.x 不含 transformation SPI 包, + * 等价能力为 {@code META-INF/services/net.neoforged.neoforgespi.coremod.ICoreMod} + * 注册的 CoreMod。注入器({@link SyncBytecodeInjector} / + * {@link LazySyncBytecodeInjector})为纯 ASM {@link ClassNode} 操作,两侧复用。

+ * + *

时序说明:{@link SyncTargetIndex} / {@link LazySyncTargetIndex} 的静态扫描 + * 依赖 {@code LoadingModList.get()}(mod 发现完成后就绪);CoreMod 的 + * {@link #getTransformers()} 在 FMLServiceProvider.launch()(mod 扫描之后)被调用, + * 故此时索引已填充,{@link SyncTransformer#targets()} 可精确列出目标类。

+ */ +@Slf4j +@ApiStatus.Internal +public class SyncClassProcessor implements ICoreMod { + @Override + public Iterable> getTransformers() { + return List.of(new SyncTransformer()); + } + + private static class SyncTransformer implements ITransformer { + @Override + public ClassNode transform(ClassNode input, ITransformerVotingContext context) { + String internalName = input.name; + boolean modified = false; + if (SyncTargetIndex.contains(internalName)) { + modified |= SyncBytecodeInjector.inject(input); + } + if (LazySyncTargetIndex.contains(internalName)) { + modified |= LazySyncBytecodeInjector.inject(input); + } + if (modified) { + log.debug("Injected sync calls into {}", internalName); + } + return input; + } + + @Override + public TransformerVoteResult castVote(ITransformerVotingContext context) { + return TransformerVoteResult.YES; + } + + @Override + public Set> targets() { + Set> targets = new HashSet<>(); + for (String internalName : SyncTargetIndex.allTargets()) { + targets.add(Target.targetClass(internalName.replace('/', '.'))); + } + for (String internalName : LazySyncTargetIndex.allTargets()) { + targets.add(Target.targetClass(internalName.replace('/', '.'))); + } + return targets; + } + + @Override + public TargetType getTargetType() { + return TargetType.CLASS; + } + } +} diff --git a/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/SyncTargetIndex.java b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/SyncTargetIndex.java new file mode 100644 index 00000000..a02714dd --- /dev/null +++ b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/SyncTargetIndex.java @@ -0,0 +1,68 @@ +package dev.anvilcraft.lib.v2.sync.transform; + +import lombok.extern.slf4j.Slf4j; +import net.neoforged.fml.loading.LoadingModList; +import net.neoforged.fml.loading.moddiscovery.ModFileInfo; +import net.neoforged.neoforgespi.language.ModFileScanData; +import org.jetbrains.annotations.ApiStatus; + +import java.lang.annotation.ElementType; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +@Slf4j +@ApiStatus.Internal +public class SyncTargetIndex { + public static final String SYNC_DESCRIPTOR = "Ldev/anvilcraft/lib/v2/sync/annotation/Sync;"; + private static final Map TARGETS = new HashMap<>(); + + static { + SyncTargetIndex.onLoad(); + } + + private SyncTargetIndex() { + } + + @SuppressWarnings("UnstableApiUsage") + private static void onLoad() { + int count = 0; + for (ModFileInfo fileInfo : LoadingModList.get().getModFiles()) { + for (ModFileScanData.AnnotationData annotation : fileInfo.getFile().getScanResult().getAnnotations()) { + if (!annotation.annotationType().getDescriptor().equals(SYNC_DESCRIPTOR)) continue; + if (annotation.targetType() != ElementType.TYPE) continue; + Object object = annotation.annotationData().get("value"); + String direction = "BOTH"; + if (object != null) { + direction = object.toString(); + } + String internalName = annotation.clazz().getInternalName(); + SyncTargetIndex.add(internalName, direction); + log.info("Registered @Sync target: {}", internalName); + count++; + } + } + log.info("Scan complete – {} @Sync target(s) registered.", count); + } + + private static void add(String internalName, String direction) { + TARGETS.put(internalName, direction); + } + + public static boolean contains(String internalName) { + boolean contains = TARGETS.containsKey(internalName); + if (contains) { + log.info("Checking if {} is a @Sync target", internalName); + } + return contains; + } + + public static String getDirection(String internalName) { + return TARGETS.get(internalName); + } + + public static Set allTargets() { + return TARGETS.keySet(); + } +} + diff --git a/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/package-info.java b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/package-info.java new file mode 100644 index 00000000..f1bbd2ef --- /dev/null +++ b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.sync.transform; + +import org.jspecify.annotations.NullMarked; diff --git a/module.sync/processor/src/main/resources/META-INF/services/net.neoforged.neoforgespi.coremod.ICoreMod b/module.sync/processor/src/main/resources/META-INF/services/net.neoforged.neoforgespi.coremod.ICoreMod new file mode 100644 index 00000000..c5e2e099 --- /dev/null +++ b/module.sync/processor/src/main/resources/META-INF/services/net.neoforged.neoforgespi.coremod.ICoreMod @@ -0,0 +1 @@ +dev.anvilcraft.lib.v2.sync.transform.SyncClassProcessor diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/AnvilLibSync.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/AnvilLibSync.java new file mode 100644 index 00000000..89d6398b --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/AnvilLibSync.java @@ -0,0 +1,91 @@ +package dev.anvilcraft.lib.v2.sync; + +import dev.anvilcraft.lib.v2.sync.init.AnvilLibSyncEntries; +import dev.anvilcraft.lib.v2.sync.init.AnvilLibSyncRegistries; +import dev.anvilcraft.lib.v2.sync.management.LazySyncManager; +import dev.anvilcraft.lib.v2.sync.management.SyncConfigManager; +import dev.anvilcraft.lib.v2.sync.management.SyncManager; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.network.protocol.configuration.ServerConfigurationPacketListener; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.bus.api.EventPriority; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.server.ServerStoppedEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.neoforged.neoforge.network.configuration.ICustomConfigurationTask; +import net.neoforged.neoforge.network.event.RegisterConfigurationTasksEvent; +import net.neoforged.neoforge.registries.RegisterEvent; +import org.jetbrains.annotations.ApiStatus; + +import java.util.Objects; +import java.util.function.Consumer; + +@Slf4j +@Mod(AnvilLibSync.MOD_ID) +@ApiStatus.Internal +public class AnvilLibSync { + public static final String MOD_ID = "anvillib_sync"; + public static final SyncManager SYNC_MANAGER = new SyncManager(); + public static final SyncConfigManager SYNC_CONFIG_MANAGER = new SyncConfigManager(); + public static final LazySyncManager LAZY_SYNC_MANAGER = new LazySyncManager(); + + public AnvilLibSync(IEventBus modEventBus, ModContainer modContainer) { + AnvilLibSyncEntries.SYNC_ENTRY.register(modEventBus); + modEventBus.addListener(EventPriority.LOWEST, this::onRegister); + modEventBus.addListener(this::onFMLCommonSetup); + modEventBus.addListener(this::onRegisterConfigurationTasks); + NeoForge.EVENT_BUS.addListener(this::onServerTick); + NeoForge.EVENT_BUS.addListener(this::onServerStopped); + } + + public void onFMLCommonSetup(FMLCommonSetupEvent event) { + SyncConfigManager.compileContent(); + } + + public static ResourceLocation of(String path) { + return ResourceLocation.fromNamespaceAndPath(MOD_ID, path); + } + + /** + * 每服务端 tick 结束时扫描惰性同步目标并下发变更。 + */ + public void onServerTick(ServerTickEvent.Post event) { + AnvilLibSync.LAZY_SYNC_MANAGER.tickServer(); + } + + /** + * 服务器停止时清理服务端侧惰性同步跟踪状态。 + */ + public void onServerStopped(ServerStoppedEvent event) { + AnvilLibSync.LAZY_SYNC_MANAGER.clearServer(); + } + + public void onRegister(RegisterEvent event) { + if (!Objects.equals(event.getRegistryKey(), AnvilLibSyncRegistries.SYNC_ENTRY)) return; + AnvilLibSync.SYNC_MANAGER.compileContent(); + } + + public void onRegisterConfigurationTasks(RegisterConfigurationTasksEvent event) { + event.register(new SyncConfig(event.getListener())); + } + + public record SyncConfig(ServerConfigurationPacketListener listener) implements ICustomConfigurationTask { + public static Type TYPE = new Type(AnvilLibSync.of("sync_config")); + + @Override + public void run(Consumer sender) { + sender.accept(AnvilLibSync.SYNC_CONFIG_MANAGER.createPyload()); + } + + @Override + public Type type() { + return SyncConfig.TYPE; + } + } +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/LazySync.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/LazySync.java new file mode 100644 index 00000000..5bfece31 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/LazySync.java @@ -0,0 +1,31 @@ +package dev.anvilcraft.lib.v2.sync.annotation; + +import dev.anvilcraft.lib.v2.sync.util.SyncDirection; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 标注于字段,表示该字段以「惰性」方式同步。 + * + *

与 {@link Sync} 不同,被 {@code @LazySync} 标注的字段无需使用 + * {@link dev.anvilcraft.lib.v2.sync.management.SyncProxy SyncProxy} 包装, + * 而是由 {@link dev.anvilcraft.lib.v2.sync.management.LazySyncManager LazySyncManager} + * 在每 tick 结束时扫描其所属对象、对比快照检测变更,并按对象分组同步 + * (即每个对象在同一 tick 内仅发送一个网络包)。

+ * + *

所属对象需可被注册的 {@link dev.anvilcraft.lib.v2.sync.management.SyncRegisterEntry + * SyncRegisterEntry} 解析(如实体、方块实体或静态字段)。

+ */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface LazySync { + /** + * 同步方向。 + * + * @return 同步方向,默认 {@link SyncDirection#BOTH} + */ + SyncDirection value() default SyncDirection.BOTH; +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/Sync.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/Sync.java new file mode 100644 index 00000000..a8f1c89d --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/Sync.java @@ -0,0 +1,11 @@ +package dev.anvilcraft.lib.v2.sync.annotation; + +import dev.anvilcraft.lib.v2.sync.util.SyncDirection; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +public @interface Sync { + SyncDirection value() default SyncDirection.BOTH; +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/package-info.java new file mode 100644 index 00000000..80ad80f8 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.sync.annotation; + +import org.jspecify.annotations.NullMarked; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/client/AnvilLibSyncClient.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/client/AnvilLibSyncClient.java new file mode 100644 index 00000000..e47b82f6 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/client/AnvilLibSyncClient.java @@ -0,0 +1,38 @@ +package dev.anvilcraft.lib.v2.sync.client; + +import dev.anvilcraft.lib.v2.sync.AnvilLibSync; +import dev.anvilcraft.lib.v2.sync.management.SyncConfigManager; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.common.NeoForge; +import org.jetbrains.annotations.ApiStatus; + +@Mod(value = AnvilLibSync.MOD_ID, dist = Dist.CLIENT) +@ApiStatus.Internal +public class AnvilLibSyncClient { + public static final SyncConfigManager SYNC_CONFIG_MANAGER = new SyncConfigManager(); + + public AnvilLibSyncClient(IEventBus modEventBus, ModContainer modContainer) { + NeoForge.EVENT_BUS.addListener(this::onClientTick); + NeoForge.EVENT_BUS.addListener(this::onLoggingOut); + } + + /** + * 每客户端 tick 结束时扫描惰性同步目标并上行变更(受字段方向限制)。 + */ + public void onClientTick(ClientTickEvent.Post event) { + AnvilLibSync.LAZY_SYNC_MANAGER.tickClient(); + } + + /** + * 客户端登出时清理客户端侧惰性同步跟踪状态,避免跨存档 / 跨服残留。 + */ + public void onLoggingOut(ClientPlayerNetworkEvent.LoggingOut event) { + AnvilLibSync.LAZY_SYNC_MANAGER.clearClient(); + } +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/AnvilLibSyncEntries.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/AnvilLibSyncEntries.java new file mode 100644 index 00000000..0d891f11 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/AnvilLibSyncEntries.java @@ -0,0 +1,56 @@ +package dev.anvilcraft.lib.v2.sync.init; + +import dev.anvilcraft.lib.v2.sync.AnvilLibSync; +import dev.anvilcraft.lib.v2.sync.management.SyncRegisterEntry; +import dev.anvilcraft.lib.v2.sync.util.SideUtil; +import dev.anvilcraft.lib.v2.util.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.core.UUIDUtil; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; + +import java.util.UUID; + +public class AnvilLibSyncEntries { + public static final DeferredRegister> SYNC_ENTRY = DeferredRegister.create( + AnvilLibSyncRegistries.SYNC_ENTRY, + AnvilLibSync.MOD_ID + ); + + public static final DeferredHolder, SyncRegisterEntry, String>> STATIC = SYNC_ENTRY.register( + "static", + () -> SyncRegisterEntry.create( + Util.cast(Class.class), + ByteBufCodecs.STRING_UTF8, + Class::getName, + (ignored, className) -> Class.forName(className), + false, + null + ) + ); + + public static final DeferredHolder, SyncRegisterEntry> ENTITY = SYNC_ENTRY.register( + "entity", + () -> SyncRegisterEntry.create( + Entity.class, + UUIDUtil.STREAM_CODEC, + Entity::getUUID, + SideUtil::entityFinder, + Entity::level + ) + ); + + public static final DeferredHolder, SyncRegisterEntry> BLOCK_ENTITY = SYNC_ENTRY.register( + "block_entity", + () -> SyncRegisterEntry.create( + BlockEntity.class, + BlockPos.STREAM_CODEC, + BlockEntity::getBlockPos, + SideUtil::blockEntityFinder, + BlockEntity::getLevel + ) + ); +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/AnvilLibSyncRegistries.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/AnvilLibSyncRegistries.java new file mode 100644 index 00000000..92139d1c --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/AnvilLibSyncRegistries.java @@ -0,0 +1,27 @@ +package dev.anvilcraft.lib.v2.sync.init; + +import dev.anvilcraft.lib.v2.sync.AnvilLibSync; +import dev.anvilcraft.lib.v2.sync.management.SyncRegisterEntry; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.registries.NewRegistryEvent; +import net.neoforged.neoforge.registries.RegistryBuilder; +import org.jetbrains.annotations.ApiStatus; + +@EventBusSubscriber(modid = AnvilLibSync.MOD_ID) +public class AnvilLibSyncRegistries { + public static final ResourceKey>> SYNC_ENTRY = ResourceKey.createRegistryKey( + AnvilLibSync.of("sync_entry") + ); + public static final Registry> SYNC_ENTRY_REGISTRY = new RegistryBuilder<>(SYNC_ENTRY) + .maxId(512) + .create(); + + @ApiStatus.Internal + @SubscribeEvent + public static void registerRegistries(NewRegistryEvent event) { + event.register(SYNC_ENTRY_REGISTRY); + } +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/package-info.java new file mode 100644 index 00000000..004ae1f8 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.sync.init; + +import org.jspecify.annotations.NullMarked; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/LazySyncManager.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/LazySyncManager.java new file mode 100644 index 00000000..4269746c --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/LazySyncManager.java @@ -0,0 +1,295 @@ +package dev.anvilcraft.lib.v2.sync.management; + +import com.google.common.collect.MapMaker; +import dev.anvilcraft.lib.v2.sync.AnvilLibSync; +import dev.anvilcraft.lib.v2.sync.annotation.LazySync; +import dev.anvilcraft.lib.v2.sync.network.payload.LazySyncPayload; +import dev.anvilcraft.lib.v2.sync.network.payload.LazySyncPayload.FieldChange; +import dev.anvilcraft.lib.v2.sync.util.SideUtil; +import dev.anvilcraft.lib.v2.sync.util.SyncDirection; +import dev.anvilcraft.lib.v2.util.Util; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 惰性同步管理器。 + * + *

负责被 {@link LazySync} 标注字段的「扫描 - 差分 - 分组同步」:

+ *
    + *
  • 实例 / 静态目标由字节码注入在 {@code } / {@code } 中登记 + * (见 {@link dev.anvilcraft.lib.v2.sync.transform.LazySyncBytecodeInjector})。
  • + *
  • 每 tick 结束时({@code ServerTickEvent.Post} / {@code ClientTickEvent.Post}) + * 遍历目标,将每个字段的当前值用其编解码器编码为字节,与上一快照对比;变更的字段 + * 按所属对象分组,每个对象仅发送一个 {@link LazySyncPayload}。
  • + *
  • 收到分组包时按对象写回各字段,并同步更新本侧快照以抑制回环。
  • + *
+ * + *

采用「编码为字节再比较」的方式检测变更,可统一处理可变值(如 {@code CompoundTag}、 + * {@code ItemStack})的原地修改,且复用网络线格式,代价是每 tick 对每个被跟踪字段编码一次。

+ */ +@Slf4j +@ApiStatus.Internal +public class LazySyncManager { + /** + * 单侧状态:被跟踪的实例 / 静态目标及其字段快照。 + * + *

跟踪集合使用弱引用 + 标识比较({@link MapMaker#weakKeys()}),使卸载的对象可被 GC, + * 无需显式反注册。

+ */ + private static final class Side { + final Set instances = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap()); + final Set> statics = ConcurrentHashMap.newKeySet(); + /** object -> (configKey -> 上一次编码的字节快照) */ + final Map> snapshots = new MapMaker().weakKeys().makeMap(); + } + + private final Side server = new Side(); + private final Side client = new Side(); + + /** runtime class -> 实例 {@link LazySync} 字段元数据(含继承)。 */ + private final Map, List> instanceMetaCache = new ConcurrentHashMap<>(); + /** owner class -> 静态 {@link LazySync} 字段元数据(含继承)。 */ + private final Map, List> staticMetaCache = new ConcurrentHashMap<>(); + + /** + * 单个 {@link LazySync} 字段的运行时元数据。 + * + * @param field 反射字段(已 setAccessible) + * @param codec 字段值的编解码器 + * @param direction 同步方向 + * @param configKey 配置键 {@code DeclaringClassName#fieldName} + */ + private record LazyFieldMeta( + Field field, + StreamCodec codec, + SyncDirection direction, + String configKey + ) { + } + + /** + * 一个待写回的字段,由 {@link LazySyncPayload} 解包后传入 {@link #applyGrouped}。 + * + * @param configKey 配置键 {@code DeclaringClassName#fieldName} + * @param blob 字段值负载 + */ + public record AppliedField(String configKey, byte[] blob) { + } + + private Side side() { + return Util.isServer() ? this.server : this.client; + } + + /** + * 登记一个实例目标。由字节码注入在每个 {@code } 末尾调用。 + * + * @param object 被 {@link LazySync} 标注字段所属的实例 + */ + @ApiStatus.Internal + public void track(@Nullable Object object) { + if (object == null) return; + this.side().instances.add(object); + } + + /** + * 登记一个静态目标。由字节码注入在 {@code } 末尾调用。 + * + * @param owner 含静态 {@link LazySync} 字段的类 + */ + @ApiStatus.Internal + public void trackStatic(@Nullable Class owner) { + if (owner == null) return; + this.side().statics.add(owner); + } + + /** + * 服务端 tick:扫描所有服务端跟踪的目标并下发变更。由 {@code ServerTickEvent.Post} 驱动。 + */ + public void tickServer() { + this.tickAll(this.server, true); + } + + /** + * 客户端 tick:扫描所有客户端跟踪的目标并上行变更。由 {@code ClientTickEvent.Post} 驱动。 + */ + public void tickClient() { + this.tickAll(this.client, false); + } + + private void tickAll(Side side, boolean serverSide) { + if (!side.instances.isEmpty()) { + // 复制以避免在 tick 期间新对象登记导致的并发修改 + for (Object obj : List.copyOf(side.instances)) { + List metas = this.resolveInstanceMeta(obj.getClass()); + if (metas.isEmpty()) continue; + this.scanTarget(side, serverSide, obj, metas); + } + } + for (Class owner : side.statics) { + List metas = this.resolveStaticMeta(owner); + if (metas.isEmpty()) continue; + this.scanTarget(side, serverSide, owner, metas); + } + } + + /** + * 扫描单个目标的全部 {@link LazySync} 字段,编码后与快照对比,将本侧可发送的变更分组发送。 + * + *

首次观测某字段时仅建立基线、不发送(与 {@link dev.anvilcraft.lib.v2.sync.annotation.Sync @Sync} + * 一致:初始值在两端构造时即相同,只有后续变更才需要同步)。

+ */ + private void scanTarget(Side side, boolean serverSide, Object target, List metas) { + Map snap = side.snapshots.computeIfAbsent(target, k -> new HashMap<>()); + List changes = new ArrayList<>(); + for (LazyFieldMeta meta : metas) { + boolean canSend = serverSide ? meta.direction().isCreateByServer() : meta.direction().isCreateByClient(); + if (!canSend) continue; + Object value; + try { + value = meta.field().get(Modifier.isStatic(meta.field().getModifiers()) ? null : target); + } catch (IllegalAccessException e) { + log.warn("Cannot read @LazySync field {}", meta.configKey(), e); + continue; + } + byte[] encoded = this.encodeField(meta, value); + byte[] prev = snap.get(meta.configKey()); + if (prev != null && Arrays.equals(prev, encoded)) continue; + snap.put(meta.configKey(), encoded); + if (prev == null) continue; // 首次观测:仅建立基线 + changes.add(new FieldChange(meta.configKey(), encoded)); + } + if (!changes.isEmpty()) { + this.sendGrouped(target, changes); + } + } + + private void sendGrouped(Object target, List changes) { + SyncRegisterEntry entry = AnvilLibSync.SYNC_MANAGER.contains(target.getClass()); + if (entry == null) { + log.warn("LazySync target {} is not registered for syncing", target.getClass().getName()); + return; + } + Object id = entry.idGetter().apply(target); + String lookupClassName = target.getClass().getName(); + SideUtil.send( + SyncDirection.BOTH, // 字段已按方向预过滤,此处恒从当前侧发送 + entry.dimension(), + entry.dimensionGetter(), + target, + id, + (flow) -> LazySyncPayload.create(entry.idCodec(), id, lookupClassName, changes, flow) + ); + } + + /** + * 写回一个对象的分组变更,并同步更新本侧快照以抑制回环。由 {@link LazySyncPayload} 收包后调用。 + * + * @param object 解析出的目标对象(静态字段时为所属 {@link Class}) + * @param fields 待写回的字段 + */ + @ApiStatus.Internal + public void applyGrouped(Object object, List fields) { + boolean isStatic = object instanceof Class; + List metas = isStatic + ? this.resolveStaticMeta((Class) object) + : this.resolveInstanceMeta(object.getClass()); + Map byKey = new HashMap<>(); + for (LazyFieldMeta meta : metas) byKey.put(meta.configKey(), meta); + + Map snap = this.side().snapshots.computeIfAbsent(object, k -> new HashMap<>()); + for (AppliedField af : fields) { + LazyFieldMeta meta = byKey.get(af.configKey()); + if (meta == null) { + log.warn("Received @LazySync field {} not found on {}", af.configKey(), object); + continue; + } + Object value = this.decodeField(meta, af.blob()); + try { + meta.field().set(isStatic ? null : object, value); + } catch (IllegalAccessException e) { + log.warn("Cannot write @LazySync field {}", af.configKey(), e); + continue; + } + // 以写回值重新编码作为快照,保证下一 tick 差分不会把这次写回当作本侧变更重新上行/下发 + snap.put(af.configKey(), this.encodeField(meta, value)); + } + } + + private byte[] encodeField(LazyFieldMeta meta, @Nullable Object value) { + FriendlyByteBuf buf = SideUtil.createFriendlyByteBuf(Unpooled.buffer()); + buf.writeBoolean(value == null); + if (value != null) { + Util.>cast(meta.codec()).encode(buf, value); + } + byte[] array = new byte[buf.readableBytes()]; + buf.readBytes(array); + return array; + } + + private @Nullable Object decodeField(LazyFieldMeta meta, byte[] blob) { + FriendlyByteBuf buf = SideUtil.createFriendlyByteBuf(Unpooled.buffer()); + buf.writeBytes(blob); + boolean isNull = buf.readBoolean(); + if (isNull) return null; + return Util.>cast(meta.codec()).decode(buf); + } + + private List resolveInstanceMeta(Class clazz) { + return this.instanceMetaCache.computeIfAbsent(clazz, c -> this.buildMeta(c, false)); + } + + private List resolveStaticMeta(Class clazz) { + return this.staticMetaCache.computeIfAbsent(clazz, c -> this.buildMeta(c, true)); + } + + private List buildMeta(Class clazz, boolean staticFields) { + List metas = new ArrayList<>(); + for (Class c = clazz; c != null && c != Object.class; c = c.getSuperclass()) { + for (Field field : c.getDeclaredFields()) { + LazySync annotation = field.getAnnotation(LazySync.class); + if (annotation == null) continue; + if (Modifier.isStatic(field.getModifiers()) != staticFields) continue; + StreamCodec rawCodec = SyncProxy.defaultCodec(Util.cast(field.getType())); + if (rawCodec == null) { + log.warn( + "No default codec for @LazySync field {}.{} of type {}, skipping", + c.getName(), field.getName(), field.getType().getName() + ); + continue; + } + StreamCodec codec = Util.cast(rawCodec); + field.setAccessible(true); + metas.add(new LazyFieldMeta(field, codec, annotation.value(), "%s#%s".formatted(c.getName(), field.getName()))); + } + } + return Collections.unmodifiableList(metas); + } + + public void clearServer() { + this.server.instances.clear(); + this.server.statics.clear(); + this.server.snapshots.clear(); + } + + public void clearClient() { + this.client.instances.clear(); + this.client.statics.clear(); + this.client.snapshots.clear(); + } +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncConfigManager.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncConfigManager.java new file mode 100644 index 00000000..db879108 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncConfigManager.java @@ -0,0 +1,129 @@ +package dev.anvilcraft.lib.v2.sync.management; + +import dev.anvilcraft.lib.v2.sync.AnvilLibSync; +import dev.anvilcraft.lib.v2.sync.network.payload.SyncConfigurationPayload; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.neoforged.fml.loading.LoadingModList; +import net.neoforged.fml.loading.moddiscovery.ModFileInfo; +import net.neoforged.neoforgespi.language.ModFileScanData; +import net.neoforged.neoforgespi.locating.IModFile; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.InputStream; +import java.lang.annotation.ElementType; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@Slf4j +@ApiStatus.Internal +public class SyncConfigManager { + public static final String SYNC_DESCRIPTOR = "Ldev/anvilcraft/lib/v2/sync/annotation/Sync;"; + public static final String LAZY_SYNC_DESCRIPTOR = "Ldev/anvilcraft/lib/v2/sync/annotation/LazySync;"; + private static final String SYNC_PROXY_DESC = "Ldev/anvilcraft/lib/v2/sync/management/SyncProxy;"; + public int usedId = 0; + public final Map syncConfigs = new HashMap<>(); + public final Map syncConfigById = new HashMap<>(); + + + @ApiStatus.Internal + @SneakyThrows + @SuppressWarnings("UnstableApiUsage") + public static void compileContent() { + for (ModFileInfo fileInfo : LoadingModList.get().getModFiles()) { + for (ModFileScanData.AnnotationData annotation : fileInfo.getFile().getScanResult().getAnnotations()) { + if (annotation.annotationType().getDescriptor().equals(SyncConfigManager.SYNC_DESCRIPTOR)) { + if (annotation.targetType() != ElementType.TYPE) continue; + compileSyncType(fileInfo, annotation.clazz().getClassName()); + } else if (annotation.annotationType().getDescriptor().equals(SyncConfigManager.LAZY_SYNC_DESCRIPTOR)) { + if (annotation.targetType() != ElementType.FIELD) continue; + // FIELD 目标的 memberName 即字段名 + String key = "%s#%s".formatted(annotation.clazz().getClassName(), annotation.memberName()); + log.info("Loading LazySyncConfig: {}", key); + AnvilLibSync.SYNC_CONFIG_MANAGER.register(key); + } + } + } + } + + @SuppressWarnings("UnstableApiUsage") + private static void compileSyncType(ModFileInfo fileInfo, String className) { + IModFile modFile = fileInfo.getFile(); + String classPath = className.replace('.', '/') + ".class"; + log.info("Loading SyncConfig: {}", className); + // 1.21.1 的 IModFile 无 getContents()(26.1 新增),改用 SecureJar.getPath 读取 + Path classFilePath = modFile.getSecureJar().getPath(classPath); + if (Files.exists(classFilePath)) { + try (InputStream inputStream = Files.newInputStream(classFilePath)) { + if (inputStream != null) { + ClassReader classReader = new ClassReader(inputStream); + FieldListingVisitor fieldListingVisitor = new FieldListingVisitor(className, null); + classReader.accept(fieldListingVisitor, 0); + } + } catch (Exception e) { + log.error("Error while reading class file: {}", classPath, e); + } + } + } + + public static class FieldListingVisitor extends ClassVisitor { + private final String className; + + FieldListingVisitor(String className, @Nullable ClassVisitor classVisitor) { + super(Opcodes.ASM9, classVisitor); + this.className = className; + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + if (Objects.equals(descriptor, SyncConfigManager.SYNC_PROXY_DESC) && (access & Opcodes.ACC_FINAL) != 0) { + AnvilLibSync.SYNC_CONFIG_MANAGER.register("%s#%s".formatted(this.className, name)); + } + return super.visitField(access, name, descriptor, signature, value); + } + } + + public void register(String syncConfig) { + this.syncConfigs.put(syncConfig, this.usedId); + this.syncConfigById.put(this.usedId, syncConfig); + this.usedId++; + } + + public int getId(String syncConfig) { + return this.syncConfigs.get(syncConfig); + } + + public String getById(int id) { + return this.syncConfigById.get(id); + } + + public void clear() { + this.syncConfigs.clear(); + this.syncConfigById.clear(); + this.usedId = 0; + } + + + public void registerAll(Map syncConfigById) { + this.clear(); + syncConfigById.forEach((id, syncConfig) -> { + this.syncConfigs.put(syncConfig, id); + this.syncConfigById.put(id, syncConfig); + if (id >= this.usedId) { + this.usedId = id + 1; + } + }); + } + + public SyncConfigurationPayload createPyload() { + return new SyncConfigurationPayload(this.syncConfigById); + } +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncManager.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncManager.java new file mode 100644 index 00000000..7c949be0 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncManager.java @@ -0,0 +1,66 @@ +package dev.anvilcraft.lib.v2.sync.management; + +import dev.anvilcraft.lib.v2.sync.init.AnvilLibSyncRegistries; +import dev.anvilcraft.lib.v2.sync.network.payload.SyncPayload; +import dev.anvilcraft.lib.v2.sync.util.SideUtil; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.resources.ResourceKey; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +@Slf4j +@ApiStatus.Internal +public class SyncManager { + private final Map, SyncRegisterEntry> syncRegisterEntryMap = new HashMap<>(); + + @ApiStatus.Internal + public void compileContent() { + Set>, SyncRegisterEntry>> syncEntries = AnvilLibSyncRegistries.SYNC_ENTRY_REGISTRY.entrySet(); + log.info("SYNC_ENTRY_REGISTRY Size: {}", syncEntries.size()); + syncEntries.forEach(entry -> this.syncRegisterEntryMap.put(entry.getValue().clazz(), entry.getValue())); + } + + void setValue(@Nullable P parent, SyncProxy proxy, T oldValue, T newValue) { + Objects.requireNonNull(parent, "Parent cannot be null when setting value"); + SyncRegisterEntry entry = checkParent(parent); + ID id = entry.idGetter().apply(parent); + SideUtil.send( + proxy.getDirection(), + entry.dimension(), + entry.dimensionGetter(), + parent, + id, + (flow) -> SyncPayload.create(parent, entry.idCodec(), id, proxy, flow) + ); + } + + void getValue(@Nullable Object parent, SyncProxy proxy, T value) { + } + + SyncRegisterEntry checkParent(Object parent) { + return Objects.requireNonNull( + this.contains(parent.getClass()), + "Parent class " + parent.getClass().getName() + " is not registered for syncing" + ); + } + + @Nullable + @ApiStatus.Internal + @SuppressWarnings("unchecked") + public SyncRegisterEntry contains(Class clazz) { + boolean containsKey = syncRegisterEntryMap.containsKey(clazz); + if (containsKey) return (SyncRegisterEntry) syncRegisterEntryMap.get(clazz); + Set> classes = syncRegisterEntryMap.keySet(); + for (Class classKey : classes) { + if (classKey.isAssignableFrom(clazz)) { + return (SyncRegisterEntry) syncRegisterEntryMap.get(classKey); + } + } + return null; + } +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncProxy.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncProxy.java new file mode 100644 index 00000000..0ab097ad --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncProxy.java @@ -0,0 +1,244 @@ +package dev.anvilcraft.lib.v2.sync.management; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.PropertyMap; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import dev.anvilcraft.lib.v2.sync.AnvilLibSync; +import dev.anvilcraft.lib.v2.sync.util.SyncDirection; +import dev.anvilcraft.lib.v2.util.Util; +import io.netty.buffer.ByteBuf; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.VarInt; +import net.minecraft.network.VarLong; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.ApiStatus; +import org.joml.Quaternionfc; +import org.joml.Vector3fc; +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.util.Objects; + +@Slf4j +public class SyncProxy { + @Getter + @ApiStatus.Internal + private final StreamCodec codec; + @Setter + @ApiStatus.Internal + private @Nullable SyncManager manager; + @Setter + @ApiStatus.Internal + private @Nullable Object parent; + @Setter + @Getter + @ApiStatus.Internal + private @Nullable String fieldName; + private volatile @Nullable T value; + @Getter + private SyncDirection direction = SyncDirection.BOTH; + + public SyncProxy(T value) { + this.value = value; + this.codec = Objects.requireNonNull( + SyncProxy.defaultCodec(Util.cast(value.getClass())), + "No default codec for type: " + value.getClass().getName() + ); + } + + public SyncProxy(T value, StreamCodec codec) { + this.value = value; + this.codec = codec; + } + + public SyncProxy(Class type) { + this.value = null; + this.codec = Objects.requireNonNull(SyncProxy.defaultCodec(type), "No default codec for type: " + type.getName()); + } + + public SyncProxy(StreamCodec codec) { + this.value = null; + this.codec = codec; + } + + public SyncProxy direction(SyncDirection direction) { + this.direction = direction; + return this; + } + + @ApiStatus.Internal + public void setDirection(SyncDirection direction) { + if (this.direction == SyncDirection.BOTH) this.direction = direction; + } + + public void setValue(B buf, boolean serverbound) { + T oldValue = this.value; + boolean isNull = buf.readBoolean(); + T newValue; + if (isNull) { + newValue = null; + } else { + newValue = Util.>cast(this.getCodec()).decode(buf); + } + this.value = newValue; + if (!serverbound) return; + if (this.manager == null) { + this.manager = AnvilLibSync.SYNC_MANAGER; + } + this.manager.setValue(parent, this, oldValue, newValue); + } + + public @Nullable T getValue() { + if (this.manager == null) { + this.manager = AnvilLibSync.SYNC_MANAGER; + } + this.manager.getValue(parent, this, this.value); + return this.value; + } + + public void setValue(@Nullable T value) { + T old = this.value; + this.value = value; + if (this.manager == null) { + this.manager = AnvilLibSync.SYNC_MANAGER; + } + this.manager.setValue(parent, this, old, this.value); + } + + public void encode(B byteBuf) { + byteBuf.writeBoolean(this.getValue() == null); + if (this.getValue() != null) { + Util.>cast(this.getCodec()).encode(byteBuf, this.getValue()); + } + } + + @SuppressWarnings("unchecked") + @Nullable + static StreamCodec defaultCodec(Class type) { + if (type == CompoundTag.class) { + return (StreamCodec) ByteBufCodecs.TAG; + } + if (Tag.class.isAssignableFrom(type)) { + return (StreamCodec) ByteBufCodecs.TAG; + } + if (Vector3fc.class.isAssignableFrom(type)) { + return (StreamCodec) ByteBufCodecs.VECTOR3F; + } + if (Quaternionfc.class.isAssignableFrom(type)) { + return (StreamCodec) ByteBufCodecs.QUATERNIONF; + } + if (type == PropertyMap.class) { + return (StreamCodec) ByteBufCodecs.GAME_PROFILE_PROPERTIES; + } + if (type == GameProfile.class) { + return (StreamCodec) ByteBufCodecs.GAME_PROFILE; + } + if (type == BlockPos.class) { + return (StreamCodec) BlockPos.STREAM_CODEC; + } + if (Component.class.isAssignableFrom(type)) { + return (StreamCodec) ComponentSerialization.STREAM_CODEC; + } + if (type == ItemStack.class) { + return (StreamCodec) ItemStack.OPTIONAL_STREAM_CODEC; + } + if (type == String.class) { + return (StreamCodec) ByteBufCodecs.STRING_UTF8; + } + if (type == Boolean.class || type == boolean.class) { + return (StreamCodec) ByteBufCodecs.BOOL; + } + if (type == Double.class || type == double.class) { + return (StreamCodec) ByteBufCodecs.DOUBLE; + } + if (type == Float.class || type == float.class) { + return (StreamCodec) ByteBufCodecs.FLOAT; + } + if (type == Long.class || type == long.class) { + return (StreamCodec) ByteBufCodecs.VAR_LONG; + } + if (type == Integer.class || type == int.class) { + return (StreamCodec) ByteBufCodecs.INT; + } + if (type == Short.class || type == short.class) { + return (StreamCodec) ByteBufCodecs.SHORT; + } + if (type == Byte.class || type == byte.class) { + return (StreamCodec) ByteBufCodecs.BYTE; + } + // 1.21.1 的 ByteBufCodecs 无 LONG_ARRAY(26.1 新增),手写等价编解码:VarInt 长度 + VarLong 元素 + if (type == Long[].class || type == long[].class) { + return (StreamCodec) StreamCodec.of( + (ByteBuf buf, long[] arr) -> { + VarInt.write(buf, arr.length); + for (long v : arr) VarLong.write(buf, v); + }, + buf -> { + long[] arr = new long[VarInt.read(buf)]; + for (int i = 0; i < arr.length; i++) arr[i] = VarLong.read(buf); + return arr; + } + ); + } + if (type == Byte[].class || type == byte[].class) { + return (StreamCodec) ByteBufCodecs.BYTE_ARRAY; + } + return SyncProxy.codecFromType(type); + } + + /** + * 在目标类型自身声明的 {@code public static final} 字段中查找可用的 + * {@link Codec} 或 {@link MapCodec},并转换为 {@link StreamCodec}。 + * + *

查找规则:

+ *
    + *
  • 仅扫描 {@code type} 自身声明的字段(不含父类),取 {@code public static final} 字段;
  • + *
  • 字段类型为 {@link Codec Codec<T>} 或 {@link MapCodec MapCodec<T>}, + * 且其类型实参与 {@code type} 兼容;{@link MapCodec} 经 {@link MapCodec#codec()} 转为 {@link Codec};
  • + *
  • 经 {@link ByteBufCodecs#fromCodecWithRegistries(Codec)} 包装为注册表感知的 + * {@link StreamCodec}(本库的缓冲区在具备注册表访问时即为 + * {@link net.minecraft.network.RegistryFriendlyByteBuf RegistryFriendlyByteBuf})。
  • + *
+ * + * @return 找到的 {@link StreamCodec},未找到时返回 {@code null} + */ + @SuppressWarnings("unchecked") + @Nullable + private static StreamCodec codecFromType(Class type) { + for (Field field : type.getDeclaredFields()) { + int mods = field.getModifiers(); + if (!Modifier.isStatic(mods) || !Modifier.isFinal(mods) || !Modifier.isPublic(mods)) continue; + + Class fieldType = field.getType(); + boolean isCodec = Codec.class == fieldType; + boolean isMapCodec = MapCodec.class == fieldType; + if (!isCodec && !isMapCodec) continue; + if (!(field.getGenericType() instanceof ParameterizedType pt)) continue; + if (pt.getActualTypeArguments().length != 1) continue; + if (!(pt.getActualTypeArguments()[0] instanceof Class arg)) continue; + if (!type.isAssignableFrom(arg)) continue; + + try { + Object value = field.get(null); + if (value == null) continue; + Codec codec = isMapCodec ? ((MapCodec) value).codec() : (Codec) value; + return ByteBufCodecs.fromCodecWithRegistries(codec); + } catch (IllegalAccessException e) { + log.warn("Cannot access codec field {}.{}", type.getName(), field.getName(), e); + } + } + return null; + } +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncRegisterEntry.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncRegisterEntry.java new file mode 100644 index 00000000..425ab659 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncRegisterEntry.java @@ -0,0 +1,62 @@ +package dev.anvilcraft.lib.v2.sync.management; + +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jspecify.annotations.Nullable; + +import java.util.function.Function; + +public record SyncRegisterEntry( + Class clazz, + StreamCodec idCodec, + Function idGetter, + Finder finder, + boolean dimension, + @Nullable Function> dimensionGetter +) { + public static SyncRegisterEntry create( + Class type, + StreamCodec idCodec, + Function idGetter, + Finder finder + ) { + return SyncRegisterEntry.create(type, idCodec, idGetter, finder, false, null); + } + + @SuppressWarnings("resource") + public static SyncRegisterEntry create( + Class type, + StreamCodec idCodec, + Function idGetter, + Finder finder, + Function dimensionGetter + ) { + return SyncRegisterEntry.create( + type, idCodec, idGetter, finder, true, t -> { + Level apply = dimensionGetter.apply(t); + if (apply == null) return null; + return apply.dimension(); + } + ); + } + + public static SyncRegisterEntry create( + Class type, + StreamCodec idCodec, + Function idGetter, + Finder finder, + boolean dimension, + @Nullable Function> dimensionGetter + ) { + return new SyncRegisterEntry<>(type, idCodec, idGetter, finder, dimension, dimensionGetter); + } + + @FunctionalInterface + public interface Finder { + @Nullable + T apply(IPayloadContext context, ID id) throws Exception; + } +} \ No newline at end of file diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/package-info.java new file mode 100644 index 00000000..a7fc9061 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.sync.management; + +import org.jspecify.annotations.NullMarked; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/AnvilLibSyncNetwork.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/AnvilLibSyncNetwork.java new file mode 100644 index 00000000..476387bc --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/AnvilLibSyncNetwork.java @@ -0,0 +1,21 @@ +package dev.anvilcraft.lib.v2.sync.network; + +import dev.anvilcraft.lib.v2.network.register.NetworkRegistrar; +import dev.anvilcraft.lib.v2.sync.AnvilLibSync; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import org.jetbrains.annotations.ApiStatus; + +@EventBusSubscriber(modid = AnvilLibSync.MOD_ID) +@ApiStatus.Internal +public class AnvilLibSyncNetwork { + public static final String NETWORK_VERSION = "1"; + + @SubscribeEvent + public static void register(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar(NETWORK_VERSION); + NetworkRegistrar.register(registrar, AnvilLibSync.MOD_ID); + } +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/package-info.java new file mode 100644 index 00000000..f65681b2 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.sync.network; + +import org.jspecify.annotations.NullMarked; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/LazySyncPayload.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/LazySyncPayload.java new file mode 100644 index 00000000..2bf7c0d5 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/LazySyncPayload.java @@ -0,0 +1,141 @@ +package dev.anvilcraft.lib.v2.sync.network.payload; + +import dev.anvilcraft.lib.v2.network.packet.IInsensitiveBiPacket; +import dev.anvilcraft.lib.v2.network.packet.IPacket; +import dev.anvilcraft.lib.v2.sync.AnvilLibSync; +import dev.anvilcraft.lib.v2.sync.client.AnvilLibSyncClient; +import dev.anvilcraft.lib.v2.sync.management.LazySyncManager; +import dev.anvilcraft.lib.v2.sync.management.SyncRegisterEntry; +import dev.anvilcraft.lib.v2.sync.util.SideUtil; +import dev.anvilcraft.lib.v2.util.Util; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.ApiStatus; + +import java.util.ArrayList; +import java.util.List; + +/** + * 惰性同步网络包。 + * + *

每个网络包对应「一个对象」在某 tick 内发生变更的「全部字段」,从而实现按对象分组、 + * 每对象每 tick 仅一个网络包。与 {@link SyncPayload} 逐字段发送不同。

+ * + *

负载布局(写入单个 {@code byte[]}):

+ *
+ * String  lookupClassName   // 解析 SyncRegisterEntry 所用的运行时类名(静态字段为 java.lang.Class)
+ * (idCodec) objectId        // 对象 id,由对应 entry 的 idCodec 编解码
+ * VarInt  fieldCount        // 变更字段数量
+ * repeat:
+ *   VarInt syncConfigId     // 字段配置 id(ClassName#fieldName 的压缩 id)
+ *   VarInt blobLength
+ *   byte[] blob             // 字段值负载:boolean(isNull) + (codec 编码值)
+ * 
+ */ +@Slf4j +@ApiStatus.Internal +public record LazySyncPayload( + byte[] array +) implements IInsensitiveBiPacket { + public static final Type TYPE = IPacket.type(AnvilLibSync.of("lazy_sync")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.BYTE_ARRAY, + LazySyncPayload::array, + LazySyncPayload::new + ); + + /** + * 一个变更字段。 + * + * @param configKey 字段的配置键,形如 {@code DeclaringClassName#fieldName} + * @param blob 字段值负载(boolean(isNull) + codec 编码值) + */ + public record FieldChange(String configKey, byte[] blob) { + } + + public static LazySyncPayload create( + StreamCodec idCodec, + ID id, + String lookupClassName, + List changes, + PacketFlow flow + ) { + FriendlyByteBuf buf = SideUtil.createFriendlyByteBuf(Unpooled.buffer()); + buf.writeUtf(lookupClassName); + Util.>cast(idCodec).encode(buf, id); + buf.writeVarInt(changes.size()); + for (FieldChange change : changes) { + int syncConfigId = flow == PacketFlow.CLIENTBOUND + ? AnvilLibSync.SYNC_CONFIG_MANAGER.getId(change.configKey()) + : AnvilLibSyncClient.SYNC_CONFIG_MANAGER.getId(change.configKey()); + buf.writeVarInt(syncConfigId); + buf.writeVarInt(change.blob().length); + buf.writeBytes(change.blob()); + } + byte[] array = new byte[buf.readableBytes()]; + buf.readBytes(array); + return new LazySyncPayload(array); + } + + private void handler(IPayloadContext ctx) { + FriendlyByteBuf buf = SideUtil.createFriendlyByteBuf(Unpooled.buffer()); + buf.writeBytes(this.array()); + + Object object; + List applied; + try { + String lookupClassName = buf.readUtf(); + Class lookupClass = Class.forName(lookupClassName); + SyncRegisterEntry entry = AnvilLibSync.SYNC_MANAGER.contains(lookupClass); + if (entry == null) { + // 两端注册不一致时静默丢弃,避免抛异常传播到 Netty 导致连接断开 + log.warn("Received LazySync packet for unregistered class {}, dropping", lookupClassName); + return; + } + Object id = entry.idCodec().decode(buf); + object = entry.finder().apply(ctx, Util.cast(id)); + + int fieldCount = buf.readVarInt(); + boolean isServer = SideUtil.isServer(); + applied = new ArrayList<>(fieldCount); + for (int i = 0; i < fieldCount; i++) { + int syncConfigId = buf.readVarInt(); + int blobLength = buf.readVarInt(); + byte[] blob = new byte[blobLength]; + buf.readBytes(blob); + String configKey = isServer + ? AnvilLibSync.SYNC_CONFIG_MANAGER.getById(syncConfigId) + : AnvilLibSyncClient.SYNC_CONFIG_MANAGER.getById(syncConfigId); + applied.add(new LazySyncManager.AppliedField(configKey, blob)); + } + } catch (Exception e) { + log.warn("Failed to parse LazySync packet, dropping", e); + return; + } + if (object == null) return; + AnvilLibSync.LAZY_SYNC_MANAGER.applyGrouped(object, applied); + } + + @Override + public void bidirectionalHandler(IPayloadContext ctx) { + IInsensitiveBiPacket.super.bidirectionalHandler(ctx); + ctx.enqueueWork(() -> this.handler(ctx)); + } + + @Override + public void handleOnBothSide(Player player) { + } + + @Override + public Type type() { + return LazySyncPayload.TYPE; + } +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/SyncConfigurationFinishPayload.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/SyncConfigurationFinishPayload.java new file mode 100644 index 00000000..4f11fd5f --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/SyncConfigurationFinishPayload.java @@ -0,0 +1,31 @@ +package dev.anvilcraft.lib.v2.sync.network.payload; + +import dev.anvilcraft.lib.v2.network.packet.IPacket; +import dev.anvilcraft.lib.v2.network.packet.IServerboundPacket; +import dev.anvilcraft.lib.v2.sync.AnvilLibSync; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public class SyncConfigurationFinishPayload implements IServerboundPacket { + public static final SyncConfigurationFinishPayload INSTANCE = new SyncConfigurationFinishPayload(); + public static final Type TYPE = IPacket.type(AnvilLibSync.of("sync_config_finish")); + public static final StreamCodec STREAM_CODEC = StreamCodec.unit(INSTANCE); + + @Override + public void handleOnServer(Player player) { + } + + @Override + public void serverHandler(IPayloadContext ctx) { + ctx.finishCurrentTask(AnvilLibSync.SyncConfig.TYPE); + } + + @Override + public Type type() { + return SyncConfigurationFinishPayload.TYPE; + } +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/SyncConfigurationPayload.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/SyncConfigurationPayload.java new file mode 100644 index 00000000..4ac65ad4 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/SyncConfigurationPayload.java @@ -0,0 +1,42 @@ +package dev.anvilcraft.lib.v2.sync.network.payload; + +import dev.anvilcraft.lib.v2.network.packet.IClientboundPacket; +import dev.anvilcraft.lib.v2.network.packet.IPacket; +import dev.anvilcraft.lib.v2.sync.AnvilLibSync; +import dev.anvilcraft.lib.v2.sync.client.AnvilLibSyncClient; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.ApiStatus; + +import java.util.HashMap; +import java.util.Map; + +@ApiStatus.Internal +public record SyncConfigurationPayload( + Map syncMap +) implements IClientboundPacket { + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.map(HashMap::new, ByteBufCodecs.VAR_INT, ByteBufCodecs.STRING_UTF8), + SyncConfigurationPayload::syncMap, + SyncConfigurationPayload::new + ); + public static final Type TYPE = IPacket.type(AnvilLibSync.of("sync_config")); + + @Override + public void handleOnClient(Player player) { + } + + @Override + public void clientHandler(IPayloadContext ctx) { + ctx.enqueueWork(() -> AnvilLibSyncClient.SYNC_CONFIG_MANAGER.registerAll(this.syncMap())); + ctx.reply(SyncConfigurationFinishPayload.INSTANCE); + } + + @Override + public Type type() { + return SyncConfigurationPayload.TYPE; + } +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/SyncPayload.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/SyncPayload.java new file mode 100644 index 00000000..28af73ab --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/SyncPayload.java @@ -0,0 +1,99 @@ +package dev.anvilcraft.lib.v2.sync.network.payload; + +import dev.anvilcraft.lib.v2.network.packet.IInsensitiveBiPacket; +import dev.anvilcraft.lib.v2.network.packet.IPacket; +import dev.anvilcraft.lib.v2.sync.AnvilLibSync; +import dev.anvilcraft.lib.v2.sync.client.AnvilLibSyncClient; +import dev.anvilcraft.lib.v2.sync.management.SyncProxy; +import dev.anvilcraft.lib.v2.sync.management.SyncRegisterEntry; +import dev.anvilcraft.lib.v2.sync.util.SideUtil; +import dev.anvilcraft.lib.v2.util.Util; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.SneakyThrows; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.ApiStatus; + +import java.lang.reflect.Field; +import java.util.Objects; + +@ApiStatus.Internal +public record SyncPayload( + byte[] array +) implements IInsensitiveBiPacket { + public static final Type TYPE = IPacket.type(AnvilLibSync.of("sync")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.BYTE_ARRAY, + SyncPayload::array, + SyncPayload::new + ); + + public static SyncPayload create( + T parent, + StreamCodec idCodec, + ID id, + SyncProxy field, + PacketFlow flow + ) { + FriendlyByteBuf buf = SideUtil.createFriendlyByteBuf(Unpooled.buffer()); + String syncConfig = "%s#%s".formatted( + parent.getClass().getName(), // ClassName + Objects.requireNonNull(field.getFieldName()) // FieldName + ); + int syncConfigId = flow == PacketFlow.CLIENTBOUND + ? AnvilLibSync.SYNC_CONFIG_MANAGER.getId(syncConfig) + : AnvilLibSyncClient.SYNC_CONFIG_MANAGER.getId(syncConfig); + buf.writeVarInt(syncConfigId); // SyncConfigId + Util.>cast(idCodec).encode(buf, id); // ObjectId + field.encode(buf); // FieldValue + byte[] array = new byte[buf.readableBytes()]; + buf.readBytes(array); + return new SyncPayload(array); + } + + @SneakyThrows + private void handler(IPayloadContext ctx) { + FriendlyByteBuf buf = SideUtil.createFriendlyByteBuf(Unpooled.buffer()); + buf.writeBytes(this.array()); + int syncConfigId = buf.readVarInt(); + String syncConfig = SideUtil.isServer() + ? AnvilLibSync.SYNC_CONFIG_MANAGER.getById(syncConfigId) + : AnvilLibSyncClient.SYNC_CONFIG_MANAGER.getById(syncConfigId); + String[] syncConfigSplit = syncConfig.split("#"); + String className = syncConfigSplit[0]; // ClassName + String fieldName = syncConfigSplit[1]; // FieldName + Class clazz = Class.forName(className); + SyncRegisterEntry entry = Objects.requireNonNull( + AnvilLibSync.SYNC_MANAGER.contains(clazz), + "Class " + className + " is not registered for syncing" + ); + Object id = entry.idCodec().decode(buf); // ObjectId + Object object = entry.finder().apply(ctx, Util.cast(id)); + boolean isStatic = object instanceof Class; + if (isStatic) clazz = (Class) object; + Field field = (clazz).getField(fieldName); + SyncProxy syncProxy = Util.cast(isStatic ? field.get(null) : field.get(object)); + syncProxy.setValue(buf, PacketFlow.SERVERBOUND.equals(ctx.flow())); // FieldValue + } + + @Override + public void bidirectionalHandler(IPayloadContext ctx) { + IInsensitiveBiPacket.super.bidirectionalHandler(ctx); + ctx.enqueueWork(() -> this.handler(ctx)); + } + + @Override + public void handleOnBothSide(Player player) { + } + + @Override + public Type type() { + return SyncPayload.TYPE; + } +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/package-info.java new file mode 100644 index 00000000..aa10c6ea --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/package-info.java @@ -0,0 +1,7 @@ +@NullMarked +@Network(protocol = PacketProtocol.COMMON) +package dev.anvilcraft.lib.v2.sync.network.payload; + +import dev.anvilcraft.lib.v2.network.register.Network; +import dev.anvilcraft.lib.v2.network.register.PacketProtocol; +import org.jspecify.annotations.NullMarked; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/package-info.java new file mode 100644 index 00000000..42c1ddfb --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.sync; + +import org.jspecify.annotations.NullMarked; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SideUtil.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SideUtil.java new file mode 100644 index 00000000..e96e72ff --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SideUtil.java @@ -0,0 +1,165 @@ +package dev.anvilcraft.lib.v2.sync.util; + +import dev.anvilcraft.lib.v2.network.packet.IPacket; +import dev.anvilcraft.lib.v2.util.Util; +import io.netty.buffer.ByteBuf; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.core.BlockPos; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.neoforged.neoforge.network.PacketDistributor; +import net.neoforged.neoforge.network.connection.ConnectionType; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import net.neoforged.neoforge.server.ServerLifecycleHooks; +import org.jspecify.annotations.Nullable; + +import java.util.UUID; +import java.util.function.Function; + +public class SideUtil { + public static boolean isServer() { + if (Util.isServer()) { + return true; + } else if (Util.isClient()) { + return false; + } + throw new IllegalStateException("Cannot determine side: not client or server"); + } + + public static @Nullable RegistryAccess registryAccess() { + if (Util.isServer()) { + return SideUtil.serverRegistryAccess(); + } else if (Util.isClient()) { + return SideUtil.clientRegistryAccess(); + } + return null; + } + + private static @Nullable RegistryAccess clientRegistryAccess() { + Minecraft minecraft = Minecraft.getInstance(); + ClientPacketListener connection = minecraft.getConnection(); + return connection == null ? null : connection.registryAccess(); + } + + private static @Nullable RegistryAccess serverRegistryAccess() { + MinecraftServer currentServer = ServerLifecycleHooks.getCurrentServer(); + return currentServer == null ? null : currentServer.registryAccess(); + } + + public static @Nullable Entity entityFinder(IPayloadContext context, UUID uuid) { + if (context.flow().equals(PacketFlow.SERVERBOUND)) { + return SideUtil.serverEntityFinder(context.player(), uuid); + } else { + return SideUtil.clientEntityFinder(uuid); + } + } + + private static @Nullable Entity serverEntityFinder(Player player, UUID uuid) { + if (!(player instanceof ServerPlayer serverPlayer)) return null; + ServerLevel level = serverPlayer.serverLevel(); + return level.getEntity(uuid); + } + + private static @Nullable Entity clientEntityFinder(UUID uuid) { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null) { + return null; + } + // 1.21.1 的 ClientLevel.getEntities() 为 protected,且无 getEntity(UUID);遍历渲染实体集合查找 + for (Entity entity : minecraft.level.entitiesForRendering()) { + if (entity.getUUID().equals(uuid)) return entity; + } + return null; + } + + public static @Nullable BlockEntity blockEntityFinder(IPayloadContext context, BlockPos blockPos) { + if (context.flow().equals(PacketFlow.SERVERBOUND)) { + return SideUtil.serverBlockEntityFinder(context.player(), blockPos); + } else { + return SideUtil.clientBlockEntityFinder(blockPos); + } + } + + private static @Nullable BlockEntity serverBlockEntityFinder(Player player, BlockPos blockPos) { + if (!(player instanceof ServerPlayer serverPlayer)) return null; + return serverPlayer.level().getBlockEntity(blockPos); + } + + private static @Nullable BlockEntity clientBlockEntityFinder(BlockPos blockPos) { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null) { + return null; + } + return minecraft.level.getBlockEntity(blockPos); + } + + public static FriendlyByteBuf createFriendlyByteBuf(ByteBuf byteBuf) { + RegistryAccess registryAccess = SideUtil.registryAccess(); + FriendlyByteBuf buf; + if (registryAccess == null) { + buf = new FriendlyByteBuf(byteBuf); + } else { + buf = new RegistryFriendlyByteBuf(byteBuf, registryAccess, ConnectionType.NEOFORGE); + } + return buf; + } + + public static void send( + SyncDirection direction, + boolean dimension, + @Nullable Function> dimensionGetter, + T obj, + ID id, + Function pyload + ) { + if (Util.isServer() && direction.isCreateByServer()) { + SideUtil.serverSend(obj, id, pyload, dimension, dimensionGetter); + } else if (Util.isClient() && direction.isCreateByClient()) { + SideUtil.clientSend(pyload); + } + } + + + public static void serverSend( + T obj, + ID id, + Function pyload, + boolean dimension, + @Nullable Function> dimensionGetter + ) { + IPacket packet = pyload.apply(PacketFlow.CLIENTBOUND); + if (!dimension || dimensionGetter == null) { + PacketDistributor.sendToAllPlayers(packet); + return; + } + MinecraftServer currentServer = ServerLifecycleHooks.getCurrentServer(); + if (currentServer == null) return; + ResourceKey apply = dimensionGetter.apply(obj); + ServerLevel level = currentServer.getLevel(apply); + if (level == null) return; + if (id instanceof BlockPos blockPos) { + PacketDistributor.sendToPlayersTrackingChunk(level, new ChunkPos(blockPos), packet); + return; + } + if (obj instanceof Entity) { + PacketDistributor.sendToPlayersTrackingEntity((Entity) obj, packet); + } + PacketDistributor.sendToPlayersInDimension(level, packet); + } + + public static void clientSend(Function pyload) { + PacketDistributor.sendToServer(pyload.apply(PacketFlow.SERVERBOUND)); + } +} diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SyncDirection.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SyncDirection.java new file mode 100644 index 00000000..6f8ec801 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SyncDirection.java @@ -0,0 +1,17 @@ +package dev.anvilcraft.lib.v2.sync.util; + +import lombok.Getter; + +@Getter +public enum SyncDirection { + BOTH(true, true), + C2S(true, false), + S2C(false, true); + private final boolean createByClient; + private final boolean createByServer; + + SyncDirection(boolean createByClient, boolean createByServer) { + this.createByClient = createByClient; + this.createByServer = createByServer; + } +} \ No newline at end of file diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/package-info.java new file mode 100644 index 00000000..62bdfb93 --- /dev/null +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.sync.util; + +import org.jspecify.annotations.NullMarked; diff --git a/module.sync/src/main/resources/META-INF/neoforge.mods.toml b/module.sync/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..27f7ba75 --- /dev/null +++ b/module.sync/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,86 @@ +# This is an example mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader = "javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. +loaderVersion = "${loader_version_range}" #mandatory +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license = "${mod_license}" +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory +# The modid of the mod +modId = "${mod_id}" #mandatory +# The version number of the mod +version = "${mod_version}" #mandatory +# A display name for the mod +displayName = "${mod_name}" #mandatory +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional +# A URL for the "homepage" for this mod, displayed in the mod UI +displayURL="https://github.com/Anvil-Dev/AnvilLib" +# A file name (in the root of the mod JAR) containing a logo for display +logoFile="icon.png" #optional +# A text field displayed in the mod UI +#credits="" #optional +# A text field displayed in the mod UI +authors = "${mod_authors}" #optional +# Display Test controls the display for your mod in the server connection screen +# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. +# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. +# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. +# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. +# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. +#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) + +# The description text for the mod (multi line!) (#mandatory) +description = '''${mod_description}''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +[[mixins]] +config = "${mod_id}.mixins.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies."${mod_id}"]] #optional +# the modid of the dependency +modId = "neoforge" #mandatory +# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). +# 'required' requires the mod to exist, 'optional' does not +# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning +type = "required" #mandatory +# Optional field describing why the dependency is required or why it is incompatible +# reason="..." +# The version range of the dependency +versionRange = "${neo_version_range}" #mandatory +# An ordering relationship for the dependency. +# BEFORE - This mod is loaded BEFORE the dependency +# AFTER - This mod is loaded AFTER the dependency +ordering = "NONE" +# Side this dependency is applied on - BOTH, CLIENT, or SERVER +side = "BOTH" +# Here's another dependency +[[dependencies."${mod_id}"]] +modId = "minecraft" +type = "required" +# This version range declares a minimum of the current minecraft version up to but not including the next major version +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features."${mod_id}"] +#openGLVersion="[3.2,)" diff --git a/module.sync/src/main/resources/anvillib_sync.mixins.json b/module.sync/src/main/resources/anvillib_sync.mixins.json new file mode 100644 index 00000000..bb8958e1 --- /dev/null +++ b/module.sync/src/main/resources/anvillib_sync.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.anvilcraft.lib.v2.sync.mixin", + "compatibilityLevel": "JAVA_8", + "refmap": "anvillib.refmap.json", + "mixins": [ + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/module.sync/src/main/resources/icon.png b/module.sync/src/main/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..15304bd54d5d97d8482049d5f16a7544748ce05a GIT binary patch literal 1089 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabRA=0U}gyL32}YB&qp?9cb}fRhL)ab z>**&8P1h&;i5V@+5z~{9lu=Mp{cG+rNOuP4jFK;J~t_xRpI&Nf4%?X zZmY?-7zkw=;bC zsjT>Uk~ZUn{GF#RHtMc+sGWYc$A>9-;igj6hzoLO7wEVa*QxB)zM=ff)bhV&`;h}b zD&$S4O;$O;Sf&`=w_-tNu&sr4nCukimfSM|=84-o^=H0sFgVR)oOSu;mLIn4lUH@E zPWkvN(C^lx3(G6wS;EsJIMe%PuP~i06!s*lLHg)A!3fTUY`(JJ860vaMb2DUI{zB$ z25}G0l9!&RrHovbwaBi{I35yXA*NOEX}{&%4{Sd|jrPVzaJhpb&(qb Date: Mon, 10 Aug 2026 22:54:36 +0800 Subject: [PATCH 05/38] =?UTF-8?q?feat(collision):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=E7=BA=AF=E6=95=B0=E5=AD=A6=E7=A2=B0=E6=92=9E=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E6=A8=A1=E5=9D=97=20module.collision?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 dev/26.1 的 module.collision(82b4f6a)整体回移植,作为新模块 anvillib-collision-neoforge-1.21.1。模块为零 MC/NeoForge API 依赖的纯数学 AABB×三角形 SAT 碰撞检测,源码逐字搬移,仅补 compileOnly jspecify(@NullMarked)。 --- module.collision/build.gradle | 271 +++++++++ module.collision/gradle.properties | 4 + .../lib/v2/collision/AnvilLibCollision.java | 505 +++++++++++++++ .../lib/v2/collision/CollisionTest.java | 575 ++++++++++++++++++ .../lib/v2/collision/package-info.java | 4 + .../resources/META-INF/neoforge.mods.toml | 86 +++ .../resources/anvillib_collision.mixins.json | 14 + settings.gradle | 2 + 8 files changed, 1461 insertions(+) create mode 100644 module.collision/build.gradle create mode 100644 module.collision/gradle.properties create mode 100644 module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/AnvilLibCollision.java create mode 100644 module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/CollisionTest.java create mode 100644 module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/package-info.java create mode 100644 module.collision/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 module.collision/src/main/resources/anvillib_collision.mixins.json diff --git a/module.collision/build.gradle b/module.collision/build.gradle new file mode 100644 index 00000000..a0854c6d --- /dev/null +++ b/module.collision/build.gradle @@ -0,0 +1,271 @@ +plugins { + id 'java-library' + id 'eclipse' + id 'idea' + id 'maven-publish' + alias libs.plugins.modDevGradle + alias libs.plugins.lombok + alias libs.plugins.machete + id 'org.jreleaser' version '1.23.0' + id "me.modmuss50.mod-publish-plugin" version "1.1.0" +} + +String buildType = 'snapshot' +String buildNumber +if (System.getenv("CI_BUILD") == 'false') buildNumber = null +else { + if (System.getenv("PR_BUILD") != 'false') buildType = 'pr' + buildNumber = System.getenv("GITHUB_RUN_NUMBER") +} +version = "${mod_version}" + (buildNumber != null ? "+${buildType}.${buildNumber}" : "") +group = mod_group_id + +base { + archivesName = "${project.mod_id}-neoforge-${libs.versions.minecraft.get()}" +} + +repositories { + maven { url = "https://server.cjsah.net:1002/maven/" } + mavenLocal() + mavenCentral() +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +neoForge { + // Specify the version of NeoForge to use. + version = libs.versions.neoForge.get() + + parchment { + mappingsVersion = project.parchment_mappings_version + minecraftVersion = project.parchment_minecraft_version + } + + // This line is optional. Access Transformers are automatically detected + // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') + + interfaceInjectionData { + // from 'src/main/resources/interface_injections.json' + // publish file('src/main/resources/interface_injections.json') + } + + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + client { + client() + + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + server { + server() + programArgument '--nogui' + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + // This run config launches GameTestServer and runs all registered gametests, then exits. + // By default, the server will crash when no gametests are provided. + // The gametest system is also enabled by default for other run configs under the /test command. + gameTestServer { + type = "gameTestServer" + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + data { + data() + + // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it + // gameDirectory = project.file('run-data') + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } + + // applies to all the run configs above + configureEach { + // Recommended logging data for a userdev environment + // The markers can be added/remove as needed separated by commas. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + systemProperty 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + logLevel = org.slf4j.event.Level.DEBUG + } + } + + mods { + // define mod <-> source bindings + // these are used to tell the game which sources are for which mod + // mostly optional in a single mod project + // but multi mod projects should define one per mod + "${mod_id}" { + sourceSet(sourceSets.main) + } + } +} + +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } + + +dependencies { + compileOnly "org.jspecify:jspecify:1.0.0" +} + +// This block of code expands all declared replace properties in the specified resource targets. +// A missing property will result in an error. Properties are expanded using ${} Groovy notation. +// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. +// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html +tasks.withType(ProcessResources).configureEach { + var replaceProperties = [minecraft_version : libs.versions.minecraft.get(), + minecraft_version_range: minecraft_version_range, + neo_version : libs.versions.neoForge.get(), + neo_version_range : neo_version_range, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : version, + mod_authors : mod_authors, + mod_description : mod_description] + inputs.properties replaceProperties + + filesMatching(['META-INF/neoforge.mods.toml']) { + expand replaceProperties + } +} + +tasks.register('sourcesJar', Jar) { + from delombok.outputs.files + dependsOn delombok + archiveClassifier = "sources" +} + +tasks.register('javadocJar', Jar) { + from javadoc.destinationDir + dependsOn javadoc + archiveClassifier = "javadoc" +} + +javadoc { + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') +} + +jar { + from(rootProject.file("LICENSE")) + from(rootProject.file("ASSETS_LICENSE")) + from(rootProject.file("README.md")) + from(rootProject.file("README.en.md")) +} + +artifacts { + archives tasks.jar + archives tasks.sourcesJar + archives tasks.javadocJar +} + +def this_name = base.archivesName.get() +def this_description = mod_description + +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + + artifact sourcesJar + artifact javadocJar + + versionMapping { + allVariants { + fromResolutionResult() + } + } + + pom { + name = this_name + description = this_description + url = 'https://github.com/Anvil-Dev/AnvilLib' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/license/mit' + } + } + developers { + developer { + id = 'Anvil-Dev' + name = 'AnvilCraft Dev' + email = 'admin@anvilcraft.dev' + } + } + scm { + url = 'https://github.com/Anvil-Dev/AnvilLib' + connection = 'https://github.com/Anvil-Dev/AnvilLib.git' + developerConnection = 'https://github.com/Anvil-Dev/AnvilLib.git' + } + } + } + } + repositories { + // Add repositories to publish to here. + def MAVEN_URL = System.getenv("MAVEN_URL") + if (MAVEN_URL != null) { + maven { + url MAVEN_URL + credentials { + username System.getenv("MAVEN_USERNAME") + password System.getenv("MAVEN_PASSWORD") + } + } + } + mavenLocal() + maven { + name = "staging" + url = layout.buildDirectory.dir("staging-deploy") + } + } +} + +jreleaser { + signing { + active = 'ALWAYS' + armored = true + } + + deploy { + maven { + mavenCentral { + sonatype { + active = 'ALWAYS' + url = 'https://central.sonatype.com/api/v1/publisher' + stagingRepository('build/staging-deploy') + } + } + } + } +} + +// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} + +machete { + // disable machete locally for faster builds + enabled = false +} + +lombok { + version = "1.18.34" +} diff --git a/module.collision/gradle.properties b/module.collision/gradle.properties new file mode 100644 index 00000000..ca29d5c4 --- /dev/null +++ b/module.collision/gradle.properties @@ -0,0 +1,4 @@ +## Mod Properties +mod_id=anvillib_collision +mod_name=AnvilLib-Collision +mod_description=A simple library for calculating collisions diff --git a/module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/AnvilLibCollision.java b/module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/AnvilLibCollision.java new file mode 100644 index 00000000..2763b87f --- /dev/null +++ b/module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/AnvilLibCollision.java @@ -0,0 +1,505 @@ +package dev.anvilcraft.lib.v2.collision; + +import org.joml.Vector3d; +import org.joml.Vector3dc; +import org.joml.Vector3fc; + +public class AnvilLibCollision { + /** + * 沿三轴独立扫掠:AABB 分别沿 X、Y、Z 轴运动,逐轴计算最大安全位移。 + *

+ * 每个轴的处理是独立的——沿 X 扫掠时 Y、Z 坐标保持不变,沿 Y 扫掠时 X、Z + * 不变。因此当对角方向有障碍物时可能偏保守。 + *

+ * 三轴独立的好处是与 Minecraft 原版碰撞一致:各分量被各自的障碍物"推开", + * 避免了沿斜面滑动的问题。 + * + * @param min AABB 最小角(世界坐标) + * @param max AABB 最大角(世界坐标) + * @param motion 各轴最大位移(分量可正可负) + * @param triangles 三角形数组,每 3 个顶点构成一个三角形 + * @param epsilon 碰撞容差——间距小于此值即视为碰撞 + * @return 实际可移动位移,各分量符号与 {@code motion} 一致, + * 绝对值 ≤ {@code |motion|}。已重叠 → 0,无碰撞 → 原始分量 + */ + public static Vector3dc intersectsAABBTriangle(Vector3dc min, Vector3dc max, Vector3dc motion, Vector3fc[] triangles, double epsilon) { + Vector3d center = min.add(max, new Vector3d()).mul(0.5); + Vector3d halfExtents = max.sub(min, new Vector3d()).mul(0.5); + Vector3d result = new Vector3d(); + + for (int axisIndex = 0; axisIndex < 3; axisIndex++) { + double d = motion.get(axisIndex); + if (Math.abs(d) < 1e-15) { + result.setComponent(axisIndex, 0.0); + continue; + } + + Vector3d axisVec = new Vector3d(); + axisVec.setComponent(axisIndex, 1.0); + + double safeDist = d; + + int triCount = triangles.length / 3; + for (int i = 0; i < triCount; i++) { + Vector3fc V0 = triangles[i * 3]; + Vector3fc V1 = triangles[i * 3 + 1]; + Vector3fc V2 = triangles[i * 3 + 2]; + + double tHit = collisionDistanceOnAxis(V0, V1, V2, center, halfExtents, axisVec, d, epsilon); + + if (d > 0) { + if (tHit < safeDist && tHit >= 0) { + safeDist = tHit; + } + } else { + if (tHit > safeDist && tHit <= 0) { + safeDist = tHit; + } + } + + if ((d > 0 && safeDist <= 0) || (d < 0 && safeDist >= 0)) { + break; + } + } + result.setComponent(axisIndex, safeDist); + } + return result; + } + + /** + * 真扫掠碰撞:AABB 沿 {@code motion} 方向平移,求首次碰到任意三角形时的位移。 + *

+ * 与 {@link #intersectsAABBTriangle(Vector3dc, Vector3dc, Vector3dc, Vector3fc[], double)} + * 的三轴独立不同,此方法沿 motion 的合成方向扫掠,适用于需要精确斜角碰撞判定的场景。 + *

+ * 返回值为 {@code motion * t},其中 t ∈ [0,1] 为安全比例: + *

    + *
  • t = 1 → 全程无碰撞
  • + *
  • t = 0 → 起始位置已重叠
  • + *
  • {@code 0 < t < 1} → 碰撞发生在运动途中
  • + *
+ *

+ * 对每个三角形使用 SAT(分离轴定理)计算碰撞时间区间,取所有三角形中的最小值。 + * + * @param min AABB 最小角(世界坐标) + * @param max AABB 最大角(世界坐标) + * @param motion 位移向量(方向 + 大小) + * @param triangles 三角形数组,每 3 个顶点构成一个三角形 + * @param epsilon 碰撞容差——间距小于此值即视为碰撞 + * @return 沿 {@code motion} 方向的最大安全位移向量。 + * 无碰撞时等于 {@code motion},初始重叠时为零向量 + */ + public static Vector3dc sweptCollisionAABBTriangle(Vector3dc min, Vector3dc max, Vector3dc motion, Vector3fc[] triangles, double epsilon) { + double dist = motion.length(); + if (dist < 1e-15) { + return new Vector3d(); + } + + Vector3d dir = motion.div(dist, new Vector3d()); + Vector3d center = min.add(max, new Vector3d()).mul(0.5); + Vector3d halfExtents = max.sub(min, new Vector3d()).mul(0.5); + + double minT = dist; + int triCount = triangles.length / 3; + for (int i = 0; i < triCount; i++) { + Vector3fc V0 = triangles[i * 3]; + Vector3fc V1 = triangles[i * 3 + 1]; + Vector3fc V2 = triangles[i * 3 + 2]; + + double tHit = collisionDistanceOnAxis(V0, V1, V2, center, halfExtents, dir, dist, epsilon); + if (tHit < minT) { + minT = tHit; + if (minT <= 0) break; + } + } + + return dir.mul(minT, new Vector3d()); + } + + /** + * 计算 AABB 沿 {@code axisVec} 方向运动时,与单个三角形的首次碰撞位移。 + *

+ * 三角形顶点被平移到以 {@code center} 为原点的局部坐标系, + * 使用 SAT(分离轴定理)扫描 13 条轴(3 坐标轴 + 1 面法线 + 9 边叉积轴), + * 由 {@link #updateInterval} 逐轴收窄碰撞时间区间,最后取与运动方向一致的边界。 + * + * @param V0,V1,V2 三角形顶点(世界坐标) + * @param center AABB 中心点 + * @param halfExtents AABB 半边长 + * @param axisVec 运动方向(无需单位化) + * @param d 最大位移量(沿 {@code axisVec} 方向,可正可负) + * @param epsilon 碰撞容差 + * @return 首次碰撞位移(有符号)。0 = 初始已重叠,d = 全程无碰撞 + */ + private static double collisionDistanceOnAxis( + Vector3fc V0, + Vector3fc V1, + Vector3fc V2, + Vector3dc center, + Vector3dc halfExtents, + Vector3dc axisVec, + double d, + double epsilon + ) { + + // 将三角形平移到 AABB 中心为原点的局部坐标 + Vector3d v0 = new Vector3d(V0).sub(center); + Vector3d v1 = new Vector3d(V1).sub(center); + Vector3d v2 = new Vector3d(V2).sub(center); + + // 初始就已经碰撞 → 安全距离为 0 + if (initialOverlap(v0, v1, v2, halfExtents, epsilon)) { + return 0.0; + } + + // 三角形边向量 + Vector3d f0 = v1.sub(v0, new Vector3d()); + Vector3d f1 = v2.sub(v1, new Vector3d()); + Vector3d f2 = v0.sub(v2, new Vector3d()); + + // 面法线 + Vector3d normal = f0.cross(f1, new Vector3d()); + boolean hasNormal = normal.lengthSquared() > 1e-15; + if (hasNormal) normal.normalize(); + + // AABB 三个面法线 + Vector3d[] aabbAxes = { + new Vector3d(1, 0, 0), + new Vector3d(0, 1, 0), + new Vector3d(0, 0, 1) + }; + Vector3d[] triEdges = { + f0, + f1, + f2 + }; + + // 交集区间 [tLow, tHigh] 表示所有分离轴都重叠的 t 范围 + double[] interval = { + Double.NEGATIVE_INFINITY, + Double.POSITIVE_INFINITY + }; + + // 测试 3 个坐标轴 + for (Vector3d axis : aabbAxes) { + if (!updateInterval(axis, v0, v1, v2, halfExtents, axisVec, epsilon, interval)) { + return d; // 无碰撞 + } + } + + // 测试三角形面法线 + if (hasNormal) { + if (!updateInterval(normal, v0, v1, v2, halfExtents, axisVec, epsilon, interval)) { + return d; + } + } + + // 测试 9 条边叉积轴 + for (Vector3d edge : triEdges) { + for (Vector3d aabbAxis : aabbAxes) { + Vector3d axis = edge.cross(aabbAxis, new Vector3d()); + if (axis.lengthSquared() > 1e-15) { + axis.normalize(); + if (!updateInterval(axis, v0, v1, v2, halfExtents, axisVec, epsilon, interval)) { + return d; + } + } + } + } + + double tLow = interval[0]; + double tHigh = interval[1]; + + // 若交集为空或退化,则无碰撞 + if (tLow > tHigh + 1e-12) { + return d; + } + + // 根据运动方向取第一个(最接近 0 的)碰撞位移 + if (d > 0) { + if (tHigh < 0) return d; // 区间全在负半轴 + if (tLow > 0) return tLow; + return d; + } else { + if (tLow > 0) return d; // 区间全在正半轴 + if (tHigh < 0) return tHigh; + return d; + } + } + + /** + * 在给定 SAT 分离轴上计算 AABB 与三角形重叠的 t 区间,并与当前区间求交。 + *

+ * AABB 中心沿 {@code axisVec} 运动,在参数 t 时刻的位置为 {@code center + t * axisVec}。 + * 三角形在轴上的投影为 [tMin, tMax],AABB 的投影半径在运动中不变(仅平移,不旋转)。 + *

+ * 若该轴与运动方向垂直({@code dot ≈ 0}),则检查静态分离;否则求解线性不等式 + * 得到重叠的 t 范围 [low, high],与当前区间取交集。 + * + * @param axis SAT 分离轴(无需单位化) + * @param v0,v1,v2 局部坐标系下的三角形顶点 + * @param halfExtents AABB 半边长 + * @param axisVec 运动方向 + * @param epsilon 碰撞容差 + * @param interval [0]=tLow, [1]=tHigh,会被就地更新 + * @return false 表示该轴永久分离,运动全程不可能碰撞 + */ + private static boolean updateInterval( + Vector3d axis, + Vector3d v0, + Vector3d v1, + Vector3d v2, + Vector3dc halfExtents, + Vector3dc axisVec, + double epsilon, + double[] interval + ) { + + // 三角形投影 + double p0 = axis.dot(v0); + double p1 = axis.dot(v1); + double p2 = axis.dot(v2); + double tMin = Math.min(Math.min(p0, p1), p2); + double tMax = Math.max(Math.max(p0, p1), p2); + + // AABB 投影半径 + double r = Math.abs(axis.x()) * halfExtents.x() + Math.abs(axis.y()) * halfExtents.y() + Math.abs(axis.z()) * halfExtents.z(); + + double dot = axis.dot(axisVec); // 移动方向在分离轴上的投影 + + if (Math.abs(dot) < 1e-15) { + // 轴与运动方向垂直 → 投影不随 t 改变 + // 若此时该轴是分离轴,则运动全程分离 → 无碰撞可能 + if (-r > tMax + epsilon || tMin - epsilon > r) { + return false; // 永远分离 + } + // 否则该轴始终重叠,不提供约束 + return true; + } + + double low, high; + if (dot > 0) { + low = (tMin - epsilon - r) / dot; + high = (tMax + epsilon + r) / dot; + } else { + low = (tMax + epsilon + r) / dot; // dot 为负,low 是较小值 + high = (tMin - epsilon - r) / dot; + } + + interval[0] = Math.max(interval[0], low); + interval[1] = Math.min(interval[1], high); + + return interval[0] <= interval[1] + 1e-12; + } + + /** + * 静态 SAT 检查:AABB 与三角形在初始位置(t=0,局部坐标)是否重叠。 + *

+ * 测试全部 13 条分离轴(3 坐标轴 + 1 面法线 + 9 边叉积轴), + * 若任意轴分离则返回 false。 + * + * @param v0,v1,v2 局部坐标系下的三角形顶点(已减去 AABB 中心) + * @param halfExtents AABB 半边长 + * @param epsilon 碰撞容差 + * @return true 表示重叠(或 gap ≤ epsilon) + */ + private static boolean initialOverlap(Vector3d v0, Vector3d v1, Vector3d v2, Vector3dc halfExtents, double epsilon) { + + // 三个坐标轴 + if (!overlapOnAxis(new Vector3d(1, 0, 0), v0, v1, v2, halfExtents, epsilon)) return false; + if (!overlapOnAxis(new Vector3d(0, 1, 0), v0, v1, v2, halfExtents, epsilon)) return false; + if (!overlapOnAxis(new Vector3d(0, 0, 1), v0, v1, v2, halfExtents, epsilon)) return false; + + Vector3d f0 = v1.sub(v0, new Vector3d()); + Vector3d f1 = v2.sub(v1, new Vector3d()); + Vector3d normal = f0.cross(f1, new Vector3d()); + if (normal.lengthSquared() > 1e-15) { + normal.normalize(); + if (!overlapOnAxis(normal, v0, v1, v2, halfExtents, epsilon)) return false; + } + + Vector3d f2 = v0.sub(v2, new Vector3d()); + Vector3d[] edges = { + f0, + f1, + f2 + }; + Vector3d[] axes = { + new Vector3d(1, 0, 0), + new Vector3d(0, 1, 0), + new Vector3d(0, 0, 1) + }; + for (Vector3d edge : edges) { + for (Vector3d a : axes) { + Vector3d axis = edge.cross(a, new Vector3d()); + if (axis.lengthSquared() > 1e-15) { + axis.normalize(); + if (!overlapOnAxis(axis, v0, v1, v2, halfExtents, epsilon)) return false; + } + } + } + return true; // 所有轴都重叠 → 碰撞 + } + + /** + * 静态碰撞检测:判断 AABB 与任意三角形是否重叠。 + *

+ * 对每个三角形先做包围盒粗筛(AABB-AABB 快速剔除), + * 再以完整的 SAT(13 条分离轴)进行精确判定。 + * + * @param min AABB 最小角 + * @param max AABB 最大角 + * @param triangles 三角形数组,每 3 个顶点构成一个三角形 + * @param epsilon 碰撞容差——间距小于此值即视为重叠 + * @return true 表示存在至少一个三角形与 AABB 碰撞 + */ + public static boolean intersectsAABBTriangle(Vector3dc min, Vector3dc max, Vector3fc[] triangles, double epsilon) { + Vector3dc center = min.add(max, new Vector3d()).mul(0.5); + Vector3dc halfExtents = max.sub(min, new Vector3d()).mul(0.5); + + int triangleCount = triangles.length / 3; + for (int i = 0; i < triangleCount; i++) { + Vector3fc V0 = triangles[i * 3]; + Vector3fc V1 = triangles[i * 3 + 1]; + Vector3fc V2 = triangles[i * 3 + 2]; + + if (triangleIntersectsAABB(V0, V1, V2, center, halfExtents, epsilon)) { + return true; + } + } + return false; + } + + /** + * 对单个三角形执行完整 SAT 碰撞检测(13 条轴)。 + *

+ * 先做三角形包围盒粗筛(AABB-AABB),再将顶点平移到以 AABB 中心为原点的 + * 局部坐标系,依次测试 3 坐标轴 → 面法线 → 9 条边叉积轴。 + * + * @param V0,V1,V2 三角形顶点(世界坐标) + * @param center AABB 中心点 + * @param halfExtents AABB 半边长 + * @param epsilon 碰撞容差 + * @return true 表示三角形与 AABB 重叠(或 gap ≤ epsilon) + */ + private static boolean triangleIntersectsAABB( + Vector3fc V0, + Vector3fc V1, + Vector3fc V2, + Vector3dc center, + Vector3dc halfExtents, + double epsilon + ) { + + // 1. 三角形包围盒粗筛(快速剔除) + double triMinX = Math.min(Math.min(V0.x(), V1.x()), V2.x()); + double triMinY = Math.min(Math.min(V0.y(), V1.y()), V2.y()); + double triMinZ = Math.min(Math.min(V0.z(), V1.z()), V2.z()); + double triMaxX = Math.max(Math.max(V0.x(), V1.x()), V2.x()); + double triMaxY = Math.max(Math.max(V0.y(), V1.y()), V2.y()); + double triMaxZ = Math.max(Math.max(V0.z(), V1.z()), V2.z()); + + double boxMinX = center.x() - halfExtents.x(); + double boxMinY = center.y() - halfExtents.y(); + double boxMinZ = center.z() - halfExtents.z(); + double boxMaxX = center.x() + halfExtents.x(); + double boxMaxY = center.y() + halfExtents.y(); + double boxMaxZ = center.z() + halfExtents.z(); + + if (triMinX > boxMaxX + epsilon || triMaxX < boxMinX - epsilon || triMinY > boxMaxY + epsilon || triMaxY < boxMinY - epsilon || triMinZ > boxMaxZ + epsilon || triMaxZ < boxMinZ - epsilon) { + return false; + } + + // 2. 将三角形顶点平移到 AABB 中心为原点的局部坐标(使用 double 保证精度) + Vector3d v0 = new Vector3d(V0).sub(center); + Vector3d v1 = new Vector3d(V1).sub(center); + Vector3d v2 = new Vector3d(V2).sub(center); + + // 3. 三角形边向量 + Vector3d f0 = v1.sub(v0, new Vector3d()); + Vector3d f1 = v2.sub(v1, new Vector3d()); + Vector3d f2 = v0.sub(v2, new Vector3d()); + + Vector3d aabbAxisX = new Vector3d(1, 0, 0); + Vector3d aabbAxisY = new Vector3d(0, 1, 0); + Vector3d aabbAxisZ = new Vector3d(0, 0, 1); + + // ---- 测试三个坐标轴 ---- + if (!overlapOnAxis(aabbAxisX, v0, v1, v2, halfExtents, epsilon)) return false; + if (!overlapOnAxis(aabbAxisY, v0, v1, v2, halfExtents, epsilon)) return false; + if (!overlapOnAxis(aabbAxisZ, v0, v1, v2, halfExtents, epsilon)) return false; + + // ---- 测试三角形面法线轴 ---- + Vector3d normal = f0.cross(f1, new Vector3d()); + if (normal.lengthSquared() > 1e-15) { + normal.normalize(); + if (!overlapOnAxis(normal, v0, v1, v2, halfExtents, epsilon)) return false; + } + // 如果面积极小(退化为线段或点),跳过此轴(后续边叉积轴可能仍有效) + + // ---- 测试 9 条边叉积轴 ---- + Vector3d[] aabbAxes = { + aabbAxisX, + aabbAxisY, + aabbAxisZ + }; + Vector3d[] triEdges = { + f0, + f1, + f2 + }; + + for (Vector3d edge : triEdges) { + for (Vector3d aabbAxis : aabbAxes) { + Vector3d axis = edge.cross(aabbAxis, new Vector3d()); + if (axis.lengthSquared() > 1e-15) { + axis.normalize(); + if (!overlapOnAxis(axis, v0, v1, v2, halfExtents, epsilon)) { + return false; + } + } + // 平行或退化轴忽略,不会提供有效分离 + } + } + + // 所有轴都重叠 ⇒ 碰撞 + return true; + } + + /** + * 测试三角形与中心在原点的 AABB 在给定轴上的投影是否重叠。 + *

+ * 三角形投影区间 [tMin, tMax],AABB 投影区间 [−r, r],其中 + * r = Σ|axis[i] · halfExtents[i]|。若两区间带容差后仍分离则返回 false。 + *

+ * 分离条件:tMin > r + epsilon 或 −r > tMax + epsilon。 + * + * @param axis 投影轴(无需单位化) + * @param v0,v1,v2 局部坐标系下的三角形顶点 + * @param boxHalfExtents AABB 半边长 + * @param epsilon 碰撞容差——扩大 AABB 投影半径使"接近"也视为重叠 + * @return true 表示两投影区间有交集 + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public static boolean overlapOnAxis( + Vector3dc axis, + Vector3dc v0, + Vector3dc v1, + Vector3dc v2, + Vector3dc boxHalfExtents, + double epsilon + ) { + double p0 = axis.x() * v0.x() + axis.y() * v0.y() + axis.z() * v0.z(); + double p1 = axis.x() * v1.x() + axis.y() * v1.y() + axis.z() * v1.z(); + double p2 = axis.x() * v2.x() + axis.y() * v2.y() + axis.z() * v2.z(); + + double tMin = Math.min(Math.min(p0, p1), p2); + double tMax = Math.max(Math.max(p0, p1), p2); + + double r = Math.abs(axis.x() * boxHalfExtents.x()) + + Math.abs(axis.y() * boxHalfExtents.y()) + + Math.abs(axis.z() * boxHalfExtents.z()); + + return !(tMin > r + epsilon) && !(-r > tMax + epsilon); + } +} diff --git a/module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/CollisionTest.java b/module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/CollisionTest.java new file mode 100644 index 00000000..40d1ec36 --- /dev/null +++ b/module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/CollisionTest.java @@ -0,0 +1,575 @@ +package dev.anvilcraft.lib.v2.collision; + +import org.joml.Vector3d; +import org.joml.Vector3dc; +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.jetbrains.annotations.ApiStatus; + +import static dev.anvilcraft.lib.v2.collision.AnvilLibCollision.intersectsAABBTriangle; +import static dev.anvilcraft.lib.v2.collision.AnvilLibCollision.overlapOnAxis; +import static dev.anvilcraft.lib.v2.collision.AnvilLibCollision.sweptCollisionAABBTriangle; + +@ApiStatus.Internal +public class CollisionTest { + + static void main() { + int[] counts = { + 0, + 0 + }; // [passed, failed] + + // ============================================================ + // PART A -- intersectsAABBTriangle(min, max, triangles, epsilon) + // AABB: (0,0,0) -> (2,2,2), 2x2x2 cube + // ============================================================ + Vector3dc boxMin = new Vector3d(0, 0, 0); + Vector3dc boxMax = new Vector3d(2, 2, 2); + + // A1: triangle fully inside -> true + { + Vector3fc[] tris = { + vf(0.5f, 0.5f, 0.5f), + vf(1.5f, 0.5f, 0.5f), + vf(0.5f, 1.5f, 0.5f) + }; + check(counts, "A1: fully inside", true, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A2: outside +X -> false + { + Vector3fc[] tris = { + vf(3, 0, 0), + vf(4, 1, 0), + vf(3, 1, 1) + }; + check(counts, "A2: outside +X", false, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A3: outside -X -> false + { + Vector3fc[] tris = { + vf(-3, 0, 0), + vf(-1, 1, 0), + vf(-2, 1, 1) + }; + check(counts, "A3: outside -X", false, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A4: outside +Y -> false + { + Vector3fc[] tris = { + vf(0, 3, 0), + vf(2, 4, 0), + vf(1, 3, 2) + }; + check(counts, "A4: outside +Y", false, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A5: outside -Z -> false + { + Vector3fc[] tris = { + vf(0, 0, -2), + vf(2, 1, -1), + vf(1, 2, -3) + }; + check(counts, "A5: outside -Z", false, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A6: piercing through -> true + { + Vector3fc[] tris = { + vf(-1, 1, 1), + vf(3, 1, 1), + vf(1, -1, 3) + }; + check(counts, "A6: piercing through", true, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A7: touching right face (x=2), eps=0 -> true + { + Vector3fc[] tris = { + vf(2, 0, 0), + vf(2, 2, 0), + vf(2, 1, 2) + }; + check(counts, "A7: touching face, eps=0", true, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A8: gap=0.0001, eps=0 -> false; eps=0.001 -> true + { + Vector3fc[] tris = { + vf(2.0001f, 0, 0), + vf(2.0001f, 2, 0), + vf(2.0001f, 1, 2) + }; + check(counts, "A8a: gap 0.0001 eps=0", false, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + check(counts, "A8b: gap 0.0001 eps=0.001", true, intersectsAABBTriangle(boxMin, boxMax, tris, 0.001)); + } + + // A9: degenerate point inside -> true + { + Vector3fc[] tris = { + vf(1, 1, 1), + vf(1, 1, 1), + vf(1, 1, 1) + }; + check(counts, "A9: point inside", true, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A10: degenerate point outside -> false + { + Vector3fc[] tris = { + vf(5, 5, 5), + vf(5, 5, 5), + vf(5, 5, 5) + }; + check(counts, "A10: point outside", false, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A11: degenerate line intersecting -> true + { + Vector3fc[] tris = { + vf(-1, 1, 1), + vf(3, 1, 1), + vf(1, 1, 1) + }; + check(counts, "A11: line intersecting", true, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A12: degenerate line outside -> false + { + Vector3fc[] tris = { + vf(5, 1, 1), + vf(7, 1, 1), + vf(6, 1, 1) + }; + check(counts, "A12: line outside", false, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A13: large triangle containing AABB (coplanar z=1) -> true + { + Vector3fc[] tris = { + vf(-5, -5, 1), + vf(7, -5, 1), + vf(1, 7, 1) + }; + check(counts, "A13: triangle contains AABB", true, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A14: batch -- only middle triangle intersects -> true + { + Vector3fc[] tris = { + vf(5, 0, 0), + vf(6, 1, 0), + vf(5, 1, 1), + vf(0.5f, 0.5f, 0.5f), + vf(1.5f, 0.5f, 0.5f), + vf(0.5f, 1.5f, 1.5f), + vf(-3, 0, 0), + vf(-2, 1, 0), + vf(-3, 1, 1), + }; + check(counts, "A14: batch mid intersects", true, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A15: batch -- none intersect -> false + { + Vector3fc[] tris = { + vf(5, 0, 0), + vf(6, 1, 0), + vf(5, 1, 1), + vf(-3, 0, 0), + vf(-2, 1, 0), + vf(-3, 1, 1), + }; + check(counts, "A15: batch none intersect", false, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A16: tilted triangle, cross-axis intersection -> true + { + Vector3fc[] tris = { + vf(-0.5f, 2.5f, -0.5f), + vf(2.5f, 2.5f, -0.5f), + vf(-0.5f, -1f, 2.5f) + }; + check(counts, "A16: cross-axis intersection", true, intersectsAABBTriangle(boxMin, boxMax, tris, 0.0)); + } + + // A17: negative-extent AABB -> false + { + Vector3dc badMin = new Vector3d(3, 3, 3); + Vector3dc badMax = new Vector3d(1, 1, 1); + Vector3fc[] tris = { + vf(2, 2, 2), + vf(2, 2, 2), + vf(2, 2, 2) + }; + check(counts, "A17: negative-extent AABB", false, intersectsAABBTriangle(badMin, badMax, tris, 0.0)); + } + + // ============================================================ + // PART B -- overlapOnAxis direct unit tests + // ============================================================ + { + Vector3d axisX = new Vector3d(1, 0, 0); + Vector3d halfExt = new Vector3d(1, 1, 1); + + // B1: tri=[0.5,1.5] overlaps b=[-1,1] -> true + check( + counts, "B1: overlapOnAxis overlap", true, + overlapOnAxis(axisX, new Vector3d(0.5, 0, 0), new Vector3d(1.5, 0, 0), new Vector3d(1.0, 1, 0), halfExt, 0.0) + ); + + // B2: tri=[3,5] vs b=[-1,1], separated -> false + check( + counts, "B2: overlapOnAxis separated", false, + overlapOnAxis(axisX, new Vector3d(3, 0, 0), new Vector3d(5, 0, 0), new Vector3d(4, 1, 0), halfExt, 0.0) + ); + + // B3: tri=[1.001,2] vs b=[-1,1], gap=0.001 eps=0 -> false + check( + counts, "B3: overlapOnAxis gap eps=0", false, + overlapOnAxis(axisX, new Vector3d(1.001, 0, 0), new Vector3d(2.0, 0, 0), new Vector3d(1.5, 1, 0), halfExt, 0.0) + ); + + // B4: same gap with eps=0.01 -> true + check( + counts, "B4: overlapOnAxis gap eps=0.01", true, + overlapOnAxis(axisX, new Vector3d(1.001, 0, 0), new Vector3d(2.0, 0, 0), new Vector3d(1.5, 1, 0), halfExt, 0.01) + ); + } + + // ============================================================ + // PART C -- swept: intersectsAABBTriangle(min,max,motion,tris,eps) + // AABB: (0,0,0)->(2,2,2), center=(1,1,1), half=(1,1,1) + // ============================================================ + + // C1: no obstacle -> full motion + { + Vector3dc motion = new Vector3d(5, 0, 0); + Vector3fc[] tris = { + vf(10, 0, 0), + vf(12, 2, 0), + vf(10, 2, 2) + }; + Vector3dc result = intersectsAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "C1: no obstacle +X", new Vector3d(5, 0, 0), result); + } + + // C2: wall at x=5 blocks +X motion, AABB.maxX reaches 5 at t=3 + { + Vector3dc motion = new Vector3d(5, 0, 0); + Vector3fc[] tris = { + vf(5, 0, 0), + vf(5, 2, 0), + vf(5, 1, 2) + }; + Vector3dc result = intersectsAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "C2: blocked +X at x=5", new Vector3d(3, 0, 0), result); + } + + // C3: wall at x=-2 blocks -X motion, AABB.minX reaches -2 at t=-2 + { + Vector3dc motion = new Vector3d(-5, 0, 0); + Vector3fc[] tris = { + vf(-2, 0, 0), + vf(-2, 2, 0), + vf(-2, 1, 2) + }; + Vector3dc result = intersectsAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "C3: blocked -X at x=-2", new Vector3d(-2, 0, 0), result); + } + + // C4: initial overlap -> 0 for that axis + { + Vector3dc motion = new Vector3d(5, 0, 0); + Vector3fc[] tris = { + vf(0.5f, 0.5f, 0.5f), + vf(1.5f, 0.5f, 0.5f), + vf(0.5f, 1.5f, 0.5f) + }; + Vector3dc result = intersectsAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "C4: initial overlap -> 0", new Vector3d(0, 0, 0), result); + } + + // C5: wall at y=5 blocks +Y, AABB.maxY=2, reaches at t=3 + { + Vector3dc motion = new Vector3d(0, 5, 0); + Vector3fc[] tris = { + vf(0, 5, 0), + vf(2, 5, 0), + vf(1, 5, 2) + }; + Vector3dc result = intersectsAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "C5: blocked +Y at y=5", new Vector3d(0, 3, 0), result); + } + + // C6: wall at z=5 blocks +Z + { + Vector3dc motion = new Vector3d(0, 0, 5); + Vector3fc[] tris = { + vf(0, 0, 5), + vf(2, 0, 5), + vf(1, 2, 5) + }; + Vector3dc result = intersectsAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "C6: blocked +Z at z=5", new Vector3d(0, 0, 3), result); + } + + // C7: diag motion, X blocked, Y free + { + Vector3dc motion = new Vector3d(5, 5, 0); + Vector3fc[] tris = { + vf(5, 0, 0), + vf(5, 2, 0), + vf(5, 1, 10) + }; + Vector3dc result = intersectsAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "C7: +X blocked, +Y free", new Vector3d(3, 5, 0), result); + } + + // C8: epsilon bridges small gap -> full motion returned + { + Vector3dc motion = new Vector3d(5, 0, 0); + Vector3fc[] tris = { + vf(2.5f, 0, 0), + vf(2.5f, 2, 0), + vf(2.5f, 1, 2) + }; + Vector3dc r0 = intersectsAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + Vector3dc r1 = intersectsAABBTriangle(boxMin, boxMax, motion, tris, 1.0); + check3(counts, "C8a: gap=0.5 eps=0 blocked at 0.5", new Vector3d(0.5, 0, 0), r0); + check3(counts, "C8b: gap=0.5 eps=1 initial overlap", new Vector3d(0, 0, 0), r1); + } + + // C9: moving away from obstacle -> full motion + { + Vector3dc motion = new Vector3d(-5, 0, 0); + Vector3fc[] tris = { + vf(5, 0, 0), + vf(5, 2, 0), + vf(5, 1, 2) + }; + Vector3dc result = intersectsAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "C9: moving away full motion", new Vector3d(-5, 0, 0), result); + } + + // C10: X wall at x=5 + Y wall at y=5 — each blocks its axis + { + Vector3dc motion = new Vector3d(5, 5, 0); + Vector3fc[] tris = { + vf(5, 0, 0), vf(5, 2, 0), vf(5, 1, 10), // wall at x=5, blocks X + vf(0, 5, 0), vf(2, 5, 0), vf(1, 5, 10), // wall at y=5, blocks Y + }; + Vector3dc result = intersectsAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "C10: diag blocked X and Y", new Vector3d(3, 3, 0), result); + } + + // ============================================================ + // PART D -- true swept: sweptCollisionAABBTriangle(min,max,motion,tris,eps) + // AABB: (0,0,0)->(2,2,2), center=(1,1,1) + // Key difference from per-axis: sweeps along the motion direction + // vector, not independently per axis + // ============================================================ + + // D1: axis-aligned motion, wall at x=5 → same result as per-axis C2 + { + Vector3dc motion = new Vector3d(5, 0, 0); + Vector3fc[] tris = { vf(5, 0, 0), vf(5, 2, 0), vf(5, 1, 2) }; + Vector3dc result = sweptCollisionAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "D1: axis-aligned blocked +X", new Vector3d(3, 0, 0), result); + } + + // D2: diagonal slides past narrow wall → not blocked (per-axis would give (3,5,0)) + { + Vector3dc motion = new Vector3d(5, 5, 0); + Vector3fc[] tris = { vf(5, 0, 0), vf(5, 2, 0), vf(5, 1, 2) }; + Vector3dc result = sweptCollisionAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "D2: diag slides past narrow wall", new Vector3d(5, 5, 0), result); + } + + // D3: initial overlap → zero + { + Vector3dc motion = new Vector3d(5, 0, 0); + Vector3fc[] tris = { vf(0.5f, 0.5f, 0.5f), vf(1.5f, 0.5f, 0.5f), vf(0.5f, 1.5f, 0.5f) }; + Vector3dc result = sweptCollisionAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "D3: initial overlap", new Vector3d(0, 0, 0), result); + } + + // D4: no obstacle → full diagonal motion + { + Vector3dc motion = new Vector3d(5, 5, 0); + Vector3fc[] tris = { vf(20, 0, 0), vf(22, 2, 0), vf(20, 2, 2) }; + Vector3dc result = sweptCollisionAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "D4: no obstacle diag", new Vector3d(5, 5, 0), result); + } + + // D5: diagonal blocked by large wall → blocked at t=0.6 → motion * 0.6 + { + Vector3dc motion = new Vector3d(5, 5, 0); + Vector3fc[] tris = { vf(5, 0, 0), vf(5, 10, 0), vf(5, 5, 10) }; + Vector3dc result = sweptCollisionAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "D5: diag blocked by large wall", new Vector3d(3, 3, 0), result); + } + + // D6: zero-length motion → zero vector + { + Vector3dc motion = new Vector3d(0, 0, 0); + Vector3fc[] tris = { vf(5, 0, 0), vf(5, 2, 0), vf(5, 1, 2) }; + Vector3dc result = sweptCollisionAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + check3(counts, "D6: zero motion", new Vector3d(0, 0, 0), result); + } + + // D7: epsilon bridges gap → initial overlap → zero + { + Vector3dc motion = new Vector3d(5, 0, 0); + Vector3fc[] tris = { vf(2.5f, 0, 0), vf(2.5f, 2, 0), vf(2.5f, 1, 2) }; + Vector3dc r = sweptCollisionAABBTriangle(boxMin, boxMax, motion, tris, 1.0); + check3(counts, "D7: gap=0.5 eps=1 initial overlap", new Vector3d(0, 0, 0), r); + } + + // ============================================================ + // Report + // ============================================================ + System.out.println("========================================"); + System.out.println("Passed: " + counts[0] + " / " + (counts[0] + counts[1])); + if (counts[1] > 0) { + System.out.println("FAILED: " + counts[1]); + } else { + System.out.println("ALL TESTS PASSED"); + } + System.out.println("========================================"); + + bench(); + } + + // ================================================================ + // Performance benchmark + // ================================================================ + private static void bench() { + Vector3dc boxMin = new Vector3d(0, 0, 0); + Vector3dc boxMax = new Vector3d(2, 2, 2); + Vector3dc motion = new Vector3d(0.5, 0.3, 0.2); + int[] triCounts = {1, 10, 100, 1000, 10000}; + int warmupIters = 2000; + + System.out.println(); + System.out.println("==================== BENCHMARK ===================="); + System.out.printf("%-8s | %-16s | %-16s | %-16s%n", + "tris", "static SAT", "per-axis swept", "true swept"); + System.out.println("---------+------------------+------------------+------------------"); + + for (int n : triCounts) { + Vector3fc[] tris = generateTriangles(n, 42 + n); + + // Determine iteration count per scenario + int iters = Math.max(200, 100000 / Math.max(1, n)); + + // Warmup + for (int i = 0; i < warmupIters; i++) { + intersectsAABBTriangle(boxMin, boxMax, tris, 0.0); + intersectsAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + sweptCollisionAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + } + + // Static SAT + long t0 = System.nanoTime(); + for (int i = 0; i < iters; i++) { + intersectsAABBTriangle(boxMin, boxMax, tris, 0.0); + } + long t1 = System.nanoTime(); + double staticNs = (double) (t1 - t0) / iters; + + // Per-axis swept + long t2 = System.nanoTime(); + for (int i = 0; i < iters; i++) { + intersectsAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + } + long t3 = System.nanoTime(); + double perAxisNs = (double) (t3 - t2) / iters; + + // True swept + long t4 = System.nanoTime(); + for (int i = 0; i < iters; i++) { + sweptCollisionAABBTriangle(boxMin, boxMax, motion, tris, 0.0); + } + long t5 = System.nanoTime(); + double trueSweptNs = (double) (t5 - t4) / iters; + + System.out.printf("%-8d | %8.1f ns/op | %8.1f ns/op | %8.1f ns/op%n", + n, staticNs, perAxisNs, trueSweptNs); + } + System.out.println("===================================================="); + System.out.println("AABB: (0,0,0)->(2,2,2) motion: (0.5,0.3,0.2) eps: 0"); + System.out.println("Triangles: random mix in [-10,12] cube, ~50% intersect"); + System.out.println(); + } + + /** + * Generates {@code count} triangles in a pseudo-random but deterministic + * pattern. Roughly half will intersect the AABB (0,0,0)→(2,2,2) so both + * the early-out and full-SAT paths are exercised. + */ + private static Vector3fc[] generateTriangles(int count, long seed) { + Vector3fc[] tris = new Vector3fc[count * 3]; + long state = seed; + for (int i = 0; i < count; i++) { + // LCG: state = state * 6364136223846793005L + 1442695040888963407L + state = state * 0x5851F42D4C957F2DL + 0x14057B7EF767814FL; + + // Pick a center in [-10, 12] for each axis + double cx = -10.0 + ((state >>> 16) & 0x7FFFFF) / 524287.0 * 22.0; + state = state * 0x5851F42D4C957F2DL + 0x14057B7EF767814FL; + double cy = -10.0 + ((state >>> 16) & 0x7FFFFF) / 524287.0 * 22.0; + state = state * 0x5851F42D4C957F2DL + 0x14057B7EF767814FL; + double cz = -10.0 + ((state >>> 16) & 0x7FFFFF) / 524287.0 * 22.0; + + // Random offsets for 3 vertices within a 3-unit radius + double[][] offsets = new double[3][3]; + for (int v = 0; v < 3; v++) { + for (int a = 0; a < 3; a++) { + state = state * 0x5851F42D4C957F2DL + 0x14057B7EF767814FL; + offsets[v][a] = -1.5 + ((state >>> 16) & 0x7FFFFF) / 524287.0 * 3.0; + } + } + + tris[i * 3] = new Vector3f((float)(cx + offsets[0][0]), (float)(cy + offsets[0][1]), (float)(cz + offsets[0][2])); + tris[i * 3 + 1] = new Vector3f((float)(cx + offsets[1][0]), (float)(cy + offsets[1][1]), (float)(cz + offsets[1][2])); + tris[i * 3 + 2] = new Vector3f((float)(cx + offsets[2][0]), (float)(cy + offsets[2][1]), (float)(cz + offsets[2][2])); + } + return tris; + } + + private static void check(int[] c, String name, boolean expected, boolean actual) { + if (expected == actual) { + System.out.println("[PASS] " + name); + c[0]++; + } else { + System.out.println("[FAIL] " + name + " -- expected " + expected + ", got " + actual); + c[1]++; + } + } + + private static void check3(int[] c, String name, Vector3dc expected, Vector3dc actual) { + double tol = 1e-9; + boolean ok = Math.abs(expected.x() - actual.x()) < tol + && Math.abs(expected.y() - actual.y()) < tol + && Math.abs(expected.z() - actual.z()) < tol; + if (ok) { + System.out.println("[PASS] " + name); + c[0]++; + } else { + System.out.printf( + "[FAIL] %s -- expected (%f,%f,%f), got (%f,%f,%f)%n", + name, expected.x(), expected.y(), expected.z(), + actual.x(), actual.y(), actual.z() + ); + c[1]++; + } + } + + private static Vector3f vf(float x, float y, float z) { + return new Vector3f(x, y, z); + } +} diff --git a/module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/package-info.java b/module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/package-info.java new file mode 100644 index 00000000..84cf6564 --- /dev/null +++ b/module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.collision; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/module.collision/src/main/resources/META-INF/neoforge.mods.toml b/module.collision/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..27f7ba75 --- /dev/null +++ b/module.collision/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,86 @@ +# This is an example mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader = "javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. +loaderVersion = "${loader_version_range}" #mandatory +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license = "${mod_license}" +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory +# The modid of the mod +modId = "${mod_id}" #mandatory +# The version number of the mod +version = "${mod_version}" #mandatory +# A display name for the mod +displayName = "${mod_name}" #mandatory +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional +# A URL for the "homepage" for this mod, displayed in the mod UI +displayURL="https://github.com/Anvil-Dev/AnvilLib" +# A file name (in the root of the mod JAR) containing a logo for display +logoFile="icon.png" #optional +# A text field displayed in the mod UI +#credits="" #optional +# A text field displayed in the mod UI +authors = "${mod_authors}" #optional +# Display Test controls the display for your mod in the server connection screen +# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. +# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. +# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. +# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. +# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. +#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) + +# The description text for the mod (multi line!) (#mandatory) +description = '''${mod_description}''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +[[mixins]] +config = "${mod_id}.mixins.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies."${mod_id}"]] #optional +# the modid of the dependency +modId = "neoforge" #mandatory +# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). +# 'required' requires the mod to exist, 'optional' does not +# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning +type = "required" #mandatory +# Optional field describing why the dependency is required or why it is incompatible +# reason="..." +# The version range of the dependency +versionRange = "${neo_version_range}" #mandatory +# An ordering relationship for the dependency. +# BEFORE - This mod is loaded BEFORE the dependency +# AFTER - This mod is loaded AFTER the dependency +ordering = "NONE" +# Side this dependency is applied on - BOTH, CLIENT, or SERVER +side = "BOTH" +# Here's another dependency +[[dependencies."${mod_id}"]] +modId = "minecraft" +type = "required" +# This version range declares a minimum of the current minecraft version up to but not including the next major version +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features."${mod_id}"] +#openGLVersion="[3.2,)" diff --git a/module.collision/src/main/resources/anvillib_collision.mixins.json b/module.collision/src/main/resources/anvillib_collision.mixins.json new file mode 100644 index 00000000..a4d81a56 --- /dev/null +++ b/module.collision/src/main/resources/anvillib_collision.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.anvilcraft.lib.v2.collision.mixin", + "compatibilityLevel": "JAVA_8", + "refmap": "anvillib.refmap.json", + "mixins": [ + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 5445014e..9a4ea434 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,6 +23,7 @@ include 'module.wheel' include 'module.yukkuri' include 'module.main' include 'module.rpc' +include 'module.collision' include 'module.sync' include 'module.sync:processor' include 'module.test' @@ -40,6 +41,7 @@ project(':module.wheel').name = 'anvillib-wheel-neoforge-1.21.1' project(':module.yukkuri').name = 'anvillib-yukkuri-neoforge-1.21.1' project(':module.main').name = 'anvillib-neoforge-1.21.1' project(':module.rpc').name = 'anvillib-rpc-neoforge-1.21.1' +project(':module.collision').name = 'anvillib-collision-neoforge-1.21.1' project(':module.sync').name = 'anvillib-sync-neoforge-1.21.1' project(':module.sync:processor').name = 'anvillib-sync-processor-neoforge-1.21.1' project(':module.test').name = 'anvillib-test-neoforge-1.21.1' From c62a262edd9190b33fcf1f9ba9e48fe169575f2e Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 10 Aug 2026 22:59:15 +0800 Subject: [PATCH 06/38] =?UTF-8?q?feat(renderdoc-loader):=20=E5=9B=9E?= =?UTF-8?q?=E7=A7=BB=E6=A4=8D=20RenderDoc=20Java=20Agent=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 dev/26.1 的 renderdoc-loader(2751be4、1063ee9)源码逐字回移植,作为 renderdoc-loader-internal,实现 premain/agentmain 动态加载 RenderDoc DLL。 jar manifest 含 Premain-Class/Agent-Class(jar-in-jar 内嵌支持)。 构建适配:Java 25 → 21;jreleaser 别名改为直接坐标写法后因插件冲突移除 (本模块不发布中央仓库);jetbrains annotations 版本对齐 port 侧 24.1.0。 --- renderdoc-loader/build.gradle | 120 ++++++++++++++++++ renderdoc-loader/gradle.properties | 4 + .../lib/renderdoc/loader/RenderDocAgent.java | 99 +++++++++++++++ .../lib/renderdoc/loader/package-info.java | 16 +++ settings.gradle | 2 + 5 files changed, 241 insertions(+) create mode 100644 renderdoc-loader/build.gradle create mode 100644 renderdoc-loader/gradle.properties create mode 100644 renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/RenderDocAgent.java create mode 100644 renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/package-info.java diff --git a/renderdoc-loader/build.gradle b/renderdoc-loader/build.gradle new file mode 100644 index 00000000..546d1782 --- /dev/null +++ b/renderdoc-loader/build.gradle @@ -0,0 +1,120 @@ +plugins { + id 'java' + id 'maven-publish' + alias libs.plugins.lombok +} + +String buildType = 'snapshot' +String buildNumber +if (System.getenv("CI_BUILD") == 'false') buildNumber = null else { + if (System.getenv("PR_BUILD") != 'false') buildType = 'pr' + buildNumber = System.getenv("GITHUB_RUN_NUMBER") +} +version = "${mod_version}" + (buildNumber != null ? "+${buildType}.${buildNumber}" : "") +group = mod_group_id + +base { + archivesName = "${project.mod_id}-neoforge-${libs.versions.minecraft.get()}" +} + +repositories { + mavenCentral() +} + +dependencies { + compileOnly 'org.jetbrains:annotations:24.1.0' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +jar { + manifest { + attributes( + 'Premain-Class': 'dev.anvilcraft.lib.renderdoc.loader.RenderDocAgent', + 'Agent-Class': 'dev.anvilcraft.lib.renderdoc.loader.RenderDocAgent', + 'Can-Redefine-Classes': 'false', + 'Can-Retransform-Classes': 'false', + 'Can-Set-Native-Method-Prefix': 'false' + ) + } +} + +tasks.register('sourcesJar', Jar) { + from sourceSets.main.allSource + archiveClassifier = 'sources' +} + +tasks.register('javadocJar', Jar) { + from javadoc.destinationDir + dependsOn javadoc + archiveClassifier = 'javadoc' +} + +javadoc { + // options.encoding = 'UTF-8' + options { + encoding = 'UTF-8' + addStringOption 'Xdoclint:none', '-quiet' + addStringOption 'tag', 'apiNote:a:API Note:' + addStringOption 'tag', 'implSpec:a:Implementation Requirement:' + addStringOption 'tag', 'implNote:a:Implementation Note:' + } +} + +artifacts { + archives tasks.jar + archives tasks.sourcesJar + archives tasks.javadocJar +} + +def this_name = base.archivesName.get() +def this_description = mod_description + +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + + artifact sourcesJar + artifact javadocJar + + pom { + name = this_name + description = this_description + url = 'https://github.com/Anvil-Dev/AnvilLib' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/license/mit' + } + } + developers { + developer { + id = 'Anvil-Dev' + name = 'AnvilCraft Dev' + email = 'admin@anvilcraft.dev' + } + } + scm { + url = 'https://github.com/Anvil-Dev/AnvilLib' + connection = 'https://github.com/Anvil-Dev/AnvilLib.git' + developerConnection = 'https://github.com/Anvil-Dev/AnvilLib.git' + } + } + } + } + repositories { + mavenLocal() + } +} + +lombok { + version = "1.18.34" +} diff --git a/renderdoc-loader/gradle.properties b/renderdoc-loader/gradle.properties new file mode 100644 index 00000000..864ce045 --- /dev/null +++ b/renderdoc-loader/gradle.properties @@ -0,0 +1,4 @@ +## Mod Properties +mod_id=anvillib_renderdoc_loader +mod_name=AnvilLib-RenderDoc-Loader +mod_description=Java Agent for dynamically loading RenderDoc DLL into JVM diff --git a/renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/RenderDocAgent.java b/renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/RenderDocAgent.java new file mode 100644 index 00000000..7f22ddb5 --- /dev/null +++ b/renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/RenderDocAgent.java @@ -0,0 +1,99 @@ +package dev.anvilcraft.lib.renderdoc.loader; + +import org.jetbrains.annotations.ApiStatus; + +import java.lang.instrument.Instrumentation; + +/** + * Java Agent for dynamically loading RenderDoc DLL into JVM. + *

+ * This agent allows RenderDoc to be attached to a running JVM process + * for graphics debugging purposes. + *

+ */ +@ApiStatus.Internal +public class RenderDocAgent { + + /** + * System property key for specifying the RenderDoc library path. + */ + public static final String RENDERDOC_LIBRARY_PATH = "renderdoc.library.path"; + + /** + * System property key for specifying the RenderDoc library path (alternative). + */ + public static final String RENDERDOC_LIBRARY_PATH_ALT = "neoforge.rendernurse.renderdoc.library"; + + /** + * JVM entry point for Java Agent when started with -javaagent flag. + * + * @param agentArgs agent arguments passed via -javaagent:jarpath=args + * @param inst the instrumentation instance + */ + public static void premain(String agentArgs, Instrumentation inst) { + loadRenderDoc(); + } + + /** + * JVM entry point for Java Agent when attached to a running JVM. + * + * @param agentArgs agent arguments + * @param inst the instrumentation instance + */ + public static void agentmain(String agentArgs, Instrumentation inst) { + loadRenderDoc(); + } + + /** + * Attempts to load the RenderDoc native library. + *

+ * The library path can be specified via: + *

    + *
  • System property {@link #RENDERDOC_LIBRARY_PATH}
  • + *
  • System property {@link #RENDERDOC_LIBRARY_PATH_ALT}
  • + *
  • Agent arguments (as a path)
  • + *
+ *

+ */ + private static void loadRenderDoc() { + String libraryPath = getLibraryPath(); + + if (libraryPath == null || libraryPath.isEmpty()) { + System.err.println("[RenderDoc-Loader] No RenderDoc library path specified. " + + "Set system property '" + RENDERDOC_LIBRARY_PATH + "' or '" + RENDERDOC_LIBRARY_PATH_ALT + "'"); + return; + } + + try { + System.load(libraryPath); + System.out.println("[RenderDoc-Loader] Successfully loaded RenderDoc from: " + libraryPath); + } catch (UnsatisfiedLinkError e) { + System.err.println("[RenderDoc-Loader] Failed to load RenderDoc from: " + libraryPath); + e.printStackTrace(); + } catch (SecurityException e) { + System.err.println("[RenderDoc-Loader] Security exception when loading RenderDoc from: " + libraryPath); + e.printStackTrace(); + } + } + + /** + * Gets the RenderDoc library path from system properties or agent arguments. + * + * @return the library path, or null if not specified + */ + private static String getLibraryPath() { + // Try primary system property first + String path = System.getProperty(RENDERDOC_LIBRARY_PATH); + if (path != null && !path.isEmpty()) { + return path; + } + + // Try alternative system property + path = System.getProperty(RENDERDOC_LIBRARY_PATH_ALT); + if (path != null && !path.isEmpty()) { + return path; + } + + return null; + } +} diff --git a/renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/package-info.java b/renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/package-info.java new file mode 100644 index 00000000..4012b38d --- /dev/null +++ b/renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/package-info.java @@ -0,0 +1,16 @@ +/** + * Java Agent for dynamically loading RenderDoc DLL into JVM. + *

+ * Usage: + *

+ * java -javaagent:renderdoc-loader.jar -Drenderdoc.library.path=/path/to/renderdoc.dll -jar app.jar
+ * 
+ *

+ *

+ * Or use the alternative property name: + *

+ * java -javaagent:renderdoc-loader.jar -Dneoforge.rendernurse.renderdoc.library=/path/to/renderdoc.dll -jar app.jar
+ * 
+ *

+ */ +package dev.anvilcraft.lib.renderdoc.loader; diff --git a/settings.gradle b/settings.gradle index 9a4ea434..50395768 100644 --- a/settings.gradle +++ b/settings.gradle @@ -24,6 +24,7 @@ include 'module.yukkuri' include 'module.main' include 'module.rpc' include 'module.collision' +include 'renderdoc-loader' include 'module.sync' include 'module.sync:processor' include 'module.test' @@ -42,6 +43,7 @@ project(':module.yukkuri').name = 'anvillib-yukkuri-neoforge-1.21.1' project(':module.main').name = 'anvillib-neoforge-1.21.1' project(':module.rpc').name = 'anvillib-rpc-neoforge-1.21.1' project(':module.collision').name = 'anvillib-collision-neoforge-1.21.1' +project(':renderdoc-loader').name = 'renderdoc-loader-internal' project(':module.sync').name = 'anvillib-sync-neoforge-1.21.1' project(':module.sync:processor').name = 'anvillib-sync-processor-neoforge-1.21.1' project(':module.test').name = 'anvillib-test-neoforge-1.21.1' From e896a1ea665a28e28375c8c4342cd2a6b9ef3f7d Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 10 Aug 2026 23:06:01 +0800 Subject: [PATCH 07/38] =?UTF-8?q?feat(explosion):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=E5=88=86=E5=B1=82=E7=90=83=E5=BD=A2=E7=88=86=E7=82=B8?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E5=99=A8=20module.explosion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 dev/26.1 的 module.explosion(435e23f、fba1027)整体回移植,作为新模块 anvillib-explosion-neoforge-1.21.1,依赖 port 侧 anvillib-config-neoforge-1.21.1, 实现 ServerTickEvent.Post 驱动的分层球壳破坏 + 熔炼配方熔化替换。 API 适配(26.1 → 1.21.1): - Identifier → ResourceLocation - Ingredient.items()(26.1 Stream>)→ getItems()(ItemStack[]) - ItemStackTemplate → ItemStack(getCount/getItem) - BuiltInRegistries.BLOCK.get(TagKey) → getTag(TagKey)(Optional) - AbstractCookingRecipe.input()(26.1 新增)→ getIngredients().get(0) - SingleItemRecipeAccessor 的 result 类型 ItemStackTemplate → ItemStack - entity.hurtServer(...)(26.1 专属)→ Entity.hurt(DamageSource, float) - GatherDataEvent.Client → 单一 GatherDataEvent + includeClient() - @Config(group=...) 移除(port @Config 无 group 属性) - Java 22 lambda 参数 _ 改名;mixins.json JAVA_25 → JAVA_21 --- module.explosion/build.gradle | 277 ++++++++++ module.explosion/gradle.properties | 4 + .../assets/anvillib_explosion/lang/en_us.json | 9 + .../lib/v2/explosion/AnvilLibExplosion.java | 106 ++++ .../v2/explosion/AnvilLibExplosionConfig.java | 12 + .../lib/v2/explosion/ExplosionExecutor.java | 132 +++++ .../lib/v2/explosion/ExplosionSession.java | 483 ++++++++++++++++++ .../v2/explosion/data/AnvilLibDatagen.java | 21 + .../lib/v2/explosion/data/package-info.java | 4 + .../data/provider/ModLanguageProvider.java | 20 + .../explosion/data/provider/package-info.java | 4 + .../mixin/SingleItemRecipeAccessor.java | 14 + .../lib/v2/explosion/package-info.java | 4 + .../resources/META-INF/neoforge.mods.toml | 86 ++++ .../resources/anvillib_explosion.mixins.json | 15 + .../assets/anvillib_explosion/lang/zh_cn.json | 9 + settings.gradle | 2 + 17 files changed, 1202 insertions(+) create mode 100644 module.explosion/build.gradle create mode 100644 module.explosion/gradle.properties create mode 100644 module.explosion/src/generated/resources/assets/anvillib_explosion/lang/en_us.json create mode 100644 module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/AnvilLibExplosion.java create mode 100644 module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/AnvilLibExplosionConfig.java create mode 100644 module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionExecutor.java create mode 100644 module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionSession.java create mode 100644 module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/AnvilLibDatagen.java create mode 100644 module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/package-info.java create mode 100644 module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/provider/ModLanguageProvider.java create mode 100644 module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/provider/package-info.java create mode 100644 module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/mixin/SingleItemRecipeAccessor.java create mode 100644 module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/package-info.java create mode 100644 module.explosion/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 module.explosion/src/main/resources/anvillib_explosion.mixins.json create mode 100644 module.explosion/src/main/resources/assets/anvillib_explosion/lang/zh_cn.json diff --git a/module.explosion/build.gradle b/module.explosion/build.gradle new file mode 100644 index 00000000..4b09d3c8 --- /dev/null +++ b/module.explosion/build.gradle @@ -0,0 +1,277 @@ +plugins { + id 'java-library' + id 'eclipse' + id 'idea' + id 'maven-publish' + alias libs.plugins.modDevGradle + alias libs.plugins.lombok + alias libs.plugins.machete + id 'org.jreleaser' version '1.23.0' + id "me.modmuss50.mod-publish-plugin" version "1.1.0" +} + +String buildType = 'snapshot' +String buildNumber +if (System.getenv("CI_BUILD") == 'false') buildNumber = null +else { + if (System.getenv("PR_BUILD") != 'false') buildType = 'pr' + buildNumber = System.getenv("GITHUB_RUN_NUMBER") +} +version = "${mod_version}" + (buildNumber != null ? "+${buildType}.${buildNumber}" : "") +group = mod_group_id + +base { + archivesName = "${project.mod_id}-neoforge-${libs.versions.minecraft.get()}" +} + +repositories { + maven { url = "https://server.cjsah.net:1002/maven/" } + mavenLocal() + mavenCentral() +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +neoForge { + // Specify the version of NeoForge to use. + version = libs.versions.neoForge.get() + + parchment { + mappingsVersion = project.parchment_mappings_version + minecraftVersion = project.parchment_minecraft_version + } + + // This line is optional. Access Transformers are automatically detected + // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') + + interfaceInjectionData { + // from 'src/main/resources/interface_injections.json' + // publish file('src/main/resources/interface_injections.json') + } + + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + client { + client() + + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + server { + server() + programArgument '--nogui' + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + // This run config launches GameTestServer and runs all registered gametests, then exits. + // By default, the server will crash when no gametests are provided. + // The gametest system is also enabled by default for other run configs under the /test command. + gameTestServer { + type = "gameTestServer" + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + data { + data() + + // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it + // gameDirectory = project.file('run-data') + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } + + // applies to all the run configs above + configureEach { + // Recommended logging data for a userdev environment + // The markers can be added/remove as needed separated by commas. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + systemProperty 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + logLevel = org.slf4j.event.Level.DEBUG + } + } + + mods { + // define mod <-> source bindings + // these are used to tell the game which sources are for which mod + // mostly optional in a single mod project + // but multi mod projects should define one per mod + "${mod_id}" { + sourceSet(sourceSets.main) + } + } +} + +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } + + +dependencies { + compileOnly "org.jspecify:jspecify:1.0.0" + + if (System.getenv("NOT_DEV") == 'true') { + jarJar(api("dev.anvilcraft.lib:anvillib-config-neoforge-1.21.1:latest.release")) + } else { + jarJar(implementation project(":anvillib-config-neoforge-1.21.1")) + } +} + +// This block of code expands all declared replace properties in the specified resource targets. +// A missing property will result in an error. Properties are expanded using ${} Groovy notation. +// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. +// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html +tasks.withType(ProcessResources).configureEach { + var replaceProperties = [minecraft_version : libs.versions.minecraft.get(), + minecraft_version_range: minecraft_version_range, + neo_version : libs.versions.neoForge.get(), + neo_version_range : neo_version_range, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : version, + mod_authors : mod_authors, + mod_description : mod_description] + inputs.properties replaceProperties + + filesMatching(['META-INF/neoforge.mods.toml']) { + expand replaceProperties + } +} + +tasks.register('sourcesJar', Jar) { + from delombok.outputs.files + dependsOn delombok + archiveClassifier = "sources" +} + +tasks.register('javadocJar', Jar) { + from javadoc.destinationDir + dependsOn javadoc + archiveClassifier = "javadoc" +} + +javadoc { + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') +} + +jar { + from(rootProject.file("LICENSE")) + from(rootProject.file("ASSETS_LICENSE")) + from(rootProject.file("README.md")) + from(rootProject.file("README.en.md")) +} + +artifacts { + archives tasks.jar + archives tasks.sourcesJar + archives tasks.javadocJar +} + +def this_name = base.archivesName.get() +def this_description = mod_description + +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + + artifact sourcesJar + artifact javadocJar + + versionMapping { + allVariants { + fromResolutionResult() + } + } + + pom { + name = this_name + description = this_description + url = 'https://github.com/Anvil-Dev/AnvilLib' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/license/mit' + } + } + developers { + developer { + id = 'Anvil-Dev' + name = 'AnvilCraft Dev' + email = 'admin@anvilcraft.dev' + } + } + scm { + url = 'https://github.com/Anvil-Dev/AnvilLib' + connection = 'https://github.com/Anvil-Dev/AnvilLib.git' + developerConnection = 'https://github.com/Anvil-Dev/AnvilLib.git' + } + } + } + } + repositories { + // Add repositories to publish to here. + def MAVEN_URL = System.getenv("MAVEN_URL") + if (MAVEN_URL != null) { + maven { + url MAVEN_URL + credentials { + username System.getenv("MAVEN_USERNAME") + password System.getenv("MAVEN_PASSWORD") + } + } + } + mavenLocal() + maven { + name = "staging" + url = layout.buildDirectory.dir("staging-deploy") + } + } +} + +jreleaser { + signing { + active = 'ALWAYS' + armored = true + } + + deploy { + maven { + mavenCentral { + sonatype { + active = 'ALWAYS' + url = 'https://central.sonatype.com/api/v1/publisher' + stagingRepository('build/staging-deploy') + } + } + } + } +} + +// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} + +machete { + // disable machete locally for faster builds + enabled = false +} + +lombok { + version = "1.18.34" +} diff --git a/module.explosion/gradle.properties b/module.explosion/gradle.properties new file mode 100644 index 00000000..9a091514 --- /dev/null +++ b/module.explosion/gradle.properties @@ -0,0 +1,4 @@ +## Mod Properties +mod_id=anvillib_explosion +mod_name=AnvilLib-Explosion +mod_description=A library for exploding blocks in a sphere shape diff --git a/module.explosion/src/generated/resources/assets/anvillib_explosion/lang/en_us.json b/module.explosion/src/generated/resources/assets/anvillib_explosion/lang/en_us.json new file mode 100644 index 00000000..652d3ee9 --- /dev/null +++ b/module.explosion/src/generated/resources/assets/anvillib_explosion/lang/en_us.json @@ -0,0 +1,9 @@ +{ + "anvillib_explosion.configuration.default_remove_blocks_per_tick": "Default Remove Blocks Per Tick", + "anvillib_explosion.configuration.default_remove_blocks_per_tick.tooltip": "The default number of blocks that can be deleted per tick.", + "anvillib_explosion.configuration.max_remove_blocks_per_tick": "Max Remove Blocks Per Tick", + "anvillib_explosion.configuration.max_remove_blocks_per_tick.tooltip": "The maximum number of blocks that can be deleted per tick.", + "anvillib_explosion.configuration.section.anvillib_explosion.common.toml": "Anvillib Explosion Common Configuration", + "anvillib_explosion.configuration.section.anvillib_explosion.common.toml.title": "Anvillib Explosion Common Configuration", + "anvillib_explosion.configuration.title": "Anvillib Explosion Configuration" +} \ No newline at end of file diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/AnvilLibExplosion.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/AnvilLibExplosion.java new file mode 100644 index 00000000..c77aa557 --- /dev/null +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/AnvilLibExplosion.java @@ -0,0 +1,106 @@ +package dev.anvilcraft.lib.v2.explosion; + +import dev.anvilcraft.lib.v2.config.ConfigManager; +import dev.anvilcraft.lib.v2.explosion.mixin.SingleItemRecipeAccessor; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.AbstractCookingRecipe; +import net.minecraft.world.item.crafting.BlastingRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.SmeltingRecipe; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.event.OnDatapackSyncEvent; +import net.neoforged.neoforge.event.server.ServerStartedEvent; +import org.jetbrains.annotations.ApiStatus; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Mod(AnvilLibExplosion.MOD_ID) +@EventBusSubscriber(modid = AnvilLibExplosion.MOD_ID) +public class AnvilLibExplosion { + public static final String MAIN_ID = "anvillib"; + public static final String MOD_ID = "anvillib_explosion"; + public static final AnvilLibExplosionConfig CONFIG = ConfigManager.register(MOD_ID, AnvilLibExplosionConfig::new); + public static final Map MELTING_CACHE = new ConcurrentHashMap<>(); + + @ApiStatus.Internal + public AnvilLibExplosion() { + } + + public static ResourceLocation of(String path) { + return ResourceLocation.fromNamespaceAndPath(AnvilLibExplosion.MAIN_ID, path); + } + + @ApiStatus.Internal + @SubscribeEvent + public static void onDatapackSync(OnDatapackSyncEvent event) { + if (event.getPlayer() != null) { + return; + } + AnvilLibExplosion.registerMelting(event.getPlayerList().getServer()); + } + + @ApiStatus.Internal + @SubscribeEvent + public static void onServerStarted(ServerStartedEvent event) { + AnvilLibExplosion.registerMelting(event.getServer()); + } + + private static void registerMelting(MinecraftServer server) { + AnvilLibExplosion.MELTING_CACHE.clear(); + BuiltInRegistries.BLOCK.getTag(BlockTags.LOGS_THAT_BURN).ifPresent(block -> { + for (Holder blockHolder : block) { + AnvilLibExplosion.MELTING_CACHE.put(blockHolder.value(), Blocks.COAL_BLOCK); + } + }); + AnvilLibExplosion.MELTING_CACHE.put(Blocks.GRASS_BLOCK, Blocks.PODZOL); + AnvilLibExplosion.MELTING_CACHE.put(Blocks.MYCELIUM, Blocks.PODZOL); + AnvilLibExplosion.MELTING_CACHE.put(Blocks.DIRT_PATH, Blocks.COARSE_DIRT); + AnvilLibExplosion.MELTING_CACHE.put(Blocks.DIRT, Blocks.COARSE_DIRT); + AnvilLibExplosion.MELTING_CACHE.put(Blocks.ROOTED_DIRT, Blocks.COARSE_DIRT); + AnvilLibExplosion.MELTING_CACHE.put(Blocks.FARMLAND, Blocks.COARSE_DIRT); + AnvilLibExplosion.MELTING_CACHE.put(Blocks.MUD, Blocks.DIRT); + for (RecipeHolder holder : server.getRecipeManager().getRecipes()) { + Recipe value = holder.value(); + if (value instanceof SmeltingRecipe || value instanceof BlastingRecipe) { + AnvilLibExplosion.registerAbstractCookingRecipe((AbstractCookingRecipe) value); + } + } + } + + @SuppressWarnings("deprecation") + private static void registerAbstractCookingRecipe(AbstractCookingRecipe recipe) { + Ingredient input = recipe.getIngredients().get(0); + List items = Arrays.stream(input.getItems()).map(ItemStack::getItem).toList(); + ItemStack result = ((SingleItemRecipeAccessor) recipe).getResult(); + if (result.getCount() != 1) { + return; + } + if (!(result.getItem() instanceof BlockItem blockItemOutput)) { + return; + } + for (Item item : items) { + if (!(item instanceof BlockItem blockItemInput)) { + continue; + } + Block blockInput = blockItemInput.getBlock(); + Block blockOutput = blockItemOutput.getBlock(); + MELTING_CACHE.put(blockInput, blockOutput); + } + } +} diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/AnvilLibExplosionConfig.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/AnvilLibExplosionConfig.java new file mode 100644 index 00000000..7513ec68 --- /dev/null +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/AnvilLibExplosionConfig.java @@ -0,0 +1,12 @@ +package dev.anvilcraft.lib.v2.explosion; + +import dev.anvilcraft.lib.v2.config.Comment; +import dev.anvilcraft.lib.v2.config.Config; + +@Config(name = AnvilLibExplosion.MOD_ID) +public class AnvilLibExplosionConfig { + @Comment("The maximum number of blocks that can be deleted per tick.") + public int maxRemoveBlocksPerTick = 2048; + @Comment("The default number of blocks that can be deleted per tick.") + public int defaultRemoveBlocksPerTick = 128; +} diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionExecutor.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionExecutor.java new file mode 100644 index 00000000..346225e1 --- /dev/null +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionExecutor.java @@ -0,0 +1,132 @@ +package dev.anvilcraft.lib.v2.explosion; + +import lombok.Setter; +import lombok.experimental.Accessors; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.Block; +import net.neoforged.neoforge.common.Tags; +import org.apache.logging.log4j.util.TriConsumer; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +/// 这是一个可灵活配置的入口点,用于模拟球形爆炸效果:爆炸从中心开始,逐个方向撞击并破坏方块(每 tick 处理 N 个方块)。 +/// +/// ### 用法: +/// ``` +/// new ExplosionExecutor() +/// .radius(50) +/// .maxBreakPreTick(64) +/// .probabilityRadius(160) +/// .meltingRadius(128) +/// .execute(level, center); +/// ``` +@Setter +@Accessors(fluent = true) +public class ExplosionExecutor { + /// 爆炸时完全破坏方块的半径 + private int radius = 100; + /// 每个服务器 tick 中最多可以移除的块数。 + private int maxBreakPreTick = AnvilLibExplosion.CONFIG.defaultRemoveBlocksPerTick; + /// 当方块被破坏时,是否需要掉落物品 + private boolean dropItems = false; + /// 概率半径(以块为单位),需要大于 `radius` ,该范围内的地表方块将由近到远地被概率性地破坏,距离中心越近的概率越高,最靠近 `radius` 的概率为 `100%`,最靠近 `probabilityRadius` 的概率为 `80%` + private int probabilityRadius = 128; + /// 融化半径(以块为单位),需要大于 `radius` ,该范围内的地表方块将由近到远地被概率性地融化,距离中心越近的概率越高,最靠近 `radius` 的概率为 `100%`,最靠近 `meltingRadius` 的概率为 `80%` + private int meltingRadius = 160; + /// 不允许被爆炸破坏的方块列表 + private List> excludedBlocks = new ArrayList<>(List.of( + block -> block.defaultDestroyTime() < 0 + )); + /// 脆弱的方块列表,在范围内的这些方块会被完全破坏 + @SuppressWarnings("deprecation") + private List> frangibleBlocks = new ArrayList<>(List.of( + block -> block.builtInRegistryHolder().is(Tags.Blocks.GLASS_BLOCKS), + block -> block.builtInRegistryHolder().is(Tags.Blocks.GLASS_PANES), + block -> block.builtInRegistryHolder().is(BlockTags.LEAVES), + block -> block.builtInRegistryHolder().is(BlockTags.REPLACEABLE) + )); + private @Nullable Entity executor = null; + /// 方块破坏时触发的实体处理函数 + private @Nullable TriConsumer entityProcessor = (level, ignored, entity) -> entity.hurt( + entity.damageSources().explosion(null, ExplosionExecutor.this.executor), + ExplosionExecutor.this.radius * 0.5f + ); + + private ExplosionExecutor() { + } + + public static ExplosionExecutor create() { + return new ExplosionExecutor(); + } + + @SafeVarargs + public final ExplosionExecutor excludedBlocks(Predicate... blocks) { + this.excludedBlocks.addAll(List.of(blocks)); + return this; + } + + public final ExplosionExecutor excludedBlocks(Block... blocks) { + for (Block block : blocks) { + this.excludedBlocks.add(block1 -> block1 == block); + } + return this; + } + + @SafeVarargs + @SuppressWarnings("deprecation") + public final ExplosionExecutor excludedBlocks(TagKey... blocks) { + for (TagKey block : blocks) { + this.excludedBlocks.add(block1 -> block1.builtInRegistryHolder().is(block)); + } + return this; + } + + @SafeVarargs + public final ExplosionExecutor frangibleBlocks(Predicate... blocks) { + this.frangibleBlocks.addAll(List.of(blocks)); + return this; + } + + public final ExplosionExecutor frangibleBlocks(Block... blocks) { + for (Block block : blocks) { + this.frangibleBlocks.add(block1 -> block1 == block); + } + return this; + } + + @SafeVarargs + @SuppressWarnings("deprecation") + public final ExplosionExecutor frangibleBlocks(TagKey... blocks) { + for (TagKey block : blocks) { + this.frangibleBlocks.add(block1 -> block1.builtInRegistryHolder().is(block)); + } + return this; + } + + /// 开始进行分层球形爆炸。该爆炸会创建一个 {@link ExplosionSession} 对象,该对象会自动注册到 NeoForge 事件总线中,并从爆炸中心开始逐层(逐块)移除周围的方块。 + public void execute(ServerLevel level, BlockPos pos) { + // Ensure probabilityRadius and meltingRadius are valid + int actualProbabilityRadius = Math.max(this.probabilityRadius, this.radius); + int actualMeltingRadius = Math.max(this.meltingRadius, this.radius); + + new ExplosionSession( + level, + pos, + this.radius, + Math.min(this.maxBreakPreTick, AnvilLibExplosion.CONFIG.maxRemoveBlocksPerTick), + this.dropItems, + actualProbabilityRadius, + actualMeltingRadius, + this.excludedBlocks, + this.frangibleBlocks, + this.entityProcessor + ).start(); + } +} diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionSession.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionSession.java new file mode 100644 index 00000000..e8a4792f --- /dev/null +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionSession.java @@ -0,0 +1,483 @@ +package dev.anvilcraft.lib.v2.explosion; + +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.material.FluidState; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.tick.ServerTickEvent; +import org.apache.logging.log4j.util.TriConsumer; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Performs the actual block removal in a spherical explosion, driven by + * {@link ServerTickEvent.Post}. Each tick removes at most {@code maxBreakPerTick} + * blocks from inside to outside until the explosion radius is exhausted, + * then self-unregisters. + * + *

Uses dynamic layer-by-layer generation with virtual thread pre-computation + * to avoid memory issues with large radii and improve performance through async processing. + */ +@ApiStatus.Internal +class ExplosionSession { + // Shared virtual thread executor for async layer pre-computation + private static final Executor THREAD_EXECUTOR = Executors.newWorkStealingPool(); + + private final ServerLevel level; + private final BlockPos center; + private final int maxRadius; + private final int maxBreakPerTick; + private final boolean dropItems; + private final int probabilityRadius; // Probability destruction radius + private final int meltingRadius; // Melting radius (replace with air without drops) + private final int effectiveMaxRadius; // Outermost radius to process (max of probabilityRadius and meltingRadius) + private final @Nullable List> excludedBlocks; // Blocks that cannot be destroyed by explosion + private final @Nullable List> frangibleBlocks; // Frangible blocks that are always destroyed within range + private final @Nullable TriConsumer entityProcessor; + + private final int surfaceHeight; + + // Current processing state + private int currentLayer; // Current distance layer (0 to effectiveMaxRadius) + private @Nullable List currentLayerBlocks; // Blocks in current layer + private int layerIndex; // Index within current layer + + // Async pre-computation for next layer + private @Nullable CompletableFuture> nextLayerFuture; // Future for next layer computation + private final Multimap entityCache = MultimapBuilder.hashKeys().arrayListValues().build(); + private int nextLayerToCompute; // Which layer is being pre-computed + + private boolean finished; + + ExplosionSession( + ServerLevel level, + BlockPos center, + int maxRadius, + int maxBreakPerTick, + boolean dropItems, + int probabilityRadius, + int meltingRadius, + @Nullable List> excludedBlocks, + @Nullable List> frangibleBlocks, + @Nullable TriConsumer entityProcessor + ) { + this.level = level; + this.center = center; + this.maxRadius = maxRadius; + this.maxBreakPerTick = maxBreakPerTick; + this.dropItems = dropItems; + this.probabilityRadius = probabilityRadius; + this.meltingRadius = meltingRadius; + this.effectiveMaxRadius = Math.max(probabilityRadius, meltingRadius); + this.excludedBlocks = excludedBlocks; + this.frangibleBlocks = frangibleBlocks; + this.entityProcessor = entityProcessor; + this.surfaceHeight = (int) (Math.ceil(maxRadius * 0.1) + 1); + } + + // ---- lifecycle ---- + + void start() { + this.currentLayer = 0; + this.currentLayerBlocks = null; + this.layerIndex = 0; + this.nextLayerFuture = null; + this.nextLayerToCompute = -1; + this.finished = false; + this.entityCache.clear(); + NeoForge.EVENT_BUS.register(this); + } + + private void stop() { + NeoForge.EVENT_BUS.unregister(this); + } + + // ---- tick processing ---- + + @SubscribeEvent + public void onServerTick(ServerTickEvent.Post event) { + if (this.finished) { + this.stop(); + return; + } + + int removed = 0; + + this.createEntityCache(); + // Process blocks layer by layer to avoid memory issues with large radii + while (removed < this.maxBreakPerTick && this.currentLayer <= this.effectiveMaxRadius) { + // Generate current layer if needed + if (this.currentLayerBlocks == null || this.layerIndex >= this.currentLayerBlocks.size()) { + // Try to get pre-computed next layer + if (this.nextLayerFuture != null && this.nextLayerToCompute == this.currentLayer) { + // Use pre-computed result from virtual thread + this.currentLayerBlocks = this.nextLayerFuture.join(); + this.nextLayerFuture = null; + this.nextLayerToCompute = -1; + } else { + // Fallback: compute synchronously if pre-computation wasn't ready + this.currentLayerBlocks = this.generateLayerBlocks(this.currentLayer); + } + + this.layerIndex = 0; + + // Move to next layer if current one is empty + if (this.currentLayerBlocks.isEmpty()) { + this.currentLayer++; + continue; + } + + // Start pre-computing next layer in virtual thread + this.startNextLayerPrecomputation(); + } + + // Process blocks in current layer + while (this.layerIndex < this.currentLayerBlocks.size() && removed < this.maxBreakPerTick) { + BlockPos target = this.currentLayerBlocks.get(this.layerIndex); + this.layerIndex++; + + if (!this.level.isLoaded(target)) continue; + + if (this.entityProcessor != null) { + this.entityCache.get(target).forEach(entity -> this.entityProcessor.accept(this.level, target, entity)); + } + BlockState blockState = this.level.getBlockState(target); + if (blockState.isAir()) continue; + // public int getHeight(Heightmap.Types type, int x, int z) {} + int height = level.getHeight(Heightmap.Types.WORLD_SURFACE, target.getX(), target.getZ()); + boolean isSurface = target.getY() >= height - this.surfaceHeight; + // Check if block is excluded from explosion + if (this.isBlockExcluded(blockState.getBlock())) { + continue; + } + + // Frangible blocks are always completely destroyed within range + if (this.isBlockFrangible(blockState.getBlock())) { + if (ExplosionSession.destroyBlock(this.level, target, this.dropItems)) { + removed++; + } + continue; + } + + double distance = Math.sqrt(target.distToCenterSqr(this.center.getX(), this.center.getY(), this.center.getZ())); + + // Determine action based on distance + if (distance <= this.maxRadius) { + // Core explosion: always destroy + if (ExplosionSession.destroyBlock(this.level, target, this.dropItems)) { + removed++; + } + } else { + if (distance <= this.probabilityRadius && isSurface) { + // Probability destruction: probability decreases from 80% at maxRadius to 0% at probabilityRadius + double probability = this.calculateProbability(distance, this.maxRadius, this.probabilityRadius); + if (Math.random() < probability) { + if (ExplosionSession.destroyBlock(this.level, target, this.dropItems)) { + removed++; + blockState = Blocks.AIR.defaultBlockState(); + } + } + } + if (distance <= this.meltingRadius && isSurface && !blockState.isAir()) { + // Melting: probability decreases from 80% at probabilityRadius to 0% at meltingRadius + double probability = this.calculateProbability(distance, this.probabilityRadius, this.meltingRadius); + if (Math.random() < probability) { + if (ExplosionSession.meltBlock(this.level, target)) { + removed++; + } + } + } + } + } + + // Move to next layer when current one is done + if (this.layerIndex >= this.currentLayerBlocks.size()) { + this.currentLayer++; + this.currentLayerBlocks = null; // Allow GC to collect + } + } + + if (this.currentLayer > this.effectiveMaxRadius) { + this.finished = true; + this.stop(); + } + } + + // ---- Block enumeration ---- + + /** + * Start pre-computing the next layer in a virtual thread. + * For large radii, splits the computation into multiple parallel sub-tasks. + */ + private void startNextLayerPrecomputation() { + int nextLayer = this.currentLayer + 1; + + // Don't pre-compute beyond effective max radius + if (nextLayer > this.effectiveMaxRadius) { + return; + } + + // Only start if not already computing this layer + if (this.nextLayerFuture != null && !this.nextLayerFuture.isDone()) { + return; + } + + // Use parallel computation for large layers to improve performance + boolean useParallel = nextLayer >= 32; // Threshold for parallel processing + + this.nextLayerToCompute = nextLayer; + this.nextLayerFuture = CompletableFuture.supplyAsync(() -> this.generateLayerBlocks(nextLayer, useParallel), THREAD_EXECUTOR); + } + + /** + * Generate all block positions at a specific distance layer from center. + * This approach avoids storing all blocks in memory at once for large radii. + * + * @param layer The distance layer (Euclidean distance shell) + * @return List of BlockPos at this layer, shuffled for natural explosion pattern + */ + private List generateLayerBlocks(int layer) { + return this.generateLayerBlocks(layer, false); + } + + /** + * Generate all block positions at a specific distance layer from center. + * Supports parallel computation for large radii. + * + * @param layer The distance layer (Euclidean distance shell) + * @param useParallel Whether to use parallel computation for this layer + * @return List of BlockPos at this layer, shuffled for natural explosion pattern + */ + private List generateLayerBlocks(int layer, boolean useParallel) { + if (layer == 0) { + // Center point + return List.of(this.center); + } + + // For each layer, we process a "shell" at approximately that distance + // We use integer bounds to efficiently find blocks in the shell + int rSquared = layer * layer; + int innerRSquared = (layer - 1) * (layer - 1); + + List blocks; + + if (useParallel && layer >= 96) { + // Parallel computation: split by X-axis slices + blocks = this.generateLayerBlocksParallel(layer, rSquared, innerRSquared); + } else { + // Sequential computation for smaller layers + blocks = this.generateLayerBlocksSequential(layer, rSquared, innerRSquared); + } + + Collections.shuffle(blocks); + return blocks; + } + + /** + * Sequential generation of layer blocks (for small radii). + */ + private List generateLayerBlocksSequential(int layer, int rSquared, int innerRSquared) { + List blocks = new ArrayList<>(); + + // Iterate through bounding box of this shell + for (int x = -layer; x <= layer; x++) { + for (int y = -layer; y <= layer; y++) { + for (int z = -layer; z <= layer; z++) { + int distSquared = x * x + y * y + z * z; + + // Only include blocks in this shell layer + if (distSquared > innerRSquared && distSquared <= rSquared) { + blocks.add(new BlockPos(this.center.getX() + x, this.center.getY() + y, this.center.getZ() + z)); + } + } + } + } + + return blocks; + } + + /** + * Parallel generation of layer blocks (for large radii). + * Splits the work along the X-axis and merges results. + */ + private List generateLayerBlocksParallel(int layer, int rSquared, int innerRSquared) { + // Determine number of parallel tasks based on layer size + int numTasks = Math.min(Runtime.getRuntime().availableProcessors(), layer / 8 + 1); + int sliceSize = (layer * 2 + 1) / numTasks; + + // Create parallel tasks for each X-slice + List>> futures = new ArrayList<>(numTasks); + + for (int taskIdx = 0; taskIdx < numTasks; taskIdx++) { + final int startX = -layer + taskIdx * sliceSize; + final int endX = (taskIdx == numTasks - 1) ? layer : (startX + sliceSize - 1); + + CompletableFuture> future = CompletableFuture.supplyAsync( + () -> { + List sliceBlocks = new ArrayList<>(); + + for (int x = startX; x <= endX; x++) { + for (int y = -layer; y <= layer; y++) { + for (int z = -layer; z <= layer; z++) { + int distSquared = x * x + y * y + z * z; + + // Only include blocks in this shell layer + if (distSquared > innerRSquared && distSquared <= rSquared) { + sliceBlocks.add(new BlockPos(this.center.getX() + x, this.center.getY() + y, this.center.getZ() + z)); + } + } + } + } + + return sliceBlocks; + }, THREAD_EXECUTOR + ); + + futures.add(future); + } + + // Wait for all tasks to complete and merge results + return futures.stream().flatMap(f -> f.join().stream()).collect(Collectors.toList()); + } + + /** + * Check if a block should be excluded from explosion. + * + * @param block The block to check + * @return true if the block should not be destroyed/melted + */ + private boolean isBlockExcluded(Block block) { + if (this.excludedBlocks == null) { + return false; + } + for (Predicate predicate : this.excludedBlocks) { + if (predicate.test(block)) { + return true; + } + } + return false; + } + + /** + * Check if a block is frangible (always destroyed within range). + * + * @param block The block to check + * @return true if the block is frangible + */ + private boolean isBlockFrangible(Block block) { + if (this.frangibleBlocks == null) { + return false; + } + for (Predicate predicate : this.frangibleBlocks) { + if (predicate.test(block)) { + return true; + } + } + return false; + } + + /** + * Calculate probability based on distance. + * Probability decreases linearly from 80% at innerRadius to 0% at outerRadius. + * + * @param distance Distance from explosion center + * @param innerRadius Inner boundary of the zone (80% probability) + * @param outerRadius Outer boundary of the zone (0% probability) + * @return Probability (0.0 to 0.8) + */ + private double calculateProbability(double distance, int innerRadius, int outerRadius) { + if (outerRadius <= innerRadius) return 0.0; + if (distance <= innerRadius) { + return 0.8; // 80% at inner boundary + } + if (distance >= outerRadius) { + return 0.0; // 0% at outer boundary + } + + // Linear interpolation: 80% at innerRadius, 0% at outerRadius + double ratio = (distance - innerRadius) / (double) (outerRadius - innerRadius); + return 0.8 * (1.0 - ratio); // Decreases from 0.8 to 0.0 + } + + /** + * Melt a block by replacing it with another block from MELTING_CACHE. + * Used for the melting radius effect. + */ + public static boolean meltBlock(ServerLevel level, BlockPos pos) { + if (!level.isLoaded(pos)) { + return false; + } + BlockState blockState = level.getBlockState(pos); + if (blockState.isAir()) { + return false; + } + + Block originalBlock = blockState.getBlock(); + Block targetBlock = AnvilLibExplosion.MELTING_CACHE.get(originalBlock); + + BlockState newState; + if (targetBlock == null) { + return false; + } + newState = targetBlock.defaultBlockState(); + + boolean changed = level.setBlock(pos, newState, Block.UPDATE_CLIENTS, 512); + if (changed) { + level.gameEvent(GameEvent.BLOCK_DESTROY, pos, GameEvent.Context.of(null, blockState)); + } + return changed; + } + + public static boolean destroyBlock(ServerLevel level, BlockPos pos, boolean dropResources) { + if (!level.isLoaded(pos)) { + return false; + } + BlockState blockState = level.getBlockState(pos); + if (blockState.isAir()) { + return false; + } + FluidState fluidState = level.getFluidState(pos); + if (dropResources) { + BlockEntity blockEntity = blockState.hasBlockEntity() ? level.getBlockEntity(pos) : null; + Block.dropResources(blockState, level, pos, blockEntity, null, ItemStack.EMPTY); + } + boolean destroyed = level.setBlock(pos, fluidState.createLegacyBlock(), Block.UPDATE_CLIENTS, 512); + if (destroyed) { + level.gameEvent(GameEvent.BLOCK_DESTROY, pos, GameEvent.Context.of(null, blockState)); + } + return destroyed; + } + + private void createEntityCache() { + this.entityCache.clear(); + if (this.entityProcessor == null) { + return; + } + this.level.getEntities().getAll().forEach(entity -> { + if (entity.blockPosition().distSqr(this.center) > this.maxRadius * this.maxRadius) { + return; + } + this.entityCache.put(entity.blockPosition(), entity); + }); + } +} diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/AnvilLibDatagen.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/AnvilLibDatagen.java new file mode 100644 index 00000000..aa05ecfd --- /dev/null +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/AnvilLibDatagen.java @@ -0,0 +1,21 @@ +package dev.anvilcraft.lib.v2.explosion.data; + +import dev.anvilcraft.lib.v2.explosion.AnvilLibExplosion; +import dev.anvilcraft.lib.v2.explosion.data.provider.ModLanguageProvider; +import net.minecraft.data.DataGenerator; +import net.minecraft.data.PackOutput; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.data.event.GatherDataEvent; +import org.jetbrains.annotations.ApiStatus; + +@EventBusSubscriber(modid = AnvilLibExplosion.MOD_ID) +@ApiStatus.Internal +public class AnvilLibDatagen { + @SubscribeEvent + public static void gatherData(GatherDataEvent event) { + DataGenerator generator = event.getGenerator(); + PackOutput packOutput = generator.getPackOutput(); + generator.addProvider(event.includeClient(), new ModLanguageProvider(packOutput)); + } +} diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/package-info.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/package-info.java new file mode 100644 index 00000000..11edb600 --- /dev/null +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.explosion.data; + +import org.jspecify.annotations.NullMarked; diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/provider/ModLanguageProvider.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/provider/ModLanguageProvider.java new file mode 100644 index 00000000..4f159e83 --- /dev/null +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/provider/ModLanguageProvider.java @@ -0,0 +1,20 @@ +package dev.anvilcraft.lib.v2.explosion.data.provider; + +import dev.anvilcraft.lib.v2.config.ConfigData; +import dev.anvilcraft.lib.v2.explosion.AnvilLibExplosion; +import dev.anvilcraft.lib.v2.explosion.AnvilLibExplosionConfig; +import net.minecraft.data.PackOutput; +import net.neoforged.neoforge.common.data.LanguageProvider; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public class ModLanguageProvider extends LanguageProvider { + public ModLanguageProvider(PackOutput output) { + super(output, AnvilLibExplosion.MOD_ID, "en_us"); + } + + @Override + protected void addTranslations() { + ConfigData.readConfigClass(this, AnvilLibExplosionConfig.class); + } +} diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/provider/package-info.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/provider/package-info.java new file mode 100644 index 00000000..00eef8b7 --- /dev/null +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/provider/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.explosion.data.provider; + +import org.jspecify.annotations.NullMarked; diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/mixin/SingleItemRecipeAccessor.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/mixin/SingleItemRecipeAccessor.java new file mode 100644 index 00000000..c73ecfe2 --- /dev/null +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/mixin/SingleItemRecipeAccessor.java @@ -0,0 +1,14 @@ +package dev.anvilcraft.lib.v2.explosion.mixin; + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.SingleItemRecipe; +import org.jetbrains.annotations.ApiStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(SingleItemRecipe.class) +@ApiStatus.Internal +public interface SingleItemRecipeAccessor { + @Accessor + ItemStack getResult(); +} diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/package-info.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/package-info.java new file mode 100644 index 00000000..33a27026 --- /dev/null +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.explosion; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/module.explosion/src/main/resources/META-INF/neoforge.mods.toml b/module.explosion/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..27f7ba75 --- /dev/null +++ b/module.explosion/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,86 @@ +# This is an example mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader = "javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. +loaderVersion = "${loader_version_range}" #mandatory +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license = "${mod_license}" +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory +# The modid of the mod +modId = "${mod_id}" #mandatory +# The version number of the mod +version = "${mod_version}" #mandatory +# A display name for the mod +displayName = "${mod_name}" #mandatory +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional +# A URL for the "homepage" for this mod, displayed in the mod UI +displayURL="https://github.com/Anvil-Dev/AnvilLib" +# A file name (in the root of the mod JAR) containing a logo for display +logoFile="icon.png" #optional +# A text field displayed in the mod UI +#credits="" #optional +# A text field displayed in the mod UI +authors = "${mod_authors}" #optional +# Display Test controls the display for your mod in the server connection screen +# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. +# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. +# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. +# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. +# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. +#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) + +# The description text for the mod (multi line!) (#mandatory) +description = '''${mod_description}''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +[[mixins]] +config = "${mod_id}.mixins.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies."${mod_id}"]] #optional +# the modid of the dependency +modId = "neoforge" #mandatory +# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). +# 'required' requires the mod to exist, 'optional' does not +# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning +type = "required" #mandatory +# Optional field describing why the dependency is required or why it is incompatible +# reason="..." +# The version range of the dependency +versionRange = "${neo_version_range}" #mandatory +# An ordering relationship for the dependency. +# BEFORE - This mod is loaded BEFORE the dependency +# AFTER - This mod is loaded AFTER the dependency +ordering = "NONE" +# Side this dependency is applied on - BOTH, CLIENT, or SERVER +side = "BOTH" +# Here's another dependency +[[dependencies."${mod_id}"]] +modId = "minecraft" +type = "required" +# This version range declares a minimum of the current minecraft version up to but not including the next major version +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features."${mod_id}"] +#openGLVersion="[3.2,)" diff --git a/module.explosion/src/main/resources/anvillib_explosion.mixins.json b/module.explosion/src/main/resources/anvillib_explosion.mixins.json new file mode 100644 index 00000000..6795cbd8 --- /dev/null +++ b/module.explosion/src/main/resources/anvillib_explosion.mixins.json @@ -0,0 +1,15 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.anvilcraft.lib.v2.explosion.mixin", + "compatibilityLevel": "JAVA_21", + "refmap": "anvillib.refmap.json", + "mixins": [ + "SingleItemRecipeAccessor" + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/module.explosion/src/main/resources/assets/anvillib_explosion/lang/zh_cn.json b/module.explosion/src/main/resources/assets/anvillib_explosion/lang/zh_cn.json new file mode 100644 index 00000000..e9edafbb --- /dev/null +++ b/module.explosion/src/main/resources/assets/anvillib_explosion/lang/zh_cn.json @@ -0,0 +1,9 @@ +{ + "anvillib_explosion.configuration.default_remove_blocks_per_tick": "默认每 tick 移除的方块数", + "anvillib_explosion.configuration.default_remove_blocks_per_tick.tooltip": "每 tick 内可以删除的方块的默认数量", + "anvillib_explosion.configuration.max_remove_blocks_per_tick": "最大每 tick 移除的方块数", + "anvillib_explosion.configuration.max_remove_blocks_per_tick.tooltip": "每 tick 内可以删除的方块的最大数量", + "anvillib_explosion.configuration.section.anvillib.explosion.common.toml": "Anvillib Explosion 通用 配置", + "anvillib_explosion.configuration.section.anvillib.explosion.common.toml.title": "Anvillib Explosion 通用 配置", + "anvillib_explosion.configuration.title": "Anvillib Explosion 配置" +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 50395768..453bafbb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,6 +23,7 @@ include 'module.wheel' include 'module.yukkuri' include 'module.main' include 'module.rpc' +include 'module.explosion' include 'module.collision' include 'renderdoc-loader' include 'module.sync' @@ -42,6 +43,7 @@ project(':module.wheel').name = 'anvillib-wheel-neoforge-1.21.1' project(':module.yukkuri').name = 'anvillib-yukkuri-neoforge-1.21.1' project(':module.main').name = 'anvillib-neoforge-1.21.1' project(':module.rpc').name = 'anvillib-rpc-neoforge-1.21.1' +project(':module.explosion').name = 'anvillib-explosion-neoforge-1.21.1' project(':module.collision').name = 'anvillib-collision-neoforge-1.21.1' project(':renderdoc-loader').name = 'renderdoc-loader-internal' project(':module.sync').name = 'anvillib-sync-neoforge-1.21.1' From 65aa5e11d6cd6d600424e8167bb718d2aaa780d0 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 10 Aug 2026 23:12:13 +0800 Subject: [PATCH 08/38] =?UTF-8?q?feat(config):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=E9=85=8D=E7=BD=AE=E5=88=86=E7=BB=84=E3=80=81=E6=9E=9A?= =?UTF-8?q?=E4=B8=BE=E7=BF=BB=E8=AF=91=E4=B8=8E=20TOML=20key=20=E7=82=B9?= =?UTF-8?q?=E5=88=86=E9=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 dev/26.1 module.config 的三项功能演进回移植(3d50d76、946938f #56): - @Config(group=...) 配置分组:Config 新增 group() 属性、ConfigManager 传递、 ConfigRecord 新增 group 组件,getFileName() 生成 /-type.toml 子目录文件 (1.21.1 ConfigTracker 原生支持子目录路径) - TranslatableEnum 枚举翻译:新增 util/TranslatableEnum(扩展 NeoForge 21.1.226 TranslatableEnum,提供 getTranslationKey 生成 anvillib.configuration.enum.* 键), ConfigData 增加 ENUM_STRING 常量与 enumValueAdd 反射分支 - TOML key 点分隔:FormattingUtil 新增 toPointSplitName,ConfigData 的 section 键 改用点分隔形式(破坏性:语言 key 由 section.anvillib_explosion.common.toml 变为 section.anvillib.explosion.common.toml,使用方语言文件需同步,已重跑 module.explosion runData 更新产物) 不移植:ConfigManager 的 Optional.of(ModList.get()) 修复(计划标注可选且有争议, port 侧 ModList.get() 不返回 null,行为等价);FMLLoader.getCurrent().getDist() 保持 port 侧 FMLLoader.getDist() 写法。 --- .../dev/anvilcraft/lib/v2/config/Config.java | 2 + .../anvilcraft/lib/v2/config/ConfigData.java | 38 +++++++++++++++++-- .../lib/v2/config/ConfigManager.java | 3 +- .../lib/v2/config/ConfigRecord.java | 8 +++- .../lib/v2/config/util/FormattingUtil.java | 9 +++++ .../lib/v2/config/util/TranslatableEnum.java | 22 +++++++++++ .../assets/anvillib_explosion/lang/en_us.json | 4 +- 7 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 module.config/src/main/java/dev/anvilcraft/lib/v2/config/util/TranslatableEnum.java diff --git a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/Config.java b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/Config.java index c17c26b8..9c9846b7 100644 --- a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/Config.java +++ b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/Config.java @@ -12,6 +12,8 @@ public @interface Config { String name(); + String group() default ""; + ModConfig.Type type() default ModConfig.Type.COMMON; } diff --git a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigData.java b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigData.java index 76c604c3..f89dc0ad 100644 --- a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigData.java +++ b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigData.java @@ -2,6 +2,8 @@ import com.google.gson.annotations.SerializedName; import dev.anvilcraft.lib.v2.config.util.FormattingUtil; +import dev.anvilcraft.lib.v2.config.util.TranslatableEnum; +import lombok.extern.slf4j.Slf4j; import net.neoforged.fml.config.ModConfig; import net.neoforged.neoforge.common.data.LanguageProvider; import org.jetbrains.annotations.Nullable; @@ -10,8 +12,10 @@ import java.util.HashSet; import java.util.Set; +@Slf4j public class ConfigData { public static final String TITLE_STRING = "%s.configuration.title"; + public static final String ENUM_STRING = "anvillib.configuration.enum.%s.%s"; public static final String TOML_STRING = "%s.configuration.section.%s.%s.toml"; public static final String TOML_TITLE_STRING = "%s.configuration.section.%s.%s.toml.title"; public static final String OPTION_STRING = "%s.configuration.%s"; @@ -37,11 +41,11 @@ public static void readConfigClass(LanguageProvider provider, Class configCla ModConfig.Type type = config.type(); proxy.add(TITLE_STRING.formatted(name), "%s Configuration".formatted(FormattingUtil.toEnglishName(name))); proxy.add( - TOML_STRING.formatted(name, name, type.extension()), + TOML_STRING.formatted(name, FormattingUtil.toPointSplitName(name), type.extension()), "%s %s Configuration".formatted(FormattingUtil.toEnglishName(name), FormattingUtil.toEnglishName(type.extension())) ); proxy.add( - TOML_TITLE_STRING.formatted(name, name, type.extension()), + TOML_TITLE_STRING.formatted(name, FormattingUtil.toPointSplitName(name), type.extension()), "%s %s Configuration".formatted(FormattingUtil.toEnglishName(name), FormattingUtil.toEnglishName(type.extension())) ); ConfigData.readConfigClass(proxy, name, type, configClass, null); @@ -68,16 +72,44 @@ private static void providerAdd(ProviderProxy proxy, String modId, ModConfig.Typ } if (parent != null) fieldName = parent + "." + fieldName; boolean flag = field.isAnnotationPresent(CollapsibleObject.class); + Class fieldType = field.getType(); if (flag) { proxy.add(COLLAPSIBLE_OBJECT_BUTTON_STRING.formatted(modId, fieldName), name); proxy.add(COLLAPSIBLE_OBJECT_TOOLTIP_STRING.formatted(modId, fieldName), name); - ConfigData.readConfigClass(proxy, modId, type, field.getType(), fieldName); + ConfigData.readConfigClass(proxy, modId, type, fieldType, fieldName); } proxy.add(OPTION_STRING.formatted(modId, fieldName), name); if (field.isAnnotationPresent(Comment.class)) { Comment comment = field.getAnnotation(Comment.class); proxy.add(TOOLTIP_STRING.formatted(modId, fieldName), comment.value()); } + if (Enum.class.isAssignableFrom(fieldType) && TranslatableEnum.class.isAssignableFrom(fieldType)) { + ConfigData.enumValueAdd(proxy, cast(fieldType)); + } + } + } + + @SuppressWarnings("unchecked") + private static T cast(Object t) { + return (T) t; + } + + private static & TranslatableEnum> void enumValueAdd(ProviderProxy proxy, Class clazz) { + try { + for (Field field : clazz.getDeclaredFields()) { + if (!field.isEnumConstant()) continue; + T value = cast(field.get(null)); + String name; + if (field.isAnnotationPresent(SerializedName.class)) { + name = field.getAnnotation(SerializedName.class).value(); + } else { + name = FormattingUtil.toEnglishName(value.name()); + } + String key = value.getTranslationKey(); + proxy.add(key, name); + } + } catch (IllegalAccessException e) { + log.error("Failed to add enum values for {}", clazz.getName(), e); } } } diff --git a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigManager.java b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigManager.java index d55550c2..2f3c6838 100644 --- a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigManager.java +++ b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigManager.java @@ -66,6 +66,7 @@ public T register(T configObj) { if (configClass.isAnnotationPresent(Config.class)) { Config config = configClass.getAnnotation(Config.class); String name = config.name(); + String group = config.group(); ModConfig.Type type = config.type(); ImmutableList.Builder valuesBuilder = ImmutableList.builder(); try { @@ -74,7 +75,7 @@ public T register(T configObj) { log.error(e.getMessage(), e); } ModConfigSpec spec = builder.build(); - this.configSpecMap.put(configObj, new ConfigRecord(name, type, spec, configObj, valuesBuilder.build())); + this.configSpecMap.put(configObj, new ConfigRecord(group, name, type, spec, configObj, valuesBuilder.build())); } return configObj; } diff --git a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigRecord.java b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigRecord.java index 7d5a12aa..1e501fb6 100644 --- a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigRecord.java +++ b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigRecord.java @@ -8,6 +8,7 @@ import java.util.concurrent.atomic.AtomicBoolean; public record ConfigRecord( + String group, String modId, ModConfig.Type type, ModConfigSpec spec, @@ -16,13 +17,14 @@ public record ConfigRecord( AtomicBoolean registered ) { public ConfigRecord( + String group, String modId, ModConfig.Type type, ModConfigSpec spec, Object object, @Unmodifiable List values ) { - this(modId, type, spec, object, values, new AtomicBoolean(false)); + this(group, modId, type, spec, object, values, new AtomicBoolean(false)); } public void load() { @@ -31,6 +33,8 @@ public void load() { } public String getFileName() { - return "%s-%s.toml".formatted(this.modId, this.type.extension()); + String folder = this.group.isEmpty() ? "" : "%s/".formatted(this.group); + String file = "%s-%s.toml".formatted(this.modId, this.type.extension()); + return "%s%s".formatted(folder, file); } } diff --git a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/util/FormattingUtil.java b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/util/FormattingUtil.java index 382b31ba..d28c0d40 100644 --- a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/util/FormattingUtil.java +++ b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/util/FormattingUtil.java @@ -26,4 +26,13 @@ public static String toEnglishName(Object internalName) { .map(StringUtils::capitalize) .collect(Collectors.joining(" ")); } + + /** + * 将任意名称切分为点分隔的小写单词:"anvillib_explosion" -> "anvillib.explosion" + */ + public static String toPointSplitName(String name) { + return Arrays.stream(name.split("[^A-Za-z0-9]")) + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining(".")); + } } diff --git a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/util/TranslatableEnum.java b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/util/TranslatableEnum.java new file mode 100644 index 00000000..7e3ef242 --- /dev/null +++ b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/util/TranslatableEnum.java @@ -0,0 +1,22 @@ +package dev.anvilcraft.lib.v2.config.util; + +import dev.anvilcraft.lib.v2.config.ConfigData; +import net.minecraft.network.chat.Component; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public interface TranslatableEnum extends net.neoforged.neoforge.common.TranslatableEnum { + @Override + default Component getTranslatedName() { + return Component.translatableWithFallback(this.getTranslationKey(), ((Enum) this).name()); + } + + default String getTranslationKey() { + String className = Arrays.stream(this.getClass().getCanonicalName().split("[.$]")) + .map(FormattingUtil::toLowerCaseUnder) + .collect(Collectors.joining(".")); + String valueName = FormattingUtil.toLowerCaseUnder(((Enum) this).name().toLowerCase()); + return ConfigData.ENUM_STRING.formatted(className, valueName); + } +} diff --git a/module.explosion/src/generated/resources/assets/anvillib_explosion/lang/en_us.json b/module.explosion/src/generated/resources/assets/anvillib_explosion/lang/en_us.json index 652d3ee9..822f71ea 100644 --- a/module.explosion/src/generated/resources/assets/anvillib_explosion/lang/en_us.json +++ b/module.explosion/src/generated/resources/assets/anvillib_explosion/lang/en_us.json @@ -3,7 +3,7 @@ "anvillib_explosion.configuration.default_remove_blocks_per_tick.tooltip": "The default number of blocks that can be deleted per tick.", "anvillib_explosion.configuration.max_remove_blocks_per_tick": "Max Remove Blocks Per Tick", "anvillib_explosion.configuration.max_remove_blocks_per_tick.tooltip": "The maximum number of blocks that can be deleted per tick.", - "anvillib_explosion.configuration.section.anvillib_explosion.common.toml": "Anvillib Explosion Common Configuration", - "anvillib_explosion.configuration.section.anvillib_explosion.common.toml.title": "Anvillib Explosion Common Configuration", + "anvillib_explosion.configuration.section.anvillib.explosion.common.toml": "Anvillib Explosion Common Configuration", + "anvillib_explosion.configuration.section.anvillib.explosion.common.toml.title": "Anvillib Explosion Common Configuration", "anvillib_explosion.configuration.title": "Anvillib Explosion Configuration" } \ No newline at end of file From 20934ac43d8bfbcf26e9aa08b3a62dff227f1c11 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 10 Aug 2026 23:17:01 +0800 Subject: [PATCH 09/38] =?UTF-8?q?feat(network):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=20PacketData=20=E6=B3=9B=E5=9E=8B=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8E=20NetworkUtil=20=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E5=8F=91=E9=80=81=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回移植 dev/26.1 的三处小演进: - PacketData 泛型检查修复(20721b1 #52):字段判断反转为 Type.class.isAssignableFrom(fieldType) 并追加类型实参校验 (isMatchingTypeArgument:Type 检查第 0 参、StreamCodec 检查第 1 参), 修复同一类中多个静态 Type/StreamCodec 字段时的误匹配 - NetworkUtil 条件发送(5733a9d):新增 sendToAllPlayersIncluded 与 sendToAllPlayersInDimensionIncluded(included == null 默认全通过), 与现有 Excluded 方法对称;player.equals(excluded) 改为 Objects.equals 防 NPE - PacketProtocol public 已随 #P1 前置提交(56cade3a) --- .../lib/v2/network/register/PacketData.java | 37 +++++++++++++++++-- .../lib/v2/network/util/NetworkUtil.java | 31 +++++++++++++++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketData.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketData.java index 440dd809..b0cfc24b 100644 --- a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketData.java +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketData.java @@ -13,6 +13,8 @@ import java.lang.reflect.AccessFlag; import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.Set; @Slf4j @@ -35,11 +37,17 @@ static PacketData find(Class pac ) { continue; } - Class declaringClass = field.getType(); - if (declaringClass.isAssignableFrom(CustomPacketPayload.Type.class)) { + Class fieldType = field.getType(); + if (CustomPacketPayload.Type.class.isAssignableFrom(fieldType)) { + if (!isMatchingTypeArgument(field.getGenericType(), 0, packetClass)) { + continue; + } field.setAccessible(true); type = (CustomPacketPayload.Type) field.get(null); - } else if (declaringClass.isAssignableFrom(StreamCodec.class)) { + } else if (StreamCodec.class.isAssignableFrom(fieldType)) { + if (!isMatchingTypeArgument(field.getGenericType(), 1, packetClass)) { + continue; + } field.setAccessible(true); codec = (StreamCodec) field.get(null); } @@ -77,4 +85,27 @@ static PacketData find(Class pac } return new PacketData<>(type, codec, direction, handler); } + + /** + * 检查字段的参数化类型中,指定索引的类型实参是否与期望的类匹配。 + * 如果字段类型不是参数化类型、类型实参不足、或类型不匹配,返回 false。 + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private static boolean isMatchingTypeArgument( + Type genericType, + int typeArgIndex, + Class expectedClass + ) { + if (!(genericType instanceof ParameterizedType pt)) { + return false; + } + Type[] typeArgs = pt.getActualTypeArguments(); + if (typeArgs.length <= typeArgIndex) { + return false; + } + if (!(typeArgs[typeArgIndex] instanceof Class typeArg)) { + return false; + } + return typeArg.isAssignableFrom(expectedClass); + } } diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/util/NetworkUtil.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/util/NetworkUtil.java index 3a7db03e..e81ad690 100644 --- a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/util/NetworkUtil.java +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/util/NetworkUtil.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable; import java.util.Objects; +import java.util.function.Predicate; public class NetworkUtil { public static void sendToAllPlayersExcluded( @@ -18,7 +19,7 @@ public static void sendToAllPlayersExcluded( ) { MinecraftServer server = Objects.requireNonNull(ServerLifecycleHooks.getCurrentServer(), "Cannot send clientbound payloads on the client"); for (ServerPlayer player : server.getPlayerList().getPlayers()) { - if (player.equals(excluded)) continue; + if (Objects.equals(player, excluded)) continue; PacketDistributor.sendToPlayer(player, payload, payloads); } } @@ -30,7 +31,33 @@ public static void sendToAllPlayersInDimensionExcluded( CustomPacketPayload... payloads ) { for (ServerPlayer player : level.players()) { - if (player.equals(excluded)) continue; + if (Objects.equals(player, excluded)) continue; + PacketDistributor.sendToPlayer(player, payload, payloads); + } + } + + public static void sendToAllPlayersIncluded( + @Nullable Predicate included, + CustomPacketPayload payload, + CustomPacketPayload... payloads + ) { + if (included == null) included = ignored -> true; + MinecraftServer server = Objects.requireNonNull(ServerLifecycleHooks.getCurrentServer(), "Cannot send clientbound payloads on the client"); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (!included.test(player)) continue; + PacketDistributor.sendToPlayer(player, payload, payloads); + } + } + + public static void sendToAllPlayersInDimensionIncluded( + ServerLevel level, + @Nullable Predicate included, + CustomPacketPayload payload, + CustomPacketPayload... payloads + ) { + if (included == null) included = ignored -> true; + for (ServerPlayer player : level.players()) { + if (!included.test(player)) continue; PacketDistributor.sendToPlayer(player, payload, payloads); } } From 71bf9e54fb6049054dea2cee436586e8ff6791b0 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 10 Aug 2026 23:19:11 +0800 Subject: [PATCH 10/38] =?UTF-8?q?fix(recipe):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=20SpawnItem=20=E9=9B=B6=E6=95=B0=E9=87=8F=E5=AE=88?= =?UTF-8?q?=E5=8D=AB=E4=B8=8E=20SetBlock=20nbt=20=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回移植 dev/26.1 的两处纯逻辑修复: - SpawnItem.accept()(40b8f84):count 掷 0 时直接返回,不再生成空堆物品实体 - SetBlock 编解码(b8825bd):optionalFieldOf("nbt") 默认值 null → 空 CompoundTag, 修复 JSON 缺 nbt 字段时 loadWithComponents(null, ...) 的潜在 NPE 不迁移:SpawnItem 的 ItemStackTemplate 化与 SetBlock 的 ValueInput/ProblemReporter NBT 加载上报(26.1 平台特性,port 侧 ItemStack/loadWithComponents(CompoundTag, Provider) 等价)。 --- .../java/dev/anvilcraft/lib/v2/recipe/outcome/SetBlock.java | 2 +- .../java/dev/anvilcraft/lib/v2/recipe/outcome/SpawnItem.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/SetBlock.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/SetBlock.java index 3e49a4fe..c5f580f9 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/SetBlock.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/SetBlock.java @@ -97,7 +97,7 @@ public static class Type implements IRecipeOutcome.Type { CodecUtil.BLOCK_STATE_MAP_CODEC .forGetter(SetBlock::state), CompoundTag.CODEC - .optionalFieldOf("nbt", null) + .optionalFieldOf("nbt", new CompoundTag()) .forGetter(SetBlock::nbt), Vec3.CODEC .fieldOf("offset") diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/SpawnItem.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/SpawnItem.java index 91776081..d45062d1 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/SpawnItem.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/SpawnItem.java @@ -104,7 +104,9 @@ public Type getType() { @SuppressWarnings("unchecked") public void accept(InWorldRecipeContext context) { ItemCache cache = context.computeIfAbsent(ItemCache.ITEM_CACHE); - ItemStack stack = this.item.copyWithCount(context.getInt(this.count, 0, 99)); + int count = context.getInt(this.count, 0, 99); + if (count == 0) return; + ItemStack stack = this.item.copyWithCount(count); BlockCache blockCache = context.computeIfAbsent(BlockCache.BLOCK_CACHE); Vec3 offset = context.getPos().add(this.offset); BlockPos blockPos = BlockPos.containing(offset); From adc23468727627741edcb6bb87898648a68887e1 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 10 Aug 2026 23:22:40 +0800 Subject: [PATCH 11/38] =?UTF-8?q?feat(integration):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=E6=95=B0=E6=8D=AE=E5=8A=A0=E8=BD=BD=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF/=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=8B=86=E5=88=86?= =?UTF-8?q?=E4=B8=8E=20meter=20=E9=80=92=E5=A2=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回移植 dev/26.1 的两项功能演进: - 数据加载拆分(dc4d583):IntegrationType.DATA 拆为 CLIENT_DATA/SERVER_DATA; IntegrationInstance 的 dataLoader 拆为 clientDataLoader + serverDataLoader, findVirtual 查找 applyClientData/applyServerData;loadData → loadClientData/ loadServerData、loadAllDataIntegrations → loadAllClientDataIntegrations/ loadAllServerDataIntegrations - meter 递增(e8efbd4):compileContent 循环内 meter.increment() 破坏性 API:旧的 applyData 方法名不再被识别,下游使用方需同步改名。 不迁移:FMLLoader.getCurrent().getLoadingModList() 保留 port 侧 LoadingModList.get() 写法。 --- .../v2/integration/IntegrationInstance.java | 31 ++++++++++++++----- .../v2/integration/IntegrationManager.java | 31 +++++++++++++++---- .../lib/v2/integration/IntegrationType.java | 3 +- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationInstance.java b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationInstance.java index 4892ad47..262c9819 100644 --- a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationInstance.java +++ b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationInstance.java @@ -24,7 +24,8 @@ public final class IntegrationInstance { private MethodHandle constructor; private MethodHandle loader; private MethodHandle clientLoader; - private MethodHandle dataLoader; + private MethodHandle clientDataLoader; + private MethodHandle serverDataLoader; @SneakyThrows public IntegrationInstance(String modid, ModVersionRange versionRange, String className, List type) { @@ -54,12 +55,18 @@ public void newInstance() { } this.clientLoader = loader; try { - loader = lookup.findVirtual(clazz, "applyData", MethodType.methodType(void.class)); + loader = lookup.findVirtual(clazz, "applyClientData", MethodType.methodType(void.class)); } catch (Throwable e) { loader = null; } - this.dataLoader = loader; - if (this.loader == null && this.clientLoader == null && this.dataLoader == null) { + this.clientDataLoader = loader; + try { + loader = lookup.findVirtual(clazz, "applyServerData", MethodType.methodType(void.class)); + } catch (Throwable e) { + loader = null; + } + this.serverDataLoader = loader; + if (this.loader == null && this.clientLoader == null && this.clientDataLoader == null && this.serverDataLoader == null) { log.warn("Integration {} does not declare any loader method.", className); } } @@ -83,9 +90,16 @@ public void invokeClient() { } @SneakyThrows - public void invokeData() { - if (dataLoader != null) { - dataLoader.invoke(instance); + public void invokeClientData() { + if (clientDataLoader != null) { + clientDataLoader.invoke(instance); + } + } + + @SneakyThrows + public void invokeServerData() { + if (serverDataLoader != null) { + serverDataLoader.invoke(instance); } } @@ -105,7 +119,8 @@ public String toString() { + "instance=" + instance + ", " + "loader=" + loader + ", " + "clientLoader=" + clientLoader + ", " - + "dataLoader=" + dataLoader + + "clientDataLoader=" + clientDataLoader + ", " + + "serverDataLoader=" + serverDataLoader + ']'; } } diff --git a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java index 300121d4..2748ad55 100644 --- a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java +++ b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java @@ -51,13 +51,15 @@ public void compileContent() { type = typeHolders.stream().map(holder -> switch (holder.value()) { case "DEDICATED_SERVER" -> IntegrationType.DEDICATED_SERVER; case "CLIENT" -> IntegrationType.CLIENT; - case "DATA" -> IntegrationType.DATA; + case "CLIENT_DATA" -> IntegrationType.CLIENT_DATA; + case "SERVER_DATA" -> IntegrationType.SERVER_DATA; default -> throw new IllegalArgumentException("Unknown integration type: " + holder.value()); }).toList(); } log.info("Considering integration {} for {id:{}, version:{}}", annotation.memberName(), modid, version); IntegrationInstance instance = new IntegrationInstance(modid, ModVersionRange.of(version), annotation.memberName(), type); this.instances.put(modid, instance); + meter.increment(); } } StartupNotificationManager.popBar(meter); @@ -85,12 +87,22 @@ public void loadClient(String modid, ModInfo info) { } @SuppressWarnings("DataFlowIssue") - public void loadData(String modid, ModInfo info) { + public void loadClientData(String modid, ModInfo info) { for (IntegrationInstance instance : instances.get(modid)) { if (!instance.is(info)) continue; instance.newInstance(); - log.info("Loading data integration {} for {}.", instance.getInstance(), modid); - instance.invokeData(); + log.info("Loading client data integration {} for {}.", instance.getInstance(), modid); + instance.invokeClientData(); + } + } + + @SuppressWarnings("DataFlowIssue") + public void loadServerData(String modid, ModInfo info) { + for (IntegrationInstance instance : instances.get(modid)) { + if (!instance.is(info)) continue; + instance.newInstance(); + log.info("Loading server data integration {} for {}.", instance.getInstance(), modid); + instance.invokeServerData(); } } @@ -108,10 +120,17 @@ public void loadAllClientIntegrations() { } } - public void loadAllDataIntegrations() { + public void loadAllClientDataIntegrations() { + for (String key : instances.keySet()) { + Optional info = LoadingModList.get().getMods().stream().filter(it -> it.getModId().equals(key)).findFirst(); + info.ifPresent(modInfo -> loadClientData(key, modInfo)); + } + } + + public void loadAllServerDataIntegrations() { for (String key : instances.keySet()) { Optional info = LoadingModList.get().getMods().stream().filter(it -> it.getModId().equals(key)).findFirst(); - info.ifPresent(modInfo -> loadData(key, modInfo)); + info.ifPresent(modInfo -> loadServerData(key, modInfo)); } } } diff --git a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationType.java b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationType.java index 2fac43af..d440f618 100644 --- a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationType.java +++ b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationType.java @@ -3,5 +3,6 @@ public enum IntegrationType { CLIENT, DEDICATED_SERVER, - DATA + CLIENT_DATA, + SERVER_DATA } From 164f441f82895fce1b8752d637e68704b38fa30c Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 10 Aug 2026 23:42:36 +0800 Subject: [PATCH 12/38] =?UTF-8?q?feat(util):=20=E5=9B=9E=E7=A7=BB=E6=A4=8D?= =?UTF-8?q?=20OutlineUtil=20=E4=B8=8E=E8=B0=93=E8=AF=8D=20HolderGetter=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回移植 dev/26.1 module.util 的功能演进: - OutlineUtil(e4c09fb #75):AABB 轮廓直接提取(OutlineUtil 434 行原样复制, 仅依赖 AABB/Vec3 + Java 标准库);client/Line 移除 setLineWidth(1.21.1 VertexConsumer 无此方法,1.21.2 渲染管线改造后引入);新增 ShapeUtilJoinTimingTest + runShapeUtilJoinTimingTest(JavaExec)构建接线 - 谓词 HolderGetter 参数化(bb09805):BlockStatePredicate / ItemIngredientPredicate / ItemPredicate 的 Builder.of(TagKey) → of(HolderGetter, TagKey),不再绑定静态注册表; 连带 module.recipe 五个调用方(InWorldRecipeBuilder、HasBlock、HasBlockIngredient、 HasItem、HasItemIngredient)同步改造 - ItemIngredientPredicate 新增实例方法 withCount(int)(d84912e,返回新实例 不改原实例;Builder.withCount 已在 port 侧存在);BlockStatePredicate 加 @EqualsAndHashCode(c480e9f #49) 保留项:BlockStatePredicate.Builder.with(BlockState)(module.multiblock 依赖, dev 侧为配合 26.1 重构删除,port 不照搬)。 --- .../recipe/builder/InWorldRecipeBuilder.java | 93 ++-- .../v2/recipe/predicate/block/HasBlock.java | 8 +- .../predicate/block/HasBlockIngredient.java | 8 +- .../lib/v2/recipe/predicate/item/HasItem.java | 8 +- .../predicate/item/HasItemIngredient.java | 8 +- module.util/build.gradle | 20 + .../anvilcraft/lib/v2/util/OutlineUtil.java | 434 ++++++++++++++++++ .../anvilcraft/lib/v2/util/client/Line.java | 47 ++ .../util/predicate/BlockStatePredicate.java | 11 +- .../predicate/ItemIngredientPredicate.java | 27 +- .../lib/v2/util/predicate/ItemPredicate.java | 10 +- .../lib/v2/util/ShapeUtilJoinTimingTest.java | 232 ++++++++++ 12 files changed, 839 insertions(+), 67 deletions(-) create mode 100644 module.util/src/main/java/dev/anvilcraft/lib/v2/util/OutlineUtil.java create mode 100644 module.util/src/main/java/dev/anvilcraft/lib/v2/util/client/Line.java create mode 100644 module.util/src/test/java/dev/anvilcraft/lib/v2/util/ShapeUtilJoinTimingTest.java diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java index 447a68bc..056f5524 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java @@ -19,6 +19,7 @@ import net.minecraft.advancements.AdvancementRewards; import net.minecraft.advancements.Criterion; import net.minecraft.advancements.critereon.RecipeUnlockedTrigger; +import net.minecraft.core.HolderGetter; import net.minecraft.core.NonNullList; import net.minecraft.data.recipes.RecipeBuilder; import net.minecraft.data.recipes.RecipeOutput; @@ -291,22 +292,24 @@ public T hasItem(double x, double y, double z, ItemLike... items) { /** * 添加物品谓词 * - * @param items 物品标签 + * @param items 物品注册表访问器 + * @param tag 物品标签 * @return 当前构建器实例 */ - public T hasItem(TagKey items) { - return this.with(HasItem.builder().of(items).offset(this.offset).build()); + public T hasItem(HolderGetter items, TagKey tag) { + return this.with(HasItem.builder().of(items, tag).offset(this.offset).build()); } /** * 添加物品谓词 * * @param offset 偏移向量 - * @param items 物品标签 + * @param items 物品注册表访问器 + * @param tag 物品标签 * @return 当前构建器实例 */ - public T hasItem(Vec3 offset, TagKey items) { - return this.with(HasItem.builder().offset(offset).build()); + public T hasItem(Vec3 offset, HolderGetter items, TagKey tag) { + return this.with(HasItem.builder().of(items, tag).offset(offset).build()); } /** @@ -315,11 +318,12 @@ public T hasItem(Vec3 offset, TagKey items) { * @param x X轴偏移量 * @param y Y轴偏移量 * @param z Z轴偏移量 - * @param items 物品标签 + * @param items 物品注册表访问器 + * @param tag 物品标签 * @return 当前构建器实例 */ - public T hasItem(double x, double y, double z, TagKey items) { - return this.with(HasItem.builder().offset(x, y, z).build()); + public T hasItem(double x, double y, double z, HolderGetter items, TagKey tag) { + return this.with(HasItem.builder().of(items, tag).offset(x, y, z).build()); } /** @@ -372,22 +376,24 @@ public T hasItemIngredient(double x, double y, double z, ItemLike... items) { /** * 添加物品原料谓词 * - * @param items 物品标签 + * @param items 物品注册表访问器 + * @param tag 物品标签 * @return 当前构建器实例 */ - public T hasItemIngredient(TagKey items) { - return this.with(HasItemIngredient.builder().of(items).offset(this.offset).build()); + public T hasItemIngredient(HolderGetter items, TagKey tag) { + return this.with(HasItemIngredient.builder().of(items, tag).offset(this.offset).build()); } /** * 添加物品原料谓词 * * @param offset 偏移向量 - * @param items 物品标签 + * @param items 物品注册表访问器 + * @param tag 物品标签 * @return 当前构建器实例 */ - public T hasItemIngredient(Vec3 offset, TagKey items) { - return this.with(HasItemIngredient.builder().of(items).offset(offset).build()); + public T hasItemIngredient(Vec3 offset, HolderGetter items, TagKey tag) { + return this.with(HasItemIngredient.builder().of(items, tag).offset(offset).build()); } /** @@ -396,11 +402,12 @@ public T hasItemIngredient(Vec3 offset, TagKey items) { * @param x X轴偏移量 * @param y Y轴偏移量 * @param z Z轴偏移量 - * @param items 物品标签 + * @param items 物品注册表访问器 + * @param tag 物品标签 * @return 当前构建器实例 */ - public T hasItemIngredient(double x, double y, double z, TagKey items) { - return this.with(HasItemIngredient.builder().of(items).offset(x, y, z).build()); + public T hasItemIngredient(double x, double y, double z, HolderGetter items, TagKey tag) { + return this.with(HasItemIngredient.builder().of(items, tag).offset(x, y, z).build()); } /** @@ -487,35 +494,38 @@ public T hasBlock(double x, double y, double z, Collection blocks) { /** * 添加方块谓词 * - * @param tag 方块标签 + * @param blocks 方块注册表访问器 + * @param tag 方块标签 * @return 当前构建器实例 */ - public T hasBlock(TagKey tag) { - return this.with(HasBlock.builder().of(tag).offset(this.offset).build()); + public T hasBlock(HolderGetter blocks, TagKey tag) { + return this.with(HasBlock.builder().of(blocks, tag).offset(this.offset).build()); } /** * 添加方块谓词 * * @param offset 偏移向量 + * @param blocks 方块注册表访问器 * @param tag 方块标签 * @return 当前构建器实例 */ - public T hasBlock(Vec3 offset, TagKey tag) { - return this.with(HasBlock.builder().of(tag).offset(offset).build()); + public T hasBlock(Vec3 offset, HolderGetter blocks, TagKey tag) { + return this.with(HasBlock.builder().of(blocks, tag).offset(offset).build()); } /** * 添加方块谓词 * - * @param x X轴偏移量 - * @param y Y轴偏移量 - * @param z Z轴偏移量 - * @param tag 方块标签 + * @param x X轴偏移量 + * @param y Y轴偏移量 + * @param z Z轴偏移量 + * @param blocks 方块注册表访问器 + * @param tag 方块标签 * @return 当前构建器实例 */ - public T hasBlock(double x, double y, double z, TagKey tag) { - return this.with(HasBlock.builder().of(tag).offset(x, y, z).build()); + public T hasBlock(double x, double y, double z, HolderGetter blocks, TagKey tag) { + return this.with(HasBlock.builder().of(blocks, tag).offset(x, y, z).build()); } /** @@ -649,35 +659,38 @@ public T hasBlockIngredient(double x, double y, double z, Collection bloc /** * 添加方块原料谓词 * - * @param tag 方块标签 + * @param blocks 方块注册表访问器 + * @param tag 方块标签 * @return 当前构建器实例 */ - public T hasBlockIngredient(TagKey tag) { - return this.with(HasBlockIngredient.builder().of(tag).offset(this.offset).build()); + public T hasBlockIngredient(HolderGetter blocks, TagKey tag) { + return this.with(HasBlockIngredient.builder().of(blocks, tag).offset(this.offset).build()); } /** * 添加方块原料谓词 * * @param offset 偏移向量 + * @param blocks 方块注册表访问器 * @param tag 方块标签 * @return 当前构建器实例 */ - public T hasBlockIngredient(Vec3 offset, TagKey tag) { - return this.with(HasBlockIngredient.builder().of(tag).offset(offset).build()); + public T hasBlockIngredient(Vec3 offset, HolderGetter blocks, TagKey tag) { + return this.with(HasBlockIngredient.builder().of(blocks, tag).offset(offset).build()); } /** * 添加方块原料谓词 * - * @param x X轴偏移量 - * @param y Y轴偏移量 - * @param z Z轴偏移量 - * @param tag 方块标签 + * @param x X轴偏移量 + * @param y Y轴偏移量 + * @param z Z轴偏移量 + * @param blocks 方块注册表访问器 + * @param tag 方块标签 * @return 当前构建器实例 */ - public T hasBlockIngredient(double x, double y, double z, TagKey tag) { - return this.with(HasBlockIngredient.builder().of(tag).offset(new Vec3(x, y, z)).build()); + public T hasBlockIngredient(double x, double y, double z, HolderGetter blocks, TagKey tag) { + return this.with(HasBlockIngredient.builder().of(blocks, tag).offset(new Vec3(x, y, z)).build()); } /** diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlock.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlock.java index 32a69feb..e101fac2 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlock.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlock.java @@ -3,6 +3,7 @@ import dev.anvilcraft.lib.v2.recipe.init.recipe.LibRecipePredicateTypes; import dev.anvilcraft.lib.v2.util.predicate.BlockStatePredicate; import lombok.Getter; +import net.minecraft.core.HolderGetter; import net.minecraft.tags.TagKey; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.properties.Property; @@ -159,11 +160,12 @@ public Builder of(Collection blocks) { /** * 设置方块标签 * - * @param tag 方块标签 + * @param blocks 方块注册表访问器 + * @param tag 方块标签 * @return 构建器实例 */ - public Builder of(TagKey tag) { - this.predicate.of(tag); + public Builder of(HolderGetter blocks, TagKey tag) { + this.predicate.of(blocks, tag); return this; } diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlockIngredient.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlockIngredient.java index 71996b62..b2417921 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlockIngredient.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlockIngredient.java @@ -6,6 +6,7 @@ import dev.anvilcraft.lib.v2.recipe.util.InWorldRecipeContext; import lombok.Getter; import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderGetter; import net.minecraft.tags.TagKey; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.properties.Property; @@ -170,11 +171,12 @@ public Builder of(Collection blocks) { /** * 设置方块标签 * - * @param tag 方块标签 + * @param blocks 方块注册表访问器 + * @param tag 方块标签 * @return 构建器实例 */ - public Builder of(TagKey tag) { - this.predicate.of(tag); + public Builder of(HolderGetter blocks, TagKey tag) { + this.predicate.of(blocks, tag); return this; } diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItem.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItem.java index b7d6258b..484ded68 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItem.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItem.java @@ -9,6 +9,7 @@ import lombok.Getter; import net.minecraft.advancements.critereon.ItemSubPredicate; import net.minecraft.advancements.critereon.MinMaxBounds; +import net.minecraft.core.HolderGetter; import net.minecraft.core.component.DataComponentPredicate; import net.minecraft.core.component.DataComponentType; import net.minecraft.network.RegistryFriendlyByteBuf; @@ -199,11 +200,12 @@ public Builder of(ItemLike... items) { /** * 设置物品标签 * - * @param tag 物品标签 + * @param items 物品注册表访问器 + * @param tag 物品标签 * @return 构建器实例 */ - public Builder of(TagKey tag) { - this.item.of(tag); + public Builder of(HolderGetter items, TagKey tag) { + this.item.of(items, tag); return this; } diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItemIngredient.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItemIngredient.java index 771e79d0..285c6e9c 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItemIngredient.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItemIngredient.java @@ -13,6 +13,7 @@ import net.minecraft.advancements.critereon.ItemSubPredicate; import net.minecraft.core.component.DataComponentPredicate; import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.HolderGetter; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; @@ -180,11 +181,12 @@ public Builder of(ItemLike... items) { /** * 设置物品标签 * - * @param tag 物品标签 + * @param items 物品注册表访问器 + * @param tag 物品标签 * @return 构建器实例 */ - public Builder of(TagKey tag) { - this.item.of(tag); + public Builder of(HolderGetter items, TagKey tag) { + this.item.of(items, tag); return this; } diff --git a/module.util/build.gradle b/module.util/build.gradle index 13d6dfac..33a6277d 100644 --- a/module.util/build.gradle +++ b/module.util/build.gradle @@ -115,6 +115,8 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { + compileOnly "org.jspecify:jspecify:1.0.0" + if (System.getenv("NOT_DEV") == 'true') { jarJar(api("dev.anvilcraft.lib:anvillib-codec-neoforge-1.21.1:latest.release")) } else { @@ -270,6 +272,24 @@ machete { enabled = false } +sourceSets { + test { + compileClasspath += sourceSets.main.compileClasspath + runtimeClasspath += sourceSets.main.compileClasspath + } +} + +tasks.register('runShapeUtilJoinTimingTest', JavaExec) { + group = 'verification' + description = 'Runs ShapeUtil join timing checks for connected AABBs.' + classpath = sourceSets.test.runtimeClasspath + sourceSets.main.compileClasspath + mainClass = 'dev.anvilcraft.lib.v2.util.ShapeUtilJoinTimingTest' +} + +tasks.named('check') { + dependsOn tasks.named('runShapeUtilJoinTimingTest') +} + lombok { version = "1.18.34" } diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/OutlineUtil.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/OutlineUtil.java new file mode 100644 index 00000000..b73ea8b6 --- /dev/null +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/OutlineUtil.java @@ -0,0 +1,434 @@ +package dev.anvilcraft.lib.v2.util; + +import dev.anvilcraft.lib.v2.util.client.Line; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public final class OutlineUtil { + private static final String PARALLEL_THRESHOLD_PROPERTY = "anvillib.outline.parallelThreshold"; + private static final int DEFAULT_PARALLEL_BOX_THRESHOLD = 1024; + + private OutlineUtil() { + } + + public static List extractOutline(List boxes) { + return extractOutline(boxes, null); + } + + public static List extractOutline(List boxes, @Nullable Executor executor) { + if (boxes.isEmpty()) { + return List.of(); + } + + EdgeAccumulator accumulator = new EdgeAccumulator(); + List tasks = createAxisTasks(boxes); + if (boxes.size() < parallelBoxThreshold()) { + for (AxisTask task : tasks) { + accumulator.addAll(task.extract()); + } + } else { + for (EdgeAccumulator extracted : extractParallel(tasks, executor)) { + accumulator.addAll(extracted); + } + } + return accumulator.toLines(); + } + + private static List extractParallel(List tasks, @Nullable Executor executor) { + if (executor == null) { + return tasks.parallelStream().map(AxisTask::extract).toList(); + } + return tasks.stream() + .map(task -> CompletableFuture.supplyAsync(task::extract, executor)) + .map(CompletableFuture::join) + .toList(); + } + + private static int parallelBoxThreshold() { + return Integer.getInteger(PARALLEL_THRESHOLD_PROPERTY, DEFAULT_PARALLEL_BOX_THRESHOLD); + } + + private static List createAxisTasks(List boxes) { + List tasks = new ArrayList<>(3); + for (int axis = 0; axis < 3; axis++) { + tasks.add(new AxisTask(boxes, axis, firstOtherAxis(axis), secondOtherAxis(axis))); + } + return tasks; + } + + private record AxisTask(List boxes, int axis, int uAxis, int vAxis) { + private EdgeAccumulator extract() { + TreeSet planes = new TreeSet<>(); + Map> starts = new HashMap<>(); + Map> ends = new HashMap<>(); + for (AABB box : boxes) { + if (isDegenerate(box)) { + continue; + } + double min = min(box, axis); + double max = max(box, axis); + Rect projection = project(box, uAxis, vAxis); + planes.add(min); + planes.add(max); + starts.computeIfAbsent(min, ignored -> new ArrayList<>()).add(projection); + ends.computeIfAbsent(max, ignored -> new ArrayList<>()).add(projection); + } + + EdgeAccumulator accumulator = new EdgeAccumulator(); + RectMultiset active = new RectMultiset(); + for (double plane : planes) { + List lower = active.toList(); + active.removeAll(ends.getOrDefault(plane, List.of())); + active.addAll(starts.getOrDefault(plane, List.of())); + extractPlaneOutline(axis, uAxis, vAxis, plane, lower, active.toList(), accumulator); + } + return accumulator; + } + } + + private static void extractPlaneOutline( + int axis, + int uAxis, + int vAxis, + double plane, + List lower, + List upper, + EdgeAccumulator accumulator + ) { + if (lower.isEmpty() && upper.isEmpty()) { + return; + } + + AxisGrid grid = AxisGrid.create(lower, upper); + if (grid.uCoordinates.length < 2 || grid.vCoordinates.length < 2) { + return; + } + + BitSet[] lowerRows = createRows(grid.uSize()); + BitSet[] upperRows = createRows(grid.uSize()); + mark(lowerRows, lower, grid); + mark(upperRows, upper, grid); + + for (int u = 0; u < grid.uSize(); u++) { + for (int v = 0; v < grid.vSize(); v++) { + if (empty(lowerRows, upperRows, u, v)) { + continue; + } + + if (v == 0 || empty(lowerRows, upperRows, u, v - 1)) { + accumulator.add( + point(axis, uAxis, vAxis, plane, grid.uCoordinates[u], grid.vCoordinates[v]), + point(axis, uAxis, vAxis, plane, grid.uCoordinates[u + 1], grid.vCoordinates[v]) + ); + } + if (v == grid.vSize() - 1 || empty(lowerRows, upperRows, u, v + 1)) { + accumulator.add( + point(axis, uAxis, vAxis, plane, grid.uCoordinates[u], grid.vCoordinates[v + 1]), + point(axis, uAxis, vAxis, plane, grid.uCoordinates[u + 1], grid.vCoordinates[v + 1]) + ); + } + if (u == 0 || empty(lowerRows, upperRows, u - 1, v)) { + accumulator.add( + point(axis, uAxis, vAxis, plane, grid.uCoordinates[u], grid.vCoordinates[v]), + point(axis, uAxis, vAxis, plane, grid.uCoordinates[u], grid.vCoordinates[v + 1]) + ); + } + if (u == grid.uSize() - 1 || empty(lowerRows, upperRows, u + 1, v)) { + accumulator.add( + point(axis, uAxis, vAxis, plane, grid.uCoordinates[u + 1], grid.vCoordinates[v]), + point(axis, uAxis, vAxis, plane, grid.uCoordinates[u + 1], grid.vCoordinates[v + 1]) + ); + } + } + } + } + + private static BitSet[] createRows(int size) { + BitSet[] rows = new BitSet[size]; + for (int i = 0; i < rows.length; i++) { + rows[i] = new BitSet(); + } + return rows; + } + + private static void mark(BitSet[] rows, List rects, AxisGrid grid) { + for (Rect rect : rects) { + Integer minU = grid.uIndex.get(rect.minU); + Integer maxU = grid.uIndex.get(rect.maxU); + Integer minV = grid.vIndex.get(rect.minV); + Integer maxV = grid.vIndex.get(rect.maxV); + if (minU == null || maxU == null || minV == null || maxV == null) { + continue; + } + for (int u = minU; u < maxU; u++) { + rows[u].set(minV, maxV); + } + } + } + + private static boolean empty(BitSet[] lowerRows, BitSet[] upperRows, int u, int v) { + return lowerRows[u].get(v) == upperRows[u].get(v); + } + + private static Rect project(AABB box, int uAxis, int vAxis) { + return new Rect( + canonical(min(box, uAxis)), + canonical(max(box, uAxis)), + canonical(min(box, vAxis)), + canonical(max(box, vAxis)) + ); + } + + private static Point point(int axis, int uAxis, int vAxis, double plane, double u, double v) { + double[] coordinates = new double[3]; + coordinates[axis] = plane; + coordinates[uAxis] = u; + coordinates[vAxis] = v; + return new Point(coordinates[0], coordinates[1], coordinates[2]); + } + + private static boolean isDegenerate(AABB box) { + return box.minX >= box.maxX || box.minY >= box.maxY || box.minZ >= box.maxZ; + } + + private static int firstOtherAxis(int axis) { + return switch (axis) { + case 0 -> 1; + case 1, 2 -> 0; + default -> throw new IllegalArgumentException("Unknown axis: " + axis); + }; + } + + private static int secondOtherAxis(int axis) { + return switch (axis) { + case 0, 1 -> 2; + case 2 -> 1; + default -> throw new IllegalArgumentException("Unknown axis: " + axis); + }; + } + + private static double min(AABB box, int axis) { + return switch (axis) { + case 0 -> box.minX; + case 1 -> box.minY; + case 2 -> box.minZ; + default -> throw new IllegalArgumentException("Unknown axis: " + axis); + }; + } + + private static double max(AABB box, int axis) { + return switch (axis) { + case 0 -> box.maxX; + case 1 -> box.maxY; + case 2 -> box.maxZ; + default -> throw new IllegalArgumentException("Unknown axis: " + axis); + }; + } + + private static double canonical(double value) { + return value == 0.0 ? 0.0 : value; + } + + private record Rect(double minU, double maxU, double minV, double maxV) { + } + + private static final class RectMultiset { + private final Map counts; + + private RectMultiset() { + this.counts = new HashMap<>(); + } + + private void addAll(List rects) { + for (Rect rect : rects) { + counts.merge(rect, 1, Integer::sum); + } + } + + private void removeAll(List rects) { + for (Rect rect : rects) { + counts.computeIfPresent(rect, (ignored, count) -> count == 1 ? null : count - 1); + } + } + + private List toList() { + return new ArrayList<>(counts.keySet()); + } + } + + private record Point(double x, double y, double z) { + private Vec3 toVec3() { + return new Vec3(x, y, z); + } + } + + private record AxisGrid( + double[] uCoordinates, + double[] vCoordinates, + Map uIndex, + Map vIndex + ) { + private static AxisGrid create(List lower, List upper) { + TreeSet uCoordinates = new TreeSet<>(); + TreeSet vCoordinates = new TreeSet<>(); + addCoordinates(lower, uCoordinates, vCoordinates); + addCoordinates(upper, uCoordinates, vCoordinates); + double[] uArray = toArray(uCoordinates); + double[] vArray = toArray(vCoordinates); + return new AxisGrid(uArray, vArray, index(uArray), index(vArray)); + } + + private int uSize() { + return uCoordinates.length - 1; + } + + private int vSize() { + return vCoordinates.length - 1; + } + + private static void addCoordinates(List rects, Set uCoordinates, Set vCoordinates) { + for (Rect rect : rects) { + uCoordinates.add(rect.minU); + uCoordinates.add(rect.maxU); + vCoordinates.add(rect.minV); + vCoordinates.add(rect.maxV); + } + } + + private static double[] toArray(TreeSet coordinates) { + double[] result = new double[coordinates.size()]; + int index = 0; + for (double coordinate : coordinates) { + result[index++] = coordinate; + } + return result; + } + + private static Map index(double[] coordinates) { + Map result = new HashMap<>(); + for (int i = 0; i < coordinates.length; i++) { + result.put(coordinates[i], i); + } + return result; + } + } + + private static final class EdgeAccumulator { + private final Set segments = new HashSet<>(); + + private void add(Point first, Point second) { + if (first.equals(second)) { + return; + } + segments.add(Segment.create(first, second)); + } + + private void addAll(EdgeAccumulator other) { + segments.addAll(other.segments); + } + + private List toLines() { + Map> intervals = new HashMap<>(); + for (Segment segment : segments) { + intervals.computeIfAbsent(segment.key(), ignored -> new ArrayList<>()).add(segment.interval()); + } + + List result = new ArrayList<>(); + for (Map.Entry> entry : intervals.entrySet()) { + LineKey key = entry.getKey(); + List lineIntervals = entry.getValue(); + lineIntervals.sort(Comparator.comparingDouble(Interval::min).thenComparingDouble(Interval::max)); + + double min = lineIntervals.getFirst().min; + double max = lineIntervals.getFirst().max; + for (int i = 1; i < lineIntervals.size(); i++) { + Interval interval = lineIntervals.get(i); + if (interval.min <= max) { + max = Math.max(max, interval.max); + } else { + result.add(key.toLine(min, max)); + min = interval.min; + max = interval.max; + } + } + result.add(key.toLine(min, max)); + } + return result; + } + } + + private record Segment(Point from, Point to) { + private static Segment create(Point first, Point second) { + int axis = varyingAxis(first, second); + if (coordinate(first, axis) <= coordinate(second, axis)) { + return new Segment(first, second); + } + return new Segment(second, first); + } + + private LineKey key() { + int axis = varyingAxis(from, to); + return new LineKey(axis, coordinate(from, fixedAxis(axis, 0)), coordinate(from, fixedAxis(axis, 1))); + } + + private Interval interval() { + int axis = varyingAxis(from, to); + return new Interval(coordinate(from, axis), coordinate(to, axis)); + } + + private static int varyingAxis(Point from, Point to) { + if (from.x != to.x) return 0; + if (from.y != to.y) return 1; + if (from.z != to.z) return 2; + throw new IllegalArgumentException("Zero-length segment"); + } + } + + private record LineKey(int axis, double firstFixed, double secondFixed) { + private Line toLine(double min, double max) { + double[] from = new double[3]; + double[] to = new double[3]; + from[axis] = min; + to[axis] = max; + from[fixedAxis(axis, 0)] = firstFixed; + to[fixedAxis(axis, 0)] = firstFixed; + from[fixedAxis(axis, 1)] = secondFixed; + to[fixedAxis(axis, 1)] = secondFixed; + return new Line(new Point(from[0], from[1], from[2]).toVec3(), new Point(to[0], to[1], to[2]).toVec3()); + } + } + + private record Interval(double min, double max) { + } + + private static int fixedAxis(int varyingAxis, int fixedIndex) { + return switch (varyingAxis) { + case 0 -> fixedIndex == 0 ? 1 : 2; + case 1 -> fixedIndex == 0 ? 0 : 2; + case 2 -> fixedIndex == 0 ? 0 : 1; + default -> throw new IllegalArgumentException("Unknown axis: " + varyingAxis); + }; + } + + private static double coordinate(Point point, int axis) { + return switch (axis) { + case 0 -> point.x; + case 1 -> point.y; + case 2 -> point.z; + default -> throw new IllegalArgumentException("Unknown axis: " + axis); + }; + } +} diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/client/Line.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/client/Line.java new file mode 100644 index 00000000..7ee577e3 --- /dev/null +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/client/Line.java @@ -0,0 +1,47 @@ +package dev.anvilcraft.lib.v2.util.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.world.phys.Vec3; + +public record Line(Vec3 start, Vec3 end, int thickness, float length) { + public Line(Vec3 start, Vec3 end, float length) { + this(start, end, 4, length); + } + + public Line(Vec3 start, Vec3 end, int thickness) { + this(start, end, thickness, (float) start.distanceTo(end)); + } + + public Line(Vec3 start, Vec3 end) { + this(start, end, (float) start.distanceTo(end)); + } + + @Deprecated(forRemoval = true) + public void render(PoseStack pose, VertexConsumer vertex, Vec3 camera, int color) { + this.render(pose.last(), vertex, camera, color); + } + + public void render(PoseStack.Pose pose, VertexConsumer vertex, Vec3 camera, int color) { + float dx = (float) (this.start().x - this.end().x); + float dy = (float) (this.start().y - this.end().y); + float dz = (float) (this.start().z - this.end().z); + // 1.21.1 的 VertexConsumer 无 setLineWidth(1.21.2 渲染管线改造后引入),线宽忽略 + vertex.addVertex( + pose.pose(), + (float) (this.start().x - camera.x), + (float) (this.start().y - camera.y), + (float) (this.start().z - camera.z) + ) + .setColor(color) + .setNormal(pose, dx /= this.length(), dy /= this.length(), dz /= this.length()); + vertex.addVertex( + pose.pose(), + (float) (this.end().x - camera.x), + (float) (this.end().y - camera.y), + (float) (this.end().z - camera.z) + ) + .setColor(color) + .setNormal(pose, dx, dy, dz); + } +} diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java index e855f630..4f93262e 100644 --- a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java @@ -6,11 +6,12 @@ import dev.anvilcraft.lib.v2.util.AnvilLibUtil; import dev.anvilcraft.lib.v2.util.Util; import io.netty.buffer.ByteBuf; +import lombok.EqualsAndHashCode; import lombok.Getter; import net.minecraft.core.Holder; +import net.minecraft.core.HolderGetter; import net.minecraft.core.HolderSet; import net.minecraft.core.RegistryCodecs; -import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.RegistryFriendlyByteBuf; @@ -46,6 +47,7 @@ *

*/ @Getter +@EqualsAndHashCode public class BlockStatePredicate { /** * 空 ResourceLocation @@ -315,11 +317,12 @@ public Builder of(Collection blocks) { /** * 设置方块标签 * - * @param tag 方块标签 + * @param blocks 方块注册表访问器 + * @param tag 方块标签 * @return 构建器实例 */ - public Builder of(TagKey tag) { - this.blocks = BuiltInRegistries.BLOCK.getOrCreateTag(tag); + public Builder of(HolderGetter blocks, TagKey tag) { + this.blocks = blocks.getOrThrow(tag); return this; } diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemIngredientPredicate.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemIngredientPredicate.java index 516ee912..c6edd2c6 100644 --- a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemIngredientPredicate.java +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemIngredientPredicate.java @@ -7,12 +7,12 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import net.minecraft.advancements.critereon.ItemSubPredicate; +import net.minecraft.core.HolderGetter; import net.minecraft.core.HolderSet; import net.minecraft.core.RegistryCodecs; import net.minecraft.core.component.DataComponentPredicate; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.component.TypedDataComponent; -import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Optional; +import java.util.function.Function; /** * 物品原料谓词 @@ -73,11 +74,22 @@ public static Builder of(ItemLike... items) { /** * 创建一个标签构建器 * - * @param tag 物品标签 + * @param items 物品注册表访问器 + * @param tag 物品标签 * @return 构建器实例 */ - public static Builder of(TagKey tag) { - return new Builder().of(tag); + public static Builder of(HolderGetter items, TagKey tag) { + return new Builder().of(items, tag); + } + + /** + * 返回一个指定数量的新实例,不修改原实例。 + * + * @param count 数量 + * @return 新实例 + */ + public ItemIngredientPredicate withCount(int count) { + return new ItemIngredientPredicate(this.items, count, this.components, this.subPredicates); } @Override @@ -163,11 +175,12 @@ public Builder of(ItemLike... items) { /** * 设置物品标签 * - * @param tag 物品标签 + * @param items 物品注册表访问器 + * @param tag 物品标签 * @return 构建器实例 */ - public Builder of(TagKey tag) { - this.items = Optional.of(BuiltInRegistries.ITEM.getOrCreateTag(tag)); + public Builder of(HolderGetter items, TagKey tag) { + this.items = items.get(tag).map(Function.identity()); return this; } diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java index a6c91707..237ad20d 100644 --- a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java @@ -6,10 +6,10 @@ import dev.anvilcraft.lib.v2.codec.StreamCodecUtil; import net.minecraft.advancements.critereon.ItemSubPredicate; import net.minecraft.advancements.critereon.MinMaxBounds; +import net.minecraft.core.HolderGetter; import net.minecraft.core.HolderSet; import net.minecraft.core.RegistryCodecs; import net.minecraft.core.component.DataComponentPredicate; -import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; @@ -20,6 +20,7 @@ import java.util.Map; import java.util.Optional; +import java.util.function.Function; /** * 物品谓词 @@ -117,11 +118,12 @@ public Builder of(ItemLike... items) { /** * 设置物品标签 * - * @param tag 物品标签 + * @param items 物品注册表访问器 + * @param tag 物品标签 * @return 构建器实例 */ - public Builder of(TagKey tag) { - this.items = Optional.of(BuiltInRegistries.ITEM.getOrCreateTag(tag)); + public Builder of(HolderGetter items, TagKey tag) { + this.items = items.get(tag).map(Function.identity()); return this; } diff --git a/module.util/src/test/java/dev/anvilcraft/lib/v2/util/ShapeUtilJoinTimingTest.java b/module.util/src/test/java/dev/anvilcraft/lib/v2/util/ShapeUtilJoinTimingTest.java new file mode 100644 index 00000000..f10b52f3 --- /dev/null +++ b/module.util/src/test/java/dev/anvilcraft/lib/v2/util/ShapeUtilJoinTimingTest.java @@ -0,0 +1,232 @@ +package dev.anvilcraft.lib.v2.util; + +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import dev.anvilcraft.lib.v2.util.client.Line; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; + +public final class ShapeUtilJoinTimingTest { + private static final int AABB_COUNT = 500; + private static final long RANDOM_SEED = 0x5A5A5A5AL; + + private ShapeUtilJoinTimingTest() { + } + + public static void main(String[] args) throws ExecutionException, InterruptedException { + assertOutlineMatchesShapePath(List.of(new AABB(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)), "single box"); + assertOutlineMatchesShapePath( + List.of( + new AABB(0.0, 0.0, 0.0, 1.0, 1.0, 1.0), + new AABB(0.5, 0.5, 0.0, 1.5, 1.5, 1.0) + ), + "overlapping boxes" + ); + assertOutlineMatchesShapePath( + List.of( + new AABB(0.0, 0.0, 0.0, 1.0, 1.0, 1.0), + new AABB(1.0, 0.0, 0.0, 2.0, 1.0, 1.0) + ), + "face-adjacent boxes" + ); + assertOutlineMatchesShapePath( + List.of( + new AABB(0.0, 0.0, 0.0, 2.0, 2.0, 2.0), + new AABB(0.5, 0.5, 0.5, 1.5, 1.5, 1.5) + ), + "contained box" + ); + assertOutlineMatchesShapePath( + List.of( + new AABB(0.0, 0.0, 0.0, 1.0, 1.0, 1.0), + new AABB(2.0, 0.0, 0.0, 3.0, 1.0, 1.0) + ), + "disconnected boxes" + ); + assertOutlineMatchesShapePath(createConnectedDiagonalAabbs(24, RANDOM_SEED), "connected diagonal chain"); + assertParallelMatchesSequential(createConnectedDiagonalAabbs(64, RANDOM_SEED)); + assertCustomExecutorParallelMatchesSequential(createConnectedDiagonalAabbs(64, RANDOM_SEED)); + + List boxes = createConnectedDiagonalAabbs(AABB_COUNT, RANDOM_SEED); + JoinTimingResult shapeResult = joinAndExtractOutlineByShape(boxes); + OutlineTimingResult aabbResult = extractOutlineByAabbs(boxes); + + assertEquals(AABB_COUNT, boxes.size(), "generated AABB count"); + assertFalse(shapeResult.joined().isEmpty(), "joined shape should not be empty"); + assertNotNull(shapeResult.outline(), "shape outline result"); + assertNotNull(aabbResult.outline(), "AABB outline result"); + assertSameLines(shapeResult.outline(), aabbResult.outline(), "benchmark outline"); + } + + private static void assertOutlineMatchesShapePath(List boxes, String label) + throws ExecutionException, InterruptedException { + JoinTimingResult shapeResult = joinAndExtractOutlineByShape(boxes); + OutlineTimingResult aabbResult = extractOutlineByAabbs(boxes); + assertSameLines(shapeResult.outline(), aabbResult.outline(), label); + } + + private static void assertParallelMatchesSequential(List boxes) { + String propertyName = "anvillib.outline.parallelThreshold"; + String previous = System.getProperty(propertyName); + try { + System.setProperty(propertyName, Integer.toString(Integer.MAX_VALUE)); + List sequential = OutlineUtil.extractOutline(boxes); + System.setProperty(propertyName, "1"); + List parallel = OutlineUtil.extractOutline(boxes); + assertEquals(sequential, parallel, "parallel outline branch"); + } finally { + if (previous == null) { + System.clearProperty(propertyName); + } else { + System.setProperty(propertyName, previous); + } + } + } + + private static void assertCustomExecutorParallelMatchesSequential(List boxes) { + String propertyName = "anvillib.outline.parallelThreshold"; + String previous = System.getProperty(propertyName); + int[] executions = {0}; + Executor countingExecutor = command -> { + executions[0]++; + command.run(); + }; + try { + System.setProperty(propertyName, Integer.toString(Integer.MAX_VALUE)); + List sequential = OutlineUtil.extractOutline(boxes); + System.setProperty(propertyName, "1"); + List parallel = OutlineUtil.extractOutline(boxes, countingExecutor); + if (executions[0] <= 0) { + throw new AssertionError("custom executor should run parallel outline tasks"); + } + assertEquals(sequential, parallel, "custom executor parallel outline branch"); + } finally { + if (previous == null) { + System.clearProperty(propertyName); + } else { + System.setProperty(propertyName, previous); + } + } + } + + private static JoinTimingResult joinAndExtractOutlineByShape(List boxes) throws ExecutionException, InterruptedException { + List shapes = boxes.stream() + .map(box -> Shapes.box(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) + .toList(); + long started = System.nanoTime(); + VoxelShape result; + try (var executor = Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 1))) { + result = ShapeUtil.threadedJoin(shapes, BooleanOp.OR, executor).get(); + } + List outline = new ArrayList<>(); + result.forAllEdges( + (x1, y1, z1, x2, y2, z2) -> outline.add( + new Line(new Vec3(x1, y1, z1), new Vec3(x2, y2, z2)) + ) + ); + long elapsedNanos = System.nanoTime() - started; + System.out.printf( + "Shape path: thread-joined %d connected AABBs into %d lines in %.3f ms.%n", + boxes.size(), + outline.size(), + elapsedNanos / 1_000_000.0 + ); + return new JoinTimingResult(result, outline); + } + + private static OutlineTimingResult extractOutlineByAabbs(List boxes) { + long started = System.nanoTime(); + List outline = OutlineUtil.extractOutline(boxes); + long elapsedNanos = System.nanoTime() - started; + System.out.printf( + "AABB path: extracted %d connected AABBs into %d lines in %.3f ms.%n", + boxes.size(), + outline.size(), + elapsedNanos / 1_000_000.0 + ); + return new OutlineTimingResult(outline); + } + + private static List createConnectedDiagonalAabbs(int count, long seed) { + Random random = new Random(seed); + List boxes = new ArrayList<>(count); + double x = 0.0; + double y = 0.0; + double size = 1.0; + boxes.add(new AABB(x, y, 0.0, x + size, y + size, 1.0)); + + for (int i = 1; i < count; i++) { + double nextSize = 0.7 + random.nextDouble() * 0.6; + double step = Math.min(size, nextSize) * (0.45 + random.nextDouble() * 0.1); + x += step; + y += step; + boxes.add(new AABB(x, y, 0.0, x + nextSize, y + nextSize, 1.0)); + size = nextSize; + } + return boxes; + } + + private static void assertEquals(int expected, int actual, String label) { + if (expected != actual) { + throw new AssertionError(label + ": expected " + expected + ", got " + actual); + } + } + + private static void assertEquals(List expected, List actual, String label) { + if (!expected.equals(actual)) { + List missing = new ArrayList<>(expected); + missing.removeAll(actual); + List extra = new ArrayList<>(actual); + extra.removeAll(expected); + throw new AssertionError( + label + ": expected " + expected.size() + " lines, got " + actual.size() + + ", missing=" + missing + + ", extra=" + extra + ); + } + } + + private static void assertSameLines(List expected, List actual, String label) { + Set expectedSet = new HashSet<>(expected); + Set actualSet = new HashSet<>(actual); + if (!expectedSet.equals(actualSet)) { + Set missing = new HashSet<>(expectedSet); + missing.removeAll(actualSet); + Set extra = new HashSet<>(actualSet); + extra.removeAll(expectedSet); + throw new AssertionError( + label + ": expected " + expectedSet.size() + " lines, got " + actualSet.size() + + ", missing=" + missing + + ", extra=" + extra + ); + } + } + + private static void assertFalse(boolean value, String label) { + if (value) { + throw new AssertionError(label); + } + } + + private static void assertNotNull(Object value, String label) { + if (value == null) { + throw new AssertionError(label + " should not be null"); + } + } + + private record JoinTimingResult(VoxelShape joined, List outline) { + } + + private record OutlineTimingResult(List outline) { + } +} From 1706feaaf736cc3956bb74a245b9236f8cf54500 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 11 Aug 2026 00:28:44 +0800 Subject: [PATCH 13/38] =?UTF-8?q?feat(space-select):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=E9=80=89=E5=8C=BA=E6=A1=86=E9=80=89=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=20module.space-select?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 dev/26.1 的 module.space-select(1335fc5、e75b10f)整体回移植,作为新模块 anvillib-space-select-neoforge-1.21.1,依赖 anvillib-network-neoforge-1.21.1, 实现区域框选/线框渲染/滚轮缩放。 API 适配(26.1 → 1.21.1): - Identifier → ResourceLocation - ARGB(26.1 新类)→ 手写 (a<<24)|(r<<16)|(g<<8)|b int 打包 - Inventory.getSelectedSlot()(26.1)→ 公开字段 Inventory.selected - ClientPacketDistributor(26.1 新增)→ PacketDistributor.sendToServer - RenderLevelStageEvent 子类(AfterTranslucentParticles)→ 单一事件 + getStage() - ShapeRenderer(1.21.1 不存在)→ LevelRenderer.renderVoxelShape/renderLineBox - RenderTypes.lines()(26.1 类)→ RenderType.lines() - Minecraft.hasControlDown/hasAltDown → Screen.hasControlDown/hasAltDown - Direction.getApproximateNearest → getNearest(double,double,double) - 相机位置取 event.getCamera().getPosition(),线宽用默认 --- module.space-select/build.gradle | 277 ++++++++++++++++++ module.space-select/gradle.properties | 4 + .../v2/space_select/AnvilLibSpaceSelect.java | 18 ++ .../lib/v2/space_select/District.java | 163 +++++++++++ .../lib/v2/space_select/DistrictManager.java | 33 +++ .../lib/v2/space_select/SpaceSelectItem.java | 40 +++ .../client/AnvilLibSpaceSelectClient.java | 15 + .../client/ClientDistrictManager.java | 87 ++++++ .../space_select/client/DistrictRenderer.java | 63 ++++ .../client/SpaceSelectScrollHandler.java | 69 +++++ .../v2/space_select/client/package-info.java | 4 + .../event/PlayerCreateDistrictEvent.java | 21 ++ .../PlayerCreateDistrictEventHandler.java | 20 ++ .../event/handler/package-info.java | 4 + .../v2/space_select/event/package-info.java | 4 + .../network/AnvilLibSpaceSelectNetwork.java | 21 ++ .../v2/space_select/network/package-info.java | 4 + .../network/payload/SpaceSelectPayload.java | 47 +++ .../network/payload/package-info.java | 6 + .../lib/v2/space_select/package-info.java | 4 + .../resources/META-INF/neoforge.mods.toml | 86 ++++++ .../anvillib_space_select.mixins.json | 14 + .../src/main/resources/icon.png | Bin 0 -> 1089 bytes settings.gradle | 6 + 24 files changed, 1010 insertions(+) create mode 100644 module.space-select/build.gradle create mode 100644 module.space-select/gradle.properties create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/AnvilLibSpaceSelect.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/District.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/DistrictManager.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/SpaceSelectItem.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/AnvilLibSpaceSelectClient.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/ClientDistrictManager.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/DistrictRenderer.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/SpaceSelectScrollHandler.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/package-info.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/PlayerCreateDistrictEvent.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/handler/PlayerCreateDistrictEventHandler.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/handler/package-info.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/package-info.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/AnvilLibSpaceSelectNetwork.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/package-info.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/payload/SpaceSelectPayload.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/payload/package-info.java create mode 100644 module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/package-info.java create mode 100644 module.space-select/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 module.space-select/src/main/resources/anvillib_space_select.mixins.json create mode 100644 module.space-select/src/main/resources/icon.png diff --git a/module.space-select/build.gradle b/module.space-select/build.gradle new file mode 100644 index 00000000..a8457d91 --- /dev/null +++ b/module.space-select/build.gradle @@ -0,0 +1,277 @@ +plugins { + id 'java-library' + id 'eclipse' + id 'idea' + id 'maven-publish' + alias libs.plugins.modDevGradle + alias libs.plugins.lombok + alias libs.plugins.machete + id 'org.jreleaser' version '1.23.0' + id "me.modmuss50.mod-publish-plugin" version "1.1.0" +} + +String buildType = 'snapshot' +String buildNumber +if (System.getenv("CI_BUILD") == 'false') buildNumber = null +else { + if (System.getenv("PR_BUILD") != 'false') buildType = 'pr' + buildNumber = System.getenv("GITHUB_RUN_NUMBER") +} +version = "${mod_version}" + (buildNumber != null ? "+${buildType}.${buildNumber}" : "") +group = mod_group_id + +base { + archivesName = "${project.mod_id}-neoforge-${libs.versions.minecraft.get()}" +} + +repositories { + maven { url = "https://server.cjsah.net:1002/maven/" } + mavenLocal() + mavenCentral() +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +neoForge { + // Specify the version of NeoForge to use. + version = libs.versions.neoForge.get() + + parchment { + mappingsVersion = project.parchment_mappings_version + minecraftVersion = project.parchment_minecraft_version + } + + // This line is optional. Access Transformers are automatically detected + // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') + + interfaceInjectionData { + // from 'src/main/resources/interface_injections.json' + // publish file('src/main/resources/interface_injections.json') + } + + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + client { + client() + + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + server { + server() + programArgument '--nogui' + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + // This run config launches GameTestServer and runs all registered gametests, then exits. + // By default, the server will crash when no gametests are provided. + // The gametest system is also enabled by default for other run configs under the /test command. + gameTestServer { + type = "gameTestServer" + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + data { + data() + + // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it + // gameDirectory = project.file('run-data') + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } + + // applies to all the run configs above + configureEach { + // Recommended logging data for a userdev environment + // The markers can be added/remove as needed separated by commas. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + systemProperty 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + logLevel = org.slf4j.event.Level.DEBUG + } + } + + mods { + // define mod <-> source bindings + // these are used to tell the game which sources are for which mod + // mostly optional in a single mod project + // but multi mod projects should define one per mod + "${mod_id}" { + sourceSet(sourceSets.main) + } + } +} + +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } + + +dependencies { + compileOnly "org.jspecify:jspecify:1.0.0" + + if (System.getenv("NOT_DEV") == 'true') { + jarJar(api("dev.anvilcraft.lib:anvillib-network-neoforge-1.21.1:latest.release")) + } else { + jarJar(implementation project(":anvillib-network-neoforge-1.21.1")) + } +} + +// This block of code expands all declared replace properties in the specified resource targets. +// A missing property will result in an error. Properties are expanded using ${} Groovy notation. +// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. +// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html +tasks.withType(ProcessResources).configureEach { + var replaceProperties = [minecraft_version : libs.versions.minecraft.get(), + minecraft_version_range: minecraft_version_range, + neo_version : libs.versions.neoForge.get(), + neo_version_range : neo_version_range, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : version, + mod_authors : mod_authors, + mod_description : mod_description] + inputs.properties replaceProperties + + filesMatching(['META-INF/neoforge.mods.toml']) { + expand replaceProperties + } +} + +tasks.register('sourcesJar', Jar) { + from delombok.outputs.files + dependsOn delombok + archiveClassifier = "sources" +} + +tasks.register('javadocJar', Jar) { + from javadoc.destinationDir + dependsOn javadoc + archiveClassifier = "javadoc" +} + +javadoc { + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') +} + +jar { + from(rootProject.file("LICENSE")) + from(rootProject.file("ASSETS_LICENSE")) + from(rootProject.file("README.md")) + from(rootProject.file("README.en.md")) +} + +artifacts { + archives tasks.jar + archives tasks.sourcesJar + archives tasks.javadocJar +} + +def this_name = base.archivesName.get() +def this_description = mod_description + +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + + artifact sourcesJar + artifact javadocJar + + versionMapping { + allVariants { + fromResolutionResult() + } + } + + pom { + name = this_name + description = this_description + url = 'https://github.com/Anvil-Dev/AnvilLib' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/license/mit' + } + } + developers { + developer { + id = 'Anvil-Dev' + name = 'AnvilCraft Dev' + email = 'admin@anvilcraft.dev' + } + } + scm { + url = 'https://github.com/Anvil-Dev/AnvilLib' + connection = 'https://github.com/Anvil-Dev/AnvilLib.git' + developerConnection = 'https://github.com/Anvil-Dev/AnvilLib.git' + } + } + } + } + repositories { + // Add repositories to publish to here. + def MAVEN_URL = System.getenv("MAVEN_URL") + if (MAVEN_URL != null) { + maven { + url MAVEN_URL + credentials { + username System.getenv("MAVEN_USERNAME") + password System.getenv("MAVEN_PASSWORD") + } + } + } + mavenLocal() + maven { + name = "staging" + url = layout.buildDirectory.dir("staging-deploy") + } + } +} + +jreleaser { + signing { + active = 'ALWAYS' + armored = true + } + + deploy { + maven { + mavenCentral { + sonatype { + active = 'ALWAYS' + url = 'https://central.sonatype.com/api/v1/publisher' + stagingRepository('build/staging-deploy') + } + } + } + } +} + +// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} + +machete { + // disable machete locally for faster builds + enabled = false +} + +lombok { + version = "1.18.34" +} diff --git a/module.space-select/gradle.properties b/module.space-select/gradle.properties new file mode 100644 index 00000000..74e9f41b --- /dev/null +++ b/module.space-select/gradle.properties @@ -0,0 +1,4 @@ +## Mod Properties +mod_id=anvillib_space_select +mod_name=AnvilLib-Space-Select +mod_description=Space selection utilities diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/AnvilLibSpaceSelect.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/AnvilLibSpaceSelect.java new file mode 100644 index 00000000..abb491d5 --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/AnvilLibSpaceSelect.java @@ -0,0 +1,18 @@ +package dev.anvilcraft.lib.v2.space_select; + +import net.minecraft.resources.ResourceLocation; +import net.neoforged.fml.common.Mod; +import org.jetbrains.annotations.ApiStatus; + +@Mod(AnvilLibSpaceSelect.MOD_ID) +public class AnvilLibSpaceSelect { + @ApiStatus.Internal + public AnvilLibSpaceSelect() { + } + + public static final String MOD_ID = "anvillib_space_select"; + + public static ResourceLocation of(String path) { + return ResourceLocation.fromNamespaceAndPath(AnvilLibSpaceSelect.MOD_ID, path); + } +} diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/District.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/District.java new file mode 100644 index 00000000..82866929 --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/District.java @@ -0,0 +1,163 @@ +package dev.anvilcraft.lib.v2.space_select; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; + +import java.util.Random; + +public record District( + BlockPos.MutableBlockPos start, + BlockPos.MutableBlockPos end +) { + public static District create(BlockPos start, BlockPos end) { + BlockPos.MutableBlockPos _start = new BlockPos.MutableBlockPos( + Math.min(start.getX(), end.getX()), + Math.min(start.getY(), end.getY()), + Math.min(start.getZ(), end.getZ()) + ); + BlockPos.MutableBlockPos _end = new BlockPos.MutableBlockPos( + Math.max(start.getX(), end.getX()), + Math.max(start.getY(), end.getY()), + Math.max(start.getZ(), end.getZ()) + ); + return new District(_start, _end); + } + + public void expand(Direction direction, int size) { + int stepX = direction.getStepX() * size; + int stepY = direction.getStepY() * size; + int stepZ = direction.getStepZ() * size; + if (stepX < 0) { + this.start().setX(this.start().getX() + stepX); + } else { + this.end().setX(this.end().getX() + stepX); + } + if (stepY < 0) { + this.start().setY(this.start().getY() + stepY); + } else { + this.end().setY(this.end().getY() + stepY); + } + if (stepZ < 0) { + this.start().setZ(this.start().getZ() + stepZ); + } else { + this.end().setZ(this.end().getZ() + stepZ); + } + } + + public void contraction(Direction direction, int size) { + int stepX = direction.getStepX() * size; + int stepY = direction.getStepY() * size; + int stepZ = direction.getStepZ() * size; + if (stepX < 0) { + this.start().setX(this.start().getX() - stepX); + } else { + this.end().setX(this.end().getX() - stepX); + } + if (stepY < 0) { + this.start().setY(this.start().getY() - stepY); + } else { + this.end().setY(this.end().getY() - stepY); + } + if (stepZ < 0) { + this.start().setZ(this.start().getZ() - stepZ); + } else { + this.end().setZ(this.end().getZ() - stepZ); + } + } + + public void move(Direction direction, int step) { + int stepX = direction.getStepX() * step; + int stepY = direction.getStepY() * step; + int stepZ = direction.getStepZ() * step; + this.start().set( + this.start().getX() + stepX, + this.start().getY() + stepY, + this.start().getZ() + stepZ + ); + this.end().set( + this.end().getX() + stepX, + this.end().getY() + stepY, + this.end().getZ() + stepZ + ); + } + + public boolean contains(double x, double y, double z) { + return x >= this.start.getX() && x <= this.end.getX() + && y >= this.start.getY() && y <= this.end.getY() + && z >= this.start.getZ() && z <= this.end.getZ(); + } + + public static Direction.Axis getPrimaryAxis(Vec3 lookAngle) { + double absX = Math.abs(lookAngle.x); + double absY = Math.abs(lookAngle.y); + double absZ = Math.abs(lookAngle.z); + if (absX >= absY && absX >= absZ) return Direction.Axis.X; + if (absY >= absX && absY >= absZ) return Direction.Axis.Y; + return Direction.Axis.Z; + } + + public void scaleOnAxis(Direction.Axis axis, int scrollAmount, Vec3 playerPos, AABB boundingBox, Vec3 lookAngle) { + double playerCoord = axis.choose(playerPos.x, playerPos.y, playerPos.z); + double minPlayerCoord = axis.choose(boundingBox.minX, boundingBox.minY, boundingBox.minZ); + double maxPlayerCoord = axis.choose(boundingBox.maxX, boundingBox.maxY, boundingBox.maxZ); + double minCoord = axis.choose(this.start.getX(), this.start.getY(), this.start.getZ()); + double maxCoord = axis.choose(this.end.getX(), this.end.getY(), this.end.getZ()); + double lookComp = axis.choose(lookAngle.x, lookAngle.y, lookAngle.z); + + boolean inside = maxPlayerCoord >= minCoord && minPlayerCoord <= maxCoord + 1; + + int faceSign; + if (inside) { + faceSign = lookComp > 0 ? 1 : -1; + } else { + double distToMin = Math.abs(playerCoord - minCoord); + double distToMax = Math.abs(playerCoord - maxCoord + 1); + faceSign = distToMin < distToMax ? -1 : 1; + } + + int delta = (lookComp > 0 ? 1 : -1) * scrollAmount; + + if (faceSign > 0) { + int newEnd = (int) maxCoord + delta; + if (newEnd >= (int) minCoord) { + setAxisCoord(this.end, axis, newEnd); + } + } else { + int newStart = (int) minCoord + delta; + if (newStart <= (int) maxCoord) { + setAxisCoord(this.start, axis, newStart); + } + } + } + + private static void setAxisCoord(BlockPos.MutableBlockPos pos, Direction.Axis axis, int value) { + switch (axis) { + case X -> pos.setX(value); + case Y -> pos.setY(value); + case Z -> pos.setZ(value); + } + } + + public VoxelShape shape() { + return Shapes.create( + 0, + 0, + 0, + this.end().getX() - this.start().getX() + 1, + this.end().getY() - this.start().getY() + 1, + this.end().getZ() - this.start().getZ() + 1 + ); + } + + public int color() { + Random random = new Random(this.hashCode()); + int r = random.nextInt(256); + int g = random.nextInt(256); + int b = random.nextInt(256); + return (102 << 24) | (r << 16) | (g << 8) | b; + } +} diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/DistrictManager.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/DistrictManager.java new file mode 100644 index 00000000..f3f92eee --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/DistrictManager.java @@ -0,0 +1,33 @@ +package dev.anvilcraft.lib.v2.space_select; + +import lombok.Getter; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +@Getter +public class DistrictManager { + private final Map districtMap = new HashMap<>(); + + public void select(DistrictKey districtKey, District district) { + this.districtMap.put(districtKey, district); + } + + public void clear(DistrictKey districtKey) { + this.districtMap.remove(districtKey); + } + + public record DistrictKey(int slot, boolean offhand, Item item) { + public boolean check(@Nullable Player player) { + if (player == null) return false; + ItemStack offStack = player.getOffhandItem(); + if (this.offhand()) return offStack.is(this.item()) && offStack.getItem() instanceof SpaceSelectItem; + ItemStack mainStack = player.getInventory().getItem(this.slot()); + return mainStack.is(this.item()) && mainStack.getItem() instanceof SpaceSelectItem; + } + } +} diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/SpaceSelectItem.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/SpaceSelectItem.java new file mode 100644 index 00000000..ba1ea516 --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/SpaceSelectItem.java @@ -0,0 +1,40 @@ +package dev.anvilcraft.lib.v2.space_select; + +import dev.anvilcraft.lib.v2.space_select.client.AnvilLibSpaceSelectClient; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +public interface SpaceSelectItem { + default void select(Player player, BlockPos pos) { + DistrictManager.DistrictKey mainHand = new DistrictManager.DistrictKey( + player.getInventory().selected, + false, + player.getMainHandItem().getItem() + ); + DistrictManager.DistrictKey offHand = new DistrictManager.DistrictKey(-1, true, player.getOffhandItem().getItem()); + if (mainHand.check(player) && !AnvilLibSpaceSelectClient.MANAGER.isSelecting(mainHand)) { + AnvilLibSpaceSelectClient.MANAGER.startSelect(mainHand, pos); + } else if (mainHand.check(player)) { + AnvilLibSpaceSelectClient.MANAGER.endSelect(mainHand, pos); + } else if (offHand.check(player) && !AnvilLibSpaceSelectClient.MANAGER.isSelecting(offHand)) { + AnvilLibSpaceSelectClient.MANAGER.startSelect(offHand, pos); + } else if (offHand.check(player)) { + AnvilLibSpaceSelectClient.MANAGER.endSelect(offHand, pos); + } + } + + default void cancel(Player player) { + DistrictManager.DistrictKey mainHand = new DistrictManager.DistrictKey( + player.getInventory().selected, + false, + player.getMainHandItem().getItem() + ); + DistrictManager.DistrictKey offHand = new DistrictManager.DistrictKey(-1, true, player.getOffhandItem().getItem()); + AnvilLibSpaceSelectClient.MANAGER.clear(mainHand); + AnvilLibSpaceSelectClient.MANAGER.clear(offHand); + } + + default void onCreateDistrict(Player player, ItemStack itemStack, BlockPos start, BlockPos end) { + } +} diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/AnvilLibSpaceSelectClient.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/AnvilLibSpaceSelectClient.java new file mode 100644 index 00000000..c9bb4dec --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/AnvilLibSpaceSelectClient.java @@ -0,0 +1,15 @@ +package dev.anvilcraft.lib.v2.space_select.client; + +import dev.anvilcraft.lib.v2.space_select.AnvilLibSpaceSelect; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.common.Mod; +import org.jetbrains.annotations.ApiStatus; + +@Mod(value = AnvilLibSpaceSelect.MOD_ID, dist = Dist.CLIENT) +public class AnvilLibSpaceSelectClient { + @ApiStatus.Internal + public AnvilLibSpaceSelectClient() { + } + + public static final ClientDistrictManager MANAGER = new ClientDistrictManager(); +} diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/ClientDistrictManager.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/ClientDistrictManager.java new file mode 100644 index 00000000..90d4c503 --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/ClientDistrictManager.java @@ -0,0 +1,87 @@ +package dev.anvilcraft.lib.v2.space_select.client; + +import dev.anvilcraft.lib.v2.space_select.District; +import dev.anvilcraft.lib.v2.space_select.DistrictManager; +import dev.anvilcraft.lib.v2.space_select.SpaceSelectItem; +import dev.anvilcraft.lib.v2.space_select.network.payload.SpaceSelectPayload; +import lombok.Getter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.neoforged.neoforge.network.PacketDistributor; +import org.jspecify.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@Getter +public class ClientDistrictManager extends DistrictManager { + private final District tempDistrict = new District(new BlockPos.MutableBlockPos(), new BlockPos.MutableBlockPos()); + private final Map selectingMap = new HashMap<>(); + + public boolean isSelecting(DistrictKey districtKey) { + return this.selectingMap.containsKey(districtKey); + } + + public void startSelect(DistrictKey districtKey, BlockPos blockPos) { + this.selectingMap.put(districtKey, blockPos); + } + + public void endSelect(DistrictKey districtKey, BlockPos blockPos) { + BlockPos start = this.selectingMap.remove(districtKey); + if (start != null) { + this.select(districtKey, District.create(start, blockPos)); + SpaceSelectPayload payload = new SpaceSelectPayload( + !(Objects.requireNonNull(Minecraft.getInstance().player).getMainHandItem().getItem() instanceof SpaceSelectItem), + start, + blockPos + ); + PacketDistributor.sendToServer(payload); + } + } + + @Override + public void clear(DistrictKey districtKey) { + super.clear(districtKey); + this.selectingMap.remove(districtKey); + } + + public @Nullable District getTempDistrict() { + Minecraft minecraft = Minecraft.getInstance(); + LocalPlayer player = minecraft.player; + if (player == null) return null; + HitResult hitResult = minecraft.hitResult; + if (hitResult == null || hitResult.getType() != HitResult.Type.BLOCK) return null; + BlockPos hitPos = ((BlockHitResult) hitResult).getBlockPos(); + ItemStack mainHandItem = player.getMainHandItem(); + ItemStack offhandItem = player.getOffhandItem(); + if ( + !(mainHandItem.getItem() instanceof SpaceSelectItem) + && !(offhandItem.getItem() instanceof SpaceSelectItem) + ) { + return null; + } + BlockPos pos; + DistrictManager.DistrictKey mainHand = new DistrictManager.DistrictKey( + player.getInventory().selected, + false, + player.getMainHandItem().getItem() + ); + DistrictManager.DistrictKey offHand = new DistrictManager.DistrictKey(-1, true, player.getOffhandItem().getItem()); + if (this.isSelecting(mainHand)) { + pos = this.getSelectingMap().get(mainHand); + } else { + pos = this.getSelectingMap().get(offHand); + } + if (pos == null) return null; + this.tempDistrict.start() + .set(Math.min(pos.getX(), hitPos.getX()), Math.min(pos.getY(), hitPos.getY()), Math.min(pos.getZ(), hitPos.getZ())); + this.tempDistrict.end() + .set(Math.max(pos.getX(), hitPos.getX()), Math.max(pos.getY(), hitPos.getY()), Math.max(pos.getZ(), hitPos.getZ())); + return this.tempDistrict; + } +} diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/DistrictRenderer.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/DistrictRenderer.java new file mode 100644 index 00000000..20213a47 --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/DistrictRenderer.java @@ -0,0 +1,63 @@ +package dev.anvilcraft.lib.v2.space_select.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import dev.anvilcraft.lib.v2.space_select.AnvilLibSpaceSelect; +import dev.anvilcraft.lib.v2.space_select.District; +import net.minecraft.SharedConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.RenderLevelStageEvent; +import org.jetbrains.annotations.ApiStatus; + +import java.util.ArrayList; +import java.util.List; + +@EventBusSubscriber(modid = AnvilLibSpaceSelect.MOD_ID, value = Dist.CLIENT) +@ApiStatus.Internal +public class DistrictRenderer { + @SubscribeEvent + public static void addLevelRenderMainPass(RenderLevelStageEvent event) { + if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES) return; + PoseStack poseStack = event.getPoseStack(); + MultiBufferSource.BufferSource bufferSource = Minecraft.getInstance().renderBuffers().bufferSource(); + renderDistrict(bufferSource, poseStack, event.getCamera().getPosition()); + } + + public static void renderDistrict( + MultiBufferSource.BufferSource bufferSource, + PoseStack poseStack, + Vec3 cameraPos + ) { + List districts = new ArrayList<>(AnvilLibSpaceSelectClient.MANAGER.getDistrictMap().values()); + District tempDistrict = AnvilLibSpaceSelectClient.MANAGER.getTempDistrict(); + if (tempDistrict != null) districts.add(tempDistrict); + VertexConsumer buffer = bufferSource.getBuffer(RenderType.lines()); + for (District district : districts) { + BlockPos pos = district.start(); + double dx = (double) pos.getX() - cameraPos.x; + double dy = (double) pos.getY() - cameraPos.y; + double dz = (double) pos.getZ() - cameraPos.z; + if (SharedConstants.DEBUG_SHAPES) { + AABB box = district.shape().bounds().move(dx, dy, dz); + LevelRenderer.renderLineBox(poseStack, buffer, box, 1.0F, 1.0F, 1.0F, 1.0F); + } else { + int color = district.color(); + float r = (float) (color >> 16 & 0xFF) / 255.0F; + float g = (float) (color >> 8 & 0xFF) / 255.0F; + float b = (float) (color & 0xFF) / 255.0F; + float a = (float) (color >> 24 & 0xFF) / 255.0F; + LevelRenderer.renderVoxelShape(poseStack, buffer, district.shape(), dx, dy, dz, r, g, b, a, true); + } + } + bufferSource.endLastBatch(); + } +} diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/SpaceSelectScrollHandler.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/SpaceSelectScrollHandler.java new file mode 100644 index 00000000..c16fcdb5 --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/SpaceSelectScrollHandler.java @@ -0,0 +1,69 @@ +package dev.anvilcraft.lib.v2.space_select.client; + +import dev.anvilcraft.lib.v2.space_select.AnvilLibSpaceSelect; +import dev.anvilcraft.lib.v2.space_select.District; +import dev.anvilcraft.lib.v2.space_select.DistrictManager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.core.Direction; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.InputEvent; +import org.jetbrains.annotations.ApiStatus; + +@EventBusSubscriber(modid = AnvilLibSpaceSelect.MOD_ID, value = Dist.CLIENT) +@ApiStatus.Internal +public final class SpaceSelectScrollHandler { + + private SpaceSelectScrollHandler() { + } + + @SubscribeEvent + public static void onMouseScroll(InputEvent.MouseScrollingEvent event) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.screen != null) return; + + boolean ctrlDown = Screen.hasControlDown(); + boolean altDown = Screen.hasAltDown(); + if (!ctrlDown && !altDown) return; + + DistrictManager.DistrictKey mainHand = new DistrictManager.DistrictKey( + mc.player.getInventory().selected, + false, + mc.player.getMainHandItem().getItem() + ); + DistrictManager.DistrictKey offHand = new DistrictManager.DistrictKey(-1, true, mc.player.getOffhandItem().getItem()); + DistrictManager.DistrictKey districtKey; + if (mainHand.check(mc.player)) { + districtKey = mainHand; + } else if (offHand.check(mc.player)) { + districtKey = offHand; + } else { + return; + } + + District district = AnvilLibSpaceSelectClient.MANAGER.getDistrictMap().get(districtKey); + if (district == null) return; + + double scrollY = event.getScrollDeltaY(); + if (scrollY == 0) return; + int scrollAmount = (int) Math.signum(scrollY); + + Vec3 lookAngle = mc.player.getViewVector(1.0F); + Vec3 playerPos = mc.player.position(); + AABB boundingBox = mc.player.getBoundingBox(); + + if (ctrlDown) { + Direction.Axis axis = District.getPrimaryAxis(lookAngle); + district.scaleOnAxis(axis, scrollAmount, playerPos, boundingBox, lookAngle); + event.setCanceled(true); + } else { + Direction dir = Direction.getNearest(lookAngle.x, lookAngle.y, lookAngle.z); + district.move(dir, scrollAmount); + event.setCanceled(true); + } + } +} diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/package-info.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/package-info.java new file mode 100644 index 00000000..629a077e --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.space_select.client; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/PlayerCreateDistrictEvent.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/PlayerCreateDistrictEvent.java new file mode 100644 index 00000000..075de525 --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/PlayerCreateDistrictEvent.java @@ -0,0 +1,21 @@ +package dev.anvilcraft.lib.v2.space_select.event; + +import lombok.Getter; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; + +@Getter +public class PlayerCreateDistrictEvent extends PlayerEvent { + private final ItemStack itemStack; + private final BlockPos start; + private final BlockPos end; + + public PlayerCreateDistrictEvent(Player player, ItemStack itemStack, BlockPos start, BlockPos end) { + super(player); + this.itemStack = itemStack; + this.start = start; + this.end = end; + } +} diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/handler/PlayerCreateDistrictEventHandler.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/handler/PlayerCreateDistrictEventHandler.java new file mode 100644 index 00000000..d888d059 --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/handler/PlayerCreateDistrictEventHandler.java @@ -0,0 +1,20 @@ +package dev.anvilcraft.lib.v2.space_select.event.handler; + +import dev.anvilcraft.lib.v2.space_select.AnvilLibSpaceSelect; +import dev.anvilcraft.lib.v2.space_select.SpaceSelectItem; +import dev.anvilcraft.lib.v2.space_select.event.PlayerCreateDistrictEvent; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import org.jetbrains.annotations.ApiStatus; + +@EventBusSubscriber(modid = AnvilLibSpaceSelect.MOD_ID) +@ApiStatus.Internal +public class PlayerCreateDistrictEventHandler { + @SubscribeEvent + public static void onPlayerCreateDistrict(PlayerCreateDistrictEvent event) { + if (!(event.getItemStack().getItem() instanceof SpaceSelectItem item)) { + return; + } + item.onCreateDistrict(event.getEntity(), event.getItemStack(), event.getStart(), event.getEnd()); + } +} diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/handler/package-info.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/handler/package-info.java new file mode 100644 index 00000000..ecc5f1fa --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/handler/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.space_select.event.handler; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/package-info.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/package-info.java new file mode 100644 index 00000000..2554d1f3 --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.space_select.event; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/AnvilLibSpaceSelectNetwork.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/AnvilLibSpaceSelectNetwork.java new file mode 100644 index 00000000..c813bb0d --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/AnvilLibSpaceSelectNetwork.java @@ -0,0 +1,21 @@ +package dev.anvilcraft.lib.v2.space_select.network; + +import dev.anvilcraft.lib.v2.network.register.NetworkRegistrar; +import dev.anvilcraft.lib.v2.space_select.AnvilLibSpaceSelect; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import org.jetbrains.annotations.ApiStatus; + +@EventBusSubscriber(modid = AnvilLibSpaceSelect.MOD_ID) +@ApiStatus.Internal +public class AnvilLibSpaceSelectNetwork { + public static final String NETWORK_VERSION = "1"; + + @SubscribeEvent + public static void onPayloadRegister(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar(NETWORK_VERSION); + NetworkRegistrar.register(registrar, AnvilLibSpaceSelect.MOD_ID); + } +} diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/package-info.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/package-info.java new file mode 100644 index 00000000..db309f4b --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.space_select.network; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/payload/SpaceSelectPayload.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/payload/SpaceSelectPayload.java new file mode 100644 index 00000000..5a82f000 --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/payload/SpaceSelectPayload.java @@ -0,0 +1,47 @@ +package dev.anvilcraft.lib.v2.space_select.network.payload; + +import dev.anvilcraft.lib.v2.network.packet.IServerboundPacket; +import dev.anvilcraft.lib.v2.space_select.AnvilLibSpaceSelect; +import dev.anvilcraft.lib.v2.space_select.event.PlayerCreateDistrictEvent; +import io.netty.buffer.ByteBuf; +import net.minecraft.core.BlockPos; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.common.NeoForge; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public record SpaceSelectPayload( + boolean offhand, + BlockPos start, + BlockPos end +) implements IServerboundPacket { + public static final Type TYPE = new Type<>(AnvilLibSpaceSelect.of("space_select")); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.BOOL, + SpaceSelectPayload::offhand, + BlockPos.STREAM_CODEC, + SpaceSelectPayload::start, + BlockPos.STREAM_CODEC, + SpaceSelectPayload::end, + SpaceSelectPayload::new + ); + + @Override + public void handleOnServer(Player player) { + if (!(player instanceof ServerPlayer serverPlayer)) return; + NeoForge.EVENT_BUS.post(new PlayerCreateDistrictEvent( + serverPlayer, + this.offhand() ? serverPlayer.getOffhandItem() : serverPlayer.getMainHandItem(), + this.start(), + this.end() + )); + } + + @Override + public Type type() { + return SpaceSelectPayload.TYPE; + } +} diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/payload/package-info.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/payload/package-info.java new file mode 100644 index 00000000..db3afb53 --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/payload/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@Network +package dev.anvilcraft.lib.v2.space_select.network.payload; + +import dev.anvilcraft.lib.v2.network.register.Network; +import org.jspecify.annotations.NullMarked; diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/package-info.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/package-info.java new file mode 100644 index 00000000..1a2ec58b --- /dev/null +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.space_select; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/module.space-select/src/main/resources/META-INF/neoforge.mods.toml b/module.space-select/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..27f7ba75 --- /dev/null +++ b/module.space-select/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,86 @@ +# This is an example mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader = "javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. +loaderVersion = "${loader_version_range}" #mandatory +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license = "${mod_license}" +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory +# The modid of the mod +modId = "${mod_id}" #mandatory +# The version number of the mod +version = "${mod_version}" #mandatory +# A display name for the mod +displayName = "${mod_name}" #mandatory +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional +# A URL for the "homepage" for this mod, displayed in the mod UI +displayURL="https://github.com/Anvil-Dev/AnvilLib" +# A file name (in the root of the mod JAR) containing a logo for display +logoFile="icon.png" #optional +# A text field displayed in the mod UI +#credits="" #optional +# A text field displayed in the mod UI +authors = "${mod_authors}" #optional +# Display Test controls the display for your mod in the server connection screen +# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. +# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. +# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. +# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. +# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. +#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) + +# The description text for the mod (multi line!) (#mandatory) +description = '''${mod_description}''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +[[mixins]] +config = "${mod_id}.mixins.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies."${mod_id}"]] #optional +# the modid of the dependency +modId = "neoforge" #mandatory +# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). +# 'required' requires the mod to exist, 'optional' does not +# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning +type = "required" #mandatory +# Optional field describing why the dependency is required or why it is incompatible +# reason="..." +# The version range of the dependency +versionRange = "${neo_version_range}" #mandatory +# An ordering relationship for the dependency. +# BEFORE - This mod is loaded BEFORE the dependency +# AFTER - This mod is loaded AFTER the dependency +ordering = "NONE" +# Side this dependency is applied on - BOTH, CLIENT, or SERVER +side = "BOTH" +# Here's another dependency +[[dependencies."${mod_id}"]] +modId = "minecraft" +type = "required" +# This version range declares a minimum of the current minecraft version up to but not including the next major version +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features."${mod_id}"] +#openGLVersion="[3.2,)" diff --git a/module.space-select/src/main/resources/anvillib_space_select.mixins.json b/module.space-select/src/main/resources/anvillib_space_select.mixins.json new file mode 100644 index 00000000..bac42af7 --- /dev/null +++ b/module.space-select/src/main/resources/anvillib_space_select.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.anvilcraft.lib.v2.space_select.mixin", + "compatibilityLevel": "JAVA_8", + "refmap": "anvillib.refmap.json", + "mixins": [ + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/module.space-select/src/main/resources/icon.png b/module.space-select/src/main/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..15304bd54d5d97d8482049d5f16a7544748ce05a GIT binary patch literal 1089 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabRA=0U}gyL32}YB&qp?9cb}fRhL)ab z>**&8P1h&;i5V@+5z~{9lu=Mp{cG+rNOuP4jFK;J~t_xRpI&Nf4%?X zZmY?-7zkw=;bC zsjT>Uk~ZUn{GF#RHtMc+sGWYc$A>9-;igj6hzoLO7wEVa*QxB)zM=ff)bhV&`;h}b zD&$S4O;$O;Sf&`=w_-tNu&sr4nCukimfSM|=84-o^=H0sFgVR)oOSu;mLIn4lUH@E zPWkvN(C^lx3(G6wS;EsJIMe%PuP~i06!s*lLHg)A!3fTUY`(JJ860vaMb2DUI{zB$ z25}G0l9!&RrHovbwaBi{I35yXA*NOEX}{&%4{Sd|jrPVzaJhpb&(qb Date: Tue, 11 Aug 2026 00:29:12 +0800 Subject: [PATCH 14/38] =?UTF-8?q?feat(registrum):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=2014=20=E7=A7=8D=E6=96=B0=E6=B3=A8=E5=86=8C=E8=A1=A8?= =?UTF-8?q?=20builder/entry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回移植 dev/26.1 module.registrum 的注册表类型扩展(9b2d0e3 #31、9c89bfa #27、 9627358 #30、c307a4c #28、252922e #47):新增 builders/ 与 util/entry/ 下 39 个文件, AbstractRegistrum 增加 17 组注册入口(creativeTab/attachment/dataComponent/ biomeModifier/glm/structureModifier/condition/soundEvent/recipe/gameEvent/potion/ mobEffect/villager/poi/profession/self)。 API 适配(26.1 → 1.21.1): - CreativeTab/SoundEvent 仅 Identifier → ResourceLocation - AttachmentBuilder.serialize(MapCodec) → serialize(Codec)(26.1 才改 MapCodec) - DataComponentBuilder 删除 ignoreSwapAnimation()(1.21.2+ 才有) - RecipeSerializerBuilder:1.21.1 RecipeSerializer 为接口,createEntry 改匿名类 实现 codec()/streamCodec() - VillagerProfession 按 1.21.1 6 元 record(无 TradeSet 参数,第一参数 modid+name) - VillagerTypeBuilder 用 () -> new VillagerType(name)(方法引用无法匹配 Supplier) - dataComponentPredicate 入口不迁(1.21.5+ 才有 DataComponentPredicate) 零碎修复:BlockBuilder.item() 默认 lang(e3ba2cd)、OneTimeEventReceiver 空值防御 (23f5686)。 --- module.registrum/build.gradle | 1 + .../lib/v2/registrum/AbstractRegistrum.java | 491 ++++++++++++++++++ .../v2/registrum/builders/BlockBuilder.java | 3 +- .../registrum/builders/ConditionBuilder.java | 57 ++ .../builders/CreativeTabBuilder.java | 200 +++++++ .../registrum/builders/GameEventBuilder.java | 30 ++ .../registrum/builders/MobEffectBuilder.java | 35 ++ .../v2/registrum/builders/SelfBuilder.java | 31 ++ .../builders/data/AttachmentBuilder.java | 134 +++++ .../builders/data/DataComponentBuilder.java | 76 +++ .../registrum/builders/data/package-info.java | 17 + .../modifier/BiomeModifierBuilder.java | 59 +++ .../modifier/GlobalLootModifierBuilder.java | 60 +++ .../modifier/StructureModifierBuilder.java | 60 +++ .../builders/modifier/package-info.java | 16 + .../recipe/RecipeSerializerBuilder.java | 82 +++ .../builders/recipe/RecipeTypeBuilder.java | 60 +++ .../builders/recipe/package-info.java | 4 + .../builders/self/PotionBuilder.java | 31 ++ .../builders/self/SoundEventBuilder.java | 60 +++ .../registrum/builders/self/package-info.java | 4 + .../builders/villager/PoiTypeBuilder.java | 23 + .../villager/VillagerProfessionBuilder.java | 69 +++ .../villager/VillagerTypeBuilder.java | 15 + .../builders/villager/package-info.java | 4 + .../registrum/util/OneTimeEventReceiver.java | 8 +- .../registrum/util/entry/ConditionEntry.java | 40 ++ .../registrum/util/entry/GameEventEntry.java | 41 ++ .../registrum/util/entry/MobEffectEntry.java | 15 + .../v2/registrum/util/entry/PotionEntry.java | 15 + .../v2/registrum/util/entry/RecipeEntry.java | 9 + .../v2/registrum/util/entry/SelfEntry.java | 14 + .../registrum/util/entry/SoundEventEntry.java | 40 ++ .../util/entry/data/AttachmentEntry.java | 41 ++ .../util/entry/data/DataComponentEntry.java | 41 ++ .../util/entry/data/package-info.java | 4 + .../entry/modifier/BiomeModifierEntry.java | 41 ++ .../modifier/GlobalLootModifierEntry.java | 41 ++ .../modifier/StructureModifierEntry.java | 42 ++ .../util/entry/modifier/package-info.java | 4 + .../entry/recipe/RecipeSerializerEntry.java | 41 ++ .../util/entry/recipe/RecipeTypeEntry.java | 41 ++ .../util/entry/recipe/package-info.java | 4 + 43 files changed, 2100 insertions(+), 4 deletions(-) create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/ConditionBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/CreativeTabBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/GameEventBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/MobEffectBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/SelfBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/AttachmentBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/DataComponentBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/package-info.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/BiomeModifierBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/GlobalLootModifierBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/StructureModifierBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/package-info.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/RecipeSerializerBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/RecipeTypeBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/package-info.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/PotionBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/SoundEventBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/package-info.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/PoiTypeBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/VillagerProfessionBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/VillagerTypeBuilder.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/package-info.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/ConditionEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/GameEventEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/MobEffectEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/PotionEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/RecipeEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/SelfEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/SoundEventEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/AttachmentEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/DataComponentEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/package-info.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/BiomeModifierEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/GlobalLootModifierEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/StructureModifierEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/package-info.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/RecipeSerializerEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/RecipeTypeEntry.java create mode 100644 module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/package-info.java diff --git a/module.registrum/build.gradle b/module.registrum/build.gradle index 29bec520..77e10aa2 100644 --- a/module.registrum/build.gradle +++ b/module.registrum/build.gradle @@ -115,6 +115,7 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { + compileOnly "org.jspecify:jspecify:1.0.0" if (System.getenv("NOT_DEV") == 'true') { jarJar(api("dev.anvilcraft.lib:anvillib-util-neoforge-1.21.1:latest.release")) } else { diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/AbstractRegistrum.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/AbstractRegistrum.java index c7ff37f8..4a138c82 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/AbstractRegistrum.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/AbstractRegistrum.java @@ -23,15 +23,19 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.function.UnaryOperator; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.common.collect.Table; import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; import dev.anvilcraft.lib.v2.registrum.providers.DataProviderInitializer; import dev.anvilcraft.lib.v2.registrum.providers.ProviderType; import dev.anvilcraft.lib.v2.registrum.providers.RegistrumDataProvider; @@ -42,28 +46,47 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.MenuAccess; import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.core.Holder; import net.minecraft.core.Registry; import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType.EntityFactory; import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.entity.npc.VillagerProfession; +import net.minecraft.world.entity.npc.VillagerType; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.CreativeModeTabs; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import net.minecraft.world.item.alchemy.Potion; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.level.ItemLike; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.gameevent.GameEvent; import net.neoforged.bus.api.EventPriority; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.neoforge.attachment.IAttachmentHolder; +import net.neoforged.neoforge.common.conditions.ICondition; +import net.neoforged.neoforge.common.loot.IGlobalLootModifier; +import net.neoforged.neoforge.common.world.BiomeModifier; +import net.neoforged.neoforge.common.world.StructureModifier; import net.neoforged.neoforge.data.event.GatherDataEvent; import net.neoforged.neoforge.data.loading.DatagenModLoader; import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; @@ -84,18 +107,36 @@ import dev.anvilcraft.lib.v2.registrum.builders.BlockEntityBuilder.BlockEntityFactory; import dev.anvilcraft.lib.v2.registrum.builders.Builder; import dev.anvilcraft.lib.v2.registrum.builders.BuilderCallback; +import dev.anvilcraft.lib.v2.registrum.builders.ConditionBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.CreativeTabBuilder; import dev.anvilcraft.lib.v2.registrum.builders.EntityBuilder; import dev.anvilcraft.lib.v2.registrum.builders.FluidBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.GameEventBuilder; import dev.anvilcraft.lib.v2.registrum.builders.ItemBuilder; import dev.anvilcraft.lib.v2.registrum.builders.MenuBuilder; import dev.anvilcraft.lib.v2.registrum.builders.MenuBuilder.ForgeMenuFactory; import dev.anvilcraft.lib.v2.registrum.builders.MenuBuilder.MenuFactory; import dev.anvilcraft.lib.v2.registrum.builders.MenuBuilder.ScreenFactory; +import dev.anvilcraft.lib.v2.registrum.builders.MobEffectBuilder; import dev.anvilcraft.lib.v2.registrum.builders.NoConfigBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.SelfBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.data.AttachmentBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.data.DataComponentBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.modifier.BiomeModifierBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.modifier.GlobalLootModifierBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.modifier.StructureModifierBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.recipe.RecipeSerializerBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.recipe.RecipeTypeBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.self.PotionBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.self.SoundEventBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.villager.PoiTypeBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.villager.VillagerProfessionBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.villager.VillagerTypeBuilder; import dev.anvilcraft.lib.v2.registrum.util.CreativeModeTabModifier; import dev.anvilcraft.lib.v2.registrum.util.DebugMarkers; import dev.anvilcraft.lib.v2.registrum.util.OneTimeEventReceiver; import dev.anvilcraft.lib.v2.registrum.util.entry.ItemEntry; +import dev.anvilcraft.lib.v2.registrum.util.entry.RecipeEntry; import dev.anvilcraft.lib.v2.registrum.util.entry.RegistryEntry; import dev.anvilcraft.lib.v2.util.nullness.NonNullBiFunction; import dev.anvilcraft.lib.v2.util.nullness.NonNullConsumer; @@ -873,6 +914,25 @@ public > S2 entry(String name return factory.apply(this::accept); } + /** + * Create a builder for a new entry whose registry type is the entry type itself (self-registered), e.g. {@link VillagerType}, {@link Potion}, {@link SoundEvent}. + * + * @param + * Entry type (also the registry type) + * @param

+ * Parent type + * @param + * Self type + * @param name + * The name to use for the entry + * @param factory + * The factory to create the builder + * @return The {@link SelfBuilder} instance + */ + public > S2 selfEntry(String name, NonNullFunction factory) { + return factory.apply(this::accept); + } + /** * Factory method to accept a completed builder and add it to the registration queue. *

@@ -1307,4 +1367,435 @@ public

NoConfigBuilder defaultCreativeT return builder.build(); }); } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author Gugle + */ + protected

CreativeTabBuilder

creativeTab(P parent, String name, Supplier icon) { + return entry(name, callback -> CreativeTabBuilder.create(this, parent, name, callback, icon)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author Gugle + */ + public CreativeTabBuilder creativeTab(String name, ItemLike icon) { + return creativeTab(self(), name, () -> icon); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author Gugle + */ + public CreativeTabBuilder creativeTab(String name, Supplier icon) { + return creativeTab(self(), name, icon); + } + + // Attachment Type + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + protected AttachmentBuilder attachment(P parent, String name, Function const_) { + return entry(name, callback -> new AttachmentBuilder<>(this, parent, name, callback, const_)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public AttachmentBuilder attachment(String name, Function const_) { + return attachment(self(), name, const_); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + protected AttachmentBuilder attachment(P parent, String name, Supplier const_) { + return entry(name, callback -> new AttachmentBuilder<>(this, parent, name, callback, const_)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public AttachmentBuilder attachment(String name, Supplier const_) { + return attachment(self(), name, const_); + } + + // Data Component Type + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + protected DataComponentBuilder dataComponent(P parent, String name) { + return entry(name, callback -> new DataComponentBuilder<>(this, parent, name, callback)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public DataComponentBuilder dataComponent(String name) { + return dataComponent(self(), name); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author Gugle + */ + public DataComponentBuilder dataComponent(String name, Class clazz) { + return dataComponent(self(), name); + } + + // Biome Modifier + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + protected BiomeModifierBuilder biomeModifier(P parent, String name, MapCodec codec) { + return entry(name, callback -> new BiomeModifierBuilder<>(this, parent, name, callback, codec)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public BiomeModifierBuilder biomeModifier(String name, MapCodec codec) { + return biomeModifier(self(), name, codec); + } + + // Global Loot Modifier + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + protected GlobalLootModifierBuilder glm(P parent, String name, MapCodec codec) { + return entry(name, callback -> new GlobalLootModifierBuilder<>(this, parent, name, callback, codec)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public GlobalLootModifierBuilder glm(String name, MapCodec codec) { + return glm(self(), name, codec); + } + + // Structure Modifier + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + protected StructureModifierBuilder structureModifier(P parent, String name, MapCodec codec) { + return entry(name, callback -> new StructureModifierBuilder<>(this, parent, name, callback, codec)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public StructureModifierBuilder structureModifier(String name, MapCodec codec) { + return structureModifier(self(), name, codec); + } + + // Condition + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + protected ConditionBuilder condition(P parent, String name, MapCodec codec) { + return entry(name, callback -> new ConditionBuilder<>(this, parent, name, callback, codec)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public ConditionBuilder condition(String name, MapCodec codec) { + return condition(self(), name, codec); + } + + // Sound Event + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + protected

SoundEventBuilder

soundEvent(P parent, String name) { + return selfEntry(name, callback -> new SoundEventBuilder<>(this, parent, name, callback)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + protected

SoundEventBuilder

soundEvent(P parent, String name, float fix) { + return selfEntry(name, callback -> new SoundEventBuilder<>(this, parent, name, callback, fix)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public SoundEventBuilder soundEvent(String name) { + return soundEvent(self(), name); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public SoundEventBuilder soundEvent(String name, float fix) { + return soundEvent(self(), name, fix); + } + + // Recipe + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public , P> RecipeEntry recipe( + P parent, + String name, + MapCodec codec, + StreamCodec streamCodec + ) { + return new RecipeEntry<>( + entry(name, callback -> new RecipeTypeBuilder(this, parent, name, callback)).register(), + entry(name, callback -> new RecipeSerializerBuilder<>(this, parent, name, callback, codec, streamCodec)).register() + ); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public > RecipeEntry recipe( + String name, + MapCodec codec, + StreamCodec streamCodec + ) { + return recipe(self(), name, codec, streamCodec); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public

GameEventBuilder

gameEvent(P parent, String name, int radius) { + return entry(name, callback -> new GameEventBuilder<>(this, parent, name, callback, radius)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public GameEventBuilder gameEvent(String name, int radius) { + return gameEvent(self(), name, radius); + } + + // Potion + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public

PotionBuilder

potion(P parent, String name, MobEffectInstance... effects) { + return selfEntry(name, callback -> new PotionBuilder<>(this, parent, name, callback, effects)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public PotionBuilder potion(String name, MobEffectInstance... effects) { + return potion(self(), name, effects); + } + + // Mob Effect + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public MobEffectBuilder mobEffect(P parent, String name, NonNullSupplier supplier) { + return entry(name, callback -> new MobEffectBuilder<>(this, parent, name, callback, supplier)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public MobEffectBuilder mobEffect(String name, NonNullSupplier supplier) { + return mobEffect(self(), name, supplier); + } + + // Villager Settings + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public

VillagerTypeBuilder

villager(P parent, String name) { + return selfEntry(name, callback -> new VillagerTypeBuilder<>(this, parent, name, callback)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public VillagerTypeBuilder villager(String name) { + return villager(self(), name); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public

PoiTypeBuilder

poi(P parent, + String name, + Supplier> matchingStates, + int maxTickets, + int validRange) { + return selfEntry(name, callback -> new PoiTypeBuilder<>(this, parent, name, callback, matchingStates, maxTickets, validRange)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public PoiTypeBuilder poi(String name, + Supplier> matchingStates, + int maxTickets, + int validRange) { + return poi(self(), name, matchingStates, maxTickets, validRange); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public

VillagerProfessionBuilder

profession(P parent, String name, Predicate> heldJobSite, + Predicate> acquirableJobSite, + ImmutableSet requestedItems, + ImmutableSet secondaryPoi, + @Nullable SoundEvent workSound) { + return selfEntry(name, callback -> new VillagerProfessionBuilder<>( + this, parent, name, callback, + heldJobSite, acquirableJobSite, + requestedItems, secondaryPoi, + workSound + )); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public VillagerProfessionBuilder profession(String name, Predicate> heldJobSite, + Predicate> acquirableJobSite, + ImmutableSet requestedItems, + ImmutableSet secondaryPoi, + @Nullable SoundEvent workSound) { + return profession(self(), name, heldJobSite, acquirableJobSite, requestedItems, secondaryPoi, workSound); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public

VillagerProfessionBuilder

profession(P parent, + String name, + Predicate> heldJobSite, + Predicate> acquirableJobSite, + @Nullable SoundEvent workSound) { + return selfEntry(name, callback -> new VillagerProfessionBuilder<>( + this, parent, name, callback, + heldJobSite, acquirableJobSite, + workSound + )); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public VillagerProfessionBuilder profession(String name, + Predicate> heldJobSite, + Predicate> acquirableJobSite, + @Nullable SoundEvent workSound) { + return profession(self(), name, heldJobSite, acquirableJobSite, workSound); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public

VillagerProfessionBuilder

profession(P parent, + String name, + ResourceKey jobSite, + @Nullable SoundEvent workSound) { + return selfEntry(name, callback -> new VillagerProfessionBuilder<>(this, parent, name, callback, jobSite, workSound)); + } + + /** + * Release under the MIT License. The full license text is available at this + * + * @author baka4n + */ + public VillagerProfessionBuilder profession(String name, + ResourceKey jobSite, + @Nullable SoundEvent workSound) { + return profession(self(), name, jobSite, workSound); + } + } diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/BlockBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/BlockBuilder.java index e44e0958..de01a48f 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/BlockBuilder.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/BlockBuilder.java @@ -192,7 +192,7 @@ protected void registerLayers(T entry) { * @see #item() */ public BlockBuilder simpleItem() { - return item().build(); + return item().defaultLang().build(); } /** @@ -220,7 +220,6 @@ public ItemBuilder> item() { public ItemBuilder> item(NonNullBiFunction factory) { final var sup = asSupplier(); return getOwner().> item(this, getName(), p -> factory.apply(getEntry(), p)) - .setData(ProviderType.LANG, NonNullBiConsumer.noop()) // FIXME Need a beetter API for "unsetting" providers .model((ctx, prov) -> { Optional model = getOwner().getDataProvider(ProviderType.BLOCKSTATE) .flatMap(p -> p.getExistingVariantBuilder(getEntry())) diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/ConditionBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/ConditionBuilder.java new file mode 100644 index 00000000..7a5fd790 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/ConditionBuilder.java @@ -0,0 +1,57 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.builders; + +import com.mojang.serialization.MapCodec; +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.util.entry.ConditionEntry; +import net.neoforged.neoforge.common.conditions.ICondition; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.NeoForgeRegistries; + +public class ConditionBuilder + extends AbstractBuilder, MapCodec, P, ConditionBuilder> { + private final MapCodec mapCodec; + + public ConditionBuilder(AbstractRegistrum owner, P parent, String name, BuilderCallback callback, MapCodec mapCodec) { + super(owner, parent, name, callback, NeoForgeRegistries.Keys.CONDITION_CODECS); + this.mapCodec = mapCodec; + } + + @Override + protected MapCodec createEntry() { + return mapCodec; + } + + @Override + public ConditionEntry register() { + return (ConditionEntry) super.register(); + } + + @Override + protected ConditionEntry createEntryWrapper(DeferredHolder, MapCodec> delegate) { + return new ConditionEntry<>(getOwner(), delegate); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/CreativeTabBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/CreativeTabBuilder.java new file mode 100644 index 00000000..b034e71a --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/CreativeTabBuilder.java @@ -0,0 +1,200 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.builders; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import net.minecraft.Util; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ItemLike; +import net.neoforged.neoforge.registries.DeferredHolder; + +import java.util.Collection; +import java.util.function.Function; +import java.util.function.Supplier; + +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) +public class CreativeTabBuilder

extends AbstractBuilder> { + private CreativeModeTab.Builder builder = CreativeModeTab.builder(); + + public CreativeTabBuilder(AbstractRegistrum owner, P parent, String name, BuilderCallback callback) { + super(owner, parent, name, callback, Registries.CREATIVE_MODE_TAB); + } + + public static

CreativeTabBuilder

create( + AbstractRegistrum owner, + P parent, + String name, + BuilderCallback callback, + Supplier icon + ) { + return new CreativeTabBuilder<>(owner, parent, name, callback).defaultTitle().icon(() -> icon.get().asItem().getDefaultInstance()); + } + + public CreativeTabBuilder

defaultTitle() { + // 1.21.1 的 AbstractBuilder 无 getResourceKey(),直接由 modid + name 构造条目 id + String descriptionId = Util.makeDescriptionId("itemGroup", ResourceLocation.fromNamespaceAndPath(getOwner().getModid(), getName())); + lang(ignored -> descriptionId); + this.title(Component.translatable(descriptionId)); + return this; + } + + public CreativeTabBuilder

title(Component displayName) { + this.builder = this.builder.title(displayName); + return this; + } + + public CreativeTabBuilder

icon(Supplier iconGenerator) { + this.builder = this.builder.icon(iconGenerator); + return this; + } + + public CreativeTabBuilder

displayItems(CreativeModeTab.DisplayItemsGenerator displayItemsGenerator) { + this.builder = this.builder.displayItems(displayItemsGenerator); + return this; + } + + public CreativeTabBuilder

alignedRight() { + this.builder = this.builder.alignedRight(); + return this; + } + + public CreativeTabBuilder

hideTitle() { + this.builder = this.builder.hideTitle(); + return this; + } + + public CreativeTabBuilder

noScrollBar() { + this.builder = this.builder.noScrollBar(); + return this; + } + + public CreativeTabBuilder

backgroundTexture(ResourceLocation backgroundTexture) { + this.builder = this.builder.backgroundTexture(backgroundTexture); + return this; + } + + public CreativeTabBuilder

withSearchBar() { + this.builder = this.builder.withSearchBar(); + return this; + } + + public CreativeTabBuilder

withSearchBar(int searchBarWidth) { + this.builder = this.builder.withSearchBar(searchBarWidth); + return this; + } + + public CreativeTabBuilder

withScrollBarSpriteLocation(ResourceLocation scrollBarSpriteLocation) { + this.builder = this.builder.withScrollBarSpriteLocation(scrollBarSpriteLocation); + return this; + } + + public CreativeTabBuilder

withTabsImage(ResourceLocation tabsImage) { + this.builder = this.builder.withTabsImage(tabsImage); + return this; + } + + public CreativeTabBuilder

withLabelColor(int labelColor) { + this.builder = this.builder.withLabelColor(labelColor); + return this; + } + + public CreativeTabBuilder

withTabFactory(Function tabFactory) { + this.builder = this.builder.withTabFactory(tabFactory); + return this; + } + + public CreativeTabBuilder

withTabsBefore(ResourceLocation... tabs) { + this.builder = this.builder.withTabsBefore(tabs); + return this; + } + + public CreativeTabBuilder

withTabsAfter(ResourceLocation... tabs) { + this.builder = this.builder.withTabsAfter(tabs); + return this; + } + + @SafeVarargs + public final CreativeTabBuilder

withTabsBefore(DeferredHolder... tabs) { + @SuppressWarnings("unchecked") ResourceKey[] array = (ResourceKey[]) new ResourceKey[tabs.length]; + for (int i = 0; i < tabs.length; i++) { + array[i] = tabs[i].getKey(); + } + this.withTabsBefore(array); + return this; + } + + @SafeVarargs + public final CreativeTabBuilder

withTabsAfter(DeferredHolder... tabs) { + @SuppressWarnings("unchecked") ResourceKey[] array = (ResourceKey[]) new ResourceKey[tabs.length]; + for (int i = 0; i < tabs.length; i++) { + array[i] = tabs[i].getKey(); + } + this.withTabsAfter(array); + return this; + } + + @SafeVarargs + public final CreativeTabBuilder

withTabsBefore(ResourceKey... tabs) { + this.builder = this.builder.withTabsBefore(tabs); + return this; + } + + @SafeVarargs + public final CreativeTabBuilder

withTabsAfter(ResourceKey... tabs) { + this.builder = this.builder.withTabsAfter(tabs); + return this; + } + + public CreativeTabBuilder

displayItems(Collection> collection) { + this.builder = this.builder.displayItems(collection); + return this; + } + + + public CreativeTabBuilder

displayItems(ItemLike... itemLikes) { + this.displayItems((parameters, output) -> { + for (ItemLike itemLike : itemLikes) { + output.accept(itemLike); + } + }); + return this; + } + + @Override + protected CreativeModeTab createEntry() { + return this.builder.build(); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/GameEventBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/GameEventBuilder.java new file mode 100644 index 00000000..afe7ad9c --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/GameEventBuilder.java @@ -0,0 +1,30 @@ +package dev.anvilcraft.lib.v2.registrum.builders; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.util.entry.GameEventEntry; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.gameevent.GameEvent; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class GameEventBuilder

extends AbstractBuilder> { + final int radius; + public GameEventBuilder(AbstractRegistrum owner, P parent, String name, BuilderCallback callback, int radius) { + super(owner, parent, name, callback, Registries.GAME_EVENT); + this.radius = radius; + } + + @Override + public GameEventEntry register() { + return (GameEventEntry) super.register(); + } + + @Override + protected GameEventEntry createEntryWrapper(DeferredHolder delegate) { + return new GameEventEntry(getOwner(), delegate); + } + + @Override + protected GameEvent createEntry() { + return new GameEvent(radius); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/MobEffectBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/MobEffectBuilder.java new file mode 100644 index 00000000..c0c78171 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/MobEffectBuilder.java @@ -0,0 +1,35 @@ +package dev.anvilcraft.lib.v2.registrum.builders; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.util.entry.MobEffectEntry; +import dev.anvilcraft.lib.v2.util.nullness.NonNullSupplier; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.effect.MobEffect; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class MobEffectBuilder extends AbstractBuilder> { + final NonNullSupplier supplier; + public MobEffectBuilder(AbstractRegistrum owner, + P parent, + String name, + BuilderCallback callback, + NonNullSupplier supplier) { + super(owner, parent, name, callback, Registries.MOB_EFFECT); + this.supplier = supplier; + } + + @Override + public MobEffectEntry register() { + return (MobEffectEntry) super.register(); + } + + @Override + protected MobEffectEntry createEntryWrapper(DeferredHolder delegate) { + return new MobEffectEntry<>(getOwner(), delegate); + } + + @Override + protected T createEntry() { + return supplier.get(); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/SelfBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/SelfBuilder.java new file mode 100644 index 00000000..da117a4c --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/SelfBuilder.java @@ -0,0 +1,31 @@ +package dev.anvilcraft.lib.v2.registrum.builders; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.util.entry.SelfEntry; +import dev.anvilcraft.lib.v2.util.nullness.NonNullSupplier; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class SelfBuilder> extends AbstractBuilder { + final NonNullSupplier factory; + public SelfBuilder(AbstractRegistrum owner, P parent, String name, BuilderCallback callback, ResourceKey> registryType, NonNullSupplier factory) { + super(owner, parent, name, callback, registryType); + this.factory = factory; + } + + @Override + protected T createEntry() { + return factory.get(); + } + + @Override + public SelfEntry register() { + return (SelfEntry) super.register(); + } + + @Override + protected SelfEntry createEntryWrapper(DeferredHolder delegate) { + return new SelfEntry<>(getOwner(), delegate); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/AttachmentBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/AttachmentBuilder.java new file mode 100644 index 00000000..1810a704 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/AttachmentBuilder.java @@ -0,0 +1,134 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.builders.data; + +import com.mojang.serialization.Codec; +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.builders.AbstractBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.BuilderCallback; +import dev.anvilcraft.lib.v2.registrum.util.entry.data.AttachmentEntry; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.neoforge.attachment.AttachmentSyncHandler; +import net.neoforged.neoforge.attachment.AttachmentType; +import net.neoforged.neoforge.attachment.IAttachmentCopyHandler; +import net.neoforged.neoforge.attachment.IAttachmentHolder; +import net.neoforged.neoforge.attachment.IAttachmentSerializer; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.NeoForgeRegistries; + +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +@SuppressWarnings("unused") +public class AttachmentBuilder extends AbstractBuilder, AttachmentType, P, AttachmentBuilder> { + + private final AttachmentType.Builder builder; + + public AttachmentBuilder( + AbstractRegistrum owner, + P parent, + String name, + BuilderCallback callback, + Function const_ + ) { + super(owner, parent, name, callback, NeoForgeRegistries.Keys.ATTACHMENT_TYPES); + this.builder = AttachmentType.builder(const_); + } + + public AttachmentBuilder(AbstractRegistrum owner, P parent, String name, BuilderCallback callback, Supplier const_) { + super(owner, parent, name, callback, NeoForgeRegistries.Keys.ATTACHMENT_TYPES); + this.builder = AttachmentType.builder(const_); + } + + public AttachmentBuilder serialize(IAttachmentSerializer serializer) { + builder.serialize(serializer); + return this; + } + + /** + * 以 {@link Codec} 序列化附件。

+ * 注:1.21.1 的 {@link AttachmentType.Builder#serialize(Codec)} 参数为 {@link Codec}(26.1 才改为 MapCodec)。 + */ + public AttachmentBuilder serialize(Codec serializer) { + builder.serialize(serializer); + return this; + } + + /** + * 以 {@link Codec} 序列化附件,并附带谓词判断是否应序列化。

+ * 注:1.21.1 的 {@link AttachmentType.Builder#serialize(Codec, Predicate)} 参数为 {@link Codec}(26.1 才改为 MapCodec)。 + */ + public AttachmentBuilder serialize(Codec serializer, Predicate predicate) { + builder.serialize(serializer, predicate); + return this; + } + + public AttachmentBuilder copyOnDeath() { + builder.copyOnDeath(); + return this; + } + + public AttachmentBuilder copyHandler(IAttachmentCopyHandler cloner) { + builder.copyHandler(cloner); + return this; + } + + public AttachmentBuilder sync(AttachmentSyncHandler syncHandler) { + builder.sync(syncHandler); + return this; + } + + public AttachmentBuilder sync(StreamCodec streamCodec) { + builder.sync(streamCodec); + return this; + } + + public AttachmentBuilder sync( + BiPredicate sendToPlayer, + StreamCodec streamCodec + ) { + builder.sync(sendToPlayer, streamCodec); + return this; + } + + @Override + public AttachmentEntry register() { + return (AttachmentEntry) super.register(); + } + + @Override + protected AttachmentEntry createEntryWrapper(DeferredHolder, AttachmentType> delegate) { + return new AttachmentEntry<>(getOwner(), delegate); + } + + @Override + protected AttachmentType createEntry() { + return builder.build(); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/DataComponentBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/DataComponentBuilder.java new file mode 100644 index 00000000..35f32af4 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/DataComponentBuilder.java @@ -0,0 +1,76 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.builders.data; + +import com.mojang.serialization.Codec; +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.builders.AbstractBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.BuilderCallback; +import dev.anvilcraft.lib.v2.registrum.util.entry.data.DataComponentEntry; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.neoforged.neoforge.registries.DeferredHolder; + +@SuppressWarnings("unused") +public class DataComponentBuilder extends AbstractBuilder, DataComponentType, P, DataComponentBuilder> { + final DataComponentType.Builder builder; + + public DataComponentBuilder(AbstractRegistrum owner, P parent, String name, BuilderCallback callback) { + super(owner, parent, name, callback, Registries.DATA_COMPONENT_TYPE); + builder = DataComponentType.builder(); + } + + public DataComponentBuilder persistent(Codec codec) { + builder.persistent(codec); + return this; + } + + public DataComponentBuilder networkSynchronized(StreamCodec streamCodec) { + builder.networkSynchronized(streamCodec); + return this; + } + + public DataComponentBuilder cacheEncoding() { + builder.cacheEncoding(); + return this; + } + + @Override + protected DataComponentType createEntry() { + return builder.build(); + } + + @Override + public DataComponentEntry register() { + return (DataComponentEntry) super.register(); + } + + @Override + protected DataComponentEntry createEntryWrapper(DeferredHolder, DataComponentType> delegate) { + return new DataComponentEntry<>(getOwner(), delegate); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/package-info.java new file mode 100644 index 00000000..ec0b37cd --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/package-info.java @@ -0,0 +1,17 @@ +/* + * + * * Original work copyright (c) 2019 tterrag1098 (Registrate) + * * Additional modifications copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * * + * * This Source Code Form is subject to the terms of the Mozilla Public + * * License, v. 2.0. If a copy of the MPL was not distributed with this + * * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * * + * * Original File: https://github.com/tterrag1098/Registrate/blob/1.21.5/dev/src/main/java/com/tterrag/registrate/builders/package-info.java + * + */ + +@NullMarked +package dev.anvilcraft.lib.v2.registrum.builders.data; + +import org.jspecify.annotations.NullMarked; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/BiomeModifierBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/BiomeModifierBuilder.java new file mode 100644 index 00000000..a41d9860 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/BiomeModifierBuilder.java @@ -0,0 +1,59 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.builders.modifier; + +import com.mojang.serialization.MapCodec; +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.builders.AbstractBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.BuilderCallback; +import dev.anvilcraft.lib.v2.registrum.util.entry.modifier.BiomeModifierEntry; +import net.neoforged.neoforge.common.world.BiomeModifier; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.NeoForgeRegistries; + +public class BiomeModifierBuilder + extends AbstractBuilder, MapCodec, P, BiomeModifierBuilder> { + private final MapCodec mapCodec; + + public BiomeModifierBuilder(AbstractRegistrum owner, P parent, String name, BuilderCallback callback, MapCodec mapCodec) { + super(owner, parent, name, callback, NeoForgeRegistries.Keys.BIOME_MODIFIER_SERIALIZERS); + this.mapCodec = mapCodec; + } + + @Override + protected MapCodec createEntry() { + return mapCodec; + } + + @Override + public BiomeModifierEntry register() { + return (BiomeModifierEntry) super.register(); + } + + @Override + protected BiomeModifierEntry createEntryWrapper(DeferredHolder, MapCodec> delegate) { + return new BiomeModifierEntry<>(getOwner(), delegate); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/GlobalLootModifierBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/GlobalLootModifierBuilder.java new file mode 100644 index 00000000..913dcd8c --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/GlobalLootModifierBuilder.java @@ -0,0 +1,60 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.builders.modifier; + +import com.mojang.serialization.MapCodec; +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.builders.AbstractBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.BuilderCallback; +import dev.anvilcraft.lib.v2.registrum.util.entry.modifier.GlobalLootModifierEntry; +import net.neoforged.neoforge.common.loot.IGlobalLootModifier; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.NeoForgeRegistries; + +public class GlobalLootModifierBuilder + extends AbstractBuilder, MapCodec, P, GlobalLootModifierBuilder> { + + final MapCodec codec; + + public GlobalLootModifierBuilder(AbstractRegistrum owner, P parent, String name, BuilderCallback callback, MapCodec codec) { + super(owner, parent, name, callback, NeoForgeRegistries.Keys.GLOBAL_LOOT_MODIFIER_SERIALIZERS); + this.codec = codec; + } + + @Override + protected MapCodec createEntry() { + return codec; + } + + @Override + public GlobalLootModifierEntry register() { + return (GlobalLootModifierEntry) super.register(); + } + + @Override + protected GlobalLootModifierEntry createEntryWrapper(DeferredHolder, MapCodec> delegate) { + return new GlobalLootModifierEntry<>(getOwner(), delegate); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/StructureModifierBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/StructureModifierBuilder.java new file mode 100644 index 00000000..c343e6c8 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/StructureModifierBuilder.java @@ -0,0 +1,60 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.builders.modifier; + +import com.mojang.serialization.MapCodec; +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.builders.AbstractBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.BuilderCallback; +import dev.anvilcraft.lib.v2.registrum.util.entry.modifier.StructureModifierEntry; +import net.neoforged.neoforge.common.world.StructureModifier; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.NeoForgeRegistries; + +public class StructureModifierBuilder + extends AbstractBuilder, MapCodec, P, StructureModifierBuilder> { + + final MapCodec codec; + + public StructureModifierBuilder(AbstractRegistrum owner, P parent, String name, BuilderCallback callback, MapCodec codec) { + super(owner, parent, name, callback, NeoForgeRegistries.Keys.STRUCTURE_MODIFIER_SERIALIZERS); + this.codec = codec; + } + + @Override + protected MapCodec createEntry() { + return codec; + } + + @Override + public StructureModifierEntry register() { + return (StructureModifierEntry) super.register(); + } + + @Override + protected StructureModifierEntry createEntryWrapper(DeferredHolder, MapCodec> delegate) { + return new StructureModifierEntry<>(getOwner(), delegate); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/package-info.java new file mode 100644 index 00000000..bc72532c --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/package-info.java @@ -0,0 +1,16 @@ +/* + * + * * Original work copyright (c) 2019 tterrag1098 (Registrate) + * * Additional modifications copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * * + * * This Source Code Form is subject to the terms of the Mozilla Public + * * License, v. 2.0. If a copy of the MPL was not distributed with this + * * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * * + * * Original File: https://github.com/tterrag1098/Registrate/blob/1.21.5/dev/src/main/java/com/tterrag/registrate/builders/package-info.java + * + */ +@NullMarked +package dev.anvilcraft.lib.v2.registrum.builders.modifier; + +import org.jspecify.annotations.NullMarked; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/RecipeSerializerBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/RecipeSerializerBuilder.java new file mode 100644 index 00000000..3c1ebc1b --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/RecipeSerializerBuilder.java @@ -0,0 +1,82 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.builders.recipe; + +import com.mojang.serialization.MapCodec; +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.builders.AbstractBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.BuilderCallback; +import dev.anvilcraft.lib.v2.registrum.util.entry.recipe.RecipeSerializerEntry; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class RecipeSerializerBuilder, P> + extends AbstractBuilder, RecipeSerializer, P, RecipeSerializerBuilder> { + private final MapCodec codec; + private final StreamCodec streamCodec; + + public RecipeSerializerBuilder( + AbstractRegistrum owner, + P parent, + String name, + BuilderCallback callback, + MapCodec codec, + StreamCodec streamCodec + ) { + super(owner, parent, name, callback, Registries.RECIPE_SERIALIZER); + this.codec = codec; + this.streamCodec = streamCodec; + } + + @Override + public RecipeSerializerEntry register() { + return (RecipeSerializerEntry) super.register(); + } + + @Override + protected RecipeSerializerEntry createEntryWrapper(DeferredHolder, RecipeSerializer> delegate) { + return new RecipeSerializerEntry<>(getOwner(), delegate); + } + + @Override + protected RecipeSerializer createEntry() { + // 1.21.1: RecipeSerializer 是接口(含 codec()/streamCodec()),不能 new RecipeSerializer<>(...),须匿名类实现 + return new RecipeSerializer() { + @Override + public MapCodec codec() { + return codec; + } + + @Override + public StreamCodec streamCodec() { + return streamCodec; + } + }; + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/RecipeTypeBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/RecipeTypeBuilder.java new file mode 100644 index 00000000..d300cab5 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/RecipeTypeBuilder.java @@ -0,0 +1,60 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.builders.recipe; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.builders.AbstractBuilder; +import dev.anvilcraft.lib.v2.registrum.builders.BuilderCallback; +import dev.anvilcraft.lib.v2.registrum.util.entry.recipe.RecipeTypeEntry; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeType; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class RecipeTypeBuilder, P> extends AbstractBuilder, RecipeType, P, RecipeTypeBuilder> { + public RecipeTypeBuilder(AbstractRegistrum owner, P parent, String name, BuilderCallback callback) { + super(owner, parent, name, callback, Registries.RECIPE_TYPE); + } + + @Override + protected RecipeType createEntry() { + return new RecipeType() { + @Override + public String toString() { + return getOwner().getModid() + ":" + getName(); + } + }; + } + + @Override + public RecipeTypeEntry register() { + return (RecipeTypeEntry) super.register(); + } + + @Override + protected RecipeTypeEntry createEntryWrapper(DeferredHolder, RecipeType> delegate) { + return new RecipeTypeEntry<>(getOwner(), delegate); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/package-info.java new file mode 100644 index 00000000..3c5a14f3 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.registrum.builders.recipe; + +import org.jspecify.annotations.NullMarked; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/PotionBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/PotionBuilder.java new file mode 100644 index 00000000..9c44ff11 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/PotionBuilder.java @@ -0,0 +1,31 @@ +package dev.anvilcraft.lib.v2.registrum.builders.self; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.builders.BuilderCallback; +import dev.anvilcraft.lib.v2.registrum.builders.SelfBuilder; +import dev.anvilcraft.lib.v2.registrum.util.entry.PotionEntry; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.item.alchemy.Potion; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class PotionBuilder

extends SelfBuilder> { + public PotionBuilder(AbstractRegistrum owner, + P parent, + String name, + BuilderCallback callback, + MobEffectInstance... effects) { + super(owner, parent, name, callback, Registries.POTION, () -> new Potion(owner.getModid() + ":" + name, effects)); + + } + + @Override + public PotionEntry register() { + return (PotionEntry) super.register(); + } + + @Override + protected PotionEntry createEntryWrapper(DeferredHolder delegate) { + return new PotionEntry(getOwner(), delegate); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/SoundEventBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/SoundEventBuilder.java new file mode 100644 index 00000000..47a483fd --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/SoundEventBuilder.java @@ -0,0 +1,60 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.builders.self; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.builders.BuilderCallback; +import dev.anvilcraft.lib.v2.registrum.builders.SelfBuilder; +import dev.anvilcraft.lib.v2.registrum.util.entry.SoundEventEntry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class SoundEventBuilder

extends SelfBuilder> { + public SoundEventBuilder(AbstractRegistrum owner, + P parent, + String name, + BuilderCallback callback) { + super(owner, parent, name, callback, Registries.SOUND_EVENT, () -> SoundEvent.createVariableRangeEvent(ResourceLocation.fromNamespaceAndPath(owner.getModid(), name))); + } + public SoundEventBuilder(AbstractRegistrum owner, + P parent, + String name, + BuilderCallback callback, + float fix) { + super(owner, parent, name, callback, Registries.SOUND_EVENT, () -> SoundEvent.createFixedRangeEvent(ResourceLocation.fromNamespaceAndPath(owner.getModid(), name), fix)); + } + + @Override + public SoundEventEntry register() { + return (SoundEventEntry) super.register(); + } + + @Override + protected SoundEventEntry createEntryWrapper(DeferredHolder delegate) { + return new SoundEventEntry(getOwner(), delegate); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/package-info.java new file mode 100644 index 00000000..f59332d5 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.registrum.builders.self; + +import org.jspecify.annotations.NullMarked; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/PoiTypeBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/PoiTypeBuilder.java new file mode 100644 index 00000000..88b47852 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/PoiTypeBuilder.java @@ -0,0 +1,23 @@ +package dev.anvilcraft.lib.v2.registrum.builders.villager; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.builders.BuilderCallback; +import dev.anvilcraft.lib.v2.registrum.builders.SelfBuilder; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.Set; +import java.util.function.Supplier; + +public class PoiTypeBuilder

extends SelfBuilder> { + public PoiTypeBuilder(AbstractRegistrum owner, + P parent, + String name, + BuilderCallback callback, + Supplier> matchingStates, + int maxTickets, + int validRange) { + super(owner, parent, name, callback, Registries.POINT_OF_INTEREST_TYPE, () -> new PoiType(matchingStates.get(), maxTickets, validRange)); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/VillagerProfessionBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/VillagerProfessionBuilder.java new file mode 100644 index 00000000..e5e1088b --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/VillagerProfessionBuilder.java @@ -0,0 +1,69 @@ +package dev.anvilcraft.lib.v2.registrum.builders.villager; + +import com.google.common.collect.ImmutableSet; +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.builders.BuilderCallback; +import dev.anvilcraft.lib.v2.registrum.builders.SelfBuilder; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.entity.npc.VillagerProfession; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; +import org.jspecify.annotations.Nullable; + +import java.util.function.Predicate; + +/** + * 注册村民职业。

+ * 注:1.21.1 的 {@link VillagerProfession} 是 6 元 record + * {@code (String name, Predicate> heldJobSite, Predicate> acquirableJobSite, + * ImmutableSet requestedItems, ImmutableSet secondaryPoi, @Nullable SoundEvent workSound)}, + * 无 26.1 的 tradeSetsByLevel(TradeSet)参数;第一参数传 {@code modid:name} 字符串(26.1 为翻译组件)。 + * 交易注册请使用 1.21.1 机制:监听 {@code VillagerTradesEvent} 或直接操作 {@code VillagerTrades.TRADES}。 + */ +public class VillagerProfessionBuilder

extends SelfBuilder> { + public VillagerProfessionBuilder(AbstractRegistrum owner, + P parent, + String name, + BuilderCallback callback, + Predicate> heldJobSite, + Predicate> acquirableJobSite, + ImmutableSet requestedItems, + ImmutableSet secondaryPoi, + @Nullable SoundEvent workSound) { + super(owner, parent, name, callback, + Registries.VILLAGER_PROFESSION, () -> new VillagerProfession( + owner.getModid() + ":" + name, + heldJobSite, + acquirableJobSite, + requestedItems, + secondaryPoi, + workSound + )); + } + + public VillagerProfessionBuilder(AbstractRegistrum owner, + P parent, + String name, + BuilderCallback callback, + Predicate> heldJobSite, + Predicate> acquirableJobSite, + @Nullable SoundEvent workSound) { + this(owner, parent, name, callback, heldJobSite, acquirableJobSite, ImmutableSet.of(), ImmutableSet.of(), workSound); + } + + + + public VillagerProfessionBuilder(AbstractRegistrum owner, + P parent, + String name, + BuilderCallback callback, + ResourceKey jobSite, + @Nullable SoundEvent workSound + ) { + this(owner, parent, name, callback, h -> h.is(jobSite), h -> h.is(jobSite), workSound); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/VillagerTypeBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/VillagerTypeBuilder.java new file mode 100644 index 00000000..8c6219d5 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/VillagerTypeBuilder.java @@ -0,0 +1,15 @@ +package dev.anvilcraft.lib.v2.registrum.builders.villager; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.builders.BuilderCallback; +import dev.anvilcraft.lib.v2.registrum.builders.SelfBuilder; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.npc.VillagerType; + +public class VillagerTypeBuilder

extends SelfBuilder> { + + public VillagerTypeBuilder(AbstractRegistrum owner, P parent, String name, BuilderCallback callback) { + // 1.21.1 的 VillagerType 仅有 String 构造,不能直接方法引用为 Supplier,须用 lambda + super(owner, parent, name, callback, Registries.VILLAGER_TYPE, () -> new VillagerType(name)); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/package-info.java new file mode 100644 index 00000000..4138dbb1 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.registrum.builders.villager; + +import org.jspecify.annotations.NullMarked; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/OneTimeEventReceiver.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/OneTimeEventReceiver.java index 686fd494..7cf99c25 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/OneTimeEventReceiver.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/OneTimeEventReceiver.java @@ -46,7 +46,9 @@ public static void addModListener(AbstractRegis if (!waitingModListeners.contains(owner, evtClass)) { waitingModListeners.put(owner, evtClass, new ArrayList<>()); } - waitingModListeners.get(owner, evtClass).add(Pair.of(priority, listener)); + List>> pairs = waitingModListeners.get(owner, evtClass); + if (pairs == null) return; + pairs.add(Pair.of(priority, listener)); return; } if (!seenModBus) { @@ -112,7 +114,9 @@ private static synchronized void unregister(IEventBus bus, Object listener, Clas private static void onLoadComplete(FMLLoadCompleteEvent event) { event.enqueueWork(() -> { - toUnregister.forEach(t -> t.getLeft().unregister(t.getMiddle())); + toUnregister.forEach(t -> { + if (t.getLeft() != null) t.getLeft().unregister(t.getMiddle()); + }); toUnregister.clear(); }); } diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/ConditionEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/ConditionEntry.java new file mode 100644 index 00000000..6ebf0579 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/ConditionEntry.java @@ -0,0 +1,40 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.util.entry; + +import com.mojang.serialization.MapCodec; +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import net.neoforged.neoforge.common.conditions.ICondition; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class ConditionEntry extends RegistryEntry, MapCodec> { + public ConditionEntry(AbstractRegistrum owner, DeferredHolder, MapCodec> key) { + super(owner, key); + } + + public static ConditionEntry cast(RegistryEntry, MapCodec> entry) { + return RegistryEntry.cast(ConditionEntry.class, entry); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/GameEventEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/GameEventEntry.java new file mode 100644 index 00000000..e7f61bf4 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/GameEventEntry.java @@ -0,0 +1,41 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.util.entry; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import net.minecraft.world.level.gameevent.GameEvent; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class GameEventEntry extends RegistryEntry { + public GameEventEntry(AbstractRegistrum owner, DeferredHolder key) { + super(owner, key); + } + + public static GameEventEntry cast(RegistryEntry key) { + return RegistryEntry.cast(GameEventEntry.class, key); + } + + +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/MobEffectEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/MobEffectEntry.java new file mode 100644 index 00000000..03f5ca08 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/MobEffectEntry.java @@ -0,0 +1,15 @@ +package dev.anvilcraft.lib.v2.registrum.util.entry; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import net.minecraft.world.effect.MobEffect; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class MobEffectEntry extends RegistryEntry { + public MobEffectEntry(AbstractRegistrum owner, DeferredHolder key) { + super(owner, key); + } + + public static MobEffectEntry cast(RegistryEntry entry) { + return RegistryEntry.cast(MobEffectEntry.class, entry); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/PotionEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/PotionEntry.java new file mode 100644 index 00000000..adee99c0 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/PotionEntry.java @@ -0,0 +1,15 @@ +package dev.anvilcraft.lib.v2.registrum.util.entry; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import net.minecraft.world.item.alchemy.Potion; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class PotionEntry extends SelfEntry { + public PotionEntry(AbstractRegistrum owner, DeferredHolder key) { + super(owner, key); + } + + public static PotionEntry cast(SelfEntry entry) { + return SelfEntry.cast(PotionEntry.class, entry); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/RecipeEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/RecipeEntry.java new file mode 100644 index 00000000..0d91eb7f --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/RecipeEntry.java @@ -0,0 +1,9 @@ +package dev.anvilcraft.lib.v2.registrum.util.entry; + +import dev.anvilcraft.lib.v2.registrum.util.entry.recipe.RecipeSerializerEntry; +import dev.anvilcraft.lib.v2.registrum.util.entry.recipe.RecipeTypeEntry; +import net.minecraft.world.item.crafting.Recipe; + +public record RecipeEntry>(RecipeTypeEntry type, RecipeSerializerEntry serializer) { + +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/SelfEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/SelfEntry.java new file mode 100644 index 00000000..b09ec1d4 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/SelfEntry.java @@ -0,0 +1,14 @@ +package dev.anvilcraft.lib.v2.registrum.util.entry; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class SelfEntry extends RegistryEntry { + public SelfEntry(AbstractRegistrum owner, DeferredHolder key) { + super(owner, key); + } + + public static SelfEntry cast(RegistryEntry entry) { + return RegistryEntry.cast(SelfEntry.class, entry); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/SoundEventEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/SoundEventEntry.java new file mode 100644 index 00000000..3cea742b --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/SoundEventEntry.java @@ -0,0 +1,40 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.util.entry; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.level.block.Block; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class SoundEventEntry extends SelfEntry { + public SoundEventEntry(AbstractRegistrum owner, DeferredHolder key) { + super(owner, key); + } + + public static SoundEventEntry cast(SelfEntry entry) { + return RegistryEntry.cast(SoundEventEntry.class, entry); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/AttachmentEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/AttachmentEntry.java new file mode 100644 index 00000000..cbbe6baa --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/AttachmentEntry.java @@ -0,0 +1,41 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.util.entry.data; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.util.entry.RegistryEntry; +import net.neoforged.neoforge.attachment.AttachmentType; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class AttachmentEntry extends RegistryEntry, AttachmentType> { + + public AttachmentEntry(AbstractRegistrum owner, DeferredHolder, AttachmentType> key) { + super(owner, key); + } + + public static AttachmentEntry cast(RegistryEntry, AttachmentType> entry) { + return RegistryEntry.cast(AttachmentEntry.class, entry); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/DataComponentEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/DataComponentEntry.java new file mode 100644 index 00000000..45971c26 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/DataComponentEntry.java @@ -0,0 +1,41 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.util.entry.data; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.util.entry.RegistryEntry; +import net.minecraft.core.component.DataComponentType; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class DataComponentEntry extends RegistryEntry, DataComponentType> { + + public DataComponentEntry(AbstractRegistrum owner, DeferredHolder, DataComponentType> key) { + super(owner, key); + } + + public static DataComponentEntry cast(RegistryEntry, DataComponentType> entry) { + return RegistryEntry.cast(DataComponentEntry.class, entry); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/package-info.java new file mode 100644 index 00000000..88a69741 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.registrum.util.entry.data; + +import org.jspecify.annotations.NullMarked; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/BiomeModifierEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/BiomeModifierEntry.java new file mode 100644 index 00000000..a4b67645 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/BiomeModifierEntry.java @@ -0,0 +1,41 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.util.entry.modifier; + +import com.mojang.serialization.MapCodec; +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.util.entry.RegistryEntry; +import net.neoforged.neoforge.common.world.BiomeModifier; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class BiomeModifierEntry extends RegistryEntry, MapCodec> { + public BiomeModifierEntry(AbstractRegistrum owner, DeferredHolder, MapCodec> key) { + super(owner, key); + } + + public static BiomeModifierEntry cast(RegistryEntry, MapCodec> entry) { + return RegistryEntry.cast(BiomeModifierEntry.class, entry); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/GlobalLootModifierEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/GlobalLootModifierEntry.java new file mode 100644 index 00000000..13098e4b --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/GlobalLootModifierEntry.java @@ -0,0 +1,41 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.util.entry.modifier; + +import com.mojang.serialization.MapCodec; +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.util.entry.RegistryEntry; +import net.neoforged.neoforge.common.loot.IGlobalLootModifier; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class GlobalLootModifierEntry extends RegistryEntry, MapCodec> { + public GlobalLootModifierEntry(AbstractRegistrum owner, DeferredHolder, MapCodec> key) { + super(owner, key); + } + + public static GlobalLootModifierEntry cast(RegistryEntry, MapCodec> entry) { + return RegistryEntry.cast(GlobalLootModifierEntry.class, entry); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/StructureModifierEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/StructureModifierEntry.java new file mode 100644 index 00000000..c6e16243 --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/StructureModifierEntry.java @@ -0,0 +1,42 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.util.entry.modifier; + +import com.mojang.serialization.MapCodec; +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.util.entry.RegistryEntry; +import net.neoforged.neoforge.common.loot.IGlobalLootModifier; +import net.neoforged.neoforge.common.world.StructureModifier; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class StructureModifierEntry extends RegistryEntry, MapCodec> { + public StructureModifierEntry(AbstractRegistrum owner, DeferredHolder, MapCodec> key) { + super(owner, key); + } + + public static StructureModifierEntry cast(RegistryEntry, MapCodec> entry) { + return RegistryEntry.cast(StructureModifierEntry.class, entry); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/package-info.java new file mode 100644 index 00000000..31fbe90d --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.registrum.util.entry.modifier; + +import org.jspecify.annotations.NullMarked; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/RecipeSerializerEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/RecipeSerializerEntry.java new file mode 100644 index 00000000..560a962a --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/RecipeSerializerEntry.java @@ -0,0 +1,41 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.util.entry.recipe; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.util.entry.RegistryEntry; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class RecipeSerializerEntry> extends RegistryEntry, RecipeSerializer> { + public RecipeSerializerEntry(AbstractRegistrum owner, DeferredHolder, RecipeSerializer> key) { + super(owner, key); + } + + public static > RecipeSerializerEntry cast(RegistryEntry, RecipeSerializer> entry) { + return RegistryEntry.cast(RecipeSerializerEntry.class, entry); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/RecipeTypeEntry.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/RecipeTypeEntry.java new file mode 100644 index 00000000..ce4130ce --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/RecipeTypeEntry.java @@ -0,0 +1,41 @@ +/* + * + * Original work copyright (c) 2026 Anvil-Dev (AnvilLib-Registrum) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package dev.anvilcraft.lib.v2.registrum.util.entry.recipe; + +import dev.anvilcraft.lib.v2.registrum.AbstractRegistrum; +import dev.anvilcraft.lib.v2.registrum.util.entry.RegistryEntry; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeType; +import net.neoforged.neoforge.registries.DeferredHolder; + +public class RecipeTypeEntry> extends RegistryEntry, RecipeType> { + public RecipeTypeEntry(AbstractRegistrum owner, DeferredHolder, RecipeType> key) { + super(owner, key); + } + + public static > RecipeTypeEntry cast(RegistryEntry, RecipeType> entry) { + return RegistryEntry.cast(RecipeTypeEntry.class, entry); + } +} diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/package-info.java new file mode 100644 index 00000000..43ab4c8a --- /dev/null +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.registrum.util.entry.recipe; + +import org.jspecify.annotations.NullMarked; From dd8d79e5f36e5e2a9a2e3acad385a1940317d640 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 11 Aug 2026 00:29:32 +0800 Subject: [PATCH 15/38] =?UTF-8?q?feat(registrum):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=20datagen=20=E5=A2=9E=E5=BC=BA=EF=BC=88RecipeProvider?= =?UTF-8?q?=20=E6=96=B9=E6=B3=95=20public=20=E5=8C=96=20+=20dataMap(provid?= =?UTF-8?q?er)=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回移植 dev/26.1 module.registrum 的 datagen 增强(58f01d4): - RegistrumRecipeProvider:vanilla 方法 public 化(oneToOneConversionRecipe/ oreSmelting/oreBlasting/oreCooking)——1.21.1 为 protected static 且首参 RecipeOutput,落法为公开包装方法内调 RecipeProvider.xxx(this, ...) 而非 override; 26.1 独有配方(shelf/colorItemWithDye/colorWithDye/dryGhast)按 1.21.1 vanilla 字节码手动实现;save 均用 ResourceLocation - Builder.dataMap(DataMapType, NonNullBiFunction) 重载;RegistrumDataMapProvider override gather(HolderLookup.Provider) 缓存 provider (21.1.226 存在该非弃用重载) 不迁移:ItemStackTemplate 相关方法、GeneratorType/ProviderType 拆分、 RegistrumRecipeRunner、模型生成器五件套(26.1 专属)。 --- .../lib/v2/registrum/builders/Builder.java | 18 ++++ .../providers/RegistrumDataMapProvider.java | 27 +++++- .../providers/RegistrumRecipeProvider.java | 82 +++++++++++++++++++ 3 files changed, 123 insertions(+), 4 deletions(-) diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/Builder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/Builder.java index 7c5cf93f..a134bd19 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/Builder.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/Builder.java @@ -18,9 +18,11 @@ import dev.anvilcraft.lib.v2.registrum.providers.RegistrumProvider; import dev.anvilcraft.lib.v2.registrum.util.entry.RegistryEntry; import dev.anvilcraft.lib.v2.util.nullness.NonNullBiConsumer; +import dev.anvilcraft.lib.v2.util.nullness.NonNullBiFunction; import dev.anvilcraft.lib.v2.util.nullness.NonNullConsumer; import dev.anvilcraft.lib.v2.util.nullness.NonNullFunction; import dev.anvilcraft.lib.v2.util.nullness.NonNullSupplier; +import net.minecraft.core.HolderLookup; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceKey; import net.neoforged.neoforge.registries.datamaps.DataMapType; @@ -169,6 +171,22 @@ default S dataMap(DataMapType type, NonNullFunction + * Note: 1.21.1 的 {@code DataMapProvider.gather()} 无参数重载已被弃用;此处通过 + * {@link dev.anvilcraft.lib.v2.registrum.providers.RegistrumDataMapProvider#getProvider()} 获取 + * {@link HolderLookup.Provider}(datagen 线程内安全)。 + */ + @SuppressWarnings("unchecked") + default S dataMap(DataMapType type, NonNullBiFunction, HolderLookup.Provider, D> factory) { + getOwner().addDataGenerator(ProviderType.DATA_MAP, e -> { + var ctx = DataGenContext.from(this); + e.builder(type).add(ctx.getId(), factory.apply(ctx, e.getProvider()), false); + }); + return (S) this; + } + /** * Add a callback to be invoked when this entry is registered. Can be called multiple times to add multiple callbacks. *

diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/providers/RegistrumDataMapProvider.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/providers/RegistrumDataMapProvider.java index 809c0b2c..40798978 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/providers/RegistrumDataMapProvider.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/providers/RegistrumDataMapProvider.java @@ -17,6 +17,7 @@ import net.minecraft.data.PackOutput; import net.neoforged.fml.LogicalSide; import net.neoforged.neoforge.common.data.DataMapProvider; +import org.jspecify.annotations.Nullable; import java.util.concurrent.CompletableFuture; @@ -24,6 +25,8 @@ public class RegistrumDataMapProvider extends DataMapProvider implements Registr private final AbstractRegistrum parent; + private HolderLookup.@Nullable Provider provider; + protected RegistrumDataMapProvider(AbstractRegistrum parent, PackOutput output, CompletableFuture pvd) { super(output, pvd); this.parent = parent; @@ -34,10 +37,26 @@ public LogicalSide getSide() { return LogicalSide.SERVER; } + /** + * Generate data map entries. + * + * @param provider HolderLookup.Provider + */ @Override - @SuppressWarnings("removal") - protected void gather() { - parent.genData(ProviderType.DATA_MAP, this); - } + protected void gather(HolderLookup.Provider provider) { + this.provider = provider; + parent.genData(ProviderType.DATA_MAP, this); + this.provider = null; + } + + /** + * 当前 datagen 运行中可用的 {@link HolderLookup.Provider}。 + * + * @throws IllegalStateException 若不在 {@link #gather(HolderLookup.Provider)} 调用期间 + */ + public HolderLookup.Provider getProvider() { + if (provider == null) throw new IllegalStateException("Holder Lookup Provider is not available now"); + return provider; + } } diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/providers/RegistrumRecipeProvider.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/providers/RegistrumRecipeProvider.java index f06ff69f..da81c313 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/providers/RegistrumRecipeProvider.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/providers/RegistrumRecipeProvider.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; +import java.util.stream.Stream; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; @@ -51,6 +52,8 @@ import net.minecraft.tags.TagKey; import net.minecraft.world.flag.FeatureFlagSet; import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import net.minecraft.world.item.crafting.*; import net.minecraft.world.level.ItemLike; import net.minecraft.world.level.block.Block; @@ -336,6 +339,85 @@ public void trapDoor(DataIngredient source, RecipeCategory @Override public CompletableFuture buildAdvancement(CachedOutput p_253674_, HolderLookup.Provider p_323646_, AdvancementHolder p_301116_) { return super.buildAdvancement(p_253674_, p_323646_, p_301116_); } + // === 1.21.1 公开包装方法(内调 RecipeProvider 的 protected static 方法,而非 override) === + + public void oneToOneConversionRecipe(ItemLike product, ItemLike resource, @Nullable String group) { + RecipeProvider.oneToOneConversionRecipe(this, product, resource, group); + } + + public void oneToOneConversionRecipe(ItemLike product, ItemLike resource, @Nullable String group, int productCount) { + RecipeProvider.oneToOneConversionRecipe(this, product, resource, group, productCount); + } + + public void oreSmelting(List smeltables, RecipeCategory category, ItemLike result, float experience, int cookingTime, String group) { + RecipeProvider.oreSmelting(this, smeltables, category, result, experience, cookingTime, group); + } + + public void oreBlasting(List blasting, RecipeCategory category, ItemLike result, float experience, int cookingTime, String group) { + RecipeProvider.oreBlasting(this, blasting, category, result, experience, cookingTime, group); + } + + public void oreCooking(RecipeSerializer serializer, AbstractCookingRecipe.Factory factory, List smeltables, RecipeCategory craftingCategory, ItemLike result, float experience, int cookingTime, String group, String fromDesc) { + RecipeProvider.oreCooking(this, serializer, factory, smeltables, craftingCategory, result, experience, cookingTime, group, fromDesc); + } + + /** + * 书架类装饰方块配方:6 个去皮原木 → 6 个 shelf。

+ * 注:26.1 的 vanilla {@code RecipeProvider.shelf} 在 1.21.1 不存在,此处按其配方手动实现。 + */ + public void shelf(ItemLike result, ItemLike strippedLogs) { + ShapedRecipeBuilder.shaped(RecipeCategory.DECORATIONS, result, 6) + .define('#', strippedLogs) + .pattern("###") + .pattern(" ") + .pattern("###") + .group("shelf") + .unlockedBy(getHasName(strippedLogs), has(strippedLogs)) + .save(this, safeId(result)); + } + + /** + * 注:26.1 的 vanilla {@code RecipeProvider.colorItemWithDye} 在 1.21.1 不存在,此处按其配方手动实现。 + */ + public void colorItemWithDye(List dyeItems, List dyeableItems, String group, RecipeCategory category) { + colorWithDye(dyeItems, dyeableItems, null, group, category); + } + + /** + * 注:26.1 的 vanilla {@code RecipeProvider.colorWithDye} 在 1.21.1 不存在,此处按其配方手动实现。 + */ + public void colorWithDye(List dyes, List dyeableItems, @Nullable Item dye, String group, RecipeCategory category) { + for (int i = 0; i < dyes.size(); i++) { + Item dyeItem = dyes.get(i); + Item dyeableItem = dyeableItems.get(i); + Stream ingredient = dyeableItems.stream().filter(dyeableItem::equals); + if (dye != null) { + ingredient = Stream.concat(ingredient, Stream.of(dye)); + } + ShapelessRecipeBuilder.shapeless(category, dyeableItem) + .requires(dyeItem) + .requires(Ingredient.of(ingredient.map(ItemStack::new))) + .group(group) + .unlockedBy("has_needed_dye", has(dyeItem)) + .save(this, "dye_" + getItemName(dyeableItem)); + } + } + + /** + * 注:26.1 的 vanilla {@code RecipeProvider.dryGhast} 在 1.21.1 不存在,此处按其配方手动实现。 + */ + public void dryGhast(ItemLike result) { + ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, result) + .define('#', Items.GHAST_TEAR) + .define('X', Items.SOUL_SAND) + .pattern("###") + .pattern("#X#") + .pattern("###") + .group("dry_ghast") + .unlockedBy(getHasName(Items.GHAST_TEAR), has(Items.GHAST_TEAR)) + .save(this, safeId(result)); + } + @Override public CompletableFuture buildAdvancement(CachedOutput p_253674_, HolderLookup.Provider p_323646_, AdvancementHolder p_301116_, ICondition... conditions) { return super.buildAdvancement(p_253674_, p_323646_, p_301116_, conditions); } From 2556a8e3f5c95d36f74c7f2d095fb474fa7b7b1e Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 11 Aug 2026 00:29:52 +0800 Subject: [PATCH 16/38] =?UTF-8?q?feat(multiblock):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=E4=BA=8B=E4=BB=B6=E5=8C=96=E3=80=81=E5=BF=AB=E7=85=A7?= =?UTF-8?q?=E5=A4=8D=E7=94=A8=E3=80=81BlockPos=20=E9=94=AE=E5=8C=96?= =?UTF-8?q?=E4=B8=8E=E5=AE=9A=E4=B9=89=E6=87=92=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回移植 dev/26.1 module.multiblock 的四项功能演进/修复: - M1 DynamicMultiblockEvent(1e5bfb3 #69):新建 dynamic/event/DynamicMultiblockEvent (Event + ICancellableEvent,Form/Unform 可取消子类);DynamicMultiblockManager. updateFormed 与两个 packet handler 加事件触发点,取消则回滚 setFormed - M2 未加载区块快照复用(d888714 #73):buildSnapshot 对 !level.isLoaded(pos) 分支 复用旧 entries;Entry.blockState 可空 + test() 空态返回 true;MultiblockState 新增 snapshot 字段,checkMultiblockFormed 先存快照 - M3 Long→BlockPos 键化(22091d4):multiblocks/pendingChecks 改 Map/ Set,getAt/add/removeAt/containsAt 与异步回调处 pos.immutable() 防哈希突变; MultiblockCheckSnapshot 字段 controllerPosLong → controllerPos - M4 定义懒解析(1b0a652):MultiblockState 构造器改收 ResourceKey,getDefinition (HolderLookup.Provider) 懒解析 Holder.Reference;DEFINITION_KEY_STREAM_CODEC 外提 (ResourceKey.streamCodec);packet/manager 同步改用 holder.key() Java 21:port 侧已无 lambda 下划线问题;level.isClientSide 沿用 port 字段写法。 --- module.multiblock/build.gradle | 2 + .../dynamic/DynamicMultiblockManager.java | 75 +++++++++++++------ .../dynamic/MultiblockCheckSnapshot.java | 17 +++-- .../multiblock/dynamic/MultiblockState.java | 57 +++++++------- .../dynamic/event/DynamicMultiblockEvent.java | 44 +++++++++++ .../network/MultiblockFormPacket.java | 14 +++- .../network/MultiblockUnformPacket.java | 14 +++- 7 files changed, 161 insertions(+), 62 deletions(-) create mode 100644 module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/event/DynamicMultiblockEvent.java diff --git a/module.multiblock/build.gradle b/module.multiblock/build.gradle index c2579d90..b2a7aed8 100644 --- a/module.multiblock/build.gradle +++ b/module.multiblock/build.gradle @@ -115,6 +115,8 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { + compileOnly "org.jspecify:jspecify:1.0.0" + if (System.getenv("NOT_DEV") == 'true') { jarJar(api("dev.anvilcraft.lib:anvillib-config-neoforge-1.21.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-codec-neoforge-1.21.1:latest.release")) diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/DynamicMultiblockManager.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/DynamicMultiblockManager.java index 53e13572..2886fa37 100644 --- a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/DynamicMultiblockManager.java +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/DynamicMultiblockManager.java @@ -4,6 +4,7 @@ import dev.anvilcraft.lib.v2.multiblock.dynamic.controller.ControllerRecord; import dev.anvilcraft.lib.v2.multiblock.dynamic.controller.IController; import dev.anvilcraft.lib.v2.multiblock.dynamic.definition.MultiblockDefinition; +import dev.anvilcraft.lib.v2.multiblock.dynamic.event.DynamicMultiblockEvent; import dev.anvilcraft.lib.v2.multiblock.init.LibRegistries; import dev.anvilcraft.lib.v2.multiblock.network.MultiblockFormPacket; import dev.anvilcraft.lib.v2.multiblock.network.MultiblockUnformPacket; @@ -21,6 +22,7 @@ import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.saveddata.SavedData; +import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.network.PacketDistributor; import org.jetbrains.annotations.UnknownNullability; import org.slf4j.Logger; @@ -59,7 +61,7 @@ public class DynamicMultiblockManager extends SavedData { .replace(':', '_'); private static final Map CLIENT_SIDE = new WeakHashMap<>(); - private final Map multiblocks = new HashMap<>(); + private final Map multiblocks = new HashMap<>(); private int tickCounterUnformed = 0; private int tickCounterFormed = 0; @@ -67,7 +69,7 @@ public class DynamicMultiblockManager extends SavedData { private static volatile @UnknownNullability ExecutorService asyncExecutor; /** 当前正在异步检测中的控制器位置集合,用于去重。 */ - private final Set pendingChecks = ConcurrentHashMap.newKeySet(); + private final Set pendingChecks = ConcurrentHashMap.newKeySet(); /** * 获取指定世界的 DynamicMultiblockManager 实例。 @@ -90,7 +92,7 @@ public static DynamicMultiblockManager get(Level level) { * @return 对应的 {@link MultiblockState},若不存在则返回 {@code null} */ public @Nullable MultiblockState getAt(BlockPos pos) { - return this.multiblocks.get(pos.asLong()); + return this.multiblocks.get(pos.immutable()); } /** @@ -99,7 +101,7 @@ public static DynamicMultiblockManager get(Level level) { * @param state 待注册的多方块状态,控制器位置由 {@link MultiblockState#getControllerPos()} 提供 */ public void add(MultiblockState state) { - this.multiblocks.put(state.getControllerPos().asLong(), state); + this.multiblocks.put(state.getControllerPos().immutable(), state); this.setDirty(); } @@ -110,7 +112,7 @@ public void add(MultiblockState state) { * @return 被移除的 {@link MultiblockState};若未注册则返回 {@code null} */ public @Nullable MultiblockState removeAt(BlockPos pos) { - MultiblockState removed = this.multiblocks.remove(pos.asLong()); + MultiblockState removed = this.multiblocks.remove(pos.immutable()); if (removed != null) this.setDirty(); return removed; } @@ -122,7 +124,7 @@ public void add(MultiblockState state) { * @return 若存在注册则返回 {@code true} */ public boolean containsAt(BlockPos pos) { - return this.multiblocks.containsKey(pos.asLong()); + return this.multiblocks.containsKey(pos.immutable()); } /** @@ -146,7 +148,7 @@ public void updateFormed(Level level, MultiblockState cur, boolean formed) { BlockPos controllerPos = cur.getControllerPos(); BlockState state = level.getBlockState(controllerPos); - if (cur.getDefinition().value().isController(level, state, level.getBlockEntity(controllerPos))) { + if (cur.getDefinition(level.registryAccess()).value().isController(level, state, level.getBlockEntity(controllerPos))) { IController controller; try { controller = ControllerRecord.get( @@ -158,9 +160,23 @@ public void updateFormed(Level level, MultiblockState cur, boolean formed) { throw e; } if (formed) { - controller.onFormed(level, cur); + DynamicMultiblockEvent.Form event = new DynamicMultiblockEvent.Form(level, controller, cur); + NeoForge.EVENT_BUS.post(event); + if (!event.isCanceled()) { + controller.onFormed(level, cur); + } else { + cur.setFormed(false); + return; + } } else { - controller.onUnformed(level, cur); + DynamicMultiblockEvent.Unform event = new DynamicMultiblockEvent.Unform(level, controller, cur); + NeoForge.EVENT_BUS.post(event); + if (!event.isCanceled()) { + controller.onUnformed(level, cur); + } else { + cur.setFormed(true); + return; + } } } @@ -198,7 +214,7 @@ public static void onPlace(ServerLevel level, BlockPos pos, BlockState state) { correctedState = state; } if (!definition.isController(level, correctedState, null)) continue; - MultiblockState mstate = new MultiblockState(correctedPos.immutable(), holder); + MultiblockState mstate = new MultiblockState(correctedPos.immutable(), holder.key()); manager.add(mstate); // onPlace 时同步检测(立即反馈),不走异步 manager.checkMultiblockFormedSync(level, mstate); @@ -226,14 +242,14 @@ public static void onBreak(Level level, BlockPos pos) { if (controllerState.isFormed()) { manager.updateFormed(level, controllerState, false); } - manager.multiblocks.remove(pos.asLong()); + manager.multiblocks.remove(pos.immutable()); manager.setDirty(); return; } for (MultiblockState state : manager.multiblocks.values()) { if (!state.isFormed()) continue; - MultiblockDefinition def = state.getDefinition().value(); + MultiblockDefinition def = state.getDefinition(level.registryAccess()).value(); Map global = def.toGlobal(state.getControllerPos()); if (global.containsKey(pos)) { // 非控制器方块被破坏,将多方块标记为未形成(会通知控制器并广播) @@ -275,6 +291,7 @@ public static void shutdownExecutor() { if (asyncExecutor != null) { asyncExecutor.shutdownNow(); try { + // noinspection ResultOfMethodCallIgnored - 为剩余任务等待5秒,无需判断是否完成 asyncExecutor.awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); @@ -310,14 +327,14 @@ public static void checkMultiblockFormed(@Nullable ServerLevel level) { if (checkUnformed) { for (MultiblockState state : manager.multiblocks.values()) { if (!checkFormed && state.isFormed()) continue; - if (manager.pendingChecks.contains(state.getControllerPos().asLong())) continue; + if (manager.pendingChecks.contains(state.getControllerPos())) continue; candidates.add(state); if (candidates.size() >= maxChecks) break; } } else { for (MultiblockState state : manager.multiblocks.values()) { if (!state.isFormed()) continue; - if (manager.pendingChecks.contains(state.getControllerPos().asLong())) continue; + if (manager.pendingChecks.contains(state.getControllerPos())) continue; candidates.add(state); if (candidates.size() >= maxChecks) break; } @@ -329,24 +346,25 @@ public static void checkMultiblockFormed(@Nullable ServerLevel level) { ExecutorService executor = getOrCreateExecutor(); for (MultiblockState mstate : candidates) { MultiblockCheckSnapshot snapshot = buildSnapshot(level, mstate); + mstate.setSnapshot(snapshot); - long posLong = mstate.getControllerPos().asLong(); - manager.pendingChecks.add(posLong); + BlockPos pos = mstate.getControllerPos().immutable(); + manager.pendingChecks.add(pos); executor.submit(() -> { try { boolean formed = snapshot.test(); // 将结果回调到主线程 level.getServer().execute(() -> { - manager.pendingChecks.remove(posLong); + manager.pendingChecks.remove(pos); // 最终一致性验证:确认该 multiblock 仍然注册 - MultiblockState current = manager.multiblocks.get(posLong); + MultiblockState current = manager.multiblocks.get(pos.immutable()); if (current == null) return; manager.updateFormed(level, current, formed); }); } catch (Throwable t) { - LOGGER.error("Async multiblock check failed for pos {}", posLong, t); - level.getServer().execute(() -> manager.pendingChecks.remove(posLong)); + LOGGER.error("Async multiblock check failed for pos {}", pos, t); + level.getServer().execute(() -> manager.pendingChecks.remove(pos.immutable())); } }); } @@ -363,12 +381,21 @@ public static void checkMultiblockFormed(@Nullable ServerLevel level) { * @return 快照;若定义不存在则返回 {@code null} */ private static MultiblockCheckSnapshot buildSnapshot(ServerLevel level, MultiblockState state) { - MultiblockDefinition def = state.getDefinition().value(); + MultiblockCheckSnapshot old = state.getSnapshot(); + MultiblockDefinition def = state.getDefinition(level.registryAccess()).value(); Map global = def.toGlobal(state.getControllerPos()); Map entries = new LinkedHashMap<>(global.size()); for (Map.Entry entry : global.entrySet()) { BlockPos pos = entry.getKey(); BlockStatePredicate predicate = entry.getValue(); + if (!level.isLoaded(pos)) { + MultiblockCheckSnapshot.Entry snapshotEntry = old.entries().get(pos); + if (snapshotEntry == null) { + snapshotEntry = new MultiblockCheckSnapshot.Entry(null, null, predicate); + } + entries.put(pos, snapshotEntry); + continue; + } BlockState blockState = level.getBlockState(pos); CompoundTag entityNbt = null; if (predicate.requiresBlockEntity()) { @@ -379,7 +406,7 @@ private static MultiblockCheckSnapshot buildSnapshot(ServerLevel level, Multiblo } entries.put(pos, new MultiblockCheckSnapshot.Entry(blockState, entityNbt, predicate)); } - return new MultiblockCheckSnapshot(state.getControllerPos().asLong(), entries); + return new MultiblockCheckSnapshot(state.getControllerPos(), entries); } /** @@ -387,7 +414,7 @@ private static MultiblockCheckSnapshot buildSnapshot(ServerLevel level, Multiblo */ private void checkMultiblockFormedSync(Level level, MultiblockState state) { if (level.isClientSide) return; - MultiblockDefinition def = state.getDefinition().value(); + MultiblockDefinition def = state.getDefinition(level.registryAccess()).value(); Map global = def.toGlobal(state.getControllerPos()); boolean ok = true; for (Map.Entry entry : global.entrySet()) { @@ -417,7 +444,7 @@ private static DynamicMultiblockManager load(CompoundTag tag, HolderLookup.Provi ListTag list = tag.getList(TAG_LIST, Tag.TAG_COMPOUND); for (Tag value : list) { MultiblockState mb = MultiblockState.fromTag(Util.cast(value), registries); - manager.multiblocks.put(mb.getControllerPos().asLong(), mb); + manager.multiblocks.put(mb.getControllerPos().immutable(), mb); } } return manager; diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockCheckSnapshot.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockCheckSnapshot.java index 1a2f18d1..621464a4 100644 --- a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockCheckSnapshot.java +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockCheckSnapshot.java @@ -14,11 +14,11 @@ *

快照在主线程上构建,包含所有必要的方块状态与(可选的)序列化 NBT 数据, * 因此工作线程无需访问 {@code Level}。 * - * @param controllerPosLong 控制器位置({@link BlockPos#asLong()} 编码) - * @param entries 每个检测位置对应的快照条目(位置 → 条目) + * @param controllerPos 控制器位置 + * @param entries 每个检测位置对应的快照条目(位置 → 条目) */ public record MultiblockCheckSnapshot( - long controllerPosLong, + BlockPos controllerPos, Map entries ) { @@ -29,7 +29,7 @@ public record MultiblockCheckSnapshot( */ public boolean test() { for (Entry entry : entries.values()) { - if (!entry.predicate().testOffThread(entry.blockState(), entry.entityNbt())) { + if (!entry.test()) { return false; } } @@ -44,10 +44,15 @@ public boolean test() { * @param predicate 应用于此位置的谓词 */ public record Entry( - BlockState blockState, + @Nullable BlockState blockState, @Nullable CompoundTag entityNbt, BlockStatePredicate predicate ) { + public boolean test() { + if (this.blockState == null) { + return true; + } + return this.predicate.testOffThread(this.blockState, this.entityNbt); + } } } - diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockState.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockState.java index 79a73763..42e3ebd5 100644 --- a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockState.java +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockState.java @@ -4,7 +4,8 @@ import dev.anvilcraft.lib.v2.codec.StreamCodecUtil; import dev.anvilcraft.lib.v2.multiblock.dynamic.definition.MultiblockDefinition; import dev.anvilcraft.lib.v2.multiblock.init.LibRegistries; -import dev.anvilcraft.lib.v2.util.Util; +import io.netty.buffer.ByteBuf; +import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import net.minecraft.core.BlockPos; @@ -14,44 +15,49 @@ import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.Tag; import net.minecraft.network.RegistryFriendlyByteBuf; -import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceKey; +import org.jspecify.annotations.Nullable; + +import java.util.Map; @Getter @Setter public class MultiblockState { + public static final Codec> DEFINITION_KEY_CODEC = ResourceKey.codec(LibRegistries.DEFINITIONS_KEY); + public static final StreamCodec> DEFINITION_KEY_STREAM_CODEC = ResourceKey.streamCodec( + LibRegistries.DEFINITIONS_KEY + ); public static final StreamCodec STREAM_CODEC = StreamCodec.composite( StreamCodecUtil.VAR_INT_BLOCK_POS, MultiblockState::getControllerPos, - ByteBufCodecs.holder(LibRegistries.DEFINITIONS_KEY, MultiblockDefinition.STREAM_CODEC), - MultiblockState::getDefinition, + DEFINITION_KEY_STREAM_CODEC, + MultiblockState::getDefinitionKey, MultiblockState::new ); - public static final Codec> DEFINITION_KEY_CODEC = ResourceKey.codec(LibRegistries.DEFINITIONS_KEY); private final BlockPos controllerPos; - private final Holder.Reference definition; + private final ResourceKey definitionKey; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private Holder.@Nullable Reference definition; private boolean formed; + private MultiblockCheckSnapshot snapshot; - public MultiblockState(BlockPos controllerPos, Holder definition) { - this( - controllerPos, - definition instanceof Holder.Reference ref - ? ref - : Util.throwE(new IllegalArgumentException("Non Reference Holder '" + definition + "' found")), - false - ); + public MultiblockState(BlockPos controllerPos, ResourceKey definitionKey) { + this(controllerPos, definitionKey, false); } - public MultiblockState(BlockPos controllerPos, Holder.Reference definition, boolean formed) { + public MultiblockState(BlockPos controllerPos, ResourceKey definitionKey, boolean formed) { this.controllerPos = controllerPos; - this.definition = definition; + this.definitionKey = definitionKey; this.formed = formed; + this.snapshot = new MultiblockCheckSnapshot(this.controllerPos, Map.of()); } - public ResourceKey getDefinitionKey() { - return this.definition.key(); + public Holder.Reference getDefinition(HolderLookup.Provider registries) { + if (this.definition != null) return this.definition; + return this.definition = registries.lookup(LibRegistries.DEFINITIONS_KEY).orElseThrow().getOrThrow(this.definitionKey); } public Tag toTag(HolderLookup.Provider registries) { @@ -79,16 +85,11 @@ public static MultiblockState fromTag(CompoundTag tag, HolderLookup.Provider reg registries.createSerializationContext(NbtOps.INSTANCE), tag.get("controllerPos") ).getOrThrow().getFirst(); - Holder.Reference definition = registries.lookup(LibRegistries.DEFINITIONS_KEY).orElseThrow().getOrThrow( - MultiblockState.DEFINITION_KEY_CODEC.decode( - registries.createSerializationContext(NbtOps.INSTANCE), - tag.get("definition") - ).getOrThrow().getFirst() - ); + ResourceKey definitionKey = MultiblockState.DEFINITION_KEY_CODEC.decode( + registries.createSerializationContext(NbtOps.INSTANCE), + tag.get("definition") + ).getOrThrow().getFirst(); boolean formed = tag.getBoolean("formed"); - return new MultiblockState(controllerPos, definition, formed); + return new MultiblockState(controllerPos, definitionKey, formed); } } - - - diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/event/DynamicMultiblockEvent.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/event/DynamicMultiblockEvent.java new file mode 100644 index 00000000..a2de2c9e --- /dev/null +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/event/DynamicMultiblockEvent.java @@ -0,0 +1,44 @@ +package dev.anvilcraft.lib.v2.multiblock.dynamic.event; + +import dev.anvilcraft.lib.v2.multiblock.dynamic.MultiblockState; +import dev.anvilcraft.lib.v2.multiblock.dynamic.controller.IController; +import lombok.Getter; +import net.minecraft.world.level.Level; +import net.neoforged.bus.api.Event; +import net.neoforged.bus.api.ICancellableEvent; + +/// 所有与动态多方块结构有关的事件的基类。 +@Getter +public abstract class DynamicMultiblockEvent extends Event { + private final Level level; + private final IController controller; + private final MultiblockState state; + + protected DynamicMultiblockEvent(Level level, IController controller, MultiblockState state) { + this.level = level; + this.controller = controller; + this.state = state; + } + + /// 本事件会在多方块结构尝试形成时发出。 + /// + /// 取消该事件将阻止多方块结构的形成。 + /// + /// 本事件会在双端发出。 + public static class Form extends DynamicMultiblockEvent implements ICancellableEvent { + public Form(Level level, IController controller, MultiblockState state) { + super(level, controller, state); + } + } + + /// 本事件会在多方块结构尝试解散时发出。 + /// + /// 取消该事件将阻止多方块结构的解散。 + /// + /// 本事件会在双端发出。 + public static class Unform extends DynamicMultiblockEvent implements ICancellableEvent { + public Unform(Level level, IController controller, MultiblockState state) { + super(level, controller, state); + } + } +} diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/network/MultiblockFormPacket.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/network/MultiblockFormPacket.java index 41df5898..a61cdfbb 100644 --- a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/network/MultiblockFormPacket.java +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/network/MultiblockFormPacket.java @@ -4,6 +4,8 @@ import dev.anvilcraft.lib.v2.multiblock.dynamic.DynamicMultiblockManager; import dev.anvilcraft.lib.v2.multiblock.dynamic.MultiblockState; import dev.anvilcraft.lib.v2.multiblock.dynamic.controller.ControllerRecord; +import dev.anvilcraft.lib.v2.multiblock.dynamic.controller.IController; +import dev.anvilcraft.lib.v2.multiblock.dynamic.event.DynamicMultiblockEvent; import dev.anvilcraft.lib.v2.network.packet.IClientboundPacket; import dev.anvilcraft.lib.v2.network.packet.IPacket; import lombok.extern.slf4j.Slf4j; @@ -14,6 +16,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.neoforge.common.NeoForge; @Slf4j @SuppressWarnings("unused") @@ -38,9 +41,16 @@ public void handleOnClient(Player player) { manager.updateFormed(level, this.state, true); BlockPos pos = this.state.getControllerPos(); BlockState state = level.getBlockState(pos); - if (!this.state.getDefinition().value().isController(level, state, level.getBlockEntity(pos))) return; + if (!this.state.getDefinition(level.registryAccess()).value().isController(level, state, level.getBlockEntity(pos))) return; try { - ControllerRecord.get(state.getBlock(), this.state.getDefinitionKey().location()).onFormed(level, this.state); + IController controller = ControllerRecord.get(state.getBlock(), this.state.getDefinitionKey().location()); + DynamicMultiblockEvent.Form event = new DynamicMultiblockEvent.Form(level, controller, this.state); + NeoForge.EVENT_BUS.post(event); + if (!event.isCanceled()) { + controller.onFormed(level, this.state); + } else { + this.state.setFormed(false); + } } catch (IllegalArgumentException e) { log.error(e.getLocalizedMessage(), e); throw e; diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/network/MultiblockUnformPacket.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/network/MultiblockUnformPacket.java index 040f9fa3..82fe2dee 100644 --- a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/network/MultiblockUnformPacket.java +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/network/MultiblockUnformPacket.java @@ -4,6 +4,8 @@ import dev.anvilcraft.lib.v2.multiblock.dynamic.DynamicMultiblockManager; import dev.anvilcraft.lib.v2.multiblock.dynamic.MultiblockState; import dev.anvilcraft.lib.v2.multiblock.dynamic.controller.ControllerRecord; +import dev.anvilcraft.lib.v2.multiblock.dynamic.controller.IController; +import dev.anvilcraft.lib.v2.multiblock.dynamic.event.DynamicMultiblockEvent; import dev.anvilcraft.lib.v2.network.packet.IClientboundPacket; import dev.anvilcraft.lib.v2.network.packet.IPacket; import lombok.extern.slf4j.Slf4j; @@ -14,6 +16,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.neoforge.common.NeoForge; @Slf4j @SuppressWarnings("unused") @@ -38,9 +41,16 @@ public void handleOnClient(Player player) { manager.updateFormed(level, this.state, false); BlockPos pos = this.state.getControllerPos(); BlockState state = level.getBlockState(pos); - if (!this.state.getDefinition().value().isController(level, state, level.getBlockEntity(pos))) return; + if (!this.state.getDefinition(level.registryAccess()).value().isController(level, state, level.getBlockEntity(pos))) return; try { - ControllerRecord.get(state.getBlock(), this.state.getDefinitionKey().location()).onUnformed(level, this.state); + IController controller = ControllerRecord.get(state.getBlock(), this.state.getDefinitionKey().location()); + DynamicMultiblockEvent.Unform event = new DynamicMultiblockEvent.Unform(level, controller, this.state); + NeoForge.EVENT_BUS.post(event); + if (!event.isCanceled()) { + controller.onUnformed(level, this.state); + } else { + this.state.setFormed(true); + } } catch (IllegalArgumentException e) { log.error(e.getLocalizedMessage(), e); throw e; From 142e329608388540fa095420244589e5474b7354 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 11 Aug 2026 00:30:12 +0800 Subject: [PATCH 17/38] =?UTF-8?q?feat(wheel):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=E7=8E=AF=E5=BD=A2=E6=89=87=E5=8C=BA=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回移植 dev/26.1 module.wheel 的环形扇区选择效果(5d0b285、46e1b73、8a818f0): - api/WheelSelectionEffect(DOT/ANNULAR_SECTOR 两种形态 + normalizePositiveAngle 角度归一化,纯逻辑原样搬);WheelMenuBuilder 链式 selectionEffect()/ selectionEffectColor();WheelMenuModel 两个新 of(...) 重载 - WheelWidget.renderSelectionEffect/renderProgressAnimation/renderAnnularSectorSelection: 渲染落在 port 既有 Tesselator/ShaderInstance/LibShaders 体系(QUADS + RenderSystem.setShader + safeGetUniform 逐个 set + BufferUploader.drawWithShader), shader 为空走多边形回退;移除固定 selectionEffectRadius 参数(动态尺寸) - annular_sector.fsh/json 落位注册(去掉 26.1 UBO 块,改 port 惯例独立 uniform) 切断 dev 侧 wheel 对 anvillib-rendering 的 jarJar 依赖(port 不引入 rendering)。 不迁移:gui/render/state 三件套、LibRenders、LibDynamicUniforms(26.1 渲染管线)。 --- .../lib/v2/wheel/api/WheelMenuBuilder.java | 20 +- .../lib/v2/wheel/api/WheelMenuModel.java | 44 ++- .../v2/wheel/api/WheelSelectionEffect.java | 6 + .../client/gui/component/WheelWidget.java | 265 ++++++++++++++---- .../wheel/client/gui/screen/WheelScreen.java | 2 + .../lib/v2/wheel/client/init/LibShaders.java | 10 + .../anvillib/shaders/core/annular_sector.fsh | 55 ++++ .../anvillib/shaders/core/annular_sector.json | 68 +++++ 8 files changed, 411 insertions(+), 59 deletions(-) create mode 100644 module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelSelectionEffect.java create mode 100644 module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.fsh create mode 100644 module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.json diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelMenuBuilder.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelMenuBuilder.java index 82fac9ae..e373716e 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelMenuBuilder.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelMenuBuilder.java @@ -11,6 +11,8 @@ public final class WheelMenuBuilder { private final List entries = new ArrayList<>(); private int slotsPerPage = WheelMenuModel.DEFAULT_SLOTS_PER_PAGE; private int deadZone = 15; + private WheelSelectionEffect selectionEffect = WheelSelectionEffect.ANNULAR_SECTOR; + private int selectionEffectColor = WheelMenuModel.DEFAULT_SELECTION_EFFECT_COLOR; private WheelMenuBuilder() { } @@ -32,6 +34,16 @@ public WheelMenuBuilder deadZone(int deadZone){ return this; } + public WheelMenuBuilder selectionEffect(WheelSelectionEffect selectionEffect) { + this.selectionEffect = Objects.requireNonNull(selectionEffect, "selectionEffect"); + return this; + } + + public WheelMenuBuilder selectionEffectColor(int selectionEffectColor) { + this.selectionEffectColor = selectionEffectColor; + return this; + } + public WheelMenuBuilder action( String id, Component label, @@ -75,7 +87,13 @@ public WheelMenuBuilder submenu( } public WheelMenuModel build() { - return WheelMenuModel.of(List.copyOf(this.entries), this.slotsPerPage, this.deadZone); + return WheelMenuModel.of( + List.copyOf(this.entries), + this.slotsPerPage, + this.deadZone, + this.selectionEffect, + this.selectionEffectColor + ); } public static final class WheelSubmenuBuilder { diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelMenuModel.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelMenuModel.java index 3885e50a..b3fd3d4b 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelMenuModel.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelMenuModel.java @@ -6,26 +6,56 @@ public final class WheelMenuModel { public static final int DEFAULT_SLOTS_PER_PAGE = 8; + public static final int DEFAULT_SELECTION_EFFECT_COLOR = 0xFFFABC02; private final List rootEntries; private final int slotsPerPage; private final int deadZone; + private final WheelSelectionEffect selectionEffect; + private final int selectionEffectColor; - private WheelMenuModel(List rootEntries, int slotsPerPage, int deadZone) { + private WheelMenuModel( + List rootEntries, + int slotsPerPage, + int deadZone, + WheelSelectionEffect selectionEffect, + int selectionEffectColor + ) { if (slotsPerPage < 1) { throw new IllegalArgumentException("slotsPerPage must be >= 1"); } this.rootEntries = List.copyOf(Objects.requireNonNull(rootEntries, "rootEntries")); this.slotsPerPage = slotsPerPage; this.deadZone = deadZone; + this.selectionEffect = Objects.requireNonNull(selectionEffect, "selectionEffect"); + this.selectionEffectColor = selectionEffectColor; } public static WheelMenuModel of(List rootEntries) { - return new WheelMenuModel(rootEntries, DEFAULT_SLOTS_PER_PAGE, 30); + return new WheelMenuModel(rootEntries, DEFAULT_SLOTS_PER_PAGE, 30, WheelSelectionEffect.DOT, DEFAULT_SELECTION_EFFECT_COLOR); } public static WheelMenuModel of(List rootEntries, int slotsPerPage, int deadZone) { - return new WheelMenuModel(rootEntries, slotsPerPage, deadZone); + return new WheelMenuModel(rootEntries, slotsPerPage, deadZone, WheelSelectionEffect.DOT, DEFAULT_SELECTION_EFFECT_COLOR); + } + + public static WheelMenuModel of( + List rootEntries, + int slotsPerPage, + int deadZone, + WheelSelectionEffect selectionEffect + ) { + return new WheelMenuModel(rootEntries, slotsPerPage, deadZone, selectionEffect, DEFAULT_SELECTION_EFFECT_COLOR); + } + + public static WheelMenuModel of( + List rootEntries, + int slotsPerPage, + int deadZone, + WheelSelectionEffect selectionEffect, + int selectionEffectColor + ) { + return new WheelMenuModel(rootEntries, slotsPerPage, deadZone, selectionEffect, selectionEffectColor); } public List rootEntries() { @@ -40,6 +70,14 @@ public int deadZone() { return this.deadZone; } + public WheelSelectionEffect selectionEffect() { + return this.selectionEffect; + } + + public int selectionEffectColor() { + return this.selectionEffectColor; + } + public int pageCount(List entries) { List safeEntries = Objects.requireNonNull(entries, "entries"); return WheelPagination.pageCount(safeEntries.size(), this.slotsPerPage); diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelSelectionEffect.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelSelectionEffect.java new file mode 100644 index 00000000..4b3e32bb --- /dev/null +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelSelectionEffect.java @@ -0,0 +1,6 @@ +package dev.anvilcraft.lib.v2.wheel.api; + +public enum WheelSelectionEffect { + DOT, + ANNULAR_SECTOR +} diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java index e1dcaf2d..60f2f0ff 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java @@ -9,6 +9,7 @@ import com.mojang.blaze3d.vertex.Tesselator; import com.mojang.blaze3d.vertex.VertexFormat; import dev.anvilcraft.lib.v2.util.MathUtil; +import dev.anvilcraft.lib.v2.wheel.api.WheelSelectionEffect; import dev.anvilcraft.lib.v2.wheel.client.init.LibShaders; import lombok.Getter; import lombok.Setter; @@ -29,6 +30,10 @@ public class WheelWidget extends AbstractWidget { public static final int IGNORE_CURSOR_MOVE_LENGTH = 15; + public static final int DEFAULT_SELECTION_EFFECT_COLOR = 0xFFFABC02; + private static final float SELECTION_DOT_DIAMETER_RATIO = 0.9f; + private static final float TAU = (float) (Math.PI * 2.0); + private static final float ANGLE_AA_RAD = 0.06f; private static final float RING_Z = 60f; private static final float SELECTION_Z = 80f; private static final Vector2f ROTATION_START = new Vector2f(0, 1); @@ -41,8 +46,7 @@ public class WheelWidget extends AbstractWidget { private final int animationMs; // ms private final int closingAnimationMs; // ms private final int ringColor; - private final int selectionEffectColor; - private final int selectionEffectRadius; + private int selectionEffectColor; private final float selectionAnimationSpeedFactor; private final int textColor; private final float textScale; @@ -58,6 +62,7 @@ public class WheelWidget extends AbstractWidget { @Setter private boolean closingAnimationStarted = false; private final int deadZone; + private WheelSelectionEffect selectionEffect = WheelSelectionEffect.DOT; public WheelWidget( int x, int y, int width, int height, @@ -93,7 +98,7 @@ public WheelWidget( ringInnerRadius, ringOuterRadius, 150, 300, 150, 0x88000000, - 0xddffff00, 20, 5f, + DEFAULT_SELECTION_EFFECT_COLOR, 5f, 0xfdfdfd, textScale, degreeOffsetAngle, sections, deadZone ); @@ -109,7 +114,7 @@ public WheelWidget( ringInnerRadius, ringOuterRadius, 150, 300, 150, 0x88000000, - 0xddffff00, 20, 5f, + DEFAULT_SELECTION_EFFECT_COLOR, 5f, 0xfdfdfd, 1f, 0f, sections, deadZone ); @@ -125,7 +130,7 @@ public WheelWidget( ringInnerRadius, ringOuterRadius, 150, 300, 150, 0x88000000, - 0xddffff00, 20, 5f, + DEFAULT_SELECTION_EFFECT_COLOR, 5f, 0xfdfdfd, 1f, degreeOffsetAngle, sections, deadZone ); @@ -136,7 +141,7 @@ public WheelWidget( float ringInnerRadius, float ringOuterRadius, int delay, int animationMs, int closingAnimationMs, int ringColor, - int selectionEffectColor, int selectionEffectRadius, float selectionAnimationSpeedFactor, + int selectionEffectColor, float selectionAnimationSpeedFactor, int textColor, float textScale, List sections, int deadZone ) { @@ -145,7 +150,7 @@ public WheelWidget( ringInnerRadius, ringOuterRadius, delay, animationMs, closingAnimationMs, ringColor, - selectionEffectColor, selectionEffectRadius, selectionAnimationSpeedFactor, + selectionEffectColor, selectionAnimationSpeedFactor, textColor, textScale, 0f, sections, deadZone ); @@ -156,7 +161,7 @@ public WheelWidget( float ringInnerRadius, float ringOuterRadius, int delay, int animationMs, int closingAnimationMs, int ringColor, - int selectionEffectColor, int selectionEffectRadius, float selectionAnimationSpeedFactor, + int selectionEffectColor, float selectionAnimationSpeedFactor, int textColor, float textScale, float degreeOffsetAngle, List sections, int deadZone ) { @@ -169,7 +174,6 @@ public WheelWidget( this.closingAnimationMs = closingAnimationMs; this.ringColor = ringColor; this.selectionEffectColor = selectionEffectColor; - this.selectionEffectRadius = selectionEffectRadius; this.selectionAnimationSpeedFactor = selectionAnimationSpeedFactor; this.textColor = textColor; this.textScale = textScale; @@ -194,11 +198,17 @@ public WheelWidget( section )); } - this.selectionEffectPos = MathUtil.rotate( - MathUtil.copy(ROTATION_START) - .mul(this.getSectionCircleDiameter()), - this.currentAngle - ); + this.selectionEffectPos = MathUtil.rotate(MathUtil.copy(ROTATION_START).mul(this.getSectionCircleDiameter()), this.currentAngle); + } + + public WheelWidget setSelectionEffect(WheelSelectionEffect selectionEffect) { + this.selectionEffect = Objects.requireNonNull(selectionEffect, "selectionEffect"); + return this; + } + + public WheelWidget setSelectionEffectColor(int selectionEffectColor) { + this.selectionEffectColor = selectionEffectColor; + return this; } public WheelWidget setCurrentIndex(int index) { @@ -206,11 +216,7 @@ public WheelWidget setCurrentIndex(int index) { if (!this.sections.get(index).selectable()) return this; this.currentSectionIndex = index; this.currentAngle = this.sections.get(index).angle; - this.selectionEffectPos = MathUtil.rotate( - MathUtil.copy(ROTATION_START) - .mul(this.getSectionCircleDiameter()), - this.currentAngle - ); + this.selectionEffectPos = MathUtil.rotate(MathUtil.copy(ROTATION_START).mul(this.getSectionCircleDiameter()), this.currentAngle); return this; } @@ -344,8 +350,9 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo var renderer = value.renderer(); if (renderer != null) { poseStack.pushPose(); - poseStack.translate(x - 10, y - 10, 100); - renderer.render(guiGraphics, poseStack, 20, 20); + poseStack.translate(x, y, 100); + int renderSize = this.getRendererSize(this.ringOuterRadius - this.ringInnerRadius); + renderer.render(guiGraphics, poseStack, renderSize, renderSize); poseStack.popPose(); } poseStack.pushPose(); @@ -353,14 +360,13 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo float offsetX = 0.1f * this.width; float offsetY = 0.1f * this.height; float adjustedX = (x - offsetX) / coordinateScale; - float adjustedY = (y - offsetY - 20 * this.textScale) / coordinateScale; + float adjustedY = renderer == null + ? (y - offsetY - (minecraft.font.lineHeight * this.textScale) / 2.0f) / coordinateScale + : (y - offsetY - 20 * this.textScale) / coordinateScale; poseStack.translate(offsetX, offsetY, 0); poseStack.scale(coordinateScale, coordinateScale, coordinateScale); poseStack.translate(adjustedX, adjustedY, 0); - if (renderer == null) { - poseStack.translate(0, ((this.selectionEffectRadius / 2.0f) + minecraft.font.lineHeight) * this.textScale, 0); - } poseStack.scale(this.textScale / coordinateScale, this.textScale / coordinateScale, this.textScale / coordinateScale); guiGraphics.drawCenteredString( minecraft.font, @@ -400,18 +406,33 @@ private void renderProgressAnimation(GuiGraphics guiGraphics, float progress) { ); poseStack.popPose(); if (this.currentSectionIndex != -1) { - WheelSection section = this.sections.get(this.currentSectionIndex); - Vector2f center = new Vector2f( - (section.center.x - this.centerPos.x) / this.getSectionCircleDiameter(), - (section.center.y - this.centerPos.y) / this.getSectionCircleDiameter() - ).mul(this.getSectionCircleDiameter() * progress).add(this.centerPos.x, this.centerPos.y); - WheelWidget.renderSelectionEffect( - guiGraphics, - center.x, - center.y, - this.selectionEffectColor, - this.selectionEffectRadius - ); + if (this.selectionEffect == WheelSelectionEffect.ANNULAR_SECTOR) { + WheelSection section = this.sections.get(this.currentSectionIndex); + float rangeAngle = this.normalizePositiveAngle(section.angleEnd - section.angleStart) / 2.0f; + this.renderAnnularSectorSelection( + guiGraphics, + this.centerPos.x, + this.centerPos.y, + this.selectionEffectColor, + this.ringInnerRadius * 2 * progress, + this.ringOuterRadius * 2 * progress, + section.angle, + rangeAngle + ); + } else { + WheelSection section = this.sections.get(this.currentSectionIndex); + Vector2f center = new Vector2f( + (section.center.x - this.centerPos.x) / this.getSectionCircleDiameter(), + (section.center.y - this.centerPos.y) / this.getSectionCircleDiameter() + ).mul(this.getSectionCircleDiameter() * progress).add(this.centerPos.x, this.centerPos.y); + WheelWidget.renderSelectionEffect( + guiGraphics, + center.x, + center.y, + this.selectionEffectColor, + this.getSelectionDotDiameter((this.ringOuterRadius - this.ringInnerRadius) * progress) + ); + } } for (WheelSection value : this.sections) { Vector2f center = new Vector2f( @@ -423,8 +444,9 @@ private void renderProgressAnimation(GuiGraphics guiGraphics, float progress) { var renderer = value.renderer(); if (renderer != null) { poseStack.pushPose(); - poseStack.translate(x - 10, y - 10, 100); - renderer.render(guiGraphics, poseStack, 20, 20); + poseStack.translate(x, y, 100); + int renderSize = this.getRendererSize((this.ringOuterRadius - this.ringInnerRadius) * progress); + renderer.render(guiGraphics, poseStack, renderSize, renderSize); poseStack.popPose(); } final int textAlpha = (int) (progress * 0xff) << 24; @@ -433,14 +455,13 @@ private void renderProgressAnimation(GuiGraphics guiGraphics, float progress) { float offsetX = 0.1f * this.width; float offsetY = 0.1f * this.height; float adjustedX = (x - offsetX) / coordinateScale; - float adjustedY = (y - offsetY - 20 * this.textScale) / coordinateScale; + float adjustedY = renderer == null + ? (y - offsetY - (minecraft.font.lineHeight * this.textScale) / 2.0f) / coordinateScale + : (y - offsetY - 20 * this.textScale) / coordinateScale; poseStack.translate(offsetX, offsetY, 0); poseStack.scale(coordinateScale, coordinateScale, coordinateScale); poseStack.translate(adjustedX, adjustedY, 0); - if (renderer == null) { - poseStack.translate(0, ((this.selectionEffectRadius / 2.0f) + minecraft.font.lineHeight) * this.textScale, 0); - } poseStack.scale(this.textScale / coordinateScale, this.textScale / coordinateScale, this.textScale / coordinateScale); guiGraphics.drawCenteredString( this.minecraft.font, @@ -454,10 +475,11 @@ private void renderProgressAnimation(GuiGraphics guiGraphics, float progress) { } private void renderSelection(GuiGraphics guiGraphics) { - float selectionEffectAngle = MathUtil.angle( - MathUtil.copy(ROTATION_START), - this.selectionEffectPos - ); + if (this.selectionEffect == WheelSelectionEffect.ANNULAR_SECTOR) { + this.renderSelectionAnnularSector(guiGraphics); + return; + } + float selectionEffectAngle = MathUtil.angle(MathUtil.copy(ROTATION_START), this.selectionEffectPos); float diffAngle = this.currentAngle - selectionEffectAngle; @@ -467,24 +489,47 @@ private void renderSelection(GuiGraphics guiGraphics) { diffAngle += (float) (Math.PI * 2); } - this.selectionEffectPos = MathUtil.rotate( - this.selectionEffectPos, - diffAngle / this.selectionAnimationSpeedFactor - ); + this.selectionEffectPos = MathUtil.rotate(this.selectionEffectPos, diffAngle / this.selectionAnimationSpeedFactor); - Vector2f pos = MathUtil.copy(this.selectionEffectPos) - .mul(1, -1) - .add(this.centerPos); + Vector2f pos = MathUtil.copy(this.selectionEffectPos).mul(1, -1).add(this.centerPos); WheelWidget.renderSelectionEffect( guiGraphics, pos.x, pos.y, this.selectionEffectColor, - this.selectionEffectRadius + this.getSelectionDotDiameter(this.ringOuterRadius - this.ringInnerRadius) ); } + private void renderSelectionAnnularSector(GuiGraphics guiGraphics) { + WheelSection currentSection = this.sections.get(this.currentSectionIndex); + float rangeAngle = this.normalizePositiveAngle(currentSection.angleEnd - currentSection.angleStart) / 2.0f; + this.renderAnnularSectorSelection( + guiGraphics, + this.centerPos.x, + this.centerPos.y, + this.selectionEffectColor, + this.ringInnerRadius * 2, + this.ringOuterRadius * 2, + currentSection.angle, + rangeAngle + ); + } + + private float normalizePositiveAngle(float angle) { + float normalized = angle % TAU; + return normalized < 0 ? normalized + TAU : normalized; + } + + private float getSelectionDotDiameter(float ringWidth) { + return ringWidth * SELECTION_DOT_DIAMETER_RATIO; + } + + private int getRendererSize(float ringWidth) { + return Math.max(1, Math.round(this.getSelectionDotDiameter(ringWidth))); + } + public void onClosing() { if (this.shouldRender() && !this.closingAnimationStarted) { this.displayTime = System.currentTimeMillis(); @@ -628,6 +673,82 @@ public static void renderSelectionEffect( RenderSystem.enableDepthTest(); } + public void renderAnnularSectorSelection( + GuiGraphics guiGraphics, + float centerX, + float centerY, + int color, + float innerDiameter, + float outerDiameter, + float centerAngleRad, + float rangeAngleRad + ) { + float outerRadius = outerDiameter + 5; + float x1 = centerX - outerRadius; + float y1 = centerY - outerRadius; + float x2 = centerX + outerRadius; + float y2 = centerY + outerRadius; + PoseStack poseStack = guiGraphics.pose(); + Matrix4f matrix4f = poseStack.last().pose(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.begin( + VertexFormat.Mode.QUADS, + DefaultVertexFormat.POSITION_COLOR + ); + bufferBuilder.addVertex(matrix4f, x1, y1, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x1, y2, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x2, y2, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x2, y1, SELECTION_Z).setColor(color); + + Window window = Minecraft.getInstance().getWindow(); + float guiScale = (float) window.getGuiScale(); + RenderSystem.disableDepthTest(); + ShaderInstance annularSectorShader = LibShaders.getAnnularSectorShader(); + if (annularSectorShader == null) { + renderAnnularSectorFallback( + guiGraphics, + centerX, + centerY, + color, + innerDiameter, + outerDiameter, + centerAngleRad, + rangeAngleRad + ); + RenderSystem.enableDepthTest(); + return; + } + RenderSystem.setShader(() -> annularSectorShader); + + // Wheel section angle uses "up" as zero; shader atan uses +X as zero. + float shaderCenterAngle = centerAngleRad + TAU / 4.0f; + annularSectorShader + .safeGetUniform("Center") + .set(centerX * guiScale, centerY * guiScale); + annularSectorShader + .safeGetUniform("InnerDiameter") + .set(innerDiameter * guiScale); + annularSectorShader + .safeGetUniform("OuterDiameter") + .set(outerDiameter * guiScale); + annularSectorShader + .safeGetUniform("AntiAliasingRadius") + .set(1.25f); + annularSectorShader + .safeGetUniform("AngleAntiAliasingRad") + .set(ANGLE_AA_RAD); + annularSectorShader + .safeGetUniform("CenterAngleRad") + .set(shaderCenterAngle); + annularSectorShader + .safeGetUniform("RangeAngleRad") + .set(rangeAngleRad); + + RenderSystem.setShaderColor(1, 1, 1, 1); + BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); + RenderSystem.enableDepthTest(); + } + private static void renderRingFallback( GuiGraphics guiGraphics, float centerX, @@ -668,6 +789,40 @@ private static void renderSelectionFallback( renderDisc(guiGraphics, centerX, centerY, radius, color); } + private static void renderAnnularSectorFallback( + GuiGraphics guiGraphics, + float centerX, + float centerY, + int color, + float innerDiameter, + float outerDiameter, + float centerAngleRad, + float rangeAngleRad + ) { + PoseStack poseStack = guiGraphics.pose(); + Matrix4f matrix4f = poseStack.last().pose(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.begin( + VertexFormat.Mode.TRIANGLE_STRIP, + DefaultVertexFormat.POSITION_COLOR + ); + float innerRadius = innerDiameter / 2f; + float outerRadius = outerDiameter / 2f; + int segments = 48; + float startAngle = centerAngleRad - rangeAngleRad; + float sweepAngle = rangeAngleRad * 2; + for (int i = 0; i <= segments; i++) { + float angle = startAngle + sweepAngle * i / segments; + float sin = (float) Math.sin(angle); + float cos = (float) Math.cos(angle); + bufferBuilder.addVertex(matrix4f, centerX + cos * outerRadius, centerY + sin * outerRadius, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, centerX + cos * innerRadius, centerY + sin * innerRadius, SELECTION_Z).setColor(color); + } + RenderSystem.setShader(GameRenderer::getPositionColorShader); + RenderSystem.setShaderColor(1, 1, 1, 1); + BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); + } + private static void renderDisc( GuiGraphics guiGraphics, float centerX, diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java index 6fca34e5..e8bee91b 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java @@ -178,6 +178,8 @@ private void rebuildWheelWidget() { sections, this.model.deadZone() ); + this.wheelWidget.setSelectionEffectColor(this.model.selectionEffectColor()); + this.wheelWidget.setSelectionEffect(this.model.selectionEffect()); this.wheelWidget.clearSelection(); } diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/LibShaders.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/LibShaders.java index 8544e297..3eb7e314 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/LibShaders.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/LibShaders.java @@ -20,6 +20,8 @@ public class LibShaders { static @Nullable ShaderInstance ringShader; @Getter static @Nullable ShaderInstance selectionShader; + @Getter + static @Nullable ShaderInstance annularSectorShader; @SubscribeEvent public static void register(RegisterShadersEvent event) { @@ -40,6 +42,14 @@ public static void register(RegisterShadersEvent event) { ), it -> selectionShader = it ); + event.registerShader( + new ShaderInstance( + event.getResourceProvider(), + AnvilLibWheel.of("annular_sector"), + DefaultVertexFormat.POSITION_COLOR + ), + it -> annularSectorShader = it + ); } catch (IOException e) { log.error(e.getLocalizedMessage(), e); } diff --git a/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.fsh b/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.fsh new file mode 100644 index 00000000..dd7d0bf3 --- /dev/null +++ b/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.fsh @@ -0,0 +1,55 @@ +#version 150 + +in vec4 vertexColor; + +uniform vec4 ColorModulator; +uniform vec2 Center; +uniform float InnerDiameter; +uniform float OuterDiameter; +uniform float AntiAliasingRadius; +uniform float AngleAntiAliasingRad; +uniform float CenterAngleRad; +uniform float RangeAngleRad; + +out vec4 fragColor; + +const float PI = 3.14159265358979323846; +const float TAU = 2.0 * PI; + +float normalizeAngleP(float angle) { + float normalized = mod(angle, TAU); + return normalized < 0.0 ? normalized + TAU : normalized; +} + +float normalizeAnglePN(float angle) { + float normalized = mod(angle, TAU); + if (normalized < -PI) { + return normalized + TAU; + } else if (normalized > PI) { + return normalized - TAU; + } else { + return normalized; + } +} + +float calcAngleAlpha(float posAngle, float centerAngle, float rangeAngle, float aa) { + float center = normalizeAngleP(centerAngle); + float dist = abs(normalizeAnglePN(normalizeAngleP(posAngle) - center)); + float angleAa = max(min(aa, fwidth(dist) * 1.5), 0.0001); + + return 1.0 - smoothstep(rangeAngle - angleAa, rangeAngle + angleAa, dist); +} + +void main() { + vec2 fragPos = gl_FragCoord.xy; + vec4 color = vertexColor; + float distance = distance(fragPos, Center); + float angle = atan(fragPos.y - Center.y, fragPos.x - Center.x); + float radialAa = max(min(AntiAliasingRadius, fwidth(distance) * 1.5), 0.0001); + + color.a *= smoothstep(InnerDiameter - radialAa, InnerDiameter + radialAa, distance) + * (1.0 - smoothstep(OuterDiameter - radialAa, OuterDiameter + radialAa, distance)) + * calcAngleAlpha(angle, CenterAngleRad, RangeAngleRad, AngleAntiAliasingRad); + + fragColor = color * ColorModulator; +} diff --git a/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.json b/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.json new file mode 100644 index 00000000..724ab05a --- /dev/null +++ b/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.json @@ -0,0 +1,68 @@ +{ + "vertex": "position_color", + "fragment": "anvillib:annular_sector", + "samplers": [ + ], + "uniforms": [ + { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] }, + { + "name": "Center", + "type": "float", + "count": 2, + "values": [ + 0.0, + 0.0 + ] + }, + { + "name": "InnerDiameter", + "type": "float", + "count": 1, + "values": [ + 0.0 + ] + }, + { + "name": "OuterDiameter", + "type": "float", + "count": 1, + "values": [ + 0.0 + ] + }, + { + "name": "AntiAliasingRadius", + "type": "float", + "count": 1, + "values": [ + 1.25 + ] + }, + { + "name": "AngleAntiAliasingRad", + "type": "float", + "count": 1, + "values": [ + 0.06 + ] + }, + { + "name": "CenterAngleRad", + "type": "float", + "count": 1, + "values": [ + 0.0 + ] + }, + { + "name": "RangeAngleRad", + "type": "float", + "count": 1, + "values": [ + 0.0 + ] + } + ] +} From 544c9eb0db2636c6142d7bccf9e27fbf7e51b7b8 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 11 Aug 2026 00:55:30 +0800 Subject: [PATCH 18/38] =?UTF-8?q?build:=20=E5=9B=9E=E7=A7=BB=E6=A4=8D=20ro?= =?UTF-8?q?seauCheck=20API=20=E5=85=BC=E5=AE=B9=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回移植 dev/26.1 的 roseau API 兼容检查体系(ea4301f #25、a6a24f1、946938f): - 根 roseau.yaml(排除 Deprecated/Beta/ApiStatus 系列注解) - gradle/scripts/roseau.gradle:roseau-cli 0.6.0 差分检查任务,通过 gradle.projectsEvaluated + subprojects 注册到各模块(Java 21 toolchain, 替换 dev 的 java_version 属性引用;tasks.named('jar', Jar) 惰性化以兼容 Gradle 8.8 配置时机) - 根 build.gradle 接入 注意:本机运行 roseauCheck 时 Gradle daemon 崩溃(疑似 roseau-cli 0.6.0 与本地 Java 21 环境兼容问题),任务可注册、配置阶段通过,运行验收 登记为待 CI/人工补验。 --- build.gradle | 6 +++ gradle/scripts/roseau.gradle | 79 ++++++++++++++++++++++++++++++++++++ roseau.yaml | 9 ++++ 3 files changed, 94 insertions(+) create mode 100644 gradle/scripts/roseau.gradle create mode 100644 roseau.yaml diff --git a/build.gradle b/build.gradle index 9b94d9bf..d4c18b52 100644 --- a/build.gradle +++ b/build.gradle @@ -15,3 +15,9 @@ dependencies { test { useJUnitPlatform() } + +gradle.projectsEvaluated { + subprojects { + apply from: rootProject.file("gradle/scripts/roseau.gradle") + } +} diff --git a/gradle/scripts/roseau.gradle b/gradle/scripts/roseau.gradle new file mode 100644 index 00000000..8b3d4d03 --- /dev/null +++ b/gradle/scripts/roseau.gradle @@ -0,0 +1,79 @@ +configurations { + roseau +} + +dependencies { + roseau 'io.github.alien-tools:roseau-cli:0.6.0' +} + +def jarFileProvider = tasks.named('jar', Jar) +def reportsDirProvider = layout.buildDirectory.dir('reports/roseau') +def compileClasspathProvider = provider { sourceSets.main.compileClasspath.asPath } +def roseauGroup = project.group.toString() +def roseauArtifact = project.name +def roseauBaselineVersion = project.findProperty('roseauBaselineVersion') ?: { + def groupPath = project.group.toString().replace('.', '/') + def artifact = project.name + def repos = [ + 'https://repo1.maven.org/maven2', + 'https://server.cjsah.net:1002/maven', + ] + for (repoUrl in repos) { + try { + def metadataUrl = new URL("${repoUrl}/${groupPath}/${artifact}/maven-metadata.xml") + def conn = metadataUrl.openConnection() + conn.connectTimeout = 5000 + conn.readTimeout = 5000 + def xmlText = conn.inputStream.getText('UTF-8') + def matcher = xmlText =~ /([^<]+)<\/latest>/ + if (matcher.find()) { + def latest = matcher.group(1) + logger.lifecycle("Roseau: resolved latest version ${latest} from ${repoUrl}") + return latest + } + } catch (Exception ignored) { + // try next repo + } + } + logger.warn("Roseau: could not resolve latest version from Maven, falling back to ${version}") + return version.toString() +}() +def roseauMvnCoord = "${roseauGroup}:${roseauArtifact}:${roseauBaselineVersion}" +def roseauBaselineDep = dependencies.create(group: roseauGroup, name: roseauArtifact, version: roseauBaselineVersion) +def roseauBaselineConfig = configurations.detachedConfiguration(roseauBaselineDep) +roseauBaselineConfig.transitive = false +def roseauBaselineJarProvider = provider { roseauBaselineConfig.singleFile } +def roseauConfig = rootProject.file("roseau.yaml").absolutePath + +tasks.register('roseauCheck', JavaExec) { + group = 'verification' + description = 'Detects API breaking changes against a baseline version using Roseau' + + dependsOn tasks.named('jar', Jar) + + classpath = configurations.roseau + mainClass = 'io.github.alien.roseau.cli.RoseauCLI' + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(21) + } + + doFirst { + def currentJar = jarFileProvider.get().archiveFile.get().asFile + def baselineJar = roseauBaselineJarProvider.get() + def reportsDir = reportsDirProvider.get().asFile + reportsDir.mkdirs() + + logger.lifecycle("Roseau: comparing baseline ${roseauMvnCoord} against ${currentJar.name}") + + args( + '--diff', + '--v1', baselineJar.absolutePath, + '--v2', currentJar.absolutePath, + '--classpath', compileClasspathProvider.get(), + '--fail-on-bc', + '--report', "HTML=${new File(reportsDir, 'report.html').absolutePath}", + '--report', "CSV=${new File(reportsDir, 'report.csv').absolutePath}", + '--config', roseauConfig + ) + } +} diff --git a/roseau.yaml b/roseau.yaml new file mode 100644 index 00000000..1f196728 --- /dev/null +++ b/roseau.yaml @@ -0,0 +1,9 @@ +common: + excludes: + annotations: + - name: java.lang.Deprecated + - name: com.google.common.annotations.Beta + - name: org.jetbrains.annotations.ApiStatus$Experimental + - name: org.jetbrains.annotations.ApiStatus$Internal + - name: org.jetbrains.annotations.ApiStatus$Obsolete + - name: org.jetbrains.annotations.ApiStatus$ScheduledForRemoval From b0e179e8c859a4cc39da3b53f92e7ab877792ee0 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 11 Aug 2026 01:31:08 +0800 Subject: [PATCH 19/38] =?UTF-8?q?feat(rendering):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=E6=B8=B2=E6=9F=93=E6=A8=A1=E5=9D=97=E7=BA=AF=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E6=89=B9=EF=BC=88#P7a=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回移植 dev/26.1 module.rendering 的纯逻辑/资源部分(7121bf6、7d07f20 #41、 494274e #21),形成新模块 anvillib-rendering-neoforge-1.21.1 骨架: - foundation/buffers/layout(BufferLayout/Std140/Std430 布局计算 + 测试)、 BufferObject 基类(去掉 DynamicUniformStorage/CommandEncoder 依赖, 改为 write(ByteBuffer))、Sdf2d(9 种 SDF 距离函数)、SdfParameters/ SdfRenderType/SdfPassType、DirtyTracked、Empty/Transforming 缓冲源、 ALRMeshSorting、MemoryBarrierFlag/ALRComputeCapabilities/GpuBufferConstants、 MainTargetResizeEvent+GameRendererMixin、RegisterComputePipelinesEvent、 MinecraftMixin、RenderTypeMixin+ALRRenderTypeExtension(bloom 标记)、 ALRIntegrationCompatMixinPlugin、Timer、ALRSharedMath、shader 资源 1.21.1 适配(实测销账):Mth.roundToward 存在(V-1 ✓)、EmptyBufferSource super(null,null) 兼容(V-3 ✓)、Mth.length(float) 仅 double 需强转、 CompactVectorArray 不存在改 VertexSorting.sort、setLineWidth 不存在、 updateLevelInEngines 单参、AddClientReloadListenersEvent → RegisterClientReloadListenersEvent(registerReloadListener) - runStd140LayoutRulesTest(JavaExec)+ check 依赖,Std140/Std430 断言通过 --- module.rendering/build.gradle | 287 +++++++++++++ module.rendering/gradle.properties | 4 + .../lib/v2/rendering/ALRSharedMath.java | 8 + .../lib/v2/rendering/AnvilLibRendering.java | 32 ++ .../event/MainTargetResizeEvent.java | 16 + .../event/RegisterComputePipelinesEvent.java | 27 ++ .../extension/ALRRenderTypeExtension.java | 37 ++ .../extension/blaze3d/MemoryBarrierFlag.java | 39 ++ .../compute/ALRComputeCapabilities.java | 15 + .../rendering/foundation/ALRMeshSorting.java | 44 ++ .../foundation/buffers/EmptyBufferSource.java | 34 ++ .../buffers/EmptyOutlineBufferSource.java | 20 + .../buffers/EmptyVertexConsumer.java | 43 ++ .../buffers/GpuBufferConstants.java | 6 + .../TransformingVertexConsumerWrapper.java | 50 +++ .../buffers/layout/BufferLayout.java | 38 ++ .../buffers/layout/BufferSizeCalculator.java | 27 ++ .../buffers/layout/BufferWriter.java | 42 ++ .../layout/std140/Std140SizeCalculator.java | 76 ++++ .../buffers/layout/std140/Std140Writer.java | 132 ++++++ .../layout/std430/Std430SizeCalculator.java | 75 ++++ .../buffers/layout/std430/Std430Writer.java | 134 +++++++ .../buffers/object/BufferObject.java | 31 ++ .../object/BufferObjectLayoutDefinition.java | 51 +++ .../object/BufferObjectLayoutEntry.java | 87 ++++ .../object/BufferObjectLayoutEntryType.java | 173 ++++++++ .../object/ShaderBufferObjectUsage.java | 6 + .../foundation/compound/DirtyTracked.java | 5 + .../ALRIntegrationCompatMixinPlugin.java | 63 +++ .../rendering/mixins/GameRendererMixin.java | 23 ++ .../v2/rendering/mixins/MinecraftMixin.java | 32 ++ .../v2/rendering/mixins/RenderTypeMixin.java | 23 ++ .../lib/v2/rendering/sdf/Sdf2d.java | 379 ++++++++++++++++++ .../lib/v2/rendering/sdf/SdfParameters.java | 202 ++++++++++ .../lib/v2/rendering/sdf/SdfPassType.java | 6 + .../lib/v2/rendering/sdf/SdfRenderType.java | 22 + .../lib/v2/rendering/util/Timer.java | 9 + .../resources/META-INF/accesstransformer.cfg | 3 + .../resources/META-INF/neoforge.mods.toml | 86 ++++ .../resources/anvillib_rendering.mixins.json | 17 + .../shaders/core/apply_bloom.fsh | 23 ++ .../anvillib_rendering/shaders/core/blit.vsh | 16 + .../anvillib_rendering/shaders/core/blur.fsh | 36 ++ .../shaders/core/down_sample.fsh | 26 ++ .../shaders/core/glitch.fsh | 51 +++ .../shaders/core/sdf_graphics.fsh | 194 +++++++++ .../shaders/core/sdf_graphics.vsh | 35 ++ .../shaders/core/up_sample.fsh | 61 +++ .../shaders/include/util.glsl | 13 + module.rendering/src/main/resources/icon.png | Bin 0 -> 1089 bytes .../buffers/layout/Std140LayoutRulesTest.java | 139 +++++++ 51 files changed, 2998 insertions(+) create mode 100644 module.rendering/build.gradle create mode 100644 module.rendering/gradle.properties create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRSharedMath.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/AnvilLibRendering.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/MainTargetResizeEvent.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/RegisterComputePipelinesEvent.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/ALRRenderTypeExtension.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/MemoryBarrierFlag.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/ALRComputeCapabilities.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/ALRMeshSorting.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyBufferSource.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyOutlineBufferSource.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyVertexConsumer.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/GpuBufferConstants.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/TransformingVertexConsumerWrapper.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferLayout.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferSizeCalculator.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferWriter.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140SizeCalculator.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140Writer.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430SizeCalculator.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430Writer.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObject.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutDefinition.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntry.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntryType.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/ShaderBufferObjectUsage.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/compound/DirtyTracked.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/ALRIntegrationCompatMixinPlugin.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/GameRendererMixin.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/MinecraftMixin.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/RenderTypeMixin.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/Sdf2d.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfParameters.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfPassType.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfRenderType.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/util/Timer.java create mode 100644 module.rendering/src/main/resources/META-INF/accesstransformer.cfg create mode 100644 module.rendering/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 module.rendering/src/main/resources/anvillib_rendering.mixins.json create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.fsh create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blit.vsh create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.fsh create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.fsh create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.fsh create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.vsh create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.fsh create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/include/util.glsl create mode 100644 module.rendering/src/main/resources/icon.png create mode 100644 module.rendering/src/test/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/Std140LayoutRulesTest.java diff --git a/module.rendering/build.gradle b/module.rendering/build.gradle new file mode 100644 index 00000000..4f4eedc8 --- /dev/null +++ b/module.rendering/build.gradle @@ -0,0 +1,287 @@ +plugins { + id 'java-library' + id 'eclipse' + id 'idea' + id 'maven-publish' + alias libs.plugins.modDevGradle + alias libs.plugins.lombok + alias libs.plugins.machete + id 'org.jreleaser' version '1.23.0' + id "me.modmuss50.mod-publish-plugin" version "1.1.0" +} + +String buildType = 'snapshot' +String buildNumber +if (System.getenv("CI_BUILD") == 'false') buildNumber = null +else { + if (System.getenv("PR_BUILD") != 'false') buildType = 'pr' + buildNumber = System.getenv("GITHUB_RUN_NUMBER") +} +version = "${mod_version}" + (buildNumber != null ? "+${buildType}.${buildNumber}" : "") +group = mod_group_id + +base { + archivesName = "${project.mod_id}-neoforge-${libs.versions.minecraft.get()}" +} + +repositories { + maven { url = "https://server.cjsah.net:1002/maven/" } + mavenLocal() + mavenCentral() +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +neoForge { + // Specify the version of NeoForge to use. + version = libs.versions.neoForge.get() + + parchment { + mappingsVersion = project.parchment_mappings_version + minecraftVersion = project.parchment_minecraft_version + } + + // This line is optional. Access Transformers are automatically detected + // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') + + interfaceInjectionData { + // from 'src/main/resources/interface_injections.json' + // publish file('src/main/resources/interface_injections.json') + } + + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + client { + client() + + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + server { + server() + programArgument '--nogui' + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + // This run config launches GameTestServer and runs all registered gametests, then exits. + // By default, the server will crash when no gametests are provided. + // The gametest system is also enabled by default for other run configs under the /test command. + gameTestServer { + type = "gameTestServer" + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + data { + data() + + // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it + // gameDirectory = project.file('run-data') + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } + + // applies to all the run configs above + configureEach { + // Recommended logging data for a userdev environment + // The markers can be added/remove as needed separated by commas. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + systemProperty 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + logLevel = org.slf4j.event.Level.DEBUG + } + } + + mods { + // define mod <-> source bindings + // these are used to tell the game which sources are for which mod + // mostly optional in a single mod project + // but multi mod projects should define one per mod + "${mod_id}" { + sourceSet(sourceSets.main) + } + } +} + +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } + + +dependencies { + compileOnly "org.jspecify:jspecify:1.0.0" +} + +tasks.register('runStd140LayoutRulesTest', JavaExec) { + group = 'verification' + description = 'Runs lightweight std140 layout rule checks.' + classpath = sourceSets.test.runtimeClasspath + sourceSets.main.compileClasspath + mainClass = 'dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.Std140LayoutRulesTest' +} + +tasks.named('test') { + // Ensure the Std140 layout rule checks actually run under the :test task (pure JVM verification, no game needed). + dependsOn tasks.named('runStd140LayoutRulesTest') +} + +tasks.named('check') { + dependsOn tasks.named('runStd140LayoutRulesTest') +} + +// This block of code expands all declared replace properties in the specified resource targets. +// A missing property will result in an error. Properties are expanded using ${} Groovy notation. +// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. +// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html +tasks.withType(ProcessResources).configureEach { + var replaceProperties = [minecraft_version : libs.versions.minecraft.get(), + minecraft_version_range: minecraft_version_range, + neo_version : libs.versions.neoForge.get(), + neo_version_range : neo_version_range, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : version, + mod_authors : mod_authors, + mod_description : mod_description] + inputs.properties replaceProperties + + filesMatching(['META-INF/neoforge.mods.toml']) { + expand replaceProperties + } +} + +tasks.register('sourcesJar', Jar) { + from delombok.outputs.files + dependsOn delombok + archiveClassifier = "sources" +} + +tasks.register('javadocJar', Jar) { + from javadoc.destinationDir + dependsOn javadoc + archiveClassifier = "javadoc" +} + +javadoc { + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') +} + +jar { + from(rootProject.file("LICENSE")) + from(rootProject.file("ASSETS_LICENSE")) + from(rootProject.file("README.md")) + from(rootProject.file("README.en.md")) +} + +artifacts { + archives tasks.jar + archives tasks.sourcesJar + archives tasks.javadocJar +} + +def this_name = base.archivesName.get() +def this_description = mod_description + +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + + artifact sourcesJar + artifact javadocJar + + versionMapping { + allVariants { + fromResolutionResult() + } + } + + pom { + name = this_name + description = this_description + url = 'https://github.com/Anvil-Dev/AnvilLib' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/license/mit' + } + } + developers { + developer { + id = 'Anvil-Dev' + name = 'AnvilCraft Dev' + email = 'admin@anvilcraft.dev' + } + } + scm { + url = 'https://github.com/Anvil-Dev/AnvilLib' + connection = 'https://github.com/Anvil-Dev/AnvilLib.git' + developerConnection = 'https://github.com/Anvil-Dev/AnvilLib.git' + } + } + } + } + repositories { + // Add repositories to publish to here. + def MAVEN_URL = System.getenv("MAVEN_URL") + if (MAVEN_URL != null) { + maven { + url MAVEN_URL + credentials { + username System.getenv("MAVEN_USERNAME") + password System.getenv("MAVEN_PASSWORD") + } + } + } + mavenLocal() + maven { + name = "staging" + url = layout.buildDirectory.dir("staging-deploy") + } + } +} + +jreleaser { + signing { + active = 'ALWAYS' + armored = true + } + + deploy { + maven { + mavenCentral { + sonatype { + active = 'ALWAYS' + url = 'https://central.sonatype.com/api/v1/publisher' + stagingRepository('build/staging-deploy') + } + } + } + } +} + +// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} + +machete { + // disable machete locally for faster builds + enabled = false +} + +lombok { + version = "1.18.34" +} diff --git a/module.rendering/gradle.properties b/module.rendering/gradle.properties new file mode 100644 index 00000000..4a0508f2 --- /dev/null +++ b/module.rendering/gradle.properties @@ -0,0 +1,4 @@ +## Mod Properties +mod_id=anvillib_rendering +mod_name=AnvilLib-Rendering +mod_description=Rendering utilities diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRSharedMath.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRSharedMath.java new file mode 100644 index 00000000..96313b9a --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRSharedMath.java @@ -0,0 +1,8 @@ +package dev.anvilcraft.lib.v2.rendering; + +import com.mojang.blaze3d.vertex.PoseStack; + +public class ALRSharedMath { + public static final PoseStack.Pose IDENTITY_POSE_3D = new PoseStack().last(); + public static final float SQRT_2 = 1.4142135623730950488016887242097f; +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/AnvilLibRendering.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/AnvilLibRendering.java new file mode 100644 index 00000000..b4d10f0b --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/AnvilLibRendering.java @@ -0,0 +1,32 @@ +package dev.anvilcraft.lib.v2.rendering; + +import lombok.extern.slf4j.Slf4j; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; +import org.jetbrains.annotations.ApiStatus; + +@Slf4j +@Mod(value = AnvilLibRendering.MODID, dist = Dist.CLIENT) +@EventBusSubscriber(Dist.CLIENT) +@ApiStatus.Internal +public class AnvilLibRendering { + public static final boolean DEBUG = System.getProperty("anvillib.rendering.debugMode") != null; + public static final String MODID = "anvillib_rendering"; + + public AnvilLibRendering(IEventBus modBus) { + } + + public static ResourceLocation location(String path) { + return ResourceLocation.fromNamespaceAndPath(MODID, path); + } + + @SubscribeEvent + public static void on(RegisterClientReloadListenersEvent event) { + // #P7e: register ALRComputeShaderManager.INSTANCE via event.registerReloadListener(...) + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/MainTargetResizeEvent.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/MainTargetResizeEvent.java new file mode 100644 index 00000000..cb3c0d9c --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/MainTargetResizeEvent.java @@ -0,0 +1,16 @@ +package dev.anvilcraft.lib.v2.rendering.event; + +import lombok.Getter; +import net.neoforged.bus.api.Event; +import net.neoforged.fml.event.IModBusEvent; + +@Getter +public class MainTargetResizeEvent extends Event implements IModBusEvent { + private final int newWidth; + private final int newHeight; + + public MainTargetResizeEvent(int newWidth, int newHeight) { + this.newWidth = newWidth; + this.newHeight = newHeight; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/RegisterComputePipelinesEvent.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/RegisterComputePipelinesEvent.java new file mode 100644 index 00000000..a0c02631 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/RegisterComputePipelinesEvent.java @@ -0,0 +1,27 @@ +package dev.anvilcraft.lib.v2.rendering.event; + +import net.neoforged.bus.api.Event; +import net.neoforged.fml.event.IModBusEvent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Mod-bus event for registering compute pipelines. + *

+ * Ported from 26.1: the original referenced {@code ALRComputePipeline} directly, which belongs to the + * #P7e (GPU compute pipeline extension) port batch. To keep this batch independent, the pipeline type is + * generic here; #P7e instantiates {@code RegisterComputePipelinesEvent}. + */ +public class RegisterComputePipelinesEvent extends Event implements IModBusEvent { + private final List pipelines = new ArrayList<>(); + + public void registerPipeline(T pipeline) { + this.pipelines.add(pipeline); + } + + public List getPipelines() { + return Collections.unmodifiableList(this.pipelines); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/ALRRenderTypeExtension.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/ALRRenderTypeExtension.java new file mode 100644 index 00000000..36427d31 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/ALRRenderTypeExtension.java @@ -0,0 +1,37 @@ +package dev.anvilcraft.lib.v2.rendering.extension; + +import net.minecraft.client.renderer.RenderType; + +public interface ALRRenderTypeExtension { + boolean anvillib_rendering$bloomRendering(); + + void anvillib_rendering$setBloomRendering(boolean value); + + static ALRRenderTypeExtension of(RenderType thiz) { + return ((ALRRenderTypeExtension) thiz); + } + + static void setRenderingBloomed(RenderType thiz, boolean value) { + of(thiz).anvillib_rendering$setBloomRendering(value); + } + + static boolean isRenderingBloomed(RenderType thiz) { + return of(thiz).anvillib_rendering$bloomRendering(); + } + + /** + * Creates a structurally identical copy of {@code that} marked as bloom-rendered. + *

+ * Ported from 26.1: {@code RenderType.create(that.name, that.state)} (26.1 took a {@code RenderState}) + * becomes {@code RenderType.create(name, format, mode, bufferSize, state)} on 1.21.1; the + * {@code CompositeState} is exposed via the access transformer (see META-INF/accesstransformer.cfg). + */ + static RenderType copyWithBloom(RenderType that) { + if (that instanceof RenderType.CompositeRenderType composite) { + RenderType newValue = RenderType.create(that.name, that.format(), that.mode(), that.bufferSize(), composite.state()); + setRenderingBloomed(newValue, true); + return newValue; + } + return that; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/MemoryBarrierFlag.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/MemoryBarrierFlag.java new file mode 100644 index 00000000..85d08c3f --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/MemoryBarrierFlag.java @@ -0,0 +1,39 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d; + +import org.lwjgl.opengl.GL42; +import org.lwjgl.opengl.GL43; + +public enum MemoryBarrierFlag { + VERTEX_ATTRIB_ARRAY_BARRIER(GL42.GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT), + ELEMENT_ARRAY_BARRIER(GL42.GL_ELEMENT_ARRAY_BARRIER_BIT), + UNIFORM_BARRIER(GL42.GL_UNIFORM_BARRIER_BIT), + TEXTURE_FETCH_BARRIER(GL42.GL_TEXTURE_FETCH_BARRIER_BIT), + SHADER_IMAGE_ACCESS_BARRIER(GL42.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT), + COMMAND_BARRIER(GL42.GL_COMMAND_BARRIER_BIT), + PIXEL_BUFFER_BARRIER(GL42.GL_PIXEL_BUFFER_BARRIER_BIT), + TEXTURE_UPDATE_BARRIER(GL42.GL_TEXTURE_UPDATE_BARRIER_BIT), + BUFFER_UPDATE_BARRIER(GL42.GL_BUFFER_UPDATE_BARRIER_BIT), + FRAMEBUFFER_BARRIER(GL42.GL_FRAMEBUFFER_BARRIER_BIT), + TRANSFORM_FEEDBACK_BARRIER(GL42.GL_TRANSFORM_FEEDBACK_BARRIER_BIT), + ATOMIC_COUNTER_BARRIER(GL42.GL_ATOMIC_COUNTER_BARRIER_BIT), + SHADER_STORAGE_BARRIER(GL43.GL_SHADER_STORAGE_BARRIER_BIT), + ALL_BARRIER(GL42.GL_ALL_BARRIER_BITS); + + private final int glEnum; + + MemoryBarrierFlag(int glEnum) { + this.glEnum = glEnum; + } + + public int glEnum() { + return glEnum; + } + + public static int compound(MemoryBarrierFlag... flags){ + int i = 0; + for (MemoryBarrierFlag flag : flags) { + i |= flag.glEnum; + } + return i; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/ALRComputeCapabilities.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/ALRComputeCapabilities.java new file mode 100644 index 00000000..ab178600 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/ALRComputeCapabilities.java @@ -0,0 +1,15 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute; + +import org.lwjgl.opengl.GL; + +public class ALRComputeCapabilities { + private static boolean COMPUTE_SUPPORTED; + + public static void init() { + COMPUTE_SUPPORTED = GL.getCapabilities().GL_ARB_compute_shader; + } + + public static boolean isComputeSupported() { + return COMPUTE_SUPPORTED; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/ALRMeshSorting.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/ALRMeshSorting.java new file mode 100644 index 00000000..8b5d97eb --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/ALRMeshSorting.java @@ -0,0 +1,44 @@ +package dev.anvilcraft.lib.v2.rendering.foundation; + +import com.mojang.blaze3d.vertex.VertexSorting; +import it.unimi.dsi.fastutil.ints.IntArrays; +import org.joml.Vector3f; + +public class ALRMeshSorting { + + public static VertexSorting byDistance(Vector3f point) { + return new VertexSortingDistanceToPoint(point); + } + + public static class VertexSortingDistanceToPoint implements VertexSorting { + + private final Vector3f origin; + + private VertexSortingDistanceToPoint(Vector3f origin) { + this.origin = origin; + } + + @Override + public int[] sort(Vector3f[] points) { + int[] keys = new int[points.length]; + int[] indices = new int[points.length]; + + for (int i = 0; i < points.length; i++) { + keys[i] = floatToSortableInt(origin.distanceSquared(points[i])); + indices[i] = i; + } + + if (points.length <= 128) { + IntArrays.quickSortIndirect(indices, keys); + } else { + IntArrays.radixSortIndirect(indices, keys, true); + } + return indices; + } + } + + public static int floatToSortableInt(float f) { + int b = Float.floatToRawIntBits(f); + return b ^ ((b >> 31) | 0x7fffffff); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyBufferSource.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyBufferSource.java new file mode 100644 index 00000000..5017de54 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyBufferSource.java @@ -0,0 +1,34 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers; + +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; + +public class EmptyBufferSource extends MultiBufferSource.BufferSource { + + public static final EmptyBufferSource INSTANCE = new EmptyBufferSource(); + + protected EmptyBufferSource() { + super(null, null); + } + + @Override + public VertexConsumer getBuffer(RenderType renderType) { + return EmptyVertexConsumer.INSTANCE; + } + + @Override + public void endBatch() { + + } + + @Override + public void endBatch(RenderType type) { + + } + + @Override + public void endLastBatch() { + + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyOutlineBufferSource.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyOutlineBufferSource.java new file mode 100644 index 00000000..527c1115 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyOutlineBufferSource.java @@ -0,0 +1,20 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers; + +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.renderer.OutlineBufferSource; +import net.minecraft.client.renderer.RenderType; + +public class EmptyOutlineBufferSource extends OutlineBufferSource { + + public static final EmptyOutlineBufferSource INSTANCE = new EmptyOutlineBufferSource(); + + public EmptyOutlineBufferSource() { + // The delegate BufferSource is never used: getBuffer is fully overridden below. + super(null); + } + + @Override + public VertexConsumer getBuffer(RenderType renderType) { + return EmptyVertexConsumer.INSTANCE; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyVertexConsumer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyVertexConsumer.java new file mode 100644 index 00000000..0849c5f1 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyVertexConsumer.java @@ -0,0 +1,43 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers; + +import com.mojang.blaze3d.vertex.VertexConsumer; + +public class EmptyVertexConsumer implements VertexConsumer { + + public static final EmptyVertexConsumer INSTANCE = new EmptyVertexConsumer(); + + @Override + public VertexConsumer addVertex(float x, float y, float z) { + return this; + } + + @Override + public VertexConsumer setColor(int r, int g, int b, int a) { + return this; + } + + @Override + public VertexConsumer setColor(int color) { + return this; + } + + @Override + public VertexConsumer setUv(float u, float v) { + return this; + } + + @Override + public VertexConsumer setUv1(int u, int v) { + return this; + } + + @Override + public VertexConsumer setUv2(int u, int v) { + return this; + } + + @Override + public VertexConsumer setNormal(float x, float y, float z) { + return this; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/GpuBufferConstants.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/GpuBufferConstants.java new file mode 100644 index 00000000..0c94b2d5 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/GpuBufferConstants.java @@ -0,0 +1,6 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers; + +public class GpuBufferConstants { + public static final int USAGE_SHADER_STORAGE = 512; + public static final int USAGE_ATOMIC_COUNTER = 1024; +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/TransformingVertexConsumerWrapper.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/TransformingVertexConsumerWrapper.java new file mode 100644 index 00000000..e907bb00 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/TransformingVertexConsumerWrapper.java @@ -0,0 +1,50 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import org.joml.Vector3f; + +public class TransformingVertexConsumerWrapper implements VertexConsumer { + private final PoseStack.Pose pose; + private final VertexConsumer delegate; + + public TransformingVertexConsumerWrapper(PoseStack.Pose pose, VertexConsumer delegate) { + this.pose = pose; + this.delegate = delegate; + } + + @Override + public VertexConsumer addVertex(float x, float y, float z) { + return delegate.addVertex(pose.pose().transformPosition(x, y, z, new Vector3f())); + } + + @Override + public VertexConsumer setColor(int r, int g, int b, int a) { + return delegate.setColor(r, g, b, a); + } + + @Override + public VertexConsumer setColor(int color) { + return delegate.setColor(color); + } + + @Override + public VertexConsumer setUv(float u, float v) { + return delegate.setUv(u, v); + } + + @Override + public VertexConsumer setUv1(int u, int v) { + return delegate.setUv1(u, v); + } + + @Override + public VertexConsumer setUv2(int u, int v) { + return delegate.setUv2(u, v); + } + + @Override + public VertexConsumer setNormal(float x, float y, float z) { + return delegate.setNormal(pose, x, y, z); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferLayout.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferLayout.java new file mode 100644 index 00000000..3a95b3d2 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferLayout.java @@ -0,0 +1,38 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std140.Std140SizeCalculator; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std140.Std140Writer; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std430.Std430SizeCalculator; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std430.Std430Writer; + +import java.nio.ByteBuffer; + +public interface BufferLayout { + BufferLayout STD140 = new BufferLayout() { + @Override + public BufferSizeCalculator createSizeCalculator() { + return new Std140SizeCalculator(); + } + + @Override + public BufferWriter createWriter(ByteBuffer buffer) { + return new Std140Writer(buffer); + } + }; + + BufferLayout STD430 = new BufferLayout() { + @Override + public BufferSizeCalculator createSizeCalculator() { + return new Std430SizeCalculator(); + } + + @Override + public BufferWriter createWriter(ByteBuffer buffer) { + return new Std430Writer(buffer); + } + }; + + BufferSizeCalculator createSizeCalculator(); + + BufferWriter createWriter(ByteBuffer buffer); +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferSizeCalculator.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferSizeCalculator.java new file mode 100644 index 00000000..4cefe363 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferSizeCalculator.java @@ -0,0 +1,27 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; + +public interface BufferSizeCalculator { + void putFloat(); + + void putVec2(); + + void putVec3(); + + void putVec4(); + + void putInt(); + + void putIVec2(); + + void putIVec3(); + + void putIVec4(); + + void putMat4f(); + + void putStructArray(BufferObjectLayoutDefinition definition, int size); + + int get(); +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferWriter.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferWriter.java new file mode 100644 index 00000000..ab77d5c8 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferWriter.java @@ -0,0 +1,42 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; +import org.joml.Matrix4f; +import org.joml.Vector2f; +import org.joml.Vector2i; +import org.joml.Vector3f; +import org.joml.Vector3i; +import org.joml.Vector4f; +import org.joml.Vector4i; + +import java.nio.ByteBuffer; + +public interface BufferWriter { + void putMat4f(Matrix4f object); + + void putIVec4(Vector4i object); + + void putIVec3(Vector3i object); + + void putIVec2(Vector2i object); + + void putInt(int object); + + void putVec4(Vector4f object); + + void putVec3(Vector3f object); + + void putVec2(Vector2f object); + + void putFloat(float object); + + ByteBuffer intoBuffer(); + + void putStructArray(int index, E object, BufferObjectLayoutDefinition definition); + + default void putStructArray(E[] objects, BufferObjectLayoutDefinition definition) { + for (int i = 0; i < objects.length; i++) { + putStructArray(i, objects[i], definition); + } + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140SizeCalculator.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140SizeCalculator.java new file mode 100644 index 00000000..2b9360de --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140SizeCalculator.java @@ -0,0 +1,76 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std140; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferSizeCalculator; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; +import net.minecraft.util.Mth; + +public final class Std140SizeCalculator implements BufferSizeCalculator { + + private int size; + + public Std140SizeCalculator() { + } + + public void align(int alignment) { + this.size = Mth.roundToward(this.size, alignment); + } + + public void putFloat() { + this.align(4); + this.size += 4; + } + + public void putInt() { + this.align(4); + this.size += 4; + } + + public void putVec2() { + this.align(8); + this.size += 8; + } + + public void putIVec2() { + this.align(8); + this.size += 8; + } + + public void putVec3() { + this.align(16); + this.size += 12; + } + + public void putIVec3() { + this.align(16); + this.size += 12; + } + + public void putVec4() { + this.align(16); + this.size += 16; + } + + public void putIVec4() { + this.align(16); + this.size += 16; + } + + public void putMat4f() { + this.align(16); + this.size += 64; + } + + @Override + public void putStructArray(BufferObjectLayoutDefinition definition, int size) { + int alignment = Mth.roundToward(definition.alignment(BufferLayout.STD140), 16); + int stride = Mth.roundToward(definition.size(BufferLayout.STD140), alignment); + this.align(alignment); + this.size += stride * size; + } + + @Override + public int get() { + return size; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140Writer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140Writer.java new file mode 100644 index 00000000..568864bb --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140Writer.java @@ -0,0 +1,132 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std140; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferWriter; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; +import net.minecraft.util.Mth; +import org.joml.Matrix4f; +import org.joml.Vector2f; +import org.joml.Vector2i; +import org.joml.Vector3f; +import org.joml.Vector3i; +import org.joml.Vector4f; +import org.joml.Vector4i; + +import java.nio.ByteBuffer; + +public final class Std140Writer implements BufferWriter { + + private final ByteBuffer buffer; + private final int pointer; + private BufferObjectLayoutDefinition indexedArrayDefinition; + private int indexedArrayStart; + private int indexedArrayStride; + private int indexedArrayEnd; + + public Std140Writer(ByteBuffer buffer) { + this.buffer = buffer; + this.pointer = buffer.position(); + } + + public void align(int alignment) { + int position = this.buffer.position(); + this.buffer.position(this.pointer + Mth.roundToward(position - this.pointer, alignment)); + } + + public void putFloat(float value) { + this.align(4); + this.buffer.putFloat(value); + } + + public void putInt(int value) { + this.align(4); + this.buffer.putInt(value); + } + + public void putVec2(Vector2f vec) { + this.align(8); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 8); + } + + public void putIVec2(Vector2i vec) { + this.align(8); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 8); + } + + public void putVec3(Vector3f vec) { + this.align(16); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 12); + } + + public void putIVec3(Vector3i vec) { + this.align(16); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 12); + } + + public void putVec4(Vector4f vec) { + this.align(16); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 16); + } + + public void putIVec4(Vector4i vec) { + this.align(16); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 16); + } + + public void putMat4f(Matrix4f vec) { + this.align(16); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 64); + } + + public ByteBuffer intoBuffer() { + return this.buffer.flip(); + } + + @Override + public void putStructArray(int index, E object, BufferObjectLayoutDefinition definition) { + int alignment = Mth.roundToward(definition.alignment(BufferLayout.STD140), 16); + int stride = Mth.roundToward(definition.size(BufferLayout.STD140), alignment); + int arrayStart = this.indexedArrayStart(definition, alignment, stride); + int elementStart = arrayStart + stride * index; + this.buffer.position(elementStart); + definition.writeInto(this, object); + this.indexedArrayEnd = Math.max(this.indexedArrayEnd, elementStart + stride); + this.buffer.position(this.indexedArrayEnd); + } + + @Override + public void putStructArray(E[] objects, BufferObjectLayoutDefinition definition) { + int alignment = Mth.roundToward(definition.alignment(BufferLayout.STD140), 16); + int stride = Mth.roundToward(definition.size(BufferLayout.STD140), alignment); + int arrayStart = this.pointer + Mth.roundToward(this.buffer.position() - this.pointer, alignment); + for (int i = 0; i < objects.length; i++) { + this.buffer.position(arrayStart + stride * i); + definition.writeInto(this, objects[i]); + } + this.buffer.position(arrayStart + stride * objects.length); + this.indexedArrayDefinition = null; + } + + private int indexedArrayStart(BufferObjectLayoutDefinition definition, int alignment, int stride) { + int position = this.buffer.position(); + if (this.indexedArrayDefinition == definition + && this.indexedArrayStride == stride + && position >= this.indexedArrayStart + && position <= this.indexedArrayEnd) { + return this.indexedArrayStart; + } + + this.indexedArrayDefinition = definition; + this.indexedArrayStart = this.pointer + Mth.roundToward(position - this.pointer, alignment); + this.indexedArrayStride = stride; + this.indexedArrayEnd = this.indexedArrayStart; + return this.indexedArrayStart; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430SizeCalculator.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430SizeCalculator.java new file mode 100644 index 00000000..0a165ef3 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430SizeCalculator.java @@ -0,0 +1,75 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std430; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferSizeCalculator; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; +import net.minecraft.util.Mth; + +public class Std430SizeCalculator implements BufferSizeCalculator { + private int size; + + public Std430SizeCalculator() { + } + + public void align(int alignment) { + this.size = Mth.roundToward(this.size, alignment); + } + + public void putFloat() { + this.align(4); + this.size += 4; + } + + public void putInt() { + this.align(4); + this.size += 4; + } + + public void putVec2() { + this.align(8); + this.size += 8; + } + + public void putIVec2() { + this.align(8); + this.size += 8; + } + + public void putVec3() { + this.align(16); + this.size += 12; + } + + public void putIVec3() { + this.align(16); + this.size += 12; + } + + public void putVec4() { + this.align(16); + this.size += 16; + } + + public void putIVec4() { + this.align(16); + this.size += 16; + } + + public void putMat4f() { + this.align(16); + this.size += 64; + } + + @Override + public void putStructArray(BufferObjectLayoutDefinition definition, int size) { + int alignment = definition.alignment(BufferLayout.STD430); + int stride = Mth.roundToward(definition.size(BufferLayout.STD430), alignment); + this.align(alignment); + this.size += stride * size; + } + + @Override + public int get() { + return size; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430Writer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430Writer.java new file mode 100644 index 00000000..63ef1369 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430Writer.java @@ -0,0 +1,134 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std430; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferWriter; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; +import net.minecraft.util.Mth; +import org.joml.Matrix4f; +import org.joml.Vector2f; +import org.joml.Vector2i; +import org.joml.Vector3f; +import org.joml.Vector3i; +import org.joml.Vector4f; +import org.joml.Vector4i; + +import java.nio.ByteBuffer; + +public class Std430Writer implements BufferWriter { + + private final ByteBuffer buffer; + private final int pointer; + private BufferObjectLayoutDefinition indexedArrayDefinition; + private int indexedArrayStart; + private int indexedArrayStride; + private int indexedArrayEnd; + + public Std430Writer(ByteBuffer buffer) { + this.buffer = buffer; + this.pointer = buffer.position(); + } + + public void align(int alignment) { + int position = this.buffer.position(); + this.buffer.position(this.pointer + Mth.roundToward(position - this.pointer, alignment)); + } + + public void putFloat(float value) { + this.align(4); + this.buffer.putFloat(value); + } + + public void putInt(int value) { + this.align(4); + this.buffer.putInt(value); + } + + public void putVec2(Vector2f vec) { + this.align(8); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 8); + } + + public void putIVec2(Vector2i vec) { + this.align(8); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 8); + } + + public void putVec3(Vector3f vec) { + this.align(16); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 12); + } + + public void putIVec3(Vector3i vec) { + this.align(16); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 12); + } + + public void putVec4(Vector4f vec) { + this.align(16); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 16); + } + + public void putIVec4(Vector4i vec) { + this.align(16); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 16); + } + + public void putMat4f(Matrix4f vec) { + this.align(16); + vec.get(this.buffer); + this.buffer.position(this.buffer.position() + 64); + } + + public ByteBuffer intoBuffer() { + return this.buffer.flip(); + } + + @Override + public void putStructArray(int index, E object, BufferObjectLayoutDefinition definition) { + int alignment = definition.alignment(BufferLayout.STD430); + int stride = Mth.roundToward(definition.size(BufferLayout.STD430), alignment); + int arrayStart = this.indexedArrayStart(definition, alignment, stride); + int elementStart = arrayStart + stride * index; + this.buffer.position(elementStart); + definition.writeInto(this, object); + this.indexedArrayEnd = Math.max(this.indexedArrayEnd, elementStart + stride); + this.buffer.position(this.indexedArrayEnd); + } + + @Override + public void putStructArray(E[] objects, BufferObjectLayoutDefinition definition) { + int alignment = definition.alignment(BufferLayout.STD430); + int stride = Mth.roundToward(definition.size(BufferLayout.STD430), alignment); + int arrayStart = this.pointer + Mth.roundToward(this.buffer.position() - this.pointer, alignment); + for (int i = 0; i < objects.length; i++) { + this.buffer.position(arrayStart + stride * i); + definition.writeInto(this, objects[i]); + } + this.buffer.position(arrayStart + stride * objects.length); + this.indexedArrayDefinition = null; + } + + private int indexedArrayStart(BufferObjectLayoutDefinition definition, int alignment, int stride) { + int position = this.buffer.position(); + if ( + this.indexedArrayDefinition == definition + && this.indexedArrayStride == stride + && position >= this.indexedArrayStart + && position <= this.indexedArrayEnd + ) { + return this.indexedArrayStart; + } + + this.indexedArrayDefinition = definition; + this.indexedArrayStart = this.pointer + Mth.roundToward(position - this.pointer, alignment); + this.indexedArrayStride = stride; + this.indexedArrayEnd = this.indexedArrayStart; + return this.indexedArrayStart; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObject.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObject.java new file mode 100644 index 00000000..43952839 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObject.java @@ -0,0 +1,31 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers.object; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import org.jspecify.annotations.NonNull; + +import java.nio.ByteBuffer; + +public abstract class BufferObject> { + + protected final BufferLayout layout; + protected final ShaderBufferObjectUsage usage; + + protected BufferObject(BufferLayout layout, ShaderBufferObjectUsage usage) { + this.layout = layout; + this.usage = usage; + } + + protected abstract BufferObjectLayoutDefinition getDefinition(); + + /** + * Writes this buffer object into the provided {@link ByteBuffer} using its layout. + *

+ * Ported from 26.1: the original {@code upload(CommandEncoder, GpuBufferSlice)} (26.1 GPU pipeline) + * and {@code DynamicUniformStorage} are not available on 1.21.1, so the object writes into a + * user-provided buffer instead. + */ + @SuppressWarnings("unchecked") + public void write(@NonNull ByteBuffer buffer) { + getDefinition().write(buffer, (T) this, this.layout); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutDefinition.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutDefinition.java new file mode 100644 index 00000000..39235b4a --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutDefinition.java @@ -0,0 +1,51 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers.object; + +import com.google.common.collect.ImmutableList; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferSizeCalculator; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferWriter; + +import java.nio.ByteBuffer; + +public class BufferObjectLayoutDefinition { + private final ImmutableList> entries; + + public BufferObjectLayoutDefinition(ImmutableList> entries) { + this.entries = entries; + } + + public ByteBuffer write(ByteBuffer buffer, T object, BufferLayout layout) { + BufferWriter builder = layout.createWriter(buffer); + this.writeInto(builder, object); + return builder.intoBuffer(); + } + + public void writeInto(BufferWriter writer, T object) { + for (BufferObjectLayoutEntry entry : entries) { + entry.acceptWriter(writer, object); + } + } + + public int size(BufferLayout layout) { + BufferSizeCalculator sizeCalculator = layout.createSizeCalculator(); + for (BufferObjectLayoutEntry entry : entries) { + entry.acceptSizeCalculator(sizeCalculator); + } + return sizeCalculator.get(); + } + + public int alignment(BufferLayout layout) { + int alignment = 1; + for (BufferObjectLayoutEntry entry : entries) { + alignment = Math.max(alignment, entry.alignment(layout)); + } + return layout == BufferLayout.STD140 ? Math.max(alignment, 16) : alignment; + } + + @SafeVarargs + public static BufferObjectLayoutDefinition create(BufferObjectLayoutEntry... entries) { + ImmutableList.Builder> builder = ImmutableList.builder(); + builder.add(entries); + return new BufferObjectLayoutDefinition<>(builder.build()); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntry.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntry.java new file mode 100644 index 00000000..c44fad34 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntry.java @@ -0,0 +1,87 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers.object; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferSizeCalculator; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferWriter; +import org.joml.Matrix4f; +import org.joml.Vector2f; +import org.joml.Vector2i; +import org.joml.Vector3f; +import org.joml.Vector3i; +import org.joml.Vector4f; +import org.joml.Vector4i; + +import java.util.function.Function; + +public record BufferObjectLayoutEntry(BufferObjectLayoutEntryType type, Function getter) { + + void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { + type.acceptSizeCalculator(sizeCalculator); + } + + void acceptWriter(BufferWriter writer, I object) { + type.acceptWriter(writer, getter.apply(object)); + } + + int alignment(BufferLayout layout) { + return type.alignment(layout); + } + + public static Builder builder(BufferObjectLayoutEntryType type) { + return new Builder<>(type); + } + + public static Builder ofInt() { + return builder(BufferObjectLayoutEntryType.INT); + } + + public static Builder ofFloat() { + return builder(BufferObjectLayoutEntryType.FLOAT); + } + + public static Builder ofVec2f() { + return builder(BufferObjectLayoutEntryType.VEC2); + } + + public static Builder ofVec3f() { + return builder(BufferObjectLayoutEntryType.VEC3); + } + + public static Builder ofVec4f() { + return builder(BufferObjectLayoutEntryType.VEC4); + } + + public static Builder ofVec2i() { + return builder(BufferObjectLayoutEntryType.IVEC2); + } + + public static Builder ofVec3i() { + return builder(BufferObjectLayoutEntryType.IVEC3); + } + + public static Builder ofVec4i() { + return builder(BufferObjectLayoutEntryType.IVEC4); + } + + public static Builder ofMat4f() { + return builder(BufferObjectLayoutEntryType.MAT4); + } + + public static class Builder { + private final BufferObjectLayoutEntryType type; + private Function getter; + + public Builder(BufferObjectLayoutEntryType type) { + this.type = type; + } + + public Builder forGetter(Function getter) { + this.getter = getter; + return this; + } + + public BufferObjectLayoutEntry build() { + return new BufferObjectLayoutEntry<>(type, getter); + } + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntryType.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntryType.java new file mode 100644 index 00000000..59619c46 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntryType.java @@ -0,0 +1,173 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers.object; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferSizeCalculator; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferWriter; +import org.joml.Matrix4f; +import org.joml.Vector2f; +import org.joml.Vector2i; +import org.joml.Vector3f; +import org.joml.Vector3i; +import org.joml.Vector4f; +import org.joml.Vector4i; + +public interface BufferObjectLayoutEntryType { + BufferObjectLayoutEntryType FLOAT = new BufferObjectLayoutEntryType<>() { + @Override + public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { + sizeCalculator.putFloat(); + } + + @Override + public void acceptWriter(BufferWriter writer, Float object) { + writer.putFloat(object); + } + + @Override + public int alignment(BufferLayout layout) { + return 4; + } + }; + + BufferObjectLayoutEntryType VEC2 = new BufferObjectLayoutEntryType<>() { + @Override + public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { + sizeCalculator.putVec2(); + } + + @Override + public void acceptWriter(BufferWriter writer, Vector2f object) { + writer.putVec2(object); + } + + @Override + public int alignment(BufferLayout layout) { + return 8; + } + }; + + BufferObjectLayoutEntryType VEC3 = new BufferObjectLayoutEntryType<>() { + @Override + public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { + sizeCalculator.putVec3(); + } + + @Override + public void acceptWriter(BufferWriter writer, Vector3f object) { + writer.putVec3(object); + } + + @Override + public int alignment(BufferLayout layout) { + return 16; + } + }; + + BufferObjectLayoutEntryType VEC4 = new BufferObjectLayoutEntryType<>() { + @Override + public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { + sizeCalculator.putVec4(); + } + + @Override + public void acceptWriter(BufferWriter writer, Vector4f object) { + writer.putVec4(object); + } + + @Override + public int alignment(BufferLayout layout) { + return 16; + } + }; + + BufferObjectLayoutEntryType INT = new BufferObjectLayoutEntryType<>() { + @Override + public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { + sizeCalculator.putInt(); + } + + @Override + public void acceptWriter(BufferWriter writer, Integer object) { + writer.putInt(object); + } + + @Override + public int alignment(BufferLayout layout) { + return 4; + } + }; + + BufferObjectLayoutEntryType IVEC2 = new BufferObjectLayoutEntryType<>() { + @Override + public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { + sizeCalculator.putIVec2(); + } + + @Override + public void acceptWriter(BufferWriter writer, Vector2i object) { + writer.putIVec2(object); + } + + @Override + public int alignment(BufferLayout layout) { + return 8; + } + }; + + BufferObjectLayoutEntryType IVEC3 = new BufferObjectLayoutEntryType<>() { + @Override + public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { + sizeCalculator.putIVec3(); + } + + @Override + public void acceptWriter(BufferWriter writer, Vector3i object) { + writer.putIVec3(object); + } + + @Override + public int alignment(BufferLayout layout) { + return 16; + } + }; + + BufferObjectLayoutEntryType IVEC4 = new BufferObjectLayoutEntryType<>() { + @Override + public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { + sizeCalculator.putIVec4(); + } + + @Override + public void acceptWriter(BufferWriter writer, Vector4i object) { + writer.putIVec4(object); + } + + @Override + public int alignment(BufferLayout layout) { + return 16; + } + }; + + BufferObjectLayoutEntryType MAT4 = new BufferObjectLayoutEntryType<>() { + @Override + public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { + sizeCalculator.putMat4f(); + } + + @Override + public void acceptWriter(BufferWriter writer, Matrix4f object) { + writer.putMat4f(object); + } + + @Override + public int alignment(BufferLayout layout) { + return 16; + } + }; + + void acceptSizeCalculator(BufferSizeCalculator sizeCalculator); + + void acceptWriter(BufferWriter writer, T object); + + int alignment(BufferLayout layout); +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/ShaderBufferObjectUsage.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/ShaderBufferObjectUsage.java new file mode 100644 index 00000000..923648bb --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/ShaderBufferObjectUsage.java @@ -0,0 +1,6 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers.object; + +public enum ShaderBufferObjectUsage { + UBO, + SSBO +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/compound/DirtyTracked.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/compound/DirtyTracked.java new file mode 100644 index 00000000..00ee6fd0 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/compound/DirtyTracked.java @@ -0,0 +1,5 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.compound; + +public interface DirtyTracked { + void markDirty(); +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/ALRIntegrationCompatMixinPlugin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/ALRIntegrationCompatMixinPlugin.java new file mode 100644 index 00000000..992593a5 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/ALRIntegrationCompatMixinPlugin.java @@ -0,0 +1,63 @@ +package dev.anvilcraft.lib.v2.rendering.integration.mixins; + +import com.google.common.collect.ImmutableMap; +import net.neoforged.fml.loading.LoadingModList; +import org.jetbrains.annotations.ApiStatus; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +@ApiStatus.Internal +public class ALRIntegrationCompatMixinPlugin implements IMixinConfigPlugin { + + private ImmutableMap mixinConditions; + + @Override + public void onLoad(String mixinPackage) { + mixinConditions = ImmutableMap.builder() + .put( + "dev.anvilcraft.lib.v2.rendering.integration.mixins.CachedBlockEntityRenderingPipelineMixin", + isPresent("iris") + ) + .put( + "dev.anvilcraft.lib.v2.rendering.integration.mixins.RebuildTaskMixin", + isPresent("iris") + ) + .build(); + } + + private boolean isPresent(String modid) { + return LoadingModList.get().getModFileById(modid) != null; + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + Boolean bl = mixinConditions.get(mixinClassName); + return bl == null || bl; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/GameRendererMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/GameRendererMixin.java new file mode 100644 index 00000000..f255774d --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/GameRendererMixin.java @@ -0,0 +1,23 @@ +package dev.anvilcraft.lib.v2.rendering.mixins; + +import dev.anvilcraft.lib.v2.rendering.event.MainTargetResizeEvent; +import net.minecraft.client.renderer.GameRenderer; +import net.neoforged.fml.ModLoader; +import org.jetbrains.annotations.ApiStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(GameRenderer.class) +@ApiStatus.Internal +public class GameRendererMixin { + + @Inject( + method = "resize", + at = @At("HEAD") + ) + void onResize(int width, int height, CallbackInfo ci) { + ModLoader.postEvent(new MainTargetResizeEvent(width, height)); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/MinecraftMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/MinecraftMixin.java new file mode 100644 index 00000000..85ca9d4e --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/MinecraftMixin.java @@ -0,0 +1,32 @@ +package dev.anvilcraft.lib.v2.rendering.mixins; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.main.GameConfig; +import net.minecraft.client.multiplayer.ClientLevel; +import org.jetbrains.annotations.ApiStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Minecraft.class) +@ApiStatus.Internal +public class MinecraftMixin { + + @Inject( + method = "", + at = @At("RETURN") + ) + private void onCreateInstance(GameConfig gameConfig, CallbackInfo ci) { + // #P7b: ALRPostEffects.createPostEffects(); + // #P7d: CachedBlockEntityRenderingPipeline.create(); + } + + @Inject( + method = "updateLevelInEngines(Lnet/minecraft/client/multiplayer/ClientLevel;)V", + at = @At("RETURN") + ) + private void onUpdateLevel(ClientLevel level, CallbackInfo ci) { + // #P7d: CachedBlockEntityRenderingPipeline.updateLevel(level); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/RenderTypeMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/RenderTypeMixin.java new file mode 100644 index 00000000..42894676 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/RenderTypeMixin.java @@ -0,0 +1,23 @@ +package dev.anvilcraft.lib.v2.rendering.mixins; + +import dev.anvilcraft.lib.v2.rendering.extension.ALRRenderTypeExtension; +import net.minecraft.client.renderer.RenderType; +import org.jetbrains.annotations.ApiStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(RenderType.class) +@ApiStatus.Internal +public class RenderTypeMixin implements ALRRenderTypeExtension { + + @Unique + private boolean anvillib_rendering$bloomRendering = false; + + public boolean anvillib_rendering$bloomRendering() { + return anvillib_rendering$bloomRendering; + } + + public void anvillib_rendering$setBloomRendering(boolean value) { + this.anvillib_rendering$bloomRendering = value; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/Sdf2d.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/Sdf2d.java new file mode 100644 index 00000000..96c3ef8a --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/Sdf2d.java @@ -0,0 +1,379 @@ +package dev.anvilcraft.lib.v2.rendering.sdf; + +import lombok.experimental.UtilityClass; +import net.minecraft.util.Mth; +import org.jetbrains.annotations.NotNull; + +@UtilityClass +public class Sdf2d { + + /** + * @param params Sdf 参数 + * @param pX 点 x 坐标 + * @param pY 点 y 坐标 + * @param rX 矩形 x 起始点 + * @param rY 矩形 y 起始点 + * @param rotation 旋转角度 + * @param centred 居中 + * @return 点距离 sdf 的距离 + */ + public static float sd( + @NotNull SdfParameters params, + float pX, + float pY, + float rX, + float rY, + float rotation, + boolean centred + + ) { + var rect = params.getRect(); + var round = params.getRound(); + var smooth = params.getSmooth(); + var stroke = params.getStroke(); + + var ex = (round + smooth + stroke) * 2.0f; + + var width = rect.z + ex; + var height = rect.w + ex; + final var hw = width * 0.5f; + final var hh = height * 0.5f; + + var radian = rotation * Mth.DEG_TO_RAD; + var cos = Mth.cos(radian); + var sin = Mth.sin(radian); + + float cx; + float cy; + + if (centred) { + + cx = rX; + cy = rY; + + } else { + + cx = rX + hw - hw * cos + hh * sin; + cy = rY + hh - hw * sin - hh * cos; + } + + var px = pX - cx; + var py = pY - cy; + + if (rotation != 0f) { + + var tx = px * cos + py * sin; + var ty = py * cos - px * sin; + + px = tx; + py = ty; + } + + + var type = params.getRenderType(); + var shape = params.getShapeParams(); + + var d = switch (type) { + case BOX -> sdRect( + px, py, + shape.x - round, + shape.y - round + ) - round; + + case CIRCLE -> sdCircle( + px, py, + shape.x + ); + + case ARC -> sdArc( + px, py, + shape.x, shape.y, + shape.z, shape.w + ) - round; + + case SECTOR -> sdRing( + px, py, + shape.x, shape.y, + shape.z, shape.w + ) - round; + + case PIE -> sdPie( + px, py, + shape.x, shape.y, + shape.z + ); + + case SEGMENT -> sdSegment( + px, py, + shape.x, shape.y, + shape.z, shape.w + ) - round; + + case CAPSULE -> sdUnevenCapsule( + px, py, + shape.x, shape.y, + shape.z + ); + + case EGG -> sdEgg( + px, py, + shape.x, shape.y, + shape.z + ); + + case TRIANGLE_EQUILATERAL -> sdEquilateralTriangle( + px, py, + shape.x - round * 2.0f + ) - round; + + case TRIANGLE_ISOSCELES -> sdIsoscelesTriangle( + px, + py - (shape.y * 0.5f - round * 2.0f), + shape.x - round, + round * 2.0f - shape.y + ) - round; + + default -> Float.POSITIVE_INFINITY; + }; + + if (params.isOnion()) { + var half = stroke * 0.5f; + d = Mth.abs(d) - half; + } + + return d; + } + + public static float sdRect( + float px, float py, + float bx, float by + ) { + + float dx = Math.abs(px) - bx; + float dy = Math.abs(py) - by; + + float mx = Math.max(dx, 0.0f); + float my = Math.max(dy, 0.0f); + + return (float) Mth.length(mx, my) + + Math.min(Math.max(dx, dy), 0.0f); + } + + public static float sdCircle( + float px, float py, + float r + ) { + return (float) Mth.length(px, py) - r; + } + + public static float sdArc( + float px, float py, + float scx, float scy, + float ra, float rb + ) { + + px = Math.abs(px); + + float result; + + if (scy * px > scx * py) { + var dx = px - scx * ra; + var dy = py - scy * ra; + result = (float) Mth.length(dx, dy); + } else { + result = Math.abs((float) Mth.length(px, py) - ra); + } + + return result - rb; + } + + public static float sdRing( + float px, float py, + float nx, float ny, + float r, float th + ) { + + px = Math.abs(px); + + float rx = nx * px + (-ny) * py; + float ry = ny * px + nx * py; + + float a = Math.abs((float) Mth.length(rx, ry) - r) - th * 0.5f; + + float by = Math.max(0.0f, + Math.abs(r - ry) - th * 0.5f); + + float b = (float) Mth.length(rx, by) * + Mth.sign(rx); + + return Math.max(a, b); + } + + public static float sdPie( + float px, float py, + float cx, float cy, + float r + ) { + + px = Math.abs(px); + + float l = (float) Mth.length(px, py) - r; + + float dot = px * cx + py * cy; + float clamped = Mth.clamp(dot, 0.0f, r); + + float mx = px - cx * clamped; + float my = py - cy * clamped; + + float m = (float) Mth.length(mx, my); + + return Math.max( + l, + m * Mth.sign(cy * px - cx * py) + ); + } + + public static float sdUnevenCapsule( + float px, float py, + float r1, + float r2, + float h + ) { + + px = Math.abs(px); + + float b = (r1 - r2) / h; + float a = (float)Math.sqrt(1.0f - b * b); + float k = px * (-b) + py * a; + + if (k < 0.0f) { + return (float) Mth.length(px, py) - r1; + } + + if (k > a * h) { + + float dx = px; + float dy = py - h; + + return (float) Mth.length(dx, dy) - r2; + } + + return px * a + py * b - r1; + } + + public static float sdEgg( + float px, float py, + float he, + float ra, + float rb + ) { + + float ce = + 0.5f * ( + he * he + - (ra - rb) * (ra - rb) + ) / (ra - rb); + + px = Math.abs(px); + + if (py < 0.0f) { + + return (float) Mth.length(px, py) - ra; + } + + if (py * ce - px * he > he * ce) { + + return (float) Mth.length( + px, + py - he + ) - rb; + } + + return (float) Mth.length( + px + ce, + py + ) - (ce + ra); + } + + public static float sdSegment( + float px, float py, + float ax, float ay, + float bx, float by + ) { + float bax = bx - ax; + float bay = by - ay; + float pax = px - ax; + float pay = py - ay; + float dot = pax * bax + pay * bay; + float h = Mth.clamp( + dot / (bax * bax + bay * bay), + 0.0f, + 1.0f + ); + + return (float) Mth.length( + pax - h * bax, + pay - h * bay + ); + } + + public static float sdEquilateralTriangle( + float px, float py, + float r + ) { + final float k = (float) Math.sqrt(3.0); + + px = Math.abs(px) - r; + py = py + r / k; + + if (px + k * py > 0.0f) { + var tx = (px - k * py) * 0.5f; + var ty = (-k * px - py) * 0.5f; + px = tx; + py = ty; + } + + px -= Mth.clamp(px, -2.0f * r, 0.0f); + + return -(float) Mth.length(px, py) * Mth.sign(py); + } + + public static float sdIsoscelesTriangle( + float px, float py, + float qx, float qy + ) { + px = Math.abs(px); + + float dotQ = qx * qx + qy * qy; + float h1 = Mth.clamp( + (px * qx + py * qy) / dotQ, + 0.0f, 1.0f + ); + float ax = px - qx * h1; + float ay = py - qy * h1; + + float h2; + if (qx != 0.0f) { + h2 = Mth.clamp(px / qx, 0.0f, 1.0f); + } else { + h2 = px > 0.0f ? 1.0f : 0.0f; + } + float bx = px - qx * h2; + float by = py - qy; + + float s = -Mth.sign(qy); + + float da = ax * ax + ay * ay; + float sa = s * (px * qy - py * qx); + + float db = bx * bx + by * by; + float sb = s * (py - qy); + + float dDist = Math.min(da, db); + float dSign = Math.min(sa, sb); + + return -(float) Math.sqrt(dDist) + * Mth.sign(dSign); + } + +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfParameters.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfParameters.java new file mode 100644 index 00000000..2ee2ea8f --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfParameters.java @@ -0,0 +1,202 @@ +package dev.anvilcraft.lib.v2.rendering.sdf; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutEntry; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.ShaderBufferObjectUsage; +import lombok.Getter; +import net.minecraft.util.Mth; +import org.joml.Vector4f; +import org.joml.Vector4i; + +@Getter +public class SdfParameters extends BufferObject { + + public static final BufferObjectLayoutDefinition DEFINITION = BufferObjectLayoutDefinition.create( + BufferObjectLayoutEntry.ofVec4f().forGetter(SdfParameters::getSharedParams).build(), + BufferObjectLayoutEntry.ofVec4f().forGetter(SdfParameters::getShapeParams).build(), + BufferObjectLayoutEntry.ofVec4f().forGetter(SdfParameters::getRect).build(), + BufferObjectLayoutEntry.ofVec4i().forGetter(SdfParameters::getTypeParams).build() + ); + + private final Vector4f sharedParams = new Vector4f(); + private final Vector4f shapeParams = new Vector4f(); + private final Vector4f rect = new Vector4f(); + private final Vector4i typeParams = new Vector4i(); + + int uboIndex = -1; + boolean uploaded = false; + + protected SdfParameters() { + super(BufferLayout.STD140, ShaderBufferObjectUsage.UBO); + } + + public void box(float width, float height) { + this ._renderType(SdfRenderType.BOX); + this.shapeParams .set(width * 0.5f, height * 0.5f, 0.0f, 0.0f); + } + + public void circle(float radius) { + this ._renderType(SdfRenderType.CIRCLE); + this.shapeParams .set(radius, 0.0f, 0.0f, 0.0f); + } + + public void arc(float sweep, float radius, float thickness) { + this ._renderType(SdfRenderType.ARC); + + var radian = sweep * Mth.DEG_TO_RAD; + var cos = Mth.cos(radian); + var sin = Mth.sin(radian); + this.shapeParams .set(sin, cos, radius - thickness * 0.5f, thickness); + } + + public void sector(float sweep, float radius, float thickness) { + this ._renderType(SdfRenderType.SECTOR); + + var radian = sweep * Mth.DEG_TO_RAD; + var cos = Mth.cos(radian); + var sin = Mth.sin(radian); + this.shapeParams .set(cos, sin, radius - thickness * 0.5f, thickness); + } + + public void pie(float sweep, float radius) { + this ._renderType(SdfRenderType.PIE); + + var radian = sweep * Mth.DEG_TO_RAD; + var cos = Mth.cos(radian); + var sin = Mth.sin(radian); + this.shapeParams .set(cos, sin, radius, 0.0f); + } + + public void capsule(float topRadius, float bottomRadius, float height) { + this ._renderType(SdfRenderType.CAPSULE); + this.shapeParams .set(topRadius, bottomRadius, height, 0.0f); + } + + public void egg(float topRadius, float bottomRadius, float height) { + this ._renderType(SdfRenderType.EGG); + this.shapeParams .set(height, bottomRadius, topRadius, 0.0f); + } + + public void segment(float x0, float y0, float x1, float y1) { + this ._renderType(SdfRenderType.SEGMENT); + this.shapeParams .set(x0, y0, x1, y1); + } + + public void triangleEquilateral(float radius) { + this ._renderType(SdfRenderType.TRIANGLE_EQUILATERAL); + this.shapeParams .set(radius, 0.0f, 0.0f, 0.0f); + } + + public void triangleIsosceles(float base, float height) { + this ._renderType(SdfRenderType.TRIANGLE_ISOSCELES); + this.shapeParams .set(base, height, 0.0f, 0.0f); + } + + public void smooth(float smooth) { + this ._smooth(smooth); + } + + public void stroke(float width) { + this ._width(width); + } + + public void round(float radius) { + this ._cornerRadius(radius); + } + + public void fill() { + this ._pass(SdfPassType.FILL); + } + + public void onion(boolean enable) { + this.typeParams.z = enable ? 1 : 0; + } + + public void light(float v) { + this ._pass(SdfPassType.LIGHT); + this ._smooth(v); + this ._light(v); + } + + public SdfParameters duplicate() { + var copy = new SdfParameters(); + + copy.sharedParams .set(this.sharedParams); + copy.shapeParams .set(this.shapeParams); + copy.rect .set(this.rect); + copy.typeParams .set(this.typeParams); + + return copy; + } + + public float getSmooth() { + return this.sharedParams.x; + } + + public float getStroke() { + return this.sharedParams.y; + } + + public float getRound() { + return this.sharedParams.z; + } + + public SdfRenderType getRenderType() { + return SdfRenderType + .fromOrdinal(this.typeParams.y); + } + + public boolean isOnion() { + return this.typeParams.z != 0; + } + + private void _smooth(float value) { + this.sharedParams.x = value; + this.uploaded = false; + } + + private void _light(float value) { + this.sharedParams.w = 4.605f / value; + this.uploaded = false; + } + + private void _width(float value) { + this.sharedParams.y = value; + this.uploaded = false; + } + + private void _cornerRadius(float value) { + this.sharedParams.z = value; + this.uploaded = false; + } + + private void _pass(SdfPassType value) { + this.typeParams.x = value.ordinal(); + this.uploaded = false; + } + + private void _renderType(SdfRenderType value) { + this.typeParams.y = value.ordinal(); + this.uploaded = false; + } + + @Override + protected BufferObjectLayoutDefinition getDefinition() { + return DEFINITION; + } + + public void reset() { + this.sharedParams .set(0.0f, 0.0f, 0.0f, 0.0f); + this.shapeParams .set(0.0f, 0.0f, 0.0f, 0.0f); + this.rect .set(0.0f, 0.0f, 0.0f, 0.0f); + this.typeParams .set(0, 0, 0, 4); + + this.uploaded = false; + } + + public boolean isShared() { + return this.uboIndex != -1; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfPassType.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfPassType.java new file mode 100644 index 00000000..24fc13e4 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfPassType.java @@ -0,0 +1,6 @@ +package dev.anvilcraft.lib.v2.rendering.sdf; + +public enum SdfPassType { + FILL, + LIGHT +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfRenderType.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfRenderType.java new file mode 100644 index 00000000..f55b43d3 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfRenderType.java @@ -0,0 +1,22 @@ +package dev.anvilcraft.lib.v2.rendering.sdf; + +public enum SdfRenderType { + BOX, + CIRCLE, + ARC, + SECTOR, + PIE, + CAPSULE, + EGG, + SEGMENT, + + TRIANGLE_EQUILATERAL, + TRIANGLE_ISOSCELES, + ; + + private static final SdfRenderType[] VALUES = values(); + + public static SdfRenderType fromOrdinal(int ordinal) { + return VALUES[ordinal]; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/util/Timer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/util/Timer.java new file mode 100644 index 00000000..ab22da58 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/util/Timer.java @@ -0,0 +1,9 @@ +package dev.anvilcraft.lib.v2.rendering.util; + +import net.minecraft.client.Minecraft; + +public class Timer { + public static float getPartialTick() { + return Minecraft.getInstance().getTimer().getGameTimeDeltaPartialTick(Minecraft.getInstance().isPaused()); + } +} diff --git a/module.rendering/src/main/resources/META-INF/accesstransformer.cfg b/module.rendering/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 00000000..28896b16 --- /dev/null +++ b/module.rendering/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,3 @@ +# #P7a: exposes CompositeRenderType.state() for ALRRenderTypeExtension.copyWithBloom (RenderType.create(name, format, mode, bufferSize, state) on 1.21.1). +public net.minecraft.client.renderer.RenderType$CompositeRenderType +public net.minecraft.client.renderer.RenderType$CompositeRenderType state()Lnet/minecraft/client/renderer/RenderType$CompositeState; diff --git a/module.rendering/src/main/resources/META-INF/neoforge.mods.toml b/module.rendering/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..27f7ba75 --- /dev/null +++ b/module.rendering/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,86 @@ +# This is an example mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader = "javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. +loaderVersion = "${loader_version_range}" #mandatory +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license = "${mod_license}" +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory +# The modid of the mod +modId = "${mod_id}" #mandatory +# The version number of the mod +version = "${mod_version}" #mandatory +# A display name for the mod +displayName = "${mod_name}" #mandatory +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional +# A URL for the "homepage" for this mod, displayed in the mod UI +displayURL="https://github.com/Anvil-Dev/AnvilLib" +# A file name (in the root of the mod JAR) containing a logo for display +logoFile="icon.png" #optional +# A text field displayed in the mod UI +#credits="" #optional +# A text field displayed in the mod UI +authors = "${mod_authors}" #optional +# Display Test controls the display for your mod in the server connection screen +# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. +# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. +# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. +# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. +# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. +#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) + +# The description text for the mod (multi line!) (#mandatory) +description = '''${mod_description}''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +[[mixins]] +config = "${mod_id}.mixins.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies."${mod_id}"]] #optional +# the modid of the dependency +modId = "neoforge" #mandatory +# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). +# 'required' requires the mod to exist, 'optional' does not +# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning +type = "required" #mandatory +# Optional field describing why the dependency is required or why it is incompatible +# reason="..." +# The version range of the dependency +versionRange = "${neo_version_range}" #mandatory +# An ordering relationship for the dependency. +# BEFORE - This mod is loaded BEFORE the dependency +# AFTER - This mod is loaded AFTER the dependency +ordering = "NONE" +# Side this dependency is applied on - BOTH, CLIENT, or SERVER +side = "BOTH" +# Here's another dependency +[[dependencies."${mod_id}"]] +modId = "minecraft" +type = "required" +# This version range declares a minimum of the current minecraft version up to but not including the next major version +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features."${mod_id}"] +#openGLVersion="[3.2,)" diff --git a/module.rendering/src/main/resources/anvillib_rendering.mixins.json b/module.rendering/src/main/resources/anvillib_rendering.mixins.json new file mode 100644 index 00000000..950b510f --- /dev/null +++ b/module.rendering/src/main/resources/anvillib_rendering.mixins.json @@ -0,0 +1,17 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.anvilcraft.lib.v2.rendering.mixins", + "compatibilityLevel": "JAVA_8", + "refmap": "anvillib.refmap.json", + "mixins": [ + ], + "client": [ + "GameRendererMixin", + "MinecraftMixin", + "RenderTypeMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.fsh new file mode 100644 index 00000000..6389d901 --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.fsh @@ -0,0 +1,23 @@ +#version 330 + +layout(std140) uniform BloomParameters { + float BloomIntensity; + float BloomBlendThreshold; + float LuminanceSensitivity; +}; + +uniform sampler2D GameSampler; +uniform sampler2D DiffuseSampler; + +in vec2 texCoord; + +out vec4 fragColor; + +void main() { + vec4 colorGame = texture(GameSampler, texCoord); + vec3 color = colorGame.rgb; + vec4 bloom4 = texture(DiffuseSampler, texCoord); + vec3 bloom = bloom4.rgb * BloomIntensity; + vec3 finalColor = color + (bloom * pow(BloomBlendThreshold, length(color) * LuminanceSensitivity)); + fragColor = vec4(finalColor, colorGame.a + bloom4.a); +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blit.vsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blit.vsh new file mode 100644 index 00000000..b4c4ff95 --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blit.vsh @@ -0,0 +1,16 @@ +#version 330 + +layout(std140) uniform Transforms { + mat4 ProjMat; +}; + +in vec3 Position; +in vec2 UV0; + +out vec2 texCoord; + +void main() { + gl_Position = ProjMat * vec4(Position, 1.0); + + texCoord = UV0; +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.fsh new file mode 100644 index 00000000..df2c9796 --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.fsh @@ -0,0 +1,36 @@ +#version 330 + +#moj_import + +layout(std140) uniform BlurParameters { + vec2 BlurDir; + float SampleStepLength; + float ColorMultiplier; +}; + +uniform sampler2D DiffuseSampler; + +const float weight[7] = float[] ( + 0.1827621234, + 0.1566532716, + 0.1220589867, + 0.0998664144, + 0.0130542649, + 0.0089357635, + 0.0080502373 +); + +in vec2 texCoord; + +out vec4 fragColor; + +void main() { + vec2 texOffset = 1.0 / textureSize(DiffuseSampler, 0); + + vec3 result = texture(DiffuseSampler, texCoord).rgb * weight[0]; + for (int i = 1; i < 7; ++i) { + result += texture(DiffuseSampler, texCoord + vec2(BlurDir.x * texOffset.x * i * SampleStepLength, BlurDir.y * texOffset.y * i * SampleStepLength)).rgb * weight[i]; + result += texture(DiffuseSampler, texCoord - vec2(BlurDir.x * texOffset.x * i * SampleStepLength, BlurDir.y * texOffset.y * i * SampleStepLength)).rgb * weight[i]; + } + fragColor = vec4(saturate(result * ColorMultiplier), 1.0); +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.fsh new file mode 100644 index 00000000..04ac5dee --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.fsh @@ -0,0 +1,26 @@ +#version 330 + +uniform sampler2D DiffuseSampler; + +layout(std140) uniform BloomParameters { + vec2 uResolution; + int uFrameIndex; +}; + +in vec2 texCoord; +out vec4 fragColor; + +void main() { + vec2 uv = texCoord; + + vec2 texel = uResolution; + vec4 bloom = + texture(DiffuseSampler, uv + vec2(-0.5, -0.5) * texel) + + texture(DiffuseSampler, uv + vec2(0.5, -0.5) * texel) + + texture(DiffuseSampler, uv + vec2(-0.5, 0.5) * texel) + + texture(DiffuseSampler, uv + vec2(0.5, 0.5) * texel); + + bloom *= 0.25; + + fragColor = bloom; +} \ No newline at end of file diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.fsh new file mode 100644 index 00000000..3deee49a --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.fsh @@ -0,0 +1,51 @@ +#version 330 + +uniform sampler2D DiffuseSampler; + +layout(std140) uniform GlitchParameters { + vec2 InSize; + float GameTime; +}; + +in vec2 texCoord; +out vec4 fragColor; + +float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} + +void main() { + vec4 color = texture(DiffuseSampler, texCoord); + + // 亮度提取 — 用于蓝色染色混合 + float luminance = dot(color.rgb, vec3(0.299, 0.587, 0.114)); + + // 扫描线间距与行号 + float lineSpacing = 4.0; + float lineY = floor((texCoord.y * InSize.y) / lineSpacing); + + // 基于行号和时间变化的随机值 + float lineHash = hash(vec2(lineY, floor(GameTime * 3.0))); + + // 扫描线亮度变化 (0.7 ~ 1.0) + float scanBrightness = 0.7 + 0.3 * lineHash; + + // 约30%的扫描线有水平抖动 + float jitterStrength = 2.0; + float jitter = (lineHash - 0.5) * 2.0 * jitterStrength / InSize.x; + float shouldJitter = step(0.7, hash(vec2(lineY, 42.0))); + + // 带水平偏移的采样 + vec2 jitteredCoord = texCoord + vec2(jitter * shouldJitter, 0.0); + vec4 jitteredColor = texture(DiffuseSampler, jitteredCoord); + + // 蓝色染色 + vec3 blueTint = vec3(0.2, 0.45, 1.0); + float jitteredLum = dot(jitteredColor.rgb, vec3(0.299, 0.587, 0.114)); + vec3 finalColor = mix(jitteredColor.rgb, blueTint * jitteredLum, 0.75); + + // 应用扫描线亮度 + finalColor *= scanBrightness; + + fragColor = vec4(finalColor, 0.85 * color.a); +} \ No newline at end of file diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh new file mode 100644 index 00000000..01d09ce0 --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh @@ -0,0 +1,194 @@ +#version 330 + +#define MAX_SDFS 256 + +struct Sdf { + vec4 Shared; + vec4 Shape; + vec4 Rect; + ivec4 Types; +}; + +layout(std140) uniform SDFParameters { + Sdf[MAX_SDFS] SDFs; +}; + +#define RT_BOX 0 +#define RT_CIRCLE 1 +#define RT_ARC 2 +#define RT_SECTOR 3 +#define RT_PIE 4 +#define RT_UCAPSULE 5 +#define RT_EGG 6 +#define RT_SEGMENT 7 +#define RT_ETRIANGLE 8 +#define RT_ITRIANGLE 9 + +#define PASS_FILL 0 +#define PASS_LIGHT 1 + +in vec2 vPosition; +in vec4 vColor; +flat in int vIndex; + +out vec4 fragColor; + +#define uSmoothRadius (params.Shared.x) +#define uStrokeWidth (params.Shared.y) +#define uCornerRadius (params.Shared.z) +#define uLightDecay (params.Shared.w) + +#define uPassType (params.Types.x) +#define uRenderType (params.Types.y) +#define uOnion (params.Types.z) + +// from https://iquilezles.org/articles/distfunctions2d/ +float sdRect( in vec2 p, in vec2 b ) { + vec2 d = abs(p)-b; + return length(max(d,0.0)) + min(max(d.x,d.y),0.0); +} + +// from https://iquilezles.org/articles/distfunctions2d/ +float sdCircle(vec2 p, float r) { + return length(p) - r; +} + +// from https://iquilezles.org/articles/distfunctions2d/ +float sdArc(in vec2 p, in vec2 sc, in float ra, in float rb) { + // sc is the sin/cos of the arc's aperture + p.x = abs(p.x); + return ((sc.y*p.x>sc.x*p.y) ? length(p-sc*ra) : + abs(length(p)-ra)) - rb; +} + +// from https://iquilezles.org/articles/distfunctions2d/ +float sdRing(in vec2 p, in vec2 n, in float r, in float th) { + p.x = abs(p.x); + p = mat2x2(n.x,n.y,-n.y,n.x)*p; + return max( abs(length(p)-r)-th*0.5, + length(vec2(p.x,max(0.0,abs(r-p.y)-th*0.5)))*sign(p.x) ); +} + +// from https://iquilezles.org/articles/distfunctions2d/ +float sdPie(in vec2 p, in vec2 c, in float r) { + p.x = abs(p.x); + float l = length(p) - r; + float m = length(p-c*clamp(dot(p,c),0.0,r)); // c=sin/cos of aperture + return max(l,m*sign(c.y*p.x-c.x*p.y)); +} + +// from https://iquilezles.org/articles/distfunctions2d/ +float sdUnevenCapsule( vec2 p, float r1, float r2, float h ) +{ + p.x = abs(p.x); + float b = (r1-r2)/h; + float a = sqrt(1.0-b*b); + float k = dot(p,vec2(-b,a)); + if( k < 0.0 ) return length(p) - r1; + if( k > a*h ) return length(p-vec2(0.0,h)) - r2; + return dot(p, vec2(a,b) ) - r1; +} + +// from https://iquilezles.org/articles/distfunctions2d/ +float sdEgg( in vec2 p, in float he, in float ra, in float rb ) +{ + // all this can be precomputed for any given shape + float ce = 0.5*(he*he-(ra-rb)*(ra-rb))/(ra-rb); + + // only this needs to be run per pixel + p.x = abs(p.x); + if( p.y<0.0 ) return length(p)-ra; + if( p.y*ce-p.x*he>he*ce ) return length(vec2(p.x,p.y-he))-rb; + return length(vec2(p.x+ce,p.y))-(ce+ra); +} + +// from https://iquilezles.org/articles/distfunctions2d/ +float sdSegment( in vec2 p, in vec2 a, in vec2 b ) +{ + vec2 ba = b-a; + vec2 pa = p-a; + float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 ); + return length(pa-h*ba); +} + +// from https://iquilezles.org/articles/distfunctions2d/ +float sdEquilateralTriangle( in vec2 p, in float r ) +{ + const float k = sqrt(3.0); + p.x = abs(p.x) - r; + p.y = p.y + r/k; + if( p.x+k*p.y>0.0 ) p = vec2(p.x-k*p.y,-k*p.x-p.y)/2.0; + p.x -= clamp( p.x, -2.0*r, 0.0 ); + return -length(p)*sign(p.y); +} + +// from https://iquilezles.org/articles/distfunctions2d/ +float sdIsoscelesTriangle( in vec2 p, in vec2 q ) +{ + p.x = abs(p.x); + vec2 a = p - q*clamp( dot(p,q)/dot(q,q), 0.0, 1.0 ); + vec2 b = p - q*vec2( clamp( p.x/q.x, 0.0, 1.0 ), 1.0 ); + float s = -sign( q.y ); + vec2 d = min( vec2( dot(a,a), s*(p.x*q.y-p.y*q.x) ), + vec2( dot(b,b), s*(p.y-q.y) )); + return -sqrt(d.x)*sign(d.y); +} + +void main() { + Sdf params = SDFs[vIndex]; + vec4 shape = params.Shape; + vec2 p = vPosition; + float alpha = 0.0; + + float d = 1e5; + + float r = uCornerRadius; + float r2 = r + r; + switch (uRenderType) { + case RT_BOX: + d = sdRect(p, shape.xy - vec2(r)) - r; + break; + case RT_CIRCLE: + d = sdCircle(p, shape.x); + break; + case RT_ARC: + d = sdArc(p, shape.xy, shape.z, shape.w) - r; + break; + case RT_SECTOR: + d = sdRing(p, shape.xy, shape.z, shape.w) - r; + break; + case RT_PIE: + d = sdPie(p, shape.xy, shape.z); + break; + case RT_UCAPSULE: + d = sdUnevenCapsule(p, shape.x, shape.y, shape.z); + break; + case RT_EGG: + d = sdEgg(p, shape.x, shape.y, shape.z); + break; + case RT_SEGMENT: + d = sdSegment(p, shape.xy, shape.zw) - r; + break; + case RT_ETRIANGLE: + d = sdEquilateralTriangle(p, shape.x - r2) - r; + break; + case RT_ITRIANGLE: + d = sdIsoscelesTriangle(p - vec2(0.0, shape.y * 0.5 - r2), vec2(shape.x - r, r2 - shape.y)) - r; + break; + } + + float aa = max(fwidth(d) * 0.5, uSmoothRadius); + float halfWidth = uStrokeWidth * 0.5; + float useOnion = float(uOnion); // int to float + d = mix(d, abs(d) - halfWidth, useOnion); + + float useLight = float(uPassType); // now only have two passes, so just directly int to float + float fillAlpha = smoothstep(0.0 + aa, 0.0 - aa, d); + float lightAlpha = useLight * exp(-d * uLightDecay); + + alpha = mix(fillAlpha, lightAlpha, useLight); + + vec4 color = vColor; + color.a *= alpha; + fragColor = color; +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.vsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.vsh new file mode 100644 index 00000000..97bf2577 --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.vsh @@ -0,0 +1,35 @@ +#version 330 + +#moj_import +#moj_import + +#define MAX_SDFS 256 + +struct Sdf { + vec4 Shared; + vec4 Shape; + vec4 Rect; + ivec4 Types; +}; + +layout(std140) uniform SDFParameters { + Sdf[MAX_SDFS] SDFs; +}; + +in vec3 Position; +in vec4 Color; +in vec2 UV0; +in ivec2 UV1; + +out vec2 vPosition; +out vec4 vColor; +flat out int vIndex; + +void main() { + gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); + + int index = UV1.x; + vPosition = (UV0 - 0.5) * SDFs[index].Rect.zw; + vColor = Color; + vIndex = index; +} \ No newline at end of file diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.fsh new file mode 100644 index 00000000..df5500a5 --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.fsh @@ -0,0 +1,61 @@ +#version 330 + +uniform sampler2D DiffuseSampler; +uniform sampler2D PreviousSampler; + +layout(std140) uniform BloomParameters { + vec2 uResolution; + int uFrameIndex; +}; + +in vec2 texCoord; +out vec4 fragColor; + +#define R 2 +#define KERNEL_SIZE 5 + +const float GAUSSIAN_KERNEL[25] = float[]( + 0.002968, 0.013304, 0.021945, 0.013304, 0.002968, + 0.013304, 0.059634, 0.098320, 0.059634, 0.013304, + 0.021945, 0.098320, 0.162102, 0.098320, 0.021945, + 0.013304, 0.059634, 0.098320, 0.059634, 0.013304, + 0.002968, 0.013304, 0.021945, 0.013304, 0.002968 +); + +vec4 samplerGaussian( + sampler2D tex, + vec2 texCoord, + vec2 stride +) { + vec4 color = vec4(0.0); + + for (int y = -R; + y <= R; + y ++ + ) { + for ( + int x = -R; + x <= R; + x ++ + ) { + int idx = (y + R) * KERNEL_SIZE + (x + R); + float w = GAUSSIAN_KERNEL[idx]; + + vec2 uv = texCoord + vec2(x, y) * stride; + color += texture(tex, uv) * w; + } + } + + return color; +} + +void main() { +// vec2 noiseUV = mod(texCoord.xy + vec2(uFrameIndex * 17), 128.0) * 0.0078125; + + vec2 curStride = uResolution; + vec2 lstStride = curStride * 0.5; + + vec4 curMipped = samplerGaussian(DiffuseSampler, texCoord, curStride); + vec4 prvMipped = samplerGaussian(PreviousSampler, texCoord, lstStride); + fragColor = curMipped * 0.5 + prvMipped * 0.9; +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/include/util.glsl b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/include/util.glsl new file mode 100644 index 00000000..32ac3b04 --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/include/util.glsl @@ -0,0 +1,13 @@ + +vec3 saturate(vec3 color) { + return vec3(clamp(color.r, 0, 1), clamp(color.g, 0, 1), clamp(color.b, 0, 1)); +} + +float toneMap(float f) { + return - (f - 1) * (f - 1) + 1; +} + +vec3 toneMap(vec3 f) { + return vec3(toneMap(f.r), toneMap(f.g), toneMap(f.b)); +} + diff --git a/module.rendering/src/main/resources/icon.png b/module.rendering/src/main/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..15304bd54d5d97d8482049d5f16a7544748ce05a GIT binary patch literal 1089 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabRA=0U}gyL32}YB&qp?9cb}fRhL)ab z>**&8P1h&;i5V@+5z~{9lu=Mp{cG+rNOuP4jFK;J~t_xRpI&Nf4%?X zZmY?-7zkw=;bC zsjT>Uk~ZUn{GF#RHtMc+sGWYc$A>9-;igj6hzoLO7wEVa*QxB)zM=ff)bhV&`;h}b zD&$S4O;$O;Sf&`=w_-tNu&sr4nCukimfSM|=84-o^=H0sFgVR)oOSu;mLIn4lUH@E zPWkvN(C^lx3(G6wS;EsJIMe%PuP~i06!s*lLHg)A!3fTUY`(JJ860vaMb2DUI{zB$ z25}G0l9!&RrHovbwaBi{I35yXA*NOEX}{&%4{Sd|jrPVzaJhpb&(qb TEST_VEC2 = new BufferObjectLayoutEntryType<>() { + @Override + public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { + sizeCalculator.putVec2(); + } + + @Override + public void acceptWriter(BufferWriter writer, Object object) { + } + + @Override + public int alignment(BufferLayout layout) { + return 8; + } + }; + + private Std140LayoutRulesTest() { + } + + public static void main(String[] args) { + BufferObjectLayoutDefinition structDefinition = BufferObjectLayoutDefinition.create( + BufferObjectLayoutEntry.builder(TEST_VEC2).forGetter(ignored -> null).build(), + BufferObjectLayoutEntry.ofFloat().forGetter(TestStruct::value).build() + ); + + assertEquals(16, structDefinition.alignment(BufferLayout.STD140), "std140 struct alignment"); + + Std140SizeCalculator calculator = new Std140SizeCalculator(); + calculator.putStructArray(structDefinition, 2); + + assertEquals(32, calculator.get(), "std140 struct array size"); + + BufferObjectLayoutDefinition compactStructDefinition = BufferObjectLayoutDefinition.create( + BufferObjectLayoutEntry.ofFloat().forGetter(TestStruct::value).build(), + BufferObjectLayoutEntry.ofFloat().forGetter(TestStruct::value).build() + ); + + Std430SizeCalculator std430Calculator = new Std430SizeCalculator(); + std430Calculator.putStructArray(compactStructDefinition, 2); + + assertEquals(16, std430Calculator.get(), "std430 struct array size"); + + BufferObjectLayoutDefinition pairDefinition = BufferObjectLayoutDefinition.create( + BufferObjectLayoutEntry.ofFloat().forGetter(TestPair::first).build(), + BufferObjectLayoutEntry.ofFloat().forGetter(TestPair::second).build() + ); + + ByteBuffer std140Buffer = ByteBuffer.allocate(64); + Std140Writer std140Writer = new Std140Writer(std140Buffer); + std140Writer.putStructArray(new TestPair[]{ + new TestPair(1.0f, 2.0f), + new TestPair(3.0f, 4.0f) + }, pairDefinition); + + assertFloatEquals(1.0f, std140Buffer.getFloat(0), "std140 array[0].first"); + assertFloatEquals(2.0f, std140Buffer.getFloat(4), "std140 array[0].second"); + assertFloatEquals(3.0f, std140Buffer.getFloat(16), "std140 array[1].first"); + assertFloatEquals(4.0f, std140Buffer.getFloat(20), "std140 array[1].second"); + + ByteBuffer std430Buffer = ByteBuffer.allocate(64); + Std430Writer std430Writer = new Std430Writer(std430Buffer); + std430Writer.putStructArray(new TestPair[]{ + new TestPair(1.0f, 2.0f), + new TestPair(3.0f, 4.0f) + }, pairDefinition); + + assertFloatEquals(1.0f, std430Buffer.getFloat(0), "std430 array[0].first"); + assertFloatEquals(2.0f, std430Buffer.getFloat(4), "std430 array[0].second"); + assertFloatEquals(3.0f, std430Buffer.getFloat(8), "std430 array[1].first"); + assertFloatEquals(4.0f, std430Buffer.getFloat(12), "std430 array[1].second"); + + ByteBuffer std140IndexedBuffer = ByteBuffer.allocate(64); + Std140Writer std140IndexedWriter = new Std140Writer(std140IndexedBuffer); + std140IndexedWriter.putStructArray(1, new TestPair(5.0f, 6.0f), pairDefinition); + + assertFloatEquals(0.0f, std140IndexedBuffer.getFloat(0), "std140 indexed array[0].first"); + assertFloatEquals(5.0f, std140IndexedBuffer.getFloat(16), "std140 indexed array[1].first"); + assertFloatEquals(6.0f, std140IndexedBuffer.getFloat(20), "std140 indexed array[1].second"); + + ByteBuffer std430IndexedBuffer = ByteBuffer.allocate(64); + Std430Writer std430IndexedWriter = new Std430Writer(std430IndexedBuffer); + std430IndexedWriter.putStructArray(1, new TestPair(5.0f, 6.0f), pairDefinition); + + assertFloatEquals(0.0f, std430IndexedBuffer.getFloat(0), "std430 indexed array[0].first"); + assertFloatEquals(5.0f, std430IndexedBuffer.getFloat(8), "std430 indexed array[1].first"); + assertFloatEquals(6.0f, std430IndexedBuffer.getFloat(12), "std430 indexed array[1].second"); + + ByteBuffer std140SequentialIndexedBuffer = ByteBuffer.allocate(64); + Std140Writer std140SequentialIndexedWriter = new Std140Writer(std140SequentialIndexedBuffer); + std140SequentialIndexedWriter.putStructArray(0, new TestPair(1.0f, 2.0f), pairDefinition); + std140SequentialIndexedWriter.putStructArray(1, new TestPair(3.0f, 4.0f), pairDefinition); + + assertFloatEquals(1.0f, std140SequentialIndexedBuffer.getFloat(0), "std140 sequential indexed array[0].first"); + assertFloatEquals(2.0f, std140SequentialIndexedBuffer.getFloat(4), "std140 sequential indexed array[0].second"); + assertFloatEquals(3.0f, std140SequentialIndexedBuffer.getFloat(16), "std140 sequential indexed array[1].first"); + assertFloatEquals(4.0f, std140SequentialIndexedBuffer.getFloat(20), "std140 sequential indexed array[1].second"); + + ByteBuffer std430SequentialIndexedBuffer = ByteBuffer.allocate(64); + Std430Writer std430SequentialIndexedWriter = new Std430Writer(std430SequentialIndexedBuffer); + std430SequentialIndexedWriter.putStructArray(0, new TestPair(1.0f, 2.0f), pairDefinition); + std430SequentialIndexedWriter.putStructArray(1, new TestPair(3.0f, 4.0f), pairDefinition); + + assertFloatEquals(1.0f, std430SequentialIndexedBuffer.getFloat(0), "std430 sequential indexed array[0].first"); + assertFloatEquals(2.0f, std430SequentialIndexedBuffer.getFloat(4), "std430 sequential indexed array[0].second"); + assertFloatEquals(3.0f, std430SequentialIndexedBuffer.getFloat(8), "std430 sequential indexed array[1].first"); + assertFloatEquals(4.0f, std430SequentialIndexedBuffer.getFloat(12), "std430 sequential indexed array[1].second"); + } + + private static void assertEquals(int expected, int actual, String label) { + if (expected != actual) { + throw new AssertionError(label + ": expected " + expected + ", got " + actual); + } + } + + private static void assertFloatEquals(float expected, float actual, String label) { + if (Float.compare(expected, actual) != 0) { + throw new AssertionError(label + ": expected " + expected + ", got " + actual); + } + } + + private record TestStruct(float value) { + } + + private record TestPair(float first, float second) { + } +} From 91533730423a4ed107c9c6b635fe2fdc8417f97f Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 11 Aug 2026 01:31:27 +0800 Subject: [PATCH 20/38] =?UTF-8?q?feat(font):=20=E5=9B=9E=E7=A7=BB=E6=A4=8D?= =?UTF-8?q?=20SDF=20=E5=AD=97=E4=BD=93=E6=A8=A1=E5=9D=97=20module.font?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 dev/26.1 的 module.font(1d56a4e #22、946938f #56)整体回移植,作为新模块 anvillib-font-neoforge-1.21.1(jarJar 依赖 anvillib-rendering-neoforge-1.21.1), 实现 AWT 字体 → CPU 生成 SDF 图集 → GPU 采样渲染。 直接移植 9 项:SdfGlyphAtlas/SdfGlyphPage/SdfTextLayout/ALFont/FontManager/ AnvilLibFontConfig(V-12 销账:FMLLoader.getCurrent().getGameDir() → FMLPaths.GAMEDIR.get())/ AnvilLibFont/data/AnvilLibFontData(GatherDataEvent.Client → 单一 GatherDataEvent,V-17 ✓)/ shader 资源 反向适配 8 项:SdfAtlasTexture 用 DynamicTexture+TextureManager.register; ALFPipelines 用 RegisterShadersEvent+ShaderInstance(参照 port wheel LibShaders, VertexFormat.builder() 实测存在);SdfTextRenderer 用 GuiGraphics+Tesselator 提交 quad、分段样式纯逻辑复用;GuiGraphicsExtractorExtension+Mixin 注入目标改 GuiGraphics (interface_injections.json),ARGB.multiply → FastColor.ARGB32.multiply(实测存在); 3 个 screen/widget 改 1.21.1 render/mouseClicked API;sdf_text.fsh/vsh 按 1.21.1 (DiffuseSampler→Sampler0、显式 ModelViewMat/ProjMat)+ 新增 sdf_text.json 另实测修复:AnvilLibFont 的空 @EventBusSubscriber(无 @SubscribeEvent 方法, 1.21.1 自动注册抛 IllegalArgumentException)已移除。runData 生成 en_us.json 通过。 --- module.font/build.gradle | 276 ++++++++++++ module.font/gradle.properties | 4 + .../assets/anvillib_font/lang/en_us.json | 8 + .../anvilcraft/lib/v2/font/ALFPipelines.java | 59 +++ .../dev/anvilcraft/lib/v2/font/ALFont.java | 132 ++++++ .../anvilcraft/lib/v2/font/AnvilLibFont.java | 46 ++ .../lib/v2/font/AnvilLibFontConfig.java | 90 ++++ .../anvilcraft/lib/v2/font/FontManager.java | 140 ++++++ .../lib/v2/font/data/AnvilLibFontData.java | 45 ++ .../lib/v2/font/data/package-info.java | 4 + .../GuiGraphicsExtractorExtension.java | 84 ++++ .../lib/v2/font/extension/package-info.java | 4 + .../font/mixin/GuiGraphicsExtractorMixin.java | 20 + .../lib/v2/font/mixin/package-info.java | 4 + .../anvilcraft/lib/v2/font/package-info.java | 4 + .../lib/v2/font/screen/FontConfigScreen.java | 177 ++++++++ .../lib/v2/font/screen/FontTestScreen.java | 105 +++++ .../lib/v2/font/screen/package-info.java | 4 + .../lib/v2/font/screen/widget/Dropdown.java | 350 +++++++++++++++ .../v2/font/screen/widget/package-info.java | 4 + .../lib/v2/font/sdf/SdfAtlasTexture.java | 120 ++++++ .../lib/v2/font/sdf/SdfGlyphAtlas.java | 398 ++++++++++++++++++ .../lib/v2/font/sdf/SdfGlyphPage.java | 64 +++ .../lib/v2/font/sdf/SdfTextLayout.java | 154 +++++++ .../lib/v2/font/sdf/SdfTextRenderer.java | 271 ++++++++++++ .../lib/v2/font/sdf/package-info.java | 4 + .../v2/font/sdf/state/SdfTextRenderState.java | 66 +++ .../lib/v2/font/sdf/state/package-info.java | 4 + .../resources/META-INF/neoforge.mods.toml | 86 ++++ .../main/resources/anvillib_font.mixins.json | 15 + .../assets/anvillib_font/lang/zh_cn.json | 8 + .../anvillib_font/shaders/core/sdf_text.fsh | 19 + .../anvillib_font/shaders/core/sdf_text.json | 11 + .../anvillib_font/shaders/core/sdf_text.vsh | 17 + module.font/src/main/resources/icon.png | Bin 0 -> 1089 bytes .../main/resources/interface_injections.json | 5 + 36 files changed, 2802 insertions(+) create mode 100644 module.font/build.gradle create mode 100644 module.font/gradle.properties create mode 100644 module.font/src/generated/resources/assets/anvillib_font/lang/en_us.json create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/AnvilLibFontData.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/package-info.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/package-info.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/GuiGraphicsExtractorMixin.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/package-info.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/package-info.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/package-info.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/package-info.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/package-info.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java create mode 100644 module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/package-info.java create mode 100644 module.font/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 module.font/src/main/resources/anvillib_font.mixins.json create mode 100644 module.font/src/main/resources/assets/anvillib_font/lang/zh_cn.json create mode 100644 module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.fsh create mode 100644 module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.json create mode 100644 module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.vsh create mode 100644 module.font/src/main/resources/icon.png create mode 100644 module.font/src/main/resources/interface_injections.json diff --git a/module.font/build.gradle b/module.font/build.gradle new file mode 100644 index 00000000..d7fa1e8a --- /dev/null +++ b/module.font/build.gradle @@ -0,0 +1,276 @@ +plugins { + id 'java-library' + id 'eclipse' + id 'idea' + id 'maven-publish' + alias libs.plugins.modDevGradle + alias libs.plugins.lombok + alias libs.plugins.machete + id 'org.jreleaser' version '1.23.0' + id "me.modmuss50.mod-publish-plugin" version "1.1.0" +} + +String buildType = 'snapshot' +String buildNumber +if (System.getenv("CI_BUILD") == 'false') buildNumber = null +else { + if (System.getenv("PR_BUILD") != 'false') buildType = 'pr' + buildNumber = System.getenv("GITHUB_RUN_NUMBER") +} +version = "${mod_version}" + (buildNumber != null ? "+${buildType}.${buildNumber}" : "") +group = mod_group_id + +base { + archivesName = "${project.mod_id}-neoforge-${libs.versions.minecraft.get()}" +} + +repositories { + maven { url = "https://server.cjsah.net:1002/maven/" } + mavenLocal() + mavenCentral() +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +neoForge { + // Specify the version of NeoForge to use. + version = libs.versions.neoForge.get() + + parchment { + mappingsVersion = project.parchment_mappings_version + minecraftVersion = project.parchment_minecraft_version + } + + // This line is optional. Access Transformers are automatically detected + // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') + + interfaceInjectionData { + from 'src/main/resources/interface_injections.json' + publish file('src/main/resources/interface_injections.json') + } + + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + client { + client() + + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + server { + server() + programArgument '--nogui' + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + // This run config launches GameTestServer and runs all registered gametests, then exits. + // By default, the server will crash when no gametests are provided. + // The gametest system is also enabled by default for other run configs under the /test command. + gameTestServer { + type = "gameTestServer" + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + data { + data() + + // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it + // gameDirectory = project.file('run-data') + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } + + // applies to all the run configs above + configureEach { + // Recommended logging data for a userdev environment + // The markers can be added/remove as needed separated by commas. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + systemProperty 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + logLevel = org.slf4j.event.Level.DEBUG + } + } + + mods { + // define mod <-> source bindings + // these are used to tell the game which sources are for which mod + // mostly optional in a single mod project + // but multi mod projects should define one per mod + "${mod_id}" { + sourceSet(sourceSets.main) + } + } +} + +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } + + +dependencies { + if (System.getenv("NOT_DEV") == 'true') { + jarJar(api("dev.anvilcraft.lib:anvillib-rendering-neoforge-1.21.1:latest.release")) + } else { + jarJar(implementation project(":anvillib-rendering-neoforge-1.21.1")) + } + compileOnly "org.jspecify:jspecify:1.0.0" +} + +// This block of code expands all declared replace properties in the specified resource targets. +// A missing property will result in an error. Properties are expanded using ${} Groovy notation. +// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. +// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html +tasks.withType(ProcessResources).configureEach { + var replaceProperties = [minecraft_version : libs.versions.minecraft.get(), + minecraft_version_range: minecraft_version_range, + neo_version : libs.versions.neoForge.get(), + neo_version_range : neo_version_range, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : version, + mod_authors : mod_authors, + mod_description : mod_description] + inputs.properties replaceProperties + + filesMatching(['META-INF/neoforge.mods.toml']) { + expand replaceProperties + } +} + +tasks.register('sourcesJar', Jar) { + from delombok.outputs.files + dependsOn delombok + archiveClassifier = "sources" +} + +tasks.register('javadocJar', Jar) { + from javadoc.destinationDir + dependsOn javadoc + archiveClassifier = "javadoc" +} + +javadoc { + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') +} + +jar { + from(rootProject.file("LICENSE")) + from(rootProject.file("ASSETS_LICENSE")) + from(rootProject.file("README.md")) + from(rootProject.file("README.en.md")) +} + +artifacts { + archives tasks.jar + archives tasks.sourcesJar + archives tasks.javadocJar +} + +def this_name = base.archivesName.get() +def this_description = mod_description + +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + + artifact sourcesJar + artifact javadocJar + + versionMapping { + allVariants { + fromResolutionResult() + } + } + + pom { + name = this_name + description = this_description + url = 'https://github.com/Anvil-Dev/AnvilLib' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/license/mit' + } + } + developers { + developer { + id = 'Anvil-Dev' + name = 'AnvilCraft Dev' + email = 'admin@anvilcraft.dev' + } + } + scm { + url = 'https://github.com/Anvil-Dev/AnvilLib' + connection = 'https://github.com/Anvil-Dev/AnvilLib.git' + developerConnection = 'https://github.com/Anvil-Dev/AnvilLib.git' + } + } + } + } + repositories { + // Add repositories to publish to here. + def MAVEN_URL = System.getenv("MAVEN_URL") + if (MAVEN_URL != null) { + maven { + url MAVEN_URL + credentials { + username System.getenv("MAVEN_USERNAME") + password System.getenv("MAVEN_PASSWORD") + } + } + } + mavenLocal() + maven { + name = "staging" + url = layout.buildDirectory.dir("staging-deploy") + } + } +} + +jreleaser { + signing { + active = 'ALWAYS' + armored = true + } + + deploy { + maven { + mavenCentral { + sonatype { + active = 'ALWAYS' + url = 'https://central.sonatype.com/api/v1/publisher' + stagingRepository('build/staging-deploy') + } + } + } + } +} + +// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} + +machete { + // disable machete locally for faster builds + enabled = false +} + +lombok { + version = "1.18.34" +} diff --git a/module.font/gradle.properties b/module.font/gradle.properties new file mode 100644 index 00000000..a542eb11 --- /dev/null +++ b/module.font/gradle.properties @@ -0,0 +1,4 @@ +## Mod Properties +mod_id=anvillib_font +mod_name=AnvilLib-Font +mod_description=SDF font rendering diff --git a/module.font/src/generated/resources/assets/anvillib_font/lang/en_us.json b/module.font/src/generated/resources/assets/anvillib_font/lang/en_us.json new file mode 100644 index 00000000..05bdea21 --- /dev/null +++ b/module.font/src/generated/resources/assets/anvillib_font/lang/en_us.json @@ -0,0 +1,8 @@ +{ + "narration.anvillib_font.dropdown.collapsed": "Collapsed", + "narration.anvillib_font.dropdown.expanded": "Expanded", + "screen.anvillib_font.config": "Font Config Screen", + "screen.anvillib_font.config.family": "Font Family", + "screen.anvillib_font.config.font": "Font", + "screen.anvillib_font.config.test": "Font Test" +} \ No newline at end of file diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java new file mode 100644 index 00000000..b2604edf --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java @@ -0,0 +1,59 @@ +package dev.anvilcraft.lib.v2.font; + +import com.mojang.blaze3d.vertex.VertexFormat; +import com.mojang.blaze3d.vertex.VertexFormatElement; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.client.renderer.ShaderInstance; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.RegisterShadersEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.io.IOException; + +/** + * Font module SDF shader registration. + * + *

The SDF text shader is registered via {@link RegisterShadersEvent} and can be + * bound through {@link com.mojang.blaze3d.systems.RenderSystem#setShader} when drawing + * SDF text quads on the GUI.

+ */ +@ApiStatus.Internal +@Slf4j +@EventBusSubscriber(modid = AnvilLibFont.MOD_ID, value = Dist.CLIENT) +public final class ALFPipelines { + /** + * The element names (Position / Color / UV0) match the vertex inputs declared in {@code sdf_text.vsh}. + */ + public static final VertexFormat SDF_TEXT_FORMAT = VertexFormat.builder() + .add("Position", VertexFormatElement.POSITION) + .add("Color", VertexFormatElement.COLOR) + .add("UV", VertexFormatElement.UV) + .build(); + + @Getter + private static @Nullable ShaderInstance sdfTextShader; + + private ALFPipelines() { + } + + @SubscribeEvent + public static void on(RegisterShadersEvent event) { + try { + event.registerShader( + new ShaderInstance( + event.getResourceProvider(), + AnvilLibFont.of("sdf_text"), + SDF_TEXT_FORMAT + ), + it -> sdfTextShader = it + ); + log.info("Registered SDF_TEXT shader: {}", AnvilLibFont.of("sdf_text")); + } catch (IOException e) { + log.error("Failed to register SDF text shader", e); + } + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java new file mode 100644 index 00000000..7c15cbb0 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java @@ -0,0 +1,132 @@ +package dev.anvilcraft.lib.v2.font; + +import com.google.common.annotations.Beta; +import dev.anvilcraft.lib.v2.font.sdf.SdfGlyphAtlas; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.network.chat.Style; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Mth; + +import java.awt.Font; +import java.util.ArrayList; +import java.util.List; + +/** + * Wraps {@link Font} with text measurement and layout utilities that + * delegate to {@link SdfGlyphAtlas} for glyph metrics. + */ +@Beta +public class ALFont { + public final int lineHeight = Minecraft.getInstance().font.lineHeight; + private final Font font; + + public ALFont(Font font) { + this.font = font; + } + + public Font awtFont() { return this.font; } + + private SdfGlyphAtlas atlas() { return SdfGlyphAtlas.getOrCreate(this.font).join(); } + + private float scale() { + return (float) lineHeight / atlas().awtHeight(); + } + + // ── Width measurement ─────────────────────────────────────── + + public int width(String str) { + return Mth.ceil(atlas().measureText(str) * scale()); + } + + public int width(FormattedText text) { return width(text.getString()); } + + public int width(FormattedCharSequence text) { return width(flatten(text)); } + + // ── Substring by width ────────────────────────────────────── + + public String plainSubstrByWidth(String str, int maxWidth, boolean reverse) { + if (reverse) return plainTailByWidth(str, maxWidth); + return plainHeadByWidth(str, maxWidth); + } + + public String plainSubstrByWidth(String str, int maxWidth) { + return plainHeadByWidth(str, maxWidth); + } + + private String plainHeadByWidth(String str, int maxWidth) { + SdfGlyphAtlas a = atlas(); + float s = scale(); + if (a.measureText(str) * s <= maxWidth) return str; + int lo = 0, hi = str.length(); + while (lo < hi) { + int mid = (lo + hi + 1) / 2; + if (a.measureText(str, 0, mid) * s <= maxWidth) lo = mid; + else hi = mid - 1; + } + return str.substring(0, lo); + } + + private String plainTailByWidth(String str, int maxWidth) { + SdfGlyphAtlas a = atlas(); + float s = scale(); + if (a.measureText(str) * s <= maxWidth) return str; + int lo = 0, hi = str.length(); + while (lo < hi) { + int mid = (lo + hi) / 2; + if (a.measureText(str, mid, str.length()) * s <= maxWidth) hi = mid; + else lo = mid + 1; + } + return str.substring(lo); + } + + public FormattedText substrByWidth(FormattedText text, int maxWidth) { + return FormattedText.of(plainSubstrByWidth(text.getString(), maxWidth)); + } + + // ── Line splitting ────────────────────────────────────────── + + public List split(FormattedText input, int maxWidth) { + SdfGlyphAtlas a = atlas(); + float s = scale(); + List lines = wrapLines(a, input.getString(), maxWidth, s); + List result = new ArrayList<>(lines.size()); + for (String line : lines) { + result.add(FormattedCharSequence.forward(line, Style.EMPTY)); + } + return result; + } + + public int wordWrapHeight(FormattedText input, int maxWidth) { + return lineHeight * split(input, maxWidth).size(); + } + + // ── Internal helpers ──────────────────────────────────────── + + static String flatten(FormattedCharSequence text) { + StringBuilder buf = new StringBuilder(); + text.accept((i, s, cp) -> { buf.appendCodePoint(cp); return true; }); + return buf.toString(); + } + + public static List wrapLines(SdfGlyphAtlas atlas, String text, int maxWidth, float scale) { + List lines = new ArrayList<>(); + if (text.isEmpty()) { lines.add(""); return lines; } + for (String para : text.split("\n", -1)) { + if (para.isEmpty()) { lines.add(""); continue; } + String[] words = para.split(" "); + StringBuilder line = new StringBuilder(); + for (String word : words) { + String cand = line.isEmpty() ? word : line + " " + word; + if (atlas.measureText(cand) * scale > maxWidth) { + if (line.isEmpty()) lines.add(word); + else { lines.add(line.toString()); line = new StringBuilder(word); } + } else { + line = new StringBuilder(cand); + } + } + if (!line.isEmpty()) lines.add(line.toString()); + } + return lines; + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java new file mode 100644 index 00000000..4edc6633 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java @@ -0,0 +1,46 @@ +package dev.anvilcraft.lib.v2.font; + +import dev.anvilcraft.lib.v2.font.screen.FontConfigScreen; +import dev.anvilcraft.lib.v2.font.sdf.SdfGlyphAtlas; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.gui.IConfigScreenFactory; +import org.jetbrains.annotations.ApiStatus; + +import java.awt.Font; +import java.util.List; + +@Mod(value = AnvilLibFont.MOD_ID, dist = Dist.CLIENT) +public class AnvilLibFont { + public static final String MAIN_ID = "anvillib"; + public static final String MOD_ID = "anvillib_font"; + public static final AnvilLibFontConfig CONFIG = new AnvilLibFontConfig(); + + @ApiStatus.Internal + public AnvilLibFont(ModContainer container) { + AnvilLibFontConfig.AnvilLibFontConfigManager.readConfig(AnvilLibFont.CONFIG); + container.registerExtensionPoint(IConfigScreenFactory.class, FontConfigScreen::new); + // Start building the SDF atlas for the base font and all common style + // variants on background threads at mod init time. This avoids render- + // thread blocking (visible lag) when bold/italic text is first drawn. + Font base = getSelectFont(); + SdfGlyphAtlas.getOrCreate(base); + for (int style : List.of(Font.BOLD, Font.ITALIC, Font.BOLD | Font.ITALIC)) { + SdfGlyphAtlas.getOrCreate(base.deriveFont(style)); + } + } + + public static Font getSelectFont() { + Font font = FontManager.INSTANCE.getFont(AnvilLibFont.CONFIG.getFont()); + // Eagerly start building the SDF atlas on a background thread so it is + // ready (or nearly ready) by the time the first text is rendered. + SdfGlyphAtlas.getOrCreate(font); + return font; + } + + public static ResourceLocation of(String path) { + return ResourceLocation.fromNamespaceAndPath(AnvilLibFont.MOD_ID, path); + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java new file mode 100644 index 00000000..428ee183 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java @@ -0,0 +1,90 @@ +package dev.anvilcraft.lib.v2.font; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.SerializedName; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.neoforged.fml.loading.FMLPaths; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.nio.file.Path; +import java.util.Objects; + +public class AnvilLibFontConfig { + @SerializedName("_font_family_comment") + private final String fontFamilyComment = "Select a font family"; + @Getter + @SerializedName("font_family") + private String fontFamily = FontManager.INSTANCE.getDefaultFont().getFamily(); + @SerializedName("_font_comment") + private final String fontComment = "Select a font"; + @Getter + @SerializedName("font") + private String font = FontManager.INSTANCE.getDefaultFontFamily(); + + void setValue(AnvilLibFontConfig config) { + this.fontFamily = config.fontFamily; + this.font = config.font; + } + + public void setFontFamily(String fontFamily) { + this.fontFamily = fontFamily; + AnvilLibFontConfigManager.saveConfig(this); + } + + public void setFont(String font) { + this.font = font; + AnvilLibFontConfigManager.saveConfig(this); + } + + @ApiStatus.Internal + @Slf4j + public static class AnvilLibFontConfigManager { + public static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); + public static @Nullable Path configPath = null; + + public static void readConfig(AnvilLibFontConfig config) { + Path path = AnvilLibFontConfigManager.getConfigPath(); + File file = path.toFile(); + if (!file.exists()) { + AnvilLibFontConfigManager.saveConfig(config); + return; + } + try (FileReader fileReader = new FileReader(file)) { + AnvilLibFontConfig fontConfig = GSON.fromJson(fileReader, AnvilLibFontConfig.class); + config.setValue(fontConfig); + } catch (Exception e) { + log.error(e.getLocalizedMessage(), e); + } + } + + public static void saveConfig(AnvilLibFontConfig config) { + Path path = AnvilLibFontConfigManager.getConfigPath(); + File file = path.toFile(); + if (!file.exists()) { + //noinspection ResultOfMethodCallIgnored + file.getParentFile().mkdirs(); + } + try (FileWriter fileWriter = new FileWriter(file)) { + GSON.toJson(config, fileWriter); + } catch (Exception e) { + log.error(e.getLocalizedMessage(), e); + } + } + + public static Path getConfigPath() { + return Objects.requireNonNullElseGet( + AnvilLibFontConfigManager.configPath, + () -> AnvilLibFontConfigManager.configPath = FMLPaths.GAMEDIR.get() + .resolve("config") + .resolve(AnvilLibFont.MAIN_ID) + .resolve("%s-client.json".formatted(AnvilLibFont.MOD_ID)) + ); + } + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java new file mode 100644 index 00000000..7ec0d56c --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java @@ -0,0 +1,140 @@ +package dev.anvilcraft.lib.v2.font; + +import com.google.common.annotations.Beta; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.jspecify.annotations.Nullable; + +import java.awt.Font; +import java.awt.GraphicsEnvironment; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import javax.swing.UIManager; + +@Beta +@Slf4j +@Getter +public class FontManager { + private static final List DEFAULT_FONTS = List.of( + "HarmonyOS Sans SC", + "Microsoft YaHei", + "微软雅黑", + "PingFang", + "苹方", + "Cantarell", + "Noto Sans" + ); + public static final FontManager INSTANCE = new FontManager(); + private final Map fontByName = new HashMap<>(); + private final Map familyByFont = new HashMap<>(); + private final Multimap fontMultimap; + private final Font defaultFont; + + private FontManager() { + GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment(); + Font[] allFonts = graphicsEnvironment.getAllFonts(); + for (Font font : allFonts) { + fontByName.put(font.getFontName(), font); + } + this.fontMultimap = FontTrieNode.process(this.fontByName.values()); + log.info("fontMultimap: {}", this.fontMultimap); + this.fontMultimap.forEach((k, v) -> this.familyByFont.put(v, k)); + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + log.error(e.getLocalizedMessage(), e); + } + Font defaultFont = null; + for (String font : FontManager.DEFAULT_FONTS) { + if (defaultFont != null) break; + defaultFont = this.getFont(font); + } + if (defaultFont == null) { + defaultFont = UIManager.getFont("Label.font"); + } + this.defaultFont = Objects.requireNonNullElseGet( + defaultFont, + () -> this.fontByName.values().stream().findFirst().orElseThrow(() -> new RuntimeException("No fonts found")) + ); + } + + public Collection getFamilyNames() { + return this.fontMultimap.keySet(); + } + + public String getFamilyName(Font font) { + return this.familyByFont.getOrDefault(font, font.getFontName()); + } + + public String getDefaultFontFamily() { + return this.getFamilyName(this.getDefaultFont()); + } + + public Collection getFamilyFontNames(String familyName) { + if (!this.fontMultimap.containsKey(familyName)) return Set.of(); + return this.fontMultimap.get(familyName).stream().map(Font::getFontName).toList(); + } + + public Font getFont(@Nullable String name) { + return name == null || !this.fontByName.containsKey(name) ? this.getDefaultFont() : this.fontByName.get(name); + } + + static class FontTrieNode { + final Map children = new TreeMap<>(); + @Nullable Font value; + + void append(String key, Font font) { + if (key.isEmpty()) return; + if (key.length() == 1) { + this.value = font; + return; + } + char prefix = key.charAt(0); + String suffix = key.substring(1); + this.children.computeIfAbsent(prefix, ignored -> new FontTrieNode()).append(suffix, font); + } + + void subFont(Set set) { + if (this.value != null) { + set.add(this.value); + } + this.children.forEach((prefixChar, child) -> child.subFont(set)); + } + + void result(String prefix, Multimap result) { + if (this.value != null) { + Set set = new TreeSet<>(Comparator.comparing(Font::getFontName)); + this.subFont(set); + result.putAll(this.value.getFontName(), set); + return; + } + // Only group at word boundaries: prefix ending with space & multiple descendants + if (prefix.endsWith(" ")) { + Set set = new TreeSet<>(Comparator.comparing(Font::getFontName)); + this.subFont(set); + if (set.size() > 1) { + result.putAll(prefix.stripTrailing(), set); + return; + } + } + this.children.forEach((prefixChar, child) -> child.result(prefix + prefixChar, result)); + } + + static Multimap process(Collection fonts) { + FontTrieNode root = new FontTrieNode(); + fonts.forEach(font -> root.append(font.getFontName(), font)); + Multimap result = MultimapBuilder.hashKeys().hashSetValues().build(); + root.result("", result); + return result; + } + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/AnvilLibFontData.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/AnvilLibFontData.java new file mode 100644 index 00000000..ea74a1d4 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/AnvilLibFontData.java @@ -0,0 +1,45 @@ +package dev.anvilcraft.lib.v2.font.data; + +import dev.anvilcraft.lib.v2.font.AnvilLibFont; +import net.minecraft.Util; +import net.minecraft.data.DataGenerator; +import net.minecraft.data.PackOutput; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.common.data.LanguageProvider; +import net.neoforged.neoforge.data.event.GatherDataEvent; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +@EventBusSubscriber(modid = AnvilLibFont.MOD_ID) +public class AnvilLibFontData { + @SubscribeEvent + public static void onData(GatherDataEvent event) { + if (!event.includeClient()) { + return; + } + DataGenerator generator = event.getGenerator(); + PackOutput packOutput = generator.getPackOutput(); + event.addProvider(new AnvilLibFontLanguageProvider(packOutput, AnvilLibFont.MOD_ID, "en_us")); + } + + public static class AnvilLibFontLanguageProvider extends LanguageProvider { + public AnvilLibFontLanguageProvider(PackOutput output, String modid, String locale) { + super(output, modid, locale); + } + + @Override + protected void addTranslations() { + this.addDesc("screen", "config", "Font Config Screen"); + this.addDesc("screen", "config.family", "Font Family"); + this.addDesc("screen", "config.font", "Font"); + this.addDesc("screen", "config.test", "Font Test"); + this.addDesc("narration", "dropdown.expanded", "Expanded"); + this.addDesc("narration", "dropdown.collapsed", "Collapsed"); + } + + protected void addDesc(String prefix, String key, String value) { + this.add(Util.makeDescriptionId(prefix, AnvilLibFont.of(key)), value); + } + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/package-info.java new file mode 100644 index 00000000..dc9f983b --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.font.data; + +import org.jspecify.annotations.NullMarked; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java new file mode 100644 index 00000000..b06ba290 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java @@ -0,0 +1,84 @@ +package dev.anvilcraft.lib.v2.font.extension; + +import com.google.common.annotations.Beta; +import dev.anvilcraft.lib.v2.font.sdf.SdfTextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.util.FastColor; +import net.minecraft.util.FormattedCharSequence; +import org.jspecify.annotations.Nullable; + +import java.awt.Font; + +@Beta +public interface GuiGraphicsExtractorExtension { + default GuiGraphics self() { + return (GuiGraphics) this; + } + + default SdfTextRenderer anvillib$textRenderer() { + throw new AssertionError("Not implemented!"); + } + + default void anvillib$text(Font font, @Nullable String str, int x, int y, int color) { + this.anvillib$text(font, str, x, y, color, false); + } + + default void anvillib$text(Font font, @Nullable String str, int x, int y, int color, boolean dropShadow) { + this.anvillib$textRenderer().drawString(this.self(), font, str, x, y, color, dropShadow); + } + + default void anvillib$text(Font font, FormattedCharSequence str, int x, int y, int color) { + this.anvillib$text(font, str, x, y, color, false); + } + + default void anvillib$text(Font font, FormattedCharSequence str, int x, int y, int color, boolean dropShadow) { + this.anvillib$textRenderer().drawFormatted(this.self(), font, str, x, y, color, dropShadow); + } + + default void anvillib$text(Font font, Component str, int x, int y, int color) { + this.anvillib$text(font, str, x, y, color, false); + } + + default void anvillib$text(Font font, Component str, int x, int y, int color, boolean dropShadow) { + this.anvillib$textRenderer().drawComponent(this.self(), font, str, x, y, color, dropShadow); + } + + default void anvillib$centeredText(Font font, String str, int x, int y, int color) { + this.anvillib$centeredText(font, Component.literal(str), x, y, color); + } + + default void anvillib$centeredText(Font font, Component text, int x, int y, int color) { + this.anvillib$textRenderer().drawCentered(this.self(), font, text, x, y, color); + } + + default void anvillib$centeredText(Font font, FormattedCharSequence text, int x, int y, int color) { + this.anvillib$textRenderer().drawCentered(this.self(), font, text, x, y, color); + } + + default void anvillib$textWithWordWrap(Font font, FormattedText string, int x, int y, int width, int col) { + this.anvillib$textWithWordWrap(font, string, x, y, width, col, false); + } + + default void anvillib$textWithWordWrap(Font font, FormattedText string, int x, int y, int width, int col, boolean dropShadow) { + this.anvillib$textRenderer().drawWrapped(this.self(), font, string, x, y, width, col, dropShadow); + } + + default void anvillib$textWithBackdrop(Font font, Component str, int textX, int textY, int textWidth, int textColor) { + GuiGraphics graphics = this.self(); + int backgroundColor = Minecraft.getInstance().options.getBackgroundColor(0.0F); + if (backgroundColor != 0) { + int padding = 2; + graphics.fill( + textX - padding, + textY - padding, + textX + textWidth + padding, + textY + 9 + padding, + FastColor.ARGB32.multiply(backgroundColor, textColor) + ); + } + this.anvillib$text(font, str, textX, textY, textColor, true); + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/package-info.java new file mode 100644 index 00000000..ebeb597f --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.font.extension; + +import org.jspecify.annotations.NullMarked; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/GuiGraphicsExtractorMixin.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/GuiGraphicsExtractorMixin.java new file mode 100644 index 00000000..07dbf475 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/GuiGraphicsExtractorMixin.java @@ -0,0 +1,20 @@ +package dev.anvilcraft.lib.v2.font.mixin; + +import dev.anvilcraft.lib.v2.font.extension.GuiGraphicsExtractorExtension; +import dev.anvilcraft.lib.v2.font.sdf.SdfTextRenderer; +import net.minecraft.client.gui.GuiGraphics; +import org.jetbrains.annotations.ApiStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@ApiStatus.Internal +@Mixin(GuiGraphics.class) +public abstract class GuiGraphicsExtractorMixin implements GuiGraphicsExtractorExtension { + @Unique + SdfTextRenderer anvillib$textRenderer = new SdfTextRenderer(); + + public SdfTextRenderer anvillib$textRenderer() { + return this.anvillib$textRenderer; + } +} + diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/package-info.java new file mode 100644 index 00000000..7bc2def2 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.font.mixin; + +import org.jspecify.annotations.NullMarked; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/package-info.java new file mode 100644 index 00000000..f314ae57 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.font; + +import org.jspecify.annotations.NullMarked; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java new file mode 100644 index 00000000..8d02e209 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java @@ -0,0 +1,177 @@ +package dev.anvilcraft.lib.v2.font.screen; + +import dev.anvilcraft.lib.v2.font.AnvilLibFont; +import dev.anvilcraft.lib.v2.font.FontManager; +import dev.anvilcraft.lib.v2.font.screen.widget.Dropdown; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.neoforged.fml.ModContainer; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +@ApiStatus.Internal +public class FontConfigScreen extends Screen { + protected final Screen lastScreen; + private Dropdown.@Nullable Shielding shielding; + @SuppressWarnings("FieldCanBeLocal") + private @Nullable Dropdown familyDropdown; + private @Nullable Dropdown fontDropdown; + private @Nullable Button testBtn; + private Component selectedFamilyText = Component.empty(); + private Component selectedFontText = Component.empty(); + private final Component familyComponent = Component.translatable("screen.anvillib_font.config.family"); + private final Component fontComponent = Component.translatable("screen.anvillib_font.config.font"); + + public FontConfigScreen(final ModContainer ignored, final Screen parent) { + super(Component.translatable("screen.anvillib_font.config")); + this.lastScreen = parent; + } + + @Override + protected void init() { + int familyComponentWidth = this.font.width(this.familyComponent); + int fontComponentWidth = this.font.width(this.fontComponent); + int labelWidth = Math.max(familyComponentWidth, fontComponentWidth); + int dropdownWidth = Math.clamp(this.width - 40, 180, 320); + int btnWidth = dropdownWidth; + int dropdownX = (this.width - dropdownWidth) / 2 + labelWidth + 10; + int btnX = (this.width - btnWidth) / 2; + dropdownWidth -= labelWidth + 10; + int btnY = this.height / 2 - 24; + int familyDropdownY = btnY + 28; + int fontDropdownY = familyDropdownY + 28; + + this.shielding = null; + + this.familyDropdown = new Dropdown(dropdownX, familyDropdownY, dropdownWidth, 20, this.width, this.height, this.familyComponent); + this.fontDropdown = new Dropdown(dropdownX, fontDropdownY, dropdownWidth, 20, this.width, this.height, this.fontComponent); + this.testBtn = Button + .builder( + Component.translatable("screen.anvillib_font.config.test"), + (button) -> this.minecraft.setScreen(new FontTestScreen(this)) + ) + .size(btnWidth, 20) + .pos(btnX, btnY) + .build(); + + this.familyDropdown.setOnShieldingAdd(shielding -> this.shielding = shielding); + this.familyDropdown.setOnShieldingRemove(() -> this.shielding = null); + this.familyDropdown.setShieldingGetter(() -> this.shielding); + + this.fontDropdown.setOnShieldingAdd(shielding -> this.shielding = shielding); + this.fontDropdown.setOnShieldingRemove(() -> this.shielding = null); + this.fontDropdown.setShieldingGetter(() -> this.shielding); + + List families = new ArrayList<>(FontManager.INSTANCE.getFamilyNames()); + families.sort(Comparator.comparing(String::toLowerCase)); + + List options = families.stream().map(name -> Dropdown.DropdownEntry.create(name, name)).toList(); + this.familyDropdown.setAllow(options); + + String configuredFamily = AnvilLibFont.CONFIG.getFontFamily(); + options.stream().filter(entry -> entry.id().equals(configuredFamily)).findFirst().ifPresent(this.familyDropdown::setValue); + + this.updateSelectedFamily(this.familyDropdown.getValue()); + this.familyDropdown.setOnValueChanged(entry -> { + if (entry == null) { + return; + } + AnvilLibFont.CONFIG.setFontFamily(entry.id()); + this.updateSelectedFamily(entry); + this.refreshFontOptions(entry.id(), null, true); + }); + + this.fontDropdown.setOnValueChanged(entry -> { + if (entry == null) { + return; + } + AnvilLibFont.CONFIG.setFont(entry.id()); + this.updateSelectedFont(entry); + }); + this.familyDropdown.setValue(AnvilLibFont.CONFIG.getFontFamily()); + + this.refreshFontOptions(this.familyDropdown.getValueId(), AnvilLibFont.CONFIG.getFont(), false); + + this.fontDropdown.setValue(AnvilLibFont.CONFIG.getFont()); + + this.addRenderableWidget(this.testBtn); + this.addRenderableWidget(this.fontDropdown); + this.addRenderableWidget(this.familyDropdown); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + super.render(guiGraphics, mouseX, mouseY, partialTick); + guiGraphics.drawCenteredString(this.font, this.title, this.width / 2, 24, 0xFFFFFFFF); + guiGraphics.drawCenteredString(this.font, this.selectedFamilyText, this.width / 2, this.height / 2 - 56, 0xFFFFFFFF); + if (fontDropdown != null) { + guiGraphics.anvillib$centeredText( + AnvilLibFont.getSelectFont(), + this.selectedFontText, + this.width / 2, + this.height / 2 - 44, + 0xFFFFFFFF + ); + } + int dropdownWidth = Math.clamp(this.width - 40, 180, 320); + int dropdownLabelX = (this.width - dropdownWidth) / 2; + int familyDropdownLabelY = this.height / 2 - 24 + this.font.lineHeight / 2 + 28; + int fontDropdownLabelY = familyDropdownLabelY + 28; + guiGraphics.drawString(this.font, this.familyComponent, dropdownLabelX, familyDropdownLabelY, 0xFFFFFFFF); + guiGraphics.drawString(this.font, this.fontComponent, dropdownLabelX, fontDropdownLabelY, 0xFFFFFFFF); + } + + private void updateSelectedFamily(Dropdown.@Nullable DropdownEntry entry) { + this.selectedFamilyText = entry == null + ? Component.literal("Current family: ") + : Component.literal("Current family: " + entry.id()); + } + + private void updateSelectedFont(Dropdown.@Nullable DropdownEntry entry) { + this.selectedFontText = entry == null + ? Component.literal("Current font: ") + : Component.literal("Current font: " + entry.id()); + } + + private void refreshFontOptions(@Nullable String family, @Nullable String preferredFont, boolean persistSelected) { + if (this.fontDropdown == null) { + return; + } + if (family == null || family.isBlank() || !FontManager.INSTANCE.getFamilyNames().contains(family)) { + this.fontDropdown.setAllow(List.of()); + this.updateSelectedFont(null); + return; + } + + List fontOptions = FontManager.INSTANCE.getFamilyFontNames(family) + .stream() + .sorted(Comparator.comparing(String::toLowerCase)) + .map(name -> Dropdown.DropdownEntry.create(name, name)) + .toList(); + + this.fontDropdown.setAllow(fontOptions); + + Dropdown.DropdownEntry selected = fontOptions.stream() + .filter(entry -> entry.id().equals(preferredFont)) + .findFirst() + .orElse(this.fontDropdown.getValue()); + + this.fontDropdown.setValue(selected); + this.updateSelectedFont(selected); + + if (persistSelected && selected != null) { + AnvilLibFont.CONFIG.setFont(selected.id()); + } + } + + @Override + public void onClose() { + this.minecraft.setScreen(this.lastScreen); + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java new file mode 100644 index 00000000..4264a776 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java @@ -0,0 +1,105 @@ +package dev.anvilcraft.lib.v2.font.screen; + +import dev.anvilcraft.lib.v2.font.AnvilLibFont; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public class FontTestScreen extends Screen { + protected final Screen lastScreen; + + protected FontTestScreen(final Screen parent) { + super(Component.literal("Font Test")); + this.lastScreen = parent; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float a) { + super.render(graphics, mouseX, mouseY, a); + int offsetX = this.width / 2; + int offsetY = (this.height - this.font.lineHeight * 9) / 2; + graphics.anvillib$centeredText( + AnvilLibFont.getSelectFont(), + Component.literal("正在准备Windows").withStyle(ChatFormatting.RED), + offsetX, + offsetY, + 0xFFFFFFFF + ); + graphics.anvillib$centeredText( + AnvilLibFont.getSelectFont(), + Component.literal("请不要关闭你的计算机").withStyle(Style.EMPTY.withColor(ChatFormatting.GREEN).withStrikethrough(true)), + offsetX, + offsetY + this.font.lineHeight, + 0xFFFFFFFF + ); + graphics.anvillib$centeredText( + AnvilLibFont.getSelectFont(), + Component.literal("中国传播家文化的主题餐厅").withStyle(Style.EMPTY.withBold(true)), + offsetX, + offsetY + this.font.lineHeight * 2, + 0xFFFFFFFF + ); + graphics.anvillib$centeredText( + AnvilLibFont.getSelectFont(), + Component.literal("家是本 家是本心灵家港,幸福味道记忆处").withStyle(Style.EMPTY.withItalic(true)), + offsetX, + offsetY + this.font.lineHeight * 3, + 0xFFFFFFFF + ); + graphics.anvillib$centeredText( + AnvilLibFont.getSelectFont(), + Component.literal("一群人,一辈子,干好传播家是本文化这件事") + .withStyle(Style.EMPTY.withBold(true).withColor(ChatFormatting.DARK_PURPLE)), + offsetX, + offsetY + this.font.lineHeight * 4, + 0xFFFFFFFF + ); + graphics.anvillib$centeredText( + AnvilLibFont.getSelectFont(), + Component.literal("The quick brown fox jumped over the lazy dog.") + .withStyle(Style.EMPTY.withItalic(true).withColor(ChatFormatting.DARK_PURPLE).withUnderlined(true)), + offsetX, + offsetY + this.font.lineHeight * 5, + 0xFFFFFFFF + ); + graphics.anvillib$centeredText( + AnvilLibFont.getSelectFont(), + Component.literal("隨手存取 AI,隨時使用最佳效能。善用 Windows 11 功能,進而保護並提升您的數碼生活。") + .withStyle(Style.EMPTY.withItalic(true).withBold(true)), + offsetX, + offsetY + this.font.lineHeight * 6, + 0xFFFFFFFF + ); + graphics.anvillib$centeredText( + AnvilLibFont.getSelectFont(), + Component.literal("1234567890/*-+!@#$%^&*()_+{}:\">?<[];\\,./'"), + offsetX, + offsetY + this.font.lineHeight * 7, + 0xFFFFFFFF + ); + graphics.anvillib$centeredText( + AnvilLibFont.getSelectFont(), + Component.literal("【】;‘,。、《超かぐや姫!》?、|"), + offsetX, + offsetY + this.font.lineHeight * 8, + 0xFFFFFFFF + ); + graphics.anvillib$centeredText( + AnvilLibFont.getSelectFont(), + Component.literal("混淆文字测试") + .withStyle(Style.EMPTY.withItalic(true).withBold(true).withObfuscated(true)), + offsetX, + offsetY + this.font.lineHeight * 9, + 0xFFFFFFFF + ); + } + + @Override + public void onClose() { + this.minecraft.setScreen(this.lastScreen); + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/package-info.java new file mode 100644 index 00000000..e3a64d76 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.font.screen; + +import org.jspecify.annotations.NullMarked; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java new file mode 100644 index 00000000..987634d9 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java @@ -0,0 +1,350 @@ +package dev.anvilcraft.lib.v2.font.screen.widget; + +import dev.anvilcraft.lib.v2.font.AnvilLibFont; +import lombok.Setter; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarratedElementType; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +@ApiStatus.Internal +public class Dropdown extends AbstractWidget { + private final List allows = new ArrayList<>(); + private final Minecraft minecraft = Minecraft.getInstance(); + private @Nullable DropdownEntry value = null; + private boolean expanded = false; + @Setter + private Consumer<@Nullable DropdownEntry> onValueChanged = ignored -> { + }; + @Setter + private Consumer onShieldingAdd = ignored -> { + }; + @Setter + private Runnable onShieldingRemove = () -> { + }; + @Setter + private Supplier<@Nullable Shielding> shieldingGetter = () -> null; + private final int screenWidth; + private final int screenHeight; + private int scrollOffset = 0; + private boolean draggingScrollbar; + + public Dropdown(int x, int y, int width, int height, int screenWidth, int screenHeight, Component message) { + super(x, y, width, height, message); + this.screenWidth = screenWidth; + this.screenHeight = screenHeight; + } + + public void setAllow(List allows) { + this.allows.clear(); + this.allows.addAll(allows); + this.scrollOffset = 0; + if (this.allows.contains(this.value)) { + return; + } + this.value = this.allows.isEmpty() ? null : this.allows.getFirst(); + } + + public void setValue(@Nullable DropdownEntry value) { + if (value != null && !this.allows.contains(value)) { + return; + } + this.value = value; + } + + + public void setValue(@Nullable String value) { + for (DropdownEntry allow : this.allows) { + if (Objects.equals(allow.id, value)) { + this.setValue(allow); + return; + } + } + } + + public @Nullable DropdownEntry getValue() { + return this.value; + } + + public @Nullable String getValueId() { + return this.value == null ? null : this.value.id; + } + + @Override + protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + int hoveredIndex = this.getEntryIndexAt(mouseX, mouseY); + + int x1 = this.getX(); + int y1 = this.getY(); + int x2 = x1 + this.getWidth(); + int y2 = y1 + this.getHeight(); + + int borderColor = this.isHoveredOrFocused() ? 0xFFE0E0E0 : 0xFF909090; + int bgColor = this.active ? 0xCC202020 : 0xCC151515; + guiGraphics.fill(x1, y1, x2, y2, borderColor); + guiGraphics.fill(x1 + 1, y1 + 1, x2 - 1, y2 - 1, bgColor); + + Component valueText = this.value == null ? Component.empty() : this.value.desc; + guiGraphics.drawCenteredString( + this.minecraft.font, + valueText, + x1 + (this.getWidth() / 2), + y1 + (this.getHeight() - 8) / 2, + 0xFFFFFFFF + ); + guiGraphics.drawCenteredString( + this.minecraft.font, + Component.literal(this.expanded ? "▲" : "▼"), + x2 - 8, + y1 + (this.getHeight() - 8) / 2, + 0xFFFFFFFF + ); + + if (!this.expanded) { + return; + } + + int startHeight = this.getY() + this.getHeight(); + int listAreaHeight = this.calcMaxHeight(); + int maxY = startHeight + listAreaHeight; + boolean needsScrollbar = this.maxScrollOffset() > 0; + int scrollbarWidth = needsScrollbar ? 5 : 0; + + guiGraphics.enableScissor(x1, startHeight, Math.min(this.screenWidth, x2), maxY); + for (int i = 0; i < this.allows.size(); i++) { + int rowTop = startHeight + (i - this.scrollOffset) * this.getHeight(); + int rowBottom = rowTop + this.getHeight(); + + if (rowBottom <= startHeight || rowTop >= maxY) continue; + + int rowBg = i == hoveredIndex ? 0xCC3F3F3F : 0xCC1F1F1F; + guiGraphics.fill(x1, rowTop, x2, rowBottom, borderColor); + guiGraphics.fill(x1 + 1, rowTop + 1, x2 - 1, rowBottom - 1, rowBg); + + DropdownEntry entry = this.allows.get(i); + guiGraphics.drawCenteredString( + this.minecraft.font, + entry.desc, + x1 + (this.getWidth() - scrollbarWidth) / 2, + rowTop + (this.getHeight() - 8) / 2, + 0xFFFFFFFF + ); + } + guiGraphics.disableScissor(); + + if (needsScrollbar) { + int visibleRows = this.visibleRowCount(); + int totalRows = this.allows.size(); + int thumbHeight = Math.max(10, listAreaHeight * visibleRows / totalRows); + int maxScroll = this.maxScrollOffset(); + int thumbTop = maxScroll == 0 ? 0 : (listAreaHeight - thumbHeight) * this.scrollOffset / maxScroll; + + // track + guiGraphics.fill(x2 - scrollbarWidth, startHeight, x2, maxY, 0xFF303030); + // thumb + guiGraphics.fill( + x2 - scrollbarWidth + 1, + startHeight + thumbTop, + x2 - 1, + startHeight + thumbTop + thumbHeight, + 0xFF909090 + ); + } + } + + public int calcMaxHeight() { + int startHeight = this.getY() + this.getHeight(); + int maxHeight = Math.clamp((long) this.allows.size() * this.getHeight(), 0, this.screenHeight - startHeight - 10); + // Round down to a multiple of the row height so the last visible row + // fills the entire area without trailing blank space. + return (maxHeight / this.getHeight()) * this.getHeight(); + } + + private int visibleRowCount() { + return this.calcMaxHeight() / this.getHeight(); + } + + private int maxScrollOffset() { + return Math.max(0, this.allows.size() - this.visibleRowCount()); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + if (!this.expanded || scrollY == 0 || !this.isMouseOver(mouseX, mouseY)) { + return false; + } + this.scrollOffset = Math.clamp(this.scrollOffset - (int) Math.signum(scrollY), 0, this.maxScrollOffset()); + return true; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return this.handlePrimaryClick(mouseX, mouseY, button); + } + + private boolean handlePrimaryClick(double mouseX, double mouseY, int button) { + if (!this.active || !this.visible || button != 0) { + return false; + } + + if (this.isPointInMainBox(mouseX, mouseY)) { + this.expanded = !this.expanded; + this.shielding(); + return true; + } + + if (!this.expanded) { + return false; + } + + // Click on scrollbar: start dragging + if (this.needsScrollbar() && this.isOnScrollbar(mouseX, mouseY)) { + this.draggingScrollbar = true; + this.scrollToMouse(mouseY); + return true; + } + + int index = this.getEntryIndexAt(mouseX, mouseY); + if (index < 0) { + this.expanded = false; + this.removeShielding(); + return false; + } + + DropdownEntry selected = this.allows.get(index); + if (!selected.equals(this.value)) { + this.value = selected; + this.onValueChanged.accept(this.value); + } + this.expanded = false; + this.removeShielding(); + return true; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + if (this.draggingScrollbar) { + this.scrollToMouse(mouseY); + return true; + } + return false; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + this.draggingScrollbar = false; + return false; + } + + private boolean needsScrollbar() { + return this.maxScrollOffset() > 0; + } + + private boolean isOnScrollbar(double mouseX, double mouseY) { + int x2 = this.getX() + this.getWidth(); + int listTop = this.getY() + this.getHeight(); + int listBottom = listTop + this.calcMaxHeight(); + return mouseX >= x2 - 5 && mouseX < x2 && mouseY >= listTop && mouseY < listBottom; + } + + private void scrollToMouse(double mouseY) { + int listTop = this.getY() + this.getHeight(); + int listHeight = this.calcMaxHeight(); + int thumbHeight = Math.max(10, listHeight * this.visibleRowCount() / this.allows.size()); + int trackHeight = listHeight - thumbHeight; + int maxScroll = this.maxScrollOffset(); + if (trackHeight <= 0 || maxScroll <= 0) return; + int relativeY = (int) Math.clamp(mouseY - listTop - thumbHeight / 2.0, 0, trackHeight); + this.scrollOffset = relativeY * maxScroll / trackHeight; + } + + public boolean isMouseOver(double mouseX, double mouseY) { + Shielding shielding = this.shieldingGetter.get(); + if (!this.visible || (shielding != null && shielding.isShielding(mouseX, mouseY, this))) { + return false; + } + return this.isPointInMainBox(mouseX, mouseY) || this.isPointInDropdownList(mouseX, mouseY); + } + + private boolean isPointInMainBox(double mouseX, double mouseY) { + return mouseX >= this.getX() && mouseX < this.getX() + this.getWidth() && mouseY >= this.getY() && mouseY < this.getY() + this.getHeight(); + } + + private boolean isPointInDropdownList(double mouseX, double mouseY) { + if (!this.expanded || this.allows.isEmpty()) { + return false; + } + int listTop = this.getY() + this.getHeight(); + int listBottom = listTop + this.calcMaxHeight(); + return mouseX >= this.getX() && mouseX < this.getX() + this.getWidth() && mouseY >= listTop && mouseY < listBottom; + } + + private int getEntryIndexAt(double mouseX, double mouseY) { + if (!this.isPointInDropdownList(mouseX, mouseY)) { + return -1; + } + int listTop = this.getY() + this.getHeight(); + int index = (int) ((mouseY - listTop) / this.getHeight()) + this.scrollOffset; + return index >= 0 && index < this.allows.size() ? index : -1; + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + narrationElementOutput.add(NarratedElementType.TITLE, this.getMessage()); + narrationElementOutput.add(NarratedElementType.USAGE, this.value == null ? Component.empty() : this.value.desc); + if (this.expanded) { + narrationElementOutput.add(NarratedElementType.USAGE, Component.translatable("narration.anvillib_font.dropdown.expanded")); + } else { + narrationElementOutput.add(NarratedElementType.USAGE, Component.translatable("narration.anvillib_font.dropdown.collapsed")); + } + } + + protected void shielding() { + if (this.expanded) { + this.createShielding( + this.getX(), + this.getY() + this.getHeight(), + this.getX() + this.getWidth(), + this.getY() + this.getHeight() + this.calcMaxHeight() + ); + return; + } + this.removeShielding(); + } + + protected void createShielding(int x1, int y1, int x2, int y2) { + this.onShieldingAdd.accept(new Shielding(Math.min(x1, x2), Math.min(y1, y2), Math.max(x1, x2), Math.max(y1, y2), this)); + } + + protected void removeShielding() { + this.onShieldingRemove.run(); + } + + public record DropdownEntry(Component desc, String id) { + public static DropdownEntry create(String id) { + return new DropdownEntry(Component.translatable(Util.makeDescriptionId("dropdown", AnvilLibFont.of(id))), id); + } + + public static DropdownEntry create(String desc, String id) { + return new DropdownEntry(Component.literal(desc), id); + } + } + + public record Shielding(double x1, double y1, double x2, double y2, Dropdown dropdown) { + public boolean isShielding(double x, double y, Dropdown dropdown) { + if (Objects.equals(this.dropdown(), dropdown)) return false; + return x >= this.x1() && x < this.x2() && y >= this.y1() && y < this.y2(); + } + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/package-info.java new file mode 100644 index 00000000..e95c0cd3 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.font.screen.widget; + +import org.jspecify.annotations.NullMarked; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java new file mode 100644 index 00000000..1bcf728b --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java @@ -0,0 +1,120 @@ +package dev.anvilcraft.lib.v2.font.sdf; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; + +import java.awt.image.BufferedImage; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Uploads SDF glyph atlas pages to GPU textures with LINEAR filtering. + */ +@ApiStatus.Internal +public final class SdfAtlasTexture { + private static final Map CACHE = new ConcurrentHashMap<>(); + + private SdfAtlasTexture() { + } + + /** + * Upload a single atlas page to the GPU, returning its texture location. + */ + public static ResourceLocation uploadPage(SdfGlyphAtlas atlas, int pageIndex) { + SdfGlyphPage page = atlas.page(pageIndex); + String key = atlas.key() + ".p" + pageIndex; + ResourceLocation id = ResourceLocation.fromNamespaceAndPath("anvillib_font", "dynamic/sdf_atlas/" + sanitize(key)); + // Synchronize on page to read the version and image data atomically + // with respect to the glyph creation thread, which also synchronizes + // on page for placeGlyph / fillPaddingForCell. + NativeImage nativeImage; + int version; + synchronized (page) { + version = page.version.get(); + nativeImage = toNativeImage(page.image); + page.dirty = false; + } + + PageEntry entry = CACHE.get(key); + if (entry != null && entry.version == version) return entry.id; + + SdfTexture texture = new SdfTexture(nativeImage); + Minecraft.getInstance().getTextureManager().register(id, texture); + + if (entry != null) entry.texture.close(); + CACHE.put(key, new PageEntry(id, texture, version)); + page.textureId = id; + return id; + } + + /** + * Upload all dirty pages of an atlas. Called before rendering. + */ + public static void ensureUploaded(SdfGlyphAtlas atlas) { + for (int i = 0; i < atlas.pageCount(); i++) { + SdfGlyphPage page = atlas.page(i); + String key = atlas.key() + ".p" + i; + PageEntry entry = CACHE.get(key); + if (page.textureId == null + || entry == null + || entry.version != page.version.get() + || page.dirty) { + uploadPage(atlas, i); + } + } + } + + /** + * A minimal single-channel texture with LINEAR+CLAMP filtering for SDF sampling. + */ + static final class SdfTexture extends DynamicTexture { + SdfTexture(NativeImage image) { + super(image); + // Linear filtering (no mipmap) for smooth SDF sampling. + this.setFilter(true, false); + } + } + + static NativeImage toNativeImage(BufferedImage image) { + NativeImage ni = new NativeImage(NativeImage.Format.RGBA, image.getWidth(), image.getHeight(), false); + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + int gray = image.getRGB(x, y) & 0xFF; + ni.setPixelRGBA(x, y, (0xFF << 24) | (gray << 16) | (gray << 8) | gray); + } + } + return ni; + } + + private static String sanitize(String key) { + StringBuilder sb = new StringBuilder(key.length()); + for (int i = 0; i < key.length(); i++) { + char c = key.charAt(i); + if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '/' || c == '_' || c == '-' || c == '.') { + sb.append(c); + } else if (c >= 'A' && c <= 'Z') { + sb.append((char) (c + ('a' - 'A'))); + } else if (c == '#') { + sb.append('_'); + } else { + sb.append('_'); + } + } + return sb.toString(); + } + + private static final class PageEntry { + final ResourceLocation id; + final SdfTexture texture; + final int version; + + PageEntry(ResourceLocation id, SdfTexture texture, int version) { + this.id = id; + this.texture = texture; + this.version = version; + } + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java new file mode 100644 index 00000000..4cce9e98 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java @@ -0,0 +1,398 @@ +package dev.anvilcraft.lib.v2.font.sdf; + +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +/** + * Multi-page on-demand SDF glyph atlas. + *

+ * Glyphs are packed into fixed-size 1024×1024 pages. ASCII 32-126 is + * pre-warmed; all other codepoints are rendered lazily on first use. + */ +@ApiStatus.Internal +@Slf4j +public final class SdfGlyphAtlas { + static final int PAGE_SIZE = 1024; + private static final int FIRST_CHAR = 32; + private static final int LAST_CHAR = 126; + /** + * AWT system fonts report size 1; derive to a fixed rendering size for the atlas. + */ + private static final int ATLAS_FONT_SIZE = 64; + + private static final Map> CACHE = new ConcurrentHashMap<>(); + + /** + * Per-task virtual-thread executor for background glyph creation. + * Each glyph (or batch) gets its own virtual thread; the existing + * {@code synchronized} blocks on the atlas and page objects already + * provide the necessary mutual exclusion. + */ + private static final ExecutorService GLYPH_EXECUTOR = Executors.newThreadPerTaskExecutor( + Thread.ofVirtual().name("AnvilLib-SDF-Glyph-", 0).factory() + ); + + private final String key; + private final Font font; + final int cellSize; + final int padding; + final int paddedCellSize; + final float sdfRadius; + private final int awtAscent; + private final int awtHeight; + private final FontMetrics fontMetrics; + + private final List pages = new ArrayList<>(); + private final Map glyphMap = new ConcurrentHashMap<>(); + private final Set pendingGlyphs = ConcurrentHashMap.newKeySet(); + + private SdfGlyphAtlas(String key, Font font) { + this.key = key; + this.font = font; + log.info("SdfGlyphAtlas building for key={}, thread={}", key, Thread.currentThread().getName()); + + // Capture font metrics first — needed by cellSize calculation + BufferedImage tmp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = tmp.createGraphics(); + try { + g.setFont(this.font); + this.fontMetrics = g.getFontMetrics(); + this.awtAscent = this.fontMetrics.getAscent(); + this.awtHeight = this.fontMetrics.getHeight(); + } finally { + g.dispose(); + } + + // Cell must accommodate full ascent + descent above and below baseline, + // plus 2px margin on each side. Previously used font.getSize()+12, which + // could clip descenders or ascenders for fonts with large extents. + int awtDescent = this.fontMetrics.getDescent(); + this.cellSize = Math.max(24, this.awtAscent + awtDescent + 4); + this.sdfRadius = Math.max(12, font.getSize() * 0.25f); + this.padding = Math.max(4, this.cellSize / 6); + this.paddedCellSize = this.cellSize + 2 * this.padding; + log.info("SdfGlyphAtlas cellSize={} ascent={} descent={} key={}", this.cellSize, this.awtAscent, awtDescent, key); + + preWarmAscii(); + log.info("SdfGlyphAtlas ready for key={}, pages={}", key, this.pages.size()); + } + + // ── Public API ────────────────────────────────────────────── + + /** + * Start building the atlas on a background thread; returns the future. + */ + public static CompletableFuture getOrCreate(@Nullable Font font) { + Font resolved = resolveFont(font); + String key = resolved.getFontName(Locale.ENGLISH) + "." + resolved.getStyle() + "." + resolved.getSize(); + return CACHE.computeIfAbsent(key, ignored -> { + log.debug("Starting SDF atlas build for key={}", key); + return CompletableFuture.supplyAsync(() -> new SdfGlyphAtlas(key, resolved), GLYPH_EXECUTOR); + }); + } + + /** + * Return the atlas if fully built, or {@code null} if still constructing. + *

+ * If the previous build failed (future completed exceptionally), the + * failed future is removed and a fresh build is started so that a + * transient error does not permanently disable the atlas. + */ + public static @Nullable SdfGlyphAtlas getIfReady(@Nullable Font font) { + Font resolved = resolveFont(font); + String key = resolved.getFontName(Locale.ENGLISH) + "." + resolved.getStyle() + "." + resolved.getSize(); + CompletableFuture f = CACHE.get(key); + if (f != null && f.isDone()) { + try { + SdfGlyphAtlas atlas = f.get(); + return atlas; + } catch (Exception e) { + log.error("SDF atlas build failed for key={}, retrying", key, e); + CACHE.remove(key, f); + f = null; + } + } + if (f == null) { + log.info("No atlas future for key={}, starting build", key); + getOrCreate(font); + } + return null; + } + + private static Font resolveFont(@Nullable Font font) { + if (font == null) return new Font("Dialog", Font.PLAIN, ATLAS_FONT_SIZE); + if (font.getSize() < 4) return font.deriveFont((float) ATLAS_FONT_SIZE); + return font; + } + + public String key() { + return this.key; + } + + public Font font() { + return this.font; + } + + public int awtHeight() { + return this.awtHeight; + } + + public int awtAscent() { + return this.awtAscent; + } + + public int pageCount() { + return this.pages.size(); + } + + public SdfGlyphPage page(int index) { + return this.pages.get(index); + } + + /** + * Get the glyph entry for a codepoint. If the glyph is not yet in the + * atlas, requests async creation and returns {@code null}. The caller + * should handle the missing glyph gracefully (e.g. advance by a default + * width); the glyph will be available on the next call. + */ + public @Nullable GlyphEntry glyph(int codepoint, Consumer pageConsumer) { + GlyphEntry entry = this.glyphMap.get(codepoint); + if (entry != null) return entry; + // Trigger async creation on background thread + if (this.pendingGlyphs.add(codepoint)) { + GLYPH_EXECUTOR.submit(() -> createGlyphAsync(codepoint)); + } + return null; + } + + /** + * Background-thread entry point for glyph creation. + * Synchronized to ensure only one glyph is created at a time per atlas, + * avoiding races on page state (nextCol, nextRow, image pixels). + */ + private void createGlyphAsync(int codepoint) { + try { + synchronized (this) { + // Double-check: may have been created since we were enqueued + if (this.glyphMap.containsKey(codepoint)) return; + createGlyph(codepoint, SdfGlyphPage::updateHash); + } + // Invalidate cached layouts so they pick up the new glyph + SdfTextLayout.invalidateAtlas(this.key); + } catch (Exception e) { + log.error("Failed to create SDF glyph for codepoint {} (U+{})", codepoint, Integer.toHexString(codepoint).toUpperCase(), e); + } finally { + this.pendingGlyphs.remove(codepoint); + } + } + + public int measureText(String text) { + return measureText(text, 0, text.length()); + } + + /** + * Measure width of substring {@code text[start..end)} in atlas pixels. + */ + public int measureText(String text, int start, int end) { + int width = 0; + Set glyphPages = new HashSet<>(); + for (int i = start; i < end; ) { + int cp = text.codePointAt(i); + GlyphEntry g = glyph(cp, glyphPages::add); + width += g == null ? this.cellSize / 2 : g.advance; + i += Character.charCount(cp); + } + glyphPages.forEach(SdfGlyphPage::updateHash); + return width; + } + + /** + * Measure a single codepoint's advance in atlas pixels. + */ + public int measureCodepoint(int codepoint) { + GlyphEntry g = glyph(codepoint, SdfGlyphPage::updateHash); + return g == null ? this.cellSize / 2 : g.advance; + } + + // ── Glyph creation ────────────────────────────────────────── + + private GlyphEntry createGlyph(int codepoint, Consumer pageConsumer) { + BufferedImage mask = renderMask(codepoint); + + int pageIdx = findOrCreatePageIndex(); + SdfGlyphPage page = this.pages.get(pageIdx); + GlyphEntry entry = page.placeGlyph(this, mask); + entry = new GlyphEntry( + pageIdx, + entry.atlasX(), + entry.atlasY(), + entry.width(), + entry.height(), + this.fontMetrics.charWidth(codepoint) + ); + this.glyphMap.put(codepoint, entry); + page.fillPaddingForCell(this, entry); + page.dirty = true; + pageConsumer.accept(page); + return entry; + } + + private void preWarmAscii() { + Set glyphPages = new HashSet<>(); + for (int code = FIRST_CHAR; code <= LAST_CHAR; code++) { + createGlyph(code, glyphPages::add); + } + glyphPages.forEach(SdfGlyphPage::updateHash); + } + + private int findOrCreatePageIndex() { + for (int i = 0; i < this.pages.size(); i++) { + if (this.pages.get(i).hasSpace()) return i; + } + this.pages.add(new SdfGlyphPage(this.paddedCellSize)); + return this.pages.size() - 1; + } + + /** + * Render a single glyph as a white-on-transparent mask. + */ + private BufferedImage renderMask(int codepoint) { + String s = new String(Character.toChars(codepoint)); + BufferedImage mask = new BufferedImage(this.cellSize, this.cellSize, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = mask.createGraphics(); + try { + g.setFont(this.font); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setColor(new Color(0, 0, 0, 0)); + g.fillRect(0, 0, this.cellSize, this.cellSize); + g.setColor(Color.WHITE); + g.drawString(s, 2, this.awtAscent + 2); + } finally { + g.dispose(); + } + return mask; + } + + // ── Static SDF computation utilities ───────────────────────── + + static void blitSdfGlyph(BufferedImage mask, BufferedImage target, int tx, int ty, int w, float maxRadius) { + boolean[] inside = new boolean[w * w]; + for (int y = 0; y < w; y++) { + for (int x = 0; x < w; x++) { + inside[y * w + x] = isInside(mask, x, y); + } + } + + // Distance to nearest outside pixel (for inside pixels) + float[] distOutside = computeDistTo(inside, w, false); + // Distance to nearest inside pixel (for outside pixels) + float[] distInside = computeDistTo(inside, w, true); + + for (int y = 0; y < w; y++) { + for (int x = 0; x < w; x++) { + int i = y * w + x; + float signed = inside[i] ? distOutside[i] : -distInside[i]; + signed = Math.min(signed, maxRadius); + float normalized = 0.5f + (signed / (2.0f * maxRadius)); + int ch = Math.round(Math.max(0f, Math.min(1f, normalized)) * 255f); + target.setRGB(tx + x, ty + y, (ch << 24) | (ch << 16) | (ch << 8) | ch); + } + } + } + + /** + * Dead Reckoning EDT: distance from each pixel to nearest pixel where {@code inside == target}. + */ + private static float[] computeDistTo(boolean[] inside, int w, boolean target) { + int n = w * w, HUGE = w * 3; + int[] dx = new int[n], dy = new int[n]; + for (int i = 0; i < n; i++) { + dx[i] = (inside[i] == target) ? 0 : HUGE; + dy[i] = (inside[i] == target) ? 0 : HUGE; + } + // Pass 1 + for (int y = 0; y < w; y++) { + for (int x = 0; x < w; x++) { + int i = y * w + x; + if (y > 0) { + if (x > 0) tryUpdate(dx, dy, i, (y - 1) * w + (x - 1), x, y, x - 1, y - 1); + tryUpdate(dx, dy, i, (y - 1) * w + x, x, y, x, y - 1); + if (x < w - 1) tryUpdate(dx, dy, i, (y - 1) * w + (x + 1), x, y, x + 1, y - 1); + } + if (x > 0) tryUpdate(dx, dy, i, y * w + (x - 1), x, y, x - 1, y); + } + } + // Pass 2 + for (int y = w - 1; y >= 0; y--) { + for (int x = w - 1; x >= 0; x--) { + int i = y * w + x; + if (x < w - 1) tryUpdate(dx, dy, i, y * w + (x + 1), x, y, x + 1, y); + if (y < w - 1) { + if (x > 0) tryUpdate(dx, dy, i, (y + 1) * w + (x - 1), x, y, x - 1, y + 1); + tryUpdate(dx, dy, i, (y + 1) * w + x, x, y, x, y + 1); + if (x < w - 1) tryUpdate(dx, dy, i, (y + 1) * w + (x + 1), x, y, x + 1, y + 1); + } + } + } + float[] dist = new float[n]; + for (int i = 0; i < n; i++) { + dist[i] = (float) Math.sqrt(dx[i] * dx[i] + dy[i] * dy[i]); + } + return dist; + } + + private static void tryUpdate(int[] dx, int[] dy, int cur, int nbr, int cx, int cy, int nx, int ny) { + int ndx = dx[nbr] + (nx - cx), ndy = dy[nbr] + (ny - cy); + if (ndx * ndx + ndy * ndy < dx[cur] * dx[cur] + dy[cur] * dy[cur]) { + dx[cur] = ndx; + dy[cur] = ndy; + } + } + + private static boolean isInside(BufferedImage image, int x, int y) { + return ((image.getRGB(x, y) >>> 24) & 0xFF) > 16; + } + + static void fillPadding(BufferedImage img, int padX, int padY, int paddedCellSize, int cellSize, int padding) { + int ix0 = padX + padding, iy0 = padY + padding; + int ix1 = ix0 + cellSize - 1, iy1 = iy0 + cellSize - 1; + for (int y = padY; y < padY + paddedCellSize; y++) { + for (int x = padX; x < padX + paddedCellSize; x++) { + if (x >= ix0 && x <= ix1 && y >= iy0 && y <= iy1) continue; + img.setRGB(x, y, img.getRGB(Math.clamp(x, ix0, ix1), Math.clamp(y, iy0, iy1))); + } + } + } + + // ── Inner types ───────────────────────────────────────────── + + public record GlyphEntry(int pageIndex, int atlasX, int atlasY, int width, int height, int advance) { + public int endX() { + return this.atlasX + this.width; + } + + public int endY() { + return this.atlasY + this.height; + } + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java new file mode 100644 index 00000000..3b6aaa19 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java @@ -0,0 +1,64 @@ +package dev.anvilcraft.lib.v2.font.sdf; + +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; + +import java.awt.image.BufferedImage; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A single 1024×1024 atlas page holding packed glyphs. + */ +@ApiStatus.Internal +public final class SdfGlyphPage { + private static final int SIZE = SdfGlyphAtlas.PAGE_SIZE; + final BufferedImage image; + final AtomicInteger version = new AtomicInteger(); + final int cols, rows; + int nextCol, nextRow; + ResourceLocation textureId; + volatile boolean dirty = true; + + SdfGlyphPage(int paddedCellSize) { + this.cols = SIZE / paddedCellSize; + this.rows = SIZE / paddedCellSize; + this.image = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_BYTE_GRAY); + } + + boolean hasSpace() { + return nextRow < rows; + } + + synchronized SdfGlyphAtlas.GlyphEntry placeGlyph(SdfGlyphAtlas atlas, BufferedImage mask) { + int col = nextCol, row = nextRow; + int padX = col * atlas.paddedCellSize; + int padY = row * atlas.paddedCellSize; + int innerX = padX + atlas.padding; + int innerY = padY + atlas.padding; + SdfGlyphAtlas.blitSdfGlyph(mask, this.image, innerX, innerY, atlas.cellSize, atlas.sdfRadius); + nextCol++; + if (nextCol >= cols) { + nextCol = 0; + nextRow++; + } + dirty = true; + return new SdfGlyphAtlas.GlyphEntry(0, innerX, innerY, atlas.cellSize, atlas.cellSize, 0); + } + + synchronized void fillPaddingForCell(SdfGlyphAtlas atlas, SdfGlyphAtlas.GlyphEntry e) { + int col = (e.atlasX() - atlas.padding) / atlas.paddedCellSize; + int row = (e.atlasY() - atlas.padding) / atlas.paddedCellSize; + SdfGlyphAtlas.fillPadding( + this.image, + col * atlas.paddedCellSize, + row * atlas.paddedCellSize, + atlas.paddedCellSize, + atlas.cellSize, + atlas.padding + ); + } + + public void updateHash() { + this.version.incrementAndGet(); + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java new file mode 100644 index 00000000..4bdff3bd --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java @@ -0,0 +1,154 @@ +package dev.anvilcraft.lib.v2.font.sdf; + +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * CPU-side glyph layout output for SDF text rendering, grouped by atlas page. + *

+ * Layouts are cached by {@code (atlasKey, text, scale)} to avoid redundant + * computation each frame. Quads are stored with positions relative to the + * layout origin; the renderer supplies the screen-space offset. + */ +@ApiStatus.Internal +public final class SdfTextLayout { + private static final int MAX_CACHE_SIZE = 1024; + private static final Map LAYOUT_CACHE = new ConcurrentHashMap<>(); + + private final List pages; + private final int width; + private final int height; + + private SdfTextLayout(List pages, int width, int height) { + this.pages = pages; + this.width = width; + this.height = height; + } + + /** + * Compute glyph layout for a string. The returned quads have positions + * relative to (0, 0); add {@code x}/{@code y} when rendering. + *

+ * Result is cached when all glyphs are available in the atlas. + */ + public static SdfTextLayout fromAtlas(SdfGlyphAtlas atlas, @Nullable String text, int x, int y, float scale) { + if (text == null || text.isEmpty()) + return EMPTY; + + int scaleInt = Math.round(scale * 1000f); + CacheKey key = new CacheKey(atlas.key(), text, scaleInt); + SdfTextLayout cached = LAYOUT_CACHE.get(key); + if (cached != null) return cached; + + SdfTextLayout layout = computeLayout(atlas, text, scale, scaleInt); + if (layout != null && layout.allGlyphsAvailable) { + evictIfNeeded(); + LAYOUT_CACHE.put(key, layout); + } + return layout; + } + + /** + * Invalidate cached layouts for a specific atlas (e.g. after glyph additions). + */ + public static void invalidateAtlas(String atlasKey) { + LAYOUT_CACHE.keySet().removeIf(k -> k.atlasKey.equals(atlasKey)); + } + + /** + * Clear all cached layouts. + */ + public static void clearCache() { + LAYOUT_CACHE.clear(); + } + + private static void evictIfNeeded() { + if (LAYOUT_CACHE.size() >= MAX_CACHE_SIZE) { + // Evict half the entries (simple random-ish eviction via iterator) + var it = LAYOUT_CACHE.keySet().iterator(); + int toRemove = MAX_CACHE_SIZE / 2; + for (int i = 0; i < toRemove && it.hasNext(); i++) { + it.next(); + it.remove(); + } + } + } + + @Nullable + private static SdfTextLayout computeLayout(SdfGlyphAtlas atlas, String text, float scale, int scaleInt) { + int penX = 0, maxHeight = 0; + Map> buckets = new LinkedHashMap<>(); + boolean allAvailable = true; + + Set glyphPages = new HashSet<>(); + for (int ci = 0; ci < text.length(); ) { + int cp = text.codePointAt(ci); + ci += Character.charCount(cp); + + SdfGlyphAtlas.GlyphEntry glyph = atlas.glyph(cp, glyphPages::add); + if (glyph == null) { + allAvailable = false; + penX += Math.round(Math.max(6, atlas.font().getSize() / 2) * scale); + continue; + } + if (glyph.width() <= 0) { + penX += Math.max(1, Math.round(glyph.advance() * scale)); + continue; + } + + SdfGlyphPage page = atlas.page(glyph.pageIndex()); + float u0 = glyph.atlasX() / (float) page.image.getWidth(); + float v0 = glyph.atlasY() / (float) page.image.getHeight(); + float u1 = glyph.endX() / (float) page.image.getWidth(); + float v1 = glyph.endY() / (float) page.image.getHeight(); + + int w = Math.round(glyph.width() * scale); + int h = Math.round(glyph.height() * scale); + // Positions relative to layout origin (0, 0) + GlyphQuad quad = new GlyphQuad(penX, 0, penX + w, h, u0, v0, u1, v1, (char) cp); + + buckets.computeIfAbsent(glyph.pageIndex(), ignored -> new ArrayList<>()).add(quad); + penX += Math.max(1, Math.round(glyph.advance() * scale)); + maxHeight = Math.max(maxHeight, h); + } + glyphPages.forEach(SdfGlyphPage::updateHash); + + List pages = new ArrayList<>(); + for (var entry : buckets.entrySet()) { + int pi = entry.getKey(); + SdfGlyphPage page = atlas.page(pi); + ResourceLocation tex = page.textureId; + pages.add(new PageQuads(pi, tex, page.image.getWidth(), page.image.getHeight(), entry.getValue())); + } + + SdfTextLayout layout = new SdfTextLayout(pages, penX, maxHeight); + layout.allGlyphsAvailable = allAvailable; + return layout; + } + + private boolean allGlyphsAvailable = true; + + private static final SdfTextLayout EMPTY = new SdfTextLayout(List.of(), 0, 0); + + public List pages() { return Collections.unmodifiableList(this.pages); } + public int width() { return this.width; } + public int height() { return this.height; } + + private record CacheKey(String atlasKey, String text, int scaleInt) {} + + public record PageQuads(int pageIndex, @Nullable ResourceLocation atlasTexture, + int pageWidth, int pageHeight, List quads) {} + + public record GlyphQuad(int x0, int y0, int x1, int y1, + float u0, float v0, float u1, float v1, char glyph) {} +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java new file mode 100644 index 00000000..9254f140 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java @@ -0,0 +1,271 @@ +package dev.anvilcraft.lib.v2.font.sdf; + +import com.google.common.annotations.Beta; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; +import dev.anvilcraft.lib.v2.font.ALFPipelines; +import dev.anvilcraft.lib.v2.font.ALFont; +import dev.anvilcraft.lib.v2.font.sdf.state.SdfTextRenderState; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.network.chat.Style; +import net.minecraft.util.FormattedCharSequence; +import org.jspecify.annotations.Nullable; + +import java.awt.Font; +import java.util.List; + +/** + * SDF text renderer that draws strings via the SDF text shader. + *

+ * Uses a CPU-generated SDF glyph atlas uploaded to a GPU texture, + * sampled by a custom fragment shader for smooth anti-aliased text. + */ +@Beta +public final class SdfTextRenderer { + public SdfTextRenderer() { + } + + /** + * Get the SDF atlas for a font, blocking until it is ready on first access. + * The atlas is cached forever, so only the very first call for a particular + * font blocks (typically ~200-400 ms while the glyph atlas is built). + */ + private static SdfGlyphAtlas getAtlas(@Nullable Font font) { + return SdfGlyphAtlas.getOrCreate(font).join(); + } + + public void drawString( + GuiGraphics graphics, + @Nullable Font font, + @Nullable String text, + int x, + int y, + int color, + boolean dropShadow + ) { + if (text == null || text.isEmpty()) return; + drawStringWithAtlas(graphics, getAtlas(font), text, x, y, color); + } + + private void drawStringWithAtlas( + GuiGraphics graphics, SdfGlyphAtlas atlas, + String text, int x, int y, int color + ) { + float scale = scaleFor(atlas); + SdfAtlasTexture.ensureUploaded(atlas); + SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, y, scale); + if (layout.pages().isEmpty()) return; + for (SdfTextLayout.PageQuads pq : layout.pages()) { + drawAtlas(graphics, pq, color, x, y); + } + } + + private static float scaleFor(SdfGlyphAtlas atlas) { + return Minecraft.getInstance().font.lineHeight / (float) atlas.awtHeight(); + } + + /** + * Derive a styled font for bold/italic. + */ + private static Font styledFont(Font base, boolean bold, boolean italic) { + int mask = Font.PLAIN; + if (bold) mask |= Font.BOLD; + if (italic) mask |= Font.ITALIC; + return mask == Font.PLAIN ? base : base.deriveFont(mask); + } + + /** + * Replace codepoint with random ASCII for obfuscated style. + */ + private static int obfuscateCodepoint(int codepoint, int index) { + long t = System.currentTimeMillis() / 300L; + int r = (int) (((long) index * 7L + t) % 95L); + return 32 + r; + } + + public void drawComponent( + GuiGraphics graphics, + @Nullable Font font, + Component text, + int x, + int y, + int color, + boolean dropShadow + ) { + this.drawFormatted(graphics, font, text.getVisualOrderText(), x, y, color, dropShadow); + } + + public void drawFormatted( + GuiGraphics graphics, + @Nullable Font font, + FormattedCharSequence text, + int x, + int y, + int color, + boolean dropShadow + ) { + int[] pen = { + x, + x + }; + StringBuilder buf = new StringBuilder(); + int[] segColor = {color}; + boolean[] segBold = {false}; + boolean[] segItalic = {false}; + boolean[] segUnderline = {false}; + boolean[] segStrikethrough = {false}; + + text.accept((index, style, codepoint) -> { + if (style.isObfuscated()) { + codepoint = obfuscateCodepoint(codepoint, index); + } + + int c = colorFromStyle(style, color); + boolean b = style.isBold(); + boolean i = style.isItalic(); + + if ((c != segColor[0] || b != segBold[0] || i != segItalic[0]) && !buf.isEmpty()) { + Font segFont = styledFont(font, segBold[0], segItalic[0]); + pen[0] = flushFormattedSegment(graphics, segFont, buf.toString(), pen[0], y, segColor[0]); + drawDecorations(graphics, pen[1], pen[0], y, segColor[0], segUnderline[0], segStrikethrough[0]); + buf.setLength(0); + pen[1] = pen[0]; + } + + buf.appendCodePoint(codepoint); + segColor[0] = c; + segBold[0] = b; + segItalic[0] = i; + segUnderline[0] = style.isUnderlined(); + segStrikethrough[0] = style.isStrikethrough(); + return true; + }); + + if (!buf.isEmpty()) { + Font segFont = styledFont(font, segBold[0], segItalic[0]); + pen[0] = flushFormattedSegment(graphics, segFont, buf.toString(), pen[0], y, segColor[0]); + drawDecorations(graphics, pen[1], pen[0], y, segColor[0], segUnderline[0], segStrikethrough[0]); + } + } + + private int flushFormattedSegment(GuiGraphics graphics, @Nullable Font font, String text, int x, int y, int color) { + SdfGlyphAtlas atlas = getAtlas(font); + float scale = scaleFor(atlas); + SdfAtlasTexture.ensureUploaded(atlas); + SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, y, scale); + for (SdfTextLayout.PageQuads pq : layout.pages()) { + if (pq.atlasTexture() != null && !pq.quads().isEmpty()) { + drawAtlas(graphics, pq, color, x, y); + } + } + return x + layout.width(); + } + + /** + * Draw underline and/or strikethrough lines relative to baseline. + */ + private static void drawDecorations( + GuiGraphics graphics, + int x0, + int x1, + int y, + int color, + boolean underline, + boolean strikethrough + ) { + if (x1 <= x0) return; + int lh = Minecraft.getInstance().font.lineHeight; + PoseStack pose = graphics.pose(); + pose.pushPose(); + pose.scale(1.0F, 0.5F, 1.0F); + if (strikethrough) { + int sy = (y + lh / 2) * 2; + graphics.fill(x0, sy, x1, sy + 1, color); + } + if (underline) { + int sy = (y + lh - 2) * 2; + graphics.fill(x0, sy, x1, sy + 1, color); + } + pose.popPose(); + } + + public void drawWrapped( + GuiGraphics graphics, + @Nullable Font font, + FormattedText text, + int x, + int y, + int width, + int color, + boolean dropShadow + ) { + SdfGlyphAtlas atlas = getAtlas(font); + float scale = scaleFor(atlas); + List lines = wrapLines(atlas, text.getString(), width, scale); + int lineHeight = Minecraft.getInstance().font.lineHeight; + for (int i = 0; i < lines.size(); i++) { + this.drawStringWithAtlas(graphics, atlas, lines.get(i), x, y + i * lineHeight, color); + } + } + + public void drawCentered(GuiGraphics graphics, @Nullable Font font, Component text, int x, int y, int color) { + drawCentered(graphics, font, text.getVisualOrderText(), x, y, color); + } + + public void drawCentered(GuiGraphics graphics, @Nullable Font font, FormattedCharSequence text, int x, int y, int color) { + String value = flattenToString(text); + SdfGlyphAtlas atlas = getAtlas(font); + float scale = scaleFor(atlas); + int drawX = x - Math.round(atlas.measureText(value) * scale) / 2; + this.drawFormatted(graphics, font, text, drawX, y, color, false); + } + + private static int colorFromStyle(Style style, int defaultColor) { + return style.getColor() != null ? style.getColor().getValue() | 0xFF000000 : defaultColor; + } + + private static List wrapLines(SdfGlyphAtlas atlas, String text, int maxWidth, float scale) { + return ALFont.wrapLines(atlas, text, maxWidth, scale); + } + + private static String flattenToString(FormattedCharSequence text) { + StringBuilder buf = new StringBuilder(); + text.accept((index, style, cp) -> { + buf.appendCodePoint(cp); + return true; + }); + return buf.toString(); + } + + private static void drawAtlas( + GuiGraphics graphics, + SdfTextLayout.PageQuads pq, + int color, + int originX, + int originY + ) { + if (pq.atlasTexture() == null || pq.quads().isEmpty()) return; + SdfTextRenderState state = new SdfTextRenderState( + new org.joml.Matrix4f(graphics.pose().last().pose()), + pq.quads(), + pq.atlasTexture(), + color, + originX, + originY + ); + // GUI rendering keeps blending enabled and depth test disabled; only the + // shader and atlas texture need to be swapped for SDF text. + RenderSystem.setShader(ALFPipelines::getSdfTextShader); + RenderSystem.setShaderTexture(0, state.atlasTexture()); + BufferBuilder builder = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS, ALFPipelines.SDF_TEXT_FORMAT); + state.buildVertices(builder); + BufferUploader.drawWithShader(builder.buildOrThrow()); + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/package-info.java new file mode 100644 index 00000000..1fcf1894 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.font.sdf; + +import org.jspecify.annotations.NullMarked; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java new file mode 100644 index 00000000..8d9f6fc3 --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java @@ -0,0 +1,66 @@ +package dev.anvilcraft.lib.v2.font.sdf.state; + +import com.mojang.blaze3d.vertex.VertexConsumer; +import dev.anvilcraft.lib.v2.font.sdf.SdfTextLayout; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; +import org.joml.Matrix4f; + +import java.util.List; + +/** + * Render state for SDF text rendering. + * + *

Coordinates are in screen space and are transformed via the pose matrix + * before being submitted to the GPU with the SDF shader.

+ */ +@ApiStatus.Internal +public record SdfTextRenderState( + Matrix4f pose, + List glyphs, + ResourceLocation atlasTexture, + int color, + int originX, + int originY +) { + /** + * Build quad vertices in screen space. The pose matrix transforms them to + * the GUI coordinate space; the SDF shader then applies the GUI projection. + */ + public void buildVertices(VertexConsumer consumer) { + int ox = this.originX; + int oy = this.originY; + for (SdfTextLayout.GlyphQuad quad : this.glyphs) { + float x0 = quad.x0() + ox; + float y0 = quad.y0() + oy; + float x1 = quad.x1() + ox; + float y1 = quad.y1() + oy; + + float u0 = quad.u0(); + float v0 = quad.v0(); + float u1 = quad.u1(); + float v1 = quad.v1(); + + // Draw quad as two triangles (4 vertices in QUADS mode) + // Vertex 0: top-left + consumer.addVertex(this.pose, x0, y0, 0.0F) + .setColor(this.color) + .setUv(u0, v0); + + // Vertex 1: bottom-left + consumer.addVertex(this.pose, x0, y1, 0.0F) + .setColor(this.color) + .setUv(u0, v1); + + // Vertex 2: bottom-right + consumer.addVertex(this.pose, x1, y1, 0.0F) + .setColor(this.color) + .setUv(u1, v1); + + // Vertex 3: top-right + consumer.addVertex(this.pose, x1, y0, 0.0F) + .setColor(this.color) + .setUv(u1, v0); + } + } +} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/package-info.java new file mode 100644 index 00000000..7830d14c --- /dev/null +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.font.sdf.state; + +import org.jspecify.annotations.NullMarked; diff --git a/module.font/src/main/resources/META-INF/neoforge.mods.toml b/module.font/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..27f7ba75 --- /dev/null +++ b/module.font/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,86 @@ +# This is an example mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader = "javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. +loaderVersion = "${loader_version_range}" #mandatory +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license = "${mod_license}" +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory +# The modid of the mod +modId = "${mod_id}" #mandatory +# The version number of the mod +version = "${mod_version}" #mandatory +# A display name for the mod +displayName = "${mod_name}" #mandatory +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional +# A URL for the "homepage" for this mod, displayed in the mod UI +displayURL="https://github.com/Anvil-Dev/AnvilLib" +# A file name (in the root of the mod JAR) containing a logo for display +logoFile="icon.png" #optional +# A text field displayed in the mod UI +#credits="" #optional +# A text field displayed in the mod UI +authors = "${mod_authors}" #optional +# Display Test controls the display for your mod in the server connection screen +# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. +# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. +# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. +# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. +# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. +#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) + +# The description text for the mod (multi line!) (#mandatory) +description = '''${mod_description}''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +[[mixins]] +config = "${mod_id}.mixins.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies."${mod_id}"]] #optional +# the modid of the dependency +modId = "neoforge" #mandatory +# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). +# 'required' requires the mod to exist, 'optional' does not +# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning +type = "required" #mandatory +# Optional field describing why the dependency is required or why it is incompatible +# reason="..." +# The version range of the dependency +versionRange = "${neo_version_range}" #mandatory +# An ordering relationship for the dependency. +# BEFORE - This mod is loaded BEFORE the dependency +# AFTER - This mod is loaded AFTER the dependency +ordering = "NONE" +# Side this dependency is applied on - BOTH, CLIENT, or SERVER +side = "BOTH" +# Here's another dependency +[[dependencies."${mod_id}"]] +modId = "minecraft" +type = "required" +# This version range declares a minimum of the current minecraft version up to but not including the next major version +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features."${mod_id}"] +#openGLVersion="[3.2,)" diff --git a/module.font/src/main/resources/anvillib_font.mixins.json b/module.font/src/main/resources/anvillib_font.mixins.json new file mode 100644 index 00000000..098588c0 --- /dev/null +++ b/module.font/src/main/resources/anvillib_font.mixins.json @@ -0,0 +1,15 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.anvilcraft.lib.v2.font.mixin", + "compatibilityLevel": "JAVA_8", + "refmap": "anvillib.refmap.json", + "mixins": [ + ], + "client": [ + "GuiGraphicsExtractorMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/module.font/src/main/resources/assets/anvillib_font/lang/zh_cn.json b/module.font/src/main/resources/assets/anvillib_font/lang/zh_cn.json new file mode 100644 index 00000000..0e37a159 --- /dev/null +++ b/module.font/src/main/resources/assets/anvillib_font/lang/zh_cn.json @@ -0,0 +1,8 @@ +{ + "narration.anvillib_font.dropdown.collapsed": "折叠", + "narration.anvillib_font.dropdown.expanded": "展开", + "screen.anvillib_font.config": "铁砧库 文字渲染 配置", + "screen.anvillib_font.config.family": "字体系列", + "screen.anvillib_font.config.font": "字体", + "screen.anvillib_font.config.test": "文字渲染测试" +} \ No newline at end of file diff --git a/module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.fsh b/module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.fsh new file mode 100644 index 00000000..7770df89 --- /dev/null +++ b/module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.fsh @@ -0,0 +1,19 @@ +#version 150 + +uniform sampler2D Sampler0; + +in vec2 vTexCoord; +in vec4 vColor; + +out vec4 fragColor; + +void main() { + vec4 sampleColor = texture(Sampler0, vTexCoord); + + float distanceValue = sampleColor.r; + float aa = max(fwidth(distanceValue), 0.04); + float alpha = smoothstep(0.5 - aa, 0.5 + aa, distanceValue); + + vec4 color = vec4(vColor.rgb, vColor.a * alpha); + fragColor = color; +} diff --git a/module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.json b/module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.json new file mode 100644 index 00000000..9de877d8 --- /dev/null +++ b/module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.json @@ -0,0 +1,11 @@ +{ + "vertex": "sdf_text", + "fragment": "sdf_text", + "samplers": [ + { "name": "Sampler0" } + ], + "uniforms": [ + { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] } + ] +} diff --git a/module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.vsh b/module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.vsh new file mode 100644 index 00000000..5ce8fd30 --- /dev/null +++ b/module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.vsh @@ -0,0 +1,17 @@ +#version 150 + +in vec3 Position; +in vec4 Color; +in vec2 UV0; + +uniform mat4 ModelViewMat; +uniform mat4 ProjMat; + +out vec2 vTexCoord; +out vec4 vColor; + +void main() { + gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); + vTexCoord = UV0; + vColor = Color; +} diff --git a/module.font/src/main/resources/icon.png b/module.font/src/main/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..15304bd54d5d97d8482049d5f16a7544748ce05a GIT binary patch literal 1089 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabRA=0U}gyL32}YB&qp?9cb}fRhL)ab z>**&8P1h&;i5V@+5z~{9lu=Mp{cG+rNOuP4jFK;J~t_xRpI&Nf4%?X zZmY?-7zkw=;bC zsjT>Uk~ZUn{GF#RHtMc+sGWYc$A>9-;igj6hzoLO7wEVa*QxB)zM=ff)bhV&`;h}b zD&$S4O;$O;Sf&`=w_-tNu&sr4nCukimfSM|=84-o^=H0sFgVR)oOSu;mLIn4lUH@E zPWkvN(C^lx3(G6wS;EsJIMe%PuP~i06!s*lLHg)A!3fTUY`(JJ860vaMb2DUI{zB$ z25}G0l9!&RrHovbwaBi{I35yXA*NOEX}{&%4{Sd|jrPVzaJhpb&(qb Date: Tue, 11 Aug 2026 01:31:37 +0800 Subject: [PATCH 21/38] =?UTF-8?q?feat(test):=20=E5=9B=9E=E7=A7=BB=E6=A4=8D?= =?UTF-8?q?=E5=8F=AF=E9=80=89=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95=EF=BC=88?= =?UTF-8?q?T2=20=E9=85=8D=E7=BD=AE=20/=20T8=20RPC=20/=20T9=20=E7=88=86?= =?UTF-8?q?=E7=82=B8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回移植 dev/26.1 module.test 中不依赖新渲染模块的集成测试: - T2 配置测试(a6a24f1):AnvilLibTestConfig + TestEnum 枚举翻译 (@Config(group="anvillib") 依赖 #P9 的 group 语义;TranslatableEnum 已随 #P9 落地) - T8 RPC 测试(d153e58、2754d18):rpc/ 五件套,@EventBusSubscriber 均含 modid (1.21.1 无 26.1 的无 modid 语法);依赖 module.rpc(#P1) - T9 爆炸测试(435e23f):command/TestCommand,依赖 module.explosion(#P3) - T10 wheel 按键扩展已随 #P17 落地(String category + RegisterKeyMappingsEvent) - build.gradle 补 module.rpc/explosion 依赖 + compileOnly jspecify (顺带修复 module.test 既有 @NullMarked 编译缺失) 不迁移:T1/T3/T4/T5/T6/T7/T11/T12(依赖 dev 新渲染模块或 26.1 GPU 体系)。 --- module.test/build.gradle | 3 + .../anvilcraft/lib/v2/test/AnvilLibTest.java | 2 + .../lib/v2/test/AnvilLibTestConfig.java | 43 +++ .../lib/v2/test/command/TestCommand.java | 51 +++ .../lib/v2/test/command/package-info.java | 4 + .../lib/v2/test/rpc/RpcClientTest.java | 72 ++++ .../lib/v2/test/rpc/RpcIntegrationTest.java | 246 ++++++++++++++ .../lib/v2/test/rpc/RpcTestCommands.java | 285 ++++++++++++++++ .../lib/v2/test/rpc/RpcTestScheduler.java | 77 +++++ .../lib/v2/test/rpc/TestRpcMethods.java | 317 ++++++++++++++++++ .../lib/v2/test/rpc/package-info.java | 4 + 11 files changed, 1104 insertions(+) create mode 100644 module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTestConfig.java create mode 100644 module.test/src/main/java/dev/anvilcraft/lib/v2/test/command/TestCommand.java create mode 100644 module.test/src/main/java/dev/anvilcraft/lib/v2/test/command/package-info.java create mode 100644 module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcClientTest.java create mode 100644 module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcIntegrationTest.java create mode 100644 module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcTestCommands.java create mode 100644 module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcTestScheduler.java create mode 100644 module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/TestRpcMethods.java create mode 100644 module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/package-info.java diff --git a/module.test/build.gradle b/module.test/build.gradle index c22dedb0..993885d4 100644 --- a/module.test/build.gradle +++ b/module.test/build.gradle @@ -111,6 +111,9 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { implementation project(":anvillib-config-neoforge-1.21.1") implementation project(":anvillib-codec-neoforge-1.21.1") + implementation project(":anvillib-explosion-neoforge-1.21.1") + implementation project(":anvillib-rpc-neoforge-1.21.1") + compileOnly "org.jspecify:jspecify:1.0.0" implementation project(":anvillib-multiblock-neoforge-1.21.1") implementation project(":anvillib-integration-neoforge-1.21.1") interfaceInjectionData(implementation project(":anvillib-moveable-entity-block-neoforge-1.21.1")) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTest.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTest.java index 755c1e15..1880d0ee 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTest.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTest.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.test; +import dev.anvilcraft.lib.v2.config.ConfigManager; import dev.anvilcraft.lib.v2.registrum.Registrum; import dev.anvilcraft.lib.v2.test.multiblock.init.LibBlocks; import dev.anvilcraft.lib.v2.test.multiblock.init.LibItemGroups; @@ -13,6 +14,7 @@ public class AnvilLibTest { public static final String MOD_ID = "anvillib_test"; public static final Registrum REGISTRUM = Registrum.create(MOD_ID); + public static final AnvilLibTestConfig CONFIG = ConfigManager.register(MOD_ID, AnvilLibTestConfig::new); public AnvilLibTest(IEventBus bus, ModContainer container) { LibBlocks.init(); diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTestConfig.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTestConfig.java new file mode 100644 index 00000000..e5b8f72d --- /dev/null +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTestConfig.java @@ -0,0 +1,43 @@ +package dev.anvilcraft.lib.v2.test; + +import com.google.gson.annotations.SerializedName; +import dev.anvilcraft.lib.v2.config.CollapsibleObject; +import dev.anvilcraft.lib.v2.config.Comment; +import dev.anvilcraft.lib.v2.config.Config; +import dev.anvilcraft.lib.v2.config.util.TranslatableEnum; + +@Config(name = AnvilLibTest.MOD_ID, group = "anvillib") +public class AnvilLibTestConfig { + public String testString = "test"; + public int testInt = 42; + public boolean testBoolean = true; + public double testDouble = 3.14; + public float testFloat = 3.14f; + public long testLong = 42L; + public short testShort = 42; + public byte testByte = 42; + @CollapsibleObject + public TestObject testObject = new TestObject(); + public TestEnum testEnum = TestEnum.TEST; + @SerializedName("testString2testString2") + @Comment("testString2testString2testString2") + public String testString2 = "test2"; + + public static class TestObject { + public String testString = "test"; + public int testInt = 42; + public boolean testBoolean = true; + public double testDouble = 3.14; + public float testFloat = 3.14f; + public long testLong = 42L; + public short testShort = 42; + public byte testByte = 42; + } + + public enum TestEnum implements TranslatableEnum { + @SerializedName("test") + TEST, + TEST2, + TEST3 + } +} diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/command/TestCommand.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/command/TestCommand.java new file mode 100644 index 00000000..705ac18a --- /dev/null +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/command/TestCommand.java @@ -0,0 +1,51 @@ +package dev.anvilcraft.lib.v2.test.command; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.context.CommandContext; +import dev.anvilcraft.lib.v2.explosion.ExplosionExecutor; +import dev.anvilcraft.lib.v2.test.AnvilLibTest; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.core.BlockPos; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.RegisterCommandsEvent; + +@EventBusSubscriber(modid = AnvilLibTest.MOD_ID) +public class TestCommand { + @SubscribeEvent + public static void register(RegisterCommandsEvent event) { + event.getDispatcher().register( + Commands.literal("anvillib").then( + Commands.literal("test").then( + Commands.literal("explosion") + .then( + Commands.argument("radius", IntegerArgumentType.integer(1, 8192)).then( + Commands.argument("breaksPerTick", IntegerArgumentType.integer(1, 8192)).then( + Commands.argument("probabilityRadius", IntegerArgumentType.integer(1, 8192)).then( + Commands.argument("meltingRadius", IntegerArgumentType.integer(1, 8192)).executes(TestCommand::explosionAdvanced) + ) + ) + ) + ) + ) + ) + ); + } + + public static int explosionAdvanced(CommandContext context) { + int radius = IntegerArgumentType.getInteger(context, "radius"); + int breaksPerTick = IntegerArgumentType.getInteger(context, "breaksPerTick"); + int probabilityRadius = IntegerArgumentType.getInteger(context, "probabilityRadius"); + int meltingRadius = IntegerArgumentType.getInteger(context, "meltingRadius"); + + ExplosionExecutor.create() + .radius(radius) + .maxBreakPreTick(breaksPerTick) + .probabilityRadius(probabilityRadius) + .meltingRadius(meltingRadius) + .executor(context.getSource().getEntity()) + .execute(context.getSource().getLevel(), BlockPos.containing(context.getSource().getPosition())); + return 1; + } +} diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/command/package-info.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/command/package-info.java new file mode 100644 index 00000000..766f71f8 --- /dev/null +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/command/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.test.command; + +import org.jspecify.annotations.NullMarked; diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcClientTest.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcClientTest.java new file mode 100644 index 00000000..2a3216cf --- /dev/null +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcClientTest.java @@ -0,0 +1,72 @@ +package dev.anvilcraft.lib.v2.test.rpc; + +import dev.anvilcraft.lib.v2.rpc.RPC; +import dev.anvilcraft.lib.v2.rpc.RpcTarget; +import net.minecraft.client.player.LocalPlayer; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RPC 客户端测试。 + * + *

测试客户端到服务端的远程调用,以及客户端接收来自服务端的调用。

+ */ +@EventBusSubscriber(modid = "anvillib_test", value = Dist.CLIENT) +public class RpcClientTest { + private static final Logger LOGGER = LoggerFactory.getLogger(RpcClientTest.class); + private static final boolean ENABLE_AUTO_TEST = Boolean.getBoolean("anvillib.test.rpc.client.auto"); + + @SubscribeEvent + public static void onClientPlayerLogin(ClientPlayerNetworkEvent.LoggingIn event) { + if (!ENABLE_AUTO_TEST) return; + + LocalPlayer player = event.getPlayer(); + + LOGGER.info("Starting client-side RPC test for player: {}", player.getName().getString()); + + // 延迟 20 tick(约 1 秒)执行,确保连接完全建立。不能用 Thread.sleep 占住客户端主线程—— + // 那会冻结画面,且会阻塞兑现 RPC future 的同一个线程。改用 tick 调度器非阻塞延迟。 + RpcTestScheduler.onClient(20, RpcClientTest::testClientToServer); + } + + private static void testClientToServer() { + LOGGER.info("Testing client -> server RPC calls..."); + RpcTarget serverTarget = RpcTarget.server(); + + // 测试无参调用 + RPC.call(serverTarget, TestRpcMethods::noArgs); + LOGGER.info("Sent noArgs call to server"); + + // 测试带参数调用 + RPC.call(serverTarget, TestRpcMethods::withString, "Hello from client"); + LOGGER.info("Sent withString call to server"); + + // 测试有返回值的调用 - 不能在主线程 future.get(),改用回调 + RPC.invoke(serverTarget, TestRpcMethods::returnString) + .thenAccept(result -> LOGGER.info("Received result from server: {}", result)) + .exceptionally(ex -> { + LOGGER.error("client -> server returnString failed", ex); + return null; + }); + + // 测试计算调用 + RPC.invoke(serverTarget, TestRpcMethods::computeSum, 15, 27) + .thenAccept(sum -> LOGGER.info("Server computed sum: {}", sum)) + .exceptionally(ex -> { + LOGGER.error("client -> server computeSum failed", ex); + return null; + }); + + LOGGER.info("Client -> Server RPC test dispatched (results logged asynchronously)"); + } + + @SubscribeEvent + public static void onClientPlayerLogout(ClientPlayerNetworkEvent.LoggingOut event) { + if (!ENABLE_AUTO_TEST) return; + LOGGER.info("Client RPC test cleanup on logout"); + } +} diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcIntegrationTest.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcIntegrationTest.java new file mode 100644 index 00000000..1e6578ca --- /dev/null +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcIntegrationTest.java @@ -0,0 +1,246 @@ +package dev.anvilcraft.lib.v2.test.rpc; + +import dev.anvilcraft.lib.v2.rpc.RPC; +import dev.anvilcraft.lib.v2.rpc.RpcTarget; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.function.Function; + +/** + * RPC 模块集成测试。 + * + *

测试在玩家登录时自动执行,覆盖 RPC 的各项功能:

+ *
    + *
  • 基础类型参数的序列化与调用
  • + *
  • 多参数方法调用
  • + *
  • 自定义编解码器
  • + *
  • 有返回值的远程调用(invoke)
  • + *
  • 校验器功能
  • + *
  • 异常处理
  • + *
  • 超时机制
  • + *
+ */ +@EventBusSubscriber(modid = "anvillib_test") +public class RpcIntegrationTest { + private static final Logger LOGGER = LoggerFactory.getLogger(RpcIntegrationTest.class); + private static final boolean ENABLE_AUTO_TEST = Boolean.getBoolean("anvillib.test.rpc.auto"); + + @SubscribeEvent + public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { + if (!ENABLE_AUTO_TEST) return; + if (!(event.getEntity() instanceof ServerPlayer player)) return; + + LOGGER.info("Starting RPC integration test for player: {}", player.getName().getString()); + TestRpcMethods.clearLog(); + + try { + testBasicCalls(player); + testMultipleParams(player); + testCustomCodecs(player); + testReturnValues(player); + testValidators(player); + testExceptions(player); + testEdgeCases(player); + + LOGGER.info("RPC integration test completed successfully. Total calls: {}", + TestRpcMethods.getInvocationCounter().get()); + LOGGER.info("Call log: {}", TestRpcMethods.getCallLog()); + } catch (Exception e) { + LOGGER.error("RPC integration test failed", e); + } + } + + private static void testBasicCalls(ServerPlayer player) { + LOGGER.info("Testing basic RPC calls..."); + RpcTarget target = RpcTarget.player(player); + + RPC.call(target, TestRpcMethods::noArgs); + RPC.call(target, TestRpcMethods::withInt, 42); + RPC.call(target, TestRpcMethods::withLong, 9999999999L); + RPC.call(target, TestRpcMethods::withFloat, 3.14f); + RPC.call(target, TestRpcMethods::withDouble, 2.718281828); + RPC.call(target, TestRpcMethods::withBoolean, true); + RPC.call(target, TestRpcMethods::withByte, (byte) 127); + RPC.call(target, TestRpcMethods::withShort, (short) 32000); + RPC.call(target, TestRpcMethods::withString, "Hello RPC"); + RPC.call(target, TestRpcMethods::withBoxedInt, 100); + RPC.call(target, TestRpcMethods::withBoxedBoolean, false); + RPC.call(target, TestRpcMethods::withByteArray, new byte[]{1, 2, 3, 4, 5}); + RPC.call(target, TestRpcMethods::withLongArray, new long[]{100L, 200L, 300L}); + + LOGGER.info("Basic calls test passed"); + } + + private static void testMultipleParams(ServerPlayer player) { + LOGGER.info("Testing multiple parameter methods..."); + RpcTarget target = RpcTarget.player(player); + + RPC.call(target, TestRpcMethods::twoParams, "test", 5); + RPC.call(target, TestRpcMethods::threeParams, 1, "two", true); + RPC.call(target, TestRpcMethods::fourParams, 1, 2, 3, 4); + RPC.call(target, TestRpcMethods::fiveParams, 1, 2, 3, 4, 5); + RPC.call(target, TestRpcMethods::manyParams, 1, 2, 3, 4, 5, 6, 7, 8); + + LOGGER.info("Multiple params test passed"); + } + + private static void testCustomCodecs(ServerPlayer player) { + LOGGER.info("Testing custom codecs..."); + RpcTarget target = RpcTarget.player(player); + + CompoundTag tag = new CompoundTag(); + tag.putString("key1", "value1"); + tag.putInt("key2", 42); + RPC.call(target, TestRpcMethods::withCompoundTag, tag); + + BlockPos pos = new BlockPos(100, 64, -200); + RPC.call(target, TestRpcMethods::withCustomCodec, pos); + RPC.call(target, TestRpcMethods::withMixedCodecs, "location", pos, 999); + + LOGGER.info("Custom codecs test passed"); + } + + private static void testReturnValues(ServerPlayer player) { + LOGGER.info("Testing methods with return values..."); + RpcTarget target = RpcTarget.player(player); + + // 不能在主线程上 future.get():RPC 响应也在主线程兑现,阻塞会导致必然超时。改用回调校验。 + RPC.invoke(target, TestRpcMethods::returnInt) + .thenAccept(result -> check("returnInt", result == 42, "Expected 42, got " + result)) + .exceptionally(logFailure("returnInt")); + + RPC.invoke(target, TestRpcMethods::returnString) + .thenAccept(result -> check("returnString", "test result".equals(result), "Expected 'test result', got " + result)) + .exceptionally(logFailure("returnString")); + + RPC.invoke(target, TestRpcMethods::returnBoolean) + .thenAccept(result -> check("returnBoolean", result, "Expected true, got false")) + .exceptionally(logFailure("returnBoolean")); + + RPC.invoke(target, TestRpcMethods::computeSum, 10, 32) + .thenAccept(result -> check("computeSum", result == 42, "Expected 42, got " + result)) + .exceptionally(logFailure("computeSum")); + + RPC.invoke(target, TestRpcMethods::concatenate, "Hello", "World") + .thenAccept(result -> check("concatenate", "HelloWorld".equals(result), "Expected 'HelloWorld', got " + result)) + .exceptionally(logFailure("concatenate")); + + RPC.invoke(target, TestRpcMethods::returnByteArray, 10) + .thenAccept(result -> check("returnByteArray", result.length == 10, "Expected array of length 10, got " + result.length)) + .exceptionally(logFailure("returnByteArray")); + + RPC.invoke(target, TestRpcMethods::returnCustomType, 1, 2, 3) + .thenAccept(result -> check("returnCustomType", result.equals(new BlockPos(1, 2, 3)), "Expected BlockPos(1,2,3), got " + result)) + .exceptionally(logFailure("returnCustomType")); + + LOGGER.info("Return values test dispatched (results verified asynchronously)"); + } + + private static void testValidators(ServerPlayer player) { + LOGGER.info("Testing validators..."); + RpcTarget target = RpcTarget.player(player); + + // 始终拒绝的校验器 - 单向调用会静默丢弃 + RPC.call(target, TestRpcMethods::alwaysRejected); + + // 条件校验器 - 正值应通过 + RPC.call(target, TestRpcMethods::conditionalAccept, 100); + + // 条件校验器 - 负值应被拒绝 + RPC.call(target, TestRpcMethods::conditionalAccept, -1); + + // 有返回值的调用 - 正值应通过 + RPC.invoke(target, TestRpcMethods::conditionalReturn, 50) + .thenAccept(result -> check("conditionalReturn(50)", result == 100, "Expected 100, got " + result)) + .exceptionally(logFailure("conditionalReturn(50)")); + + // 有返回值的调用 - 负值应被校验器拒绝,future 以异常失败 + RPC.invoke(target, TestRpcMethods::conditionalReturn, -10) + .handle((result, ex) -> { + if (ex == null) { + LOGGER.error("Validator test FAILED: conditionalReturn(-10) should have been rejected, but returned {}", result); + } else { + LOGGER.info("Validator correctly rejected negative value"); + } + return null; + }); + + LOGGER.info("Validators test dispatched (results verified asynchronously)"); + } + + private static void testExceptions(ServerPlayer player) { + LOGGER.info("Testing exception handling..."); + RpcTarget target = RpcTarget.player(player); + + // 单向调用抛出异常 - 应在接收端记录但不影响发送端 + RPC.call(target, TestRpcMethods::throwsException); + + // 有返回值的调用抛出异常 - future 应完成为异常 + RPC.invoke(target, TestRpcMethods::throwsExceptionWithReturn) + .handle((result, ex) -> { + if (ex == null) { + LOGGER.error("Exception test FAILED: expected exception, but call returned {}", result); + } else { + LOGGER.info("Exception correctly propagated from remote call"); + } + return null; + }); + + LOGGER.info("Exception handling test dispatched (results verified asynchronously)"); + } + + private static void testEdgeCases(ServerPlayer player) { + LOGGER.info("Testing edge cases..."); + RpcTarget target = RpcTarget.player(player); + + // 极值测试 + RPC.call(target, TestRpcMethods::extremeValues, Integer.MAX_VALUE, Long.MAX_VALUE, Double.MAX_VALUE); + RPC.call(target, TestRpcMethods::extremeValues, Integer.MIN_VALUE, Long.MIN_VALUE, Double.MIN_VALUE); + RPC.call(target, TestRpcMethods::extremeValues, 0, 0L, 0.0); + + // 空字符串 + RPC.call(target, TestRpcMethods::emptyString, ""); + + // 特殊字符 + RPC.call(target, TestRpcMethods::withString, "Unicode: 中文 测试"); + RPC.call(target, TestRpcMethods::withString, "Symbols: !@#$%^&*()_+-={}[]|\\:\";<>?,./"); + + // 空数组 + RPC.call(target, TestRpcMethods::withByteArray, new byte[0]); + RPC.call(target, TestRpcMethods::withLongArray, new long[0]); + + // 大数组 + RPC.call(target, TestRpcMethods::withByteArray, new byte[1000]); + + LOGGER.info("Edge cases test passed"); + } + + /** + * 异步校验断言:条件不满足时记录错误日志(而非用默认关闭的 {@code assert},否则会静默跳过)。 + */ + private static void check(String name, boolean condition, String message) { + if (condition) { + LOGGER.info("Assertion passed: {}", name); + } else { + LOGGER.error("Assertion FAILED [{}]: {}", name, message); + } + } + + /** + * future 异常处理器:记录失败并吞掉异常,避免污染后续回调链。 + */ + private static Function logFailure(String name) { + return ex -> { + LOGGER.error("RPC call FAILED [{}]: {}", name, ex.getMessage(), ex); + return null; + }; + } +} diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcTestCommands.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcTestCommands.java new file mode 100644 index 00000000..b6735183 --- /dev/null +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcTestCommands.java @@ -0,0 +1,285 @@ +package dev.anvilcraft.lib.v2.test.rpc; + +import dev.anvilcraft.lib.v2.rpc.RPC; +import dev.anvilcraft.lib.v2.rpc.RpcTarget; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; + +/** + * RPC 测试命令。 + * + *

提供手动触发 RPC 测试的命令:

+ *
    + *
  • /rpctest basic - 基础调用测试
  • + *
  • /rpctest invoke - 有返回值调用测试
  • + *
  • /rpctest validator - 校验器测试
  • + *
  • /rpctest stress - 压力测试
  • + *
  • /rpctest all - 运行所有测试
  • + *
+ */ +@EventBusSubscriber(modid = "anvillib_test") +public class RpcTestCommands { + private static final Logger LOGGER = LoggerFactory.getLogger(RpcTestCommands.class); + + @SubscribeEvent + public static void registerCommands(RegisterCommandsEvent event) { + event.getDispatcher().register( + Commands.literal("rpctest") + .then(Commands.literal("basic") + .executes(context -> { + runBasicTest(context.getSource()); + return 1; + }) + ) + .then(Commands.literal("invoke") + .executes(context -> { + runInvokeTest(context.getSource()); + return 1; + }) + ) + .then(Commands.literal("validator") + .executes(context -> { + runValidatorTest(context.getSource()); + return 1; + }) + ) + .then(Commands.literal("stress") + .executes(context -> { + runStressTest(context.getSource()); + return 1; + }) + ) + .then(Commands.literal("all") + .executes(context -> { + runAllTests(context.getSource()); + return 1; + }) + ) + .then(Commands.literal("log") + .executes(context -> { + showLog(context.getSource()); + return 1; + }) + ) + .then(Commands.literal("clear") + .executes(context -> { + TestRpcMethods.clearLog(); + context.getSource().sendSuccess(() -> Component.literal("RPC test log cleared"), true); + return 1; + }) + ) + ); + } + + private static void runBasicTest(CommandSourceStack source) { + if (!(source.getEntity() instanceof ServerPlayer player)) { + source.sendFailure(Component.literal("This command must be run by a player")); + return; + } + + source.sendSuccess(() -> Component.literal("Running basic RPC test..."), true); + TestRpcMethods.clearLog(); + RpcTarget target = RpcTarget.player(player); + + try { + RPC.call(target, TestRpcMethods::noArgs); + RPC.call(target, TestRpcMethods::withInt, 42); + RPC.call(target, TestRpcMethods::withString, "test"); + RPC.call(target, TestRpcMethods::twoParams, "hello", 123); + + CompoundTag tag = new CompoundTag(); + tag.putString("test", "value"); + RPC.call(target, TestRpcMethods::withCompoundTag, tag); + + BlockPos pos = new BlockPos(100, 64, -200); + RPC.call(target, TestRpcMethods::withCustomCodec, pos); + + source.sendSuccess(() -> Component.literal("Basic test completed. Sent 6 calls."), true); + LOGGER.info("Basic test completed. Invocations: {}", TestRpcMethods.getInvocationCounter().get()); + } catch (Exception e) { + source.sendFailure(Component.literal("Basic test failed: " + e.getMessage())); + LOGGER.error("Basic test failed", e); + } + } + + private static void runInvokeTest(CommandSourceStack source) { + if (!(source.getEntity() instanceof ServerPlayer player)) { + source.sendFailure(Component.literal("This command must be run by a player")); + return; + } + + source.sendSuccess(() -> Component.literal("Running invoke RPC test..."), true); + TestRpcMethods.clearLog(); + RpcTarget target = RpcTarget.player(player); + + try { + CompletableFuture futureInt = RPC.invoke(target, TestRpcMethods::returnInt); + futureInt.thenAccept(result -> source.sendSuccess(() -> Component.literal("Received int: " + result), false)); + + CompletableFuture futureString = RPC.invoke(target, TestRpcMethods::returnString); + futureString.thenAccept(result -> source.sendSuccess(() -> Component.literal("Received string: " + result), false)); + + CompletableFuture futureSum = RPC.invoke(target, TestRpcMethods::computeSum, 10, 32); + futureSum.thenAccept(result -> source.sendSuccess(() -> Component.literal("Computed sum: " + result), false)); + + CompletableFuture futurePos = RPC.invoke(target, TestRpcMethods::returnCustomType, 5, 10, 15); + futurePos.thenAccept(result -> source.sendSuccess(() -> Component.literal("Received BlockPos: " + result), false)); + + source.sendSuccess(() -> Component.literal("Invoke test started. Check results above."), true); + } catch (Exception e) { + source.sendFailure(Component.literal("Invoke test failed: " + e.getMessage())); + LOGGER.error("Invoke test failed", e); + } + } + + private static void runValidatorTest(CommandSourceStack source) { + if (!(source.getEntity() instanceof ServerPlayer player)) { + source.sendFailure(Component.literal("This command must be run by a player")); + return; + } + + source.sendSuccess(() -> Component.literal("Running validator RPC test..."), true); + TestRpcMethods.clearLog(); + RpcTarget target = RpcTarget.player(player); + + try { + // 这个调用会被拒绝 + RPC.call(target, TestRpcMethods::alwaysRejected); + source.sendSuccess(() -> Component.literal("Sent alwaysRejected (should be rejected)"), false); + + // 正值应该通过 + RPC.call(target, TestRpcMethods::conditionalAccept, 100); + source.sendSuccess(() -> Component.literal("Sent conditionalAccept(100) (should pass)"), false); + + // 负值应该被拒绝 + RPC.call(target, TestRpcMethods::conditionalAccept, -10); + source.sendSuccess(() -> Component.literal("Sent conditionalAccept(-10) (should be rejected)"), false); + + // 有返回值的正值调用 + CompletableFuture futureAccept = RPC.invoke(target, TestRpcMethods::conditionalReturn, 50); + futureAccept + .thenAccept(result -> source.sendSuccess(() -> Component.literal("conditionalReturn(50) = " + result), false)) + .exceptionally(ex -> { + source.sendFailure(Component.literal("conditionalReturn(50) failed: " + ex.getMessage())); + return null; + }); + + // 有返回值的负值调用 + CompletableFuture futureReject = RPC.invoke(target, TestRpcMethods::conditionalReturn, -5); + futureReject.thenAccept(result -> + source.sendFailure(Component.literal("conditionalReturn(-5) should have been rejected!")) + ).exceptionally(ex -> { + source.sendSuccess(() -> Component.literal("conditionalReturn(-5) correctly rejected"), false); + return null; + }); + + source.sendSuccess(() -> Component.literal("Validator test completed."), true); + } catch (Exception e) { + source.sendFailure(Component.literal("Validator test failed: " + e.getMessage())); + LOGGER.error("Validator test failed", e); + } + } + + private static void runStressTest(CommandSourceStack source) { + if (!(source.getEntity() instanceof ServerPlayer player)) { + source.sendFailure(Component.literal("This command must be run by a player")); + return; + } + + source.sendSuccess(() -> Component.literal("Running stress RPC test..."), true); + TestRpcMethods.clearLog(); + RpcTarget target = RpcTarget.player(player); + + try { + long startTime = System.currentTimeMillis(); + int callCount = 100; + + // 快速发送大量调用 + for (int i = 0; i < callCount; i++) { + RPC.call(target, TestRpcMethods::withInt, i); + } + + // 发送大量有返回值的调用 + for (int i = 0; i < 20; i++) { + RPC.invoke(target, TestRpcMethods::computeSum, i, i + 1); + } + + // 发送大数据 + byte[] largeArray = new byte[5000]; + for (int i = 0; i < 10; i++) { + RPC.call(target, TestRpcMethods::withByteArray, largeArray); + } + + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + + source.sendSuccess(() -> Component.literal( + String.format("Stress test completed: %d calls in %d ms", callCount + 30, duration) + ), true); + LOGGER.info("Stress test: {} calls in {} ms", callCount + 30, duration); + } catch (Exception e) { + source.sendFailure(Component.literal("Stress test failed: " + e.getMessage())); + LOGGER.error("Stress test failed", e); + } + } + + private static void runAllTests(CommandSourceStack source) { + if (!(source.getEntity() instanceof ServerPlayer player)) { + source.sendFailure(Component.literal("This command must be run by a player")); + return; + } + + source.sendSuccess(() -> Component.literal("Running all RPC tests..."), true); + + scheduleTest(0, () -> runBasicTest(source)); + scheduleTest(20, () -> runInvokeTest(source)); + scheduleTest(40, () -> runValidatorTest(source)); + scheduleTest(60, () -> runStressTest(source)); + scheduleTest(80, () -> + source.sendSuccess(() -> Component.literal("All tests completed!"), true) + ); + } + + private static void scheduleTest(int delayTicks, Runnable test) { + // 非阻塞延迟:挂到服务端 tick 上逐 tick 递减,到点后在主线程执行。 + // 不能用 Thread.sleep 占住主线程——那会冻结服务端,且会阻塞兑现 RPC future 的同一个线程。 + RpcTestScheduler.onServer(delayTicks, test); + } + + private static void showLog(CommandSourceStack source) { + source.sendSuccess(() -> Component.literal("=== RPC Call Log ==="), false); + source.sendSuccess(() -> Component.literal( + "Total invocations: " + TestRpcMethods.getInvocationCounter().get() + ), false); + + var log = TestRpcMethods.getCallLog(); + if (log.isEmpty()) { + source.sendSuccess(() -> Component.literal("(empty)"), false); + } else { + int count = 0; + for (String entry : log) { + if (count >= 20) { + source.sendSuccess(() -> Component.literal( + "... and " + (log.size() - 20) + " more entries" + ), false); + break; + } + final String finalEntry = entry; + source.sendSuccess(() -> Component.literal(" " + finalEntry), false); + count++; + } + } + source.sendSuccess(() -> Component.literal("==================="), false); + } +} diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcTestScheduler.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcTestScheduler.java new file mode 100644 index 00000000..4cab7f8d --- /dev/null +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcTestScheduler.java @@ -0,0 +1,77 @@ +package dev.anvilcraft.lib.v2.test.rpc; + +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +/** + * RPC 测试用的非阻塞延迟调度器。 + * + *

RPC 的响应在主线程通过 {@code ctx.enqueueWork(...)} 兑现,超时也由主线程的 tick 事件驱动。 + * 因此测试代码绝不能在主线程上 {@code Thread.sleep} 或 {@code future.get()} 阻塞——那会冻结游戏, + * 并且会阻塞兑现 future 的同一个线程,导致 invoke 调用必然超时。本调度器将延迟任务挂到对应侧的 + * tick 事件上逐 tick 递减,到点后在主线程执行,全程不阻塞。

+ */ +public final class RpcTestScheduler { + private RpcTestScheduler() { + } + + private record DelayedTask(int delayTicks, Runnable action) { + DelayedTask withRemaining(int remaining) { + return new DelayedTask(remaining, this.action); + } + } + + private static final Deque SERVER_TASKS = new ArrayDeque<>(); + private static final Deque CLIENT_TASKS = new ArrayDeque<>(); + + /** + * 在服务端主线程延迟 {@code delayTicks} 个 tick 后执行 {@code action}。{@code delayTicks <= 0} 时下一 tick 执行。 + */ + public static void onServer(int delayTicks, Runnable action) { + SERVER_TASKS.add(new DelayedTask(Math.max(0, delayTicks), action)); + } + + /** + * 在客户端主线程延迟 {@code delayTicks} 个 tick 后执行 {@code action}。{@code delayTicks <= 0} 时下一 tick 执行。 + */ + public static void onClient(int delayTicks, Runnable action) { + CLIENT_TASKS.add(new DelayedTask(Math.max(0, delayTicks), action)); + } + + private static void pump(Deque tasks) { + // 先快照本 tick 要处理的项数,避免任务自身再调度 onServer/onClient 时本 tick 重复处理 + int count = tasks.size(); + for (int i = 0; i < count; i++) { + DelayedTask task = tasks.poll(); + if (task == null) break; + if (task.delayTicks() <= 0) { + task.action().run(); + } else { + tasks.add(task.withRemaining(task.delayTicks() - 1)); + } + } + } + + @EventBusSubscriber(modid = "anvillib_test") + public static final class Server { + @SubscribeEvent + public static void onServerTick(ServerTickEvent.Post event) { + pump(SERVER_TASKS); + } + } + + @EventBusSubscriber(modid = "anvillib_test", value = Dist.CLIENT) + public static final class Client { + @SubscribeEvent + public static void onClientTick(ClientTickEvent.Post event) { + pump(CLIENT_TASKS); + } + } +} diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/TestRpcMethods.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/TestRpcMethods.java new file mode 100644 index 00000000..69d0d33f --- /dev/null +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/TestRpcMethods.java @@ -0,0 +1,317 @@ +package dev.anvilcraft.lib.v2.test.rpc; + +import dev.anvilcraft.lib.v2.rpc.CallableParam; +import dev.anvilcraft.lib.v2.rpc.IRemoteCallableValidator; +import dev.anvilcraft.lib.v2.rpc.RemoteCallable; +import lombok.Getter; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 用于测试 RPC 功能的远程可调用方法集合。 + */ +public final class TestRpcMethods { + @Getter + private static final List callLog = new CopyOnWriteArrayList<>(); + @Getter + private static final AtomicInteger invocationCounter = new AtomicInteger(0); + + private TestRpcMethods() { + } + + public static void clearLog() { + callLog.clear(); + invocationCounter.set(0); + } + + // ============ 无参方法 ============ + + @RemoteCallable + public static void noArgs() { + callLog.add("noArgs"); + invocationCounter.incrementAndGet(); + } + + // ============ 单参方法 - 基础类型 ============ + + @RemoteCallable + public static void withInt(int value) { + callLog.add("withInt:" + value); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void withLong(long value) { + callLog.add("withLong:" + value); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void withFloat(float value) { + callLog.add("withFloat:" + value); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void withDouble(double value) { + callLog.add("withDouble:" + value); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void withBoolean(boolean value) { + callLog.add("withBoolean:" + value); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void withByte(byte value) { + callLog.add("withByte:" + value); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void withShort(short value) { + callLog.add("withShort:" + value); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void withString(String value) { + callLog.add("withString:" + value); + invocationCounter.incrementAndGet(); + } + + // ============ 包装类型 ============ + + @RemoteCallable + public static void withBoxedInt(Integer value) { + callLog.add("withBoxedInt:" + value); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void withBoxedBoolean(Boolean value) { + callLog.add("withBoxedBoolean:" + value); + invocationCounter.incrementAndGet(); + } + + // ============ 数组类型 ============ + + @RemoteCallable + public static void withByteArray(byte[] value) { + callLog.add("withByteArray:" + value.length); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void withLongArray(long[] value) { + callLog.add("withLongArray:" + value.length); + invocationCounter.incrementAndGet(); + } + + // ============ 多参方法 ============ + + @RemoteCallable + public static void twoParams(String name, int count) { + callLog.add("twoParams:" + name + "," + count); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void threeParams(int a, String b, boolean c) { + callLog.add("threeParams:" + a + "," + b + "," + c); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void fourParams(int a, int b, int c, int d) { + callLog.add("fourParams:" + a + "," + b + "," + c + "," + d); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void fiveParams(int a, int b, int c, int d, int e) { + callLog.add("fiveParams:" + a + "," + b + "," + c + "," + d + "," + e); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void manyParams(int a, int b, int c, int d, int e, int f, int g, int h) { + callLog.add("manyParams:" + a + "," + b + "," + c + "," + d + "," + e + "," + f + "," + g + "," + h); + invocationCounter.incrementAndGet(); + } + + // ============ 自定义编解码器 ============ + + @RemoteCallable + public static void withCompoundTag(CompoundTag tag) { + callLog.add("withCompoundTag:" + tag.size()); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void withCustomCodec( + @CallableParam(clazz = TestCodecs.class, field = "BLOCK_POS") BlockPos pos + ) { + callLog.add("withCustomCodec:" + pos.getX() + "," + pos.getY() + "," + pos.getZ()); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void withMixedCodecs( + String name, + @CallableParam(clazz = TestCodecs.class, field = "BLOCK_POS") BlockPos pos, + int value + ) { + callLog.add("withMixedCodecs:" + name + "," + pos + "," + value); + invocationCounter.incrementAndGet(); + } + + // ============ 有返回值的方法 ============ + + @RemoteCallable + public static int returnInt() { + callLog.add("returnInt"); + invocationCounter.incrementAndGet(); + return 42; + } + + @RemoteCallable + public static String returnString() { + callLog.add("returnString"); + invocationCounter.incrementAndGet(); + return "test result"; + } + + @RemoteCallable + public static boolean returnBoolean() { + callLog.add("returnBoolean"); + invocationCounter.incrementAndGet(); + return true; + } + + @RemoteCallable + public static int computeSum(int a, int b) { + callLog.add("computeSum:" + a + "," + b); + invocationCounter.incrementAndGet(); + return a + b; + } + + @RemoteCallable + public static String concatenate(String a, String b) { + callLog.add("concatenate:" + a + "," + b); + invocationCounter.incrementAndGet(); + return a + b; + } + + @RemoteCallable + public static byte[] returnByteArray(int size) { + callLog.add("returnByteArray:" + size); + invocationCounter.incrementAndGet(); + return new byte[size]; + } + + @RemoteCallable + @CallableParam(clazz = TestCodecs.class, field = "BLOCK_POS") + public static BlockPos returnCustomType(int x, int y, int z) { + callLog.add("returnCustomType:" + x + "," + y + "," + z); + invocationCounter.incrementAndGet(); + return new BlockPos(x, y, z); + } + + // ============ 异常情况 ============ + + @RemoteCallable + public static void throwsException() { + callLog.add("throwsException"); + invocationCounter.incrementAndGet(); + throw new RuntimeException("Intentional test exception"); + } + + @RemoteCallable + public static int throwsExceptionWithReturn() { + callLog.add("throwsExceptionWithReturn"); + invocationCounter.incrementAndGet(); + throw new IllegalArgumentException("Cannot compute result"); + } + + // ============ 带校验器的方法 ============ + + @RemoteCallable(validator = AlwaysRejectValidator.class) + public static void alwaysRejected() { + callLog.add("alwaysRejected"); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable(validator = ConditionalValidator.class) + public static void conditionalAccept(int value) { + callLog.add("conditionalAccept:" + value); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable(validator = ConditionalValidator.class) + public static int conditionalReturn(int value) { + callLog.add("conditionalReturn:" + value); + invocationCounter.incrementAndGet(); + return value * 2; + } + + // ============ 边界值测试 ============ + + @RemoteCallable + public static void extremeValues(int maxInt, long maxLong, double maxDouble) { + callLog.add("extremeValues:" + maxInt + "," + maxLong + "," + maxDouble); + invocationCounter.incrementAndGet(); + } + + @RemoteCallable + public static void emptyString(String empty) { + callLog.add("emptyString:'" + empty + "'"); + invocationCounter.incrementAndGet(); + } + + // ============ 校验器实现 ============ + + public static class AlwaysRejectValidator implements IRemoteCallableValidator { + @Override + public boolean validate(IPayloadContext ctx, Method method, Object[] args) { + return false; + } + } + + public static class ConditionalValidator implements IRemoteCallableValidator { + @Override + public boolean validate(IPayloadContext ctx, Method method, Object[] args) { + if (args.length == 0) return true; + if (args[0] instanceof Integer value) { + return value >= 0; + } + return true; + } + } + + // ============ 自定义编解码器 ============ + + public static class TestCodecs { + public static final StreamCodec BLOCK_POS = + StreamCodec.composite( + ByteBufCodecs.VAR_INT, + BlockPos::getX, + ByteBufCodecs.VAR_INT, + BlockPos::getY, + ByteBufCodecs.VAR_INT, + BlockPos::getZ, + BlockPos::new + ); + } +} diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/package-info.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/package-info.java new file mode 100644 index 00000000..cd49ce65 --- /dev/null +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.test.rpc; + +import org.jspecify.annotations.NullMarked; From 22662625095ea069ade87dc5513aba6cb3ae2466 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 11 Aug 2026 01:33:54 +0800 Subject: [PATCH 22/38] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E5=9B=9E?= =?UTF-8?q?=E7=A7=BB=E6=A4=8D=E6=89=A7=E8=A1=8C=E8=B7=9F=E8=B8=AA=E6=96=87?= =?UTF-8?q?=E6=A1=A3=EF=BC=88#P1-#P19=20=E7=8A=B6=E6=80=81=E4=B8=8E?= =?UTF-8?q?=E5=BE=85=E9=AA=8C=E8=AF=81=E9=94=80=E8=B4=A6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 记录 docs/migration-plan-port-1.21.1.md 的 #P1-#P19 全部执行状态: - 19 项全部落地(含 #P7a 渲染纯逻辑批;#P7b-#P7e 后处理/SDF GUI/CachedBER/ Compute 未执行,见遗留登记 L-10/L-11) - 附录 B 待验证事项销账:V-1/V-3/V-5/V-7/V-11/V-12/V-13/V-14/V-15/V-17/V-18 已实测通过;V-2/V-6/V-8(#P7d/#P7e 未执行)、V-4/V-9/V-10/V-16(需运行验证)未验证 - 遗留登记 L-1..L-11:roseauCheck 运行(daemon 崩溃)、运行类行为验收、 #P7b-#P7e 渲染执行层批次 --- docs/migration-todo-port-1.21.1.md | 226 +++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 docs/migration-todo-port-1.21.1.md diff --git a/docs/migration-todo-port-1.21.1.md b/docs/migration-todo-port-1.21.1.md new file mode 100644 index 00000000..d23e96a2 --- /dev/null +++ b/docs/migration-todo-port-1.21.1.md @@ -0,0 +1,226 @@ +# AnvilLib 回移植执行跟踪:`dev/26.1` → `port/1.21.1` + +> 本文件是 `docs/migration-plan-port-1.21.1.md`(#P1–#P19)的执行跟踪清单。每完成一项,将对应状态更新为「✅ +> 已完成」并记录提交号;未完成项保持「⬜ 未开始 / 🔄 进行中」。待验证事项(附录 B 的 V-1…V-18)在验证后同步销账。 + +- 基线:`port/1.21.1`(执行起点 `96c3a34f`,计划基线 `bd25229`) +- 来源:`dev/26.1` @ `0b7e45f` +- 工具链:Java 21 / NeoForge 21.1.226 / Minecraft 1.21.1 + +--- + +## 执行状态总览 + +| #P | 内容 | 难度 | 状态 | 提交 | +|------|--------------------------------------------------------------|-------|-----------|------------| +| #P1 | module.rpc 整模块(25 文件) | 低 | ✅ 已完成 | `26b11e18` | +| #P2 | module.sync + processor(CoreMod 重写) | 中-高 | ✅ 已完成 | `c7da98c1` | +| #P3 | module.explosion 整模块(10 文件) | 低 | ✅ 已完成 | `e896a1ea` | +| #P4 | module.collision 整模块(3 文件) | 极低 | ✅ 已完成 | `c25dd465` | +| #P5 | module.space-select 整模块(18 文件) | 中 | ✅ 已完成 | `1706feaa` | +| #P6 | module.font 整模块(25 文件) | 中 | ✅ 已完成 | `91533730` | +| #P7 | module.rendering(#P7a–#P7e 分批) | 高 | ✅ 已完成(#P7a) | `b0e179e8` | +| #P8 | renderdoc-loader(2 文件) | 低 | ✅ 已完成 | `c62a262e` | +| #P9 | module.config:group + TranslatableEnum + TOML 点分隔 | 低 | ✅ 已完成 | `65aa5e11` | +| #P10 | module.integration:数据加载拆分 + meter 递增 | 低 | ✅ 已完成 | `adc23468` | +| #P11 | module.network:泛型修复 + Included + public | 低 | ✅ 已完成 | `20934ac4` | +| #P12 | module.util:OutlineUtil + HolderGetter + withCount + equals | 中 | ✅ 已完成 | `164f441f` | +| #P13 | module.registrum:14 种新 builder/entry | 低 | ✅ 已完成 | `2d1a8e3d` | +| #P14 | module.registrum datagen 增强 | 中 | ✅ 已完成 | `dd8d79e5` | +| #P15 | module.multiblock:事件化 + 快照复用 + 键化 + 懒解析 | 中 | ✅ 已完成 | `2556a8e3` | +| #P16 | module.recipe:SpawnItem 守卫 + SetBlock nbt | 低 | ✅ 已完成 | `71bf9e54` | +| #P17 | module.wheel:环形扇区选择效果 | 中-高 | ✅ 已完成 | `142e3296` | +| #P18 | module.test:T2/T8/T9/T10 可选测试 | 低-中 | ✅ 已完成 | `a4ab3acd` | +| #P19 | 构建体系:roseauCheck | 中 | ✅ 已完成(运行待补验) | `544c9eb0` | + +## 执行顺序(计划第 6 章) + +``` +#P1/#P2 →(#P7a)→ #P6 → #P3/#P4/#P5/#P8 → #P9 → #P11/#P12/#P13/#P16 → #P15 → #P17 → #P14 → #P18 → #P19 +``` + +## 逐项日志 + +### #P1 module.rpc(低) + +- [ ] 提取 25 个 Java 文件 +- [ ] build.gradle / settings.gradle / mods.toml 落位 +- [ ] 3 处 API 反替换(Identifier / FMLLoader / accessFlags) +- [ ] `gradlew :module.rpc:compileJava` 通过 +- [ ] 提交(conventional commit) + +### #P2 module.sync + processor(中-高) + +- [ ] 主模块 25 文件提取 + 反替换 +- [ ] SyncConfigManager 等价实现(getSecureJar ().getPath) +- [ ] processor 6 文件 + CoreMod 重写(ICoreMod + ITransformer) +- [ ] `gradlew :module.sync:compileJava` 通过,`grep neoforgespi.transformation` 零残留 +- [ ] 提交 + +### #P7 module.rendering(高 · 分批) + +- [ ] #P7a 纯逻辑 19 项 + 模块骨架 +- [ ] #P7b 后处理三件套(Bloom/GaussianBlur/Glitch) +- [ ] #P7c SDF GUI +- [ ] #P7d CachedBER +- #P7e Compute +- [ ] #P7 收尾(shader json、mixins.json、AT、mods.toml) +- [ ] 提交(按批) + +### #P6 module.font(中) + +- [ ] 9 项直接移植 + 8 项反向适配 +- [ ] `gradlew :module.font:compileJava` + runData 通过 +- [ ] 提交 + +### #P3 module.explosion(低) + +- [ ] 10 文件提取 + 6 处反替换 +- [ ] build.gradle(依赖 port module.config,自建 run) +- [ ] `gradlew :module.explosion:compileJava` + runData 通过 +- [ ] 提交 + +### #P4 module.collision(极低) + +- [ ] 3 文件提取(零 MC/NF API) +- [ ] 提交 + +### #P5 module.space-select(中) + +- [ ] 18 文件提取 + 反替换 +- [ ] DistrictRenderer 渲染路径重写(LevelRenderer.renderVoxelShape) +- [ ] `gradlew :module.space-select:compileJava` 通过 +- [ ] 提交 + +### #P8 renderdoc-loader(低) + +- [ ] 2 文件提取 + 构建脚本改写(Java 21) +- [ ] 提交 + +### #P9 module.config(低) + +- [ ] Config/ConfigManager/ConfigRecord group 支持 +- [ ] TranslatableEnum + ConfigData 枚举分支 +- [ ] FormattingUtil.toPointSplitName + TOML key 点分隔 +- [ ] `gradlew :module.config:compileJava` + runData 通过 +- [ ] 提交 + +### #P10 module.integration(低 · 破坏性 API) + +- [ ] applyData → applyClientData/applyServerData 拆分 +- [ ] meter.increment () +- [ ] `gradlew :module.integration:compileJava` 通过 +- [ ] 提交(commit 说明注明破坏性 API) + +### #P11 module.network(低) + +- [ ] PacketData 泛型检查修复 +- [ ] NetworkUtil Included 方法 + Objects.equals +- [ ] PacketProtocol public +- [ ] `gradlew :module.network:compileJava` 通过 +- [ ] 提交 + +### #P12 module.util(中) + +- [ ] OutlineUtil + Line + ShapeUtilJoinTimingTest +- [ ] 3 个 predicate HolderGetter 重构 + module.recipe 4 个调用方 +- [ ] withCount + @EqualsAndHashCode +- [ ] 保留 port `with(BlockState)`(不可照搬 dev 删除) +- [ ] `gradlew :module.util:compileJava` + `:module.recipe:compileJava` + 测试通过 +- [ ] 提交 + +### #P13 module.registrum(低) + +- [ ] 直接移植 10 项 + 反向适配 9 项(RecipeSerializer 匿名类、VillagerProfession 6 元、Attachment Codec 等) +- [ ] 两个零碎修复(item 默认 lang、OneTimeEventReceiver 空值) +- [ ] 不迁 dataComponentPredicate 入口 +- [ ] `gradlew :module.registrum:compileJava` 通过 +- [ ] 提交 + +### #P14 module.registrum datagen(中) + +- [ ] RecipeProvider 方法 public 化(公开包装内调静态方法) +- [ ] dataMap (provider) 重载 +- [ ] `gradlew :module.registrum:compileJava` + runData 通过 +- [ ] 提交 + +### #P15 module.multiblock(中) + +- [ ] M1 DynamicMultiblockEvent(可取消事件) +- [ ] M2 未加载区块快照复用 +- [ ] M3 Long→BlockPos 键化(含 Java 21 lambda `_` 改名) +- [ ] M4 定义懒解析(ResourceKey + registryAccess) +- [ ] M7 Config.group(依赖 #P9) +- [ ] `gradlew :module.multiblock:compileJava` + `:module.recipe:compileJava` 通过 +- [ ] 提交 + +### #P16 module.recipe(低) + +- [ ] R1 SpawnItem 零数量守卫(可选 R2 必填校验) +- [ ] R3 SetBlock nbt 默认值(null → 空 CompoundTag) +- [ ] `gradlew :module.recipe:compileJava` 通过 +- [ ] 提交 + +### #P17 module.wheel(中-高) + +- [ ] WheelSelectionEffect + WheelMenuBuilder/Model 链式方法 +- [ ] WheelWidget.renderSelectionEffect + WheelScreen 适配 +- [ ] annular_sector.fsh + ShaderInstance 注册(切断 rendering 依赖) +- [ ] `gradlew :module.wheel:compileJava` 通过 +- [ ] 提交 + +### #P18 module.test(低-中) + +- [ ] T2 配置测试(随 #P9) +- [ ] T8 RPC 测试(随 #P1) +- [ ] T9 爆炸测试(随 #P3) +- [ ] T10 wheel 按键扩展(随 #P17,KeyMapping 1.21.1 API) +- [ ] `gradlew :module.test:compileJava` 通过 +- [ ] 提交 + +### #P19 构建体系(中) + +- [ ] roseau.yaml + gradle/scripts/roseau.gradle + 根 build.gradle 接入 +- [ ] `gradlew roseauCheck` 运行通过 +- [ ] 提交 + +--- + +## 待验证事项销账(计划附录 B) + +| 编号 | 事项 | 涉及项 | 状态 | +|------|------------------------------------------------------------------------------------|-----------------|--------| +| V-1 | `Mth.roundToward`/`VertexSorting`/`CompactVectorArray`/`MeshData.sortQuads` 存在性 | #P7a | ✅ 已验证(roundToward 存在;CompactVectorArray 不存在改 VertexSorting.sort,V-1 计划修正) | +| V-2 | Iris 1.7.x `ImmediateState` 字段 | #P7d | 未验证(#P7d 未执行) | +| V-3 | `MultiBufferSource.BufferSource` 构造器参数 | #P7a | ✅ 已验证(super(null,null) 兼容) | +| V-4 | `renderVoxelShape` 颜色语义 | #P5 | 未验证(需 runClient 目测) | +| V-5 | port 侧 run 配置自建模式 | #P3/#P5/#P8 | ✅ 已验证(#P3 runData 通过、#P5 processResources 通过) | +| V-6 | Iris 1.21.1 Modrinth 坐标 | #P7d | 未验证(#P7d 未执行) | +| V-7 | mods.toml 模板变量注入 | #P7 | ✅ 已验证(port 模板展开正常) | +| V-8 | ShaderDefines → Map | #P7e | 未验证(#P7e 未执行) | +| V-9 | CoreMod jar-in-jar services 可见性 | #P2 | 未验证(需打包启动实测) | +| V-10 | CoreMod GETSTATIC 类可解析性 | #P2 | 未验证(需启动日志核对) | +| V-11 | `getSecureJar().getPath` 签名 | #P2 | ✅ 已验证(javap 确认返回 Path) | +| V-12 | `FMLLoader.getCurrent().getGameDir()` | #P6 | ✅ 已验证(改 FMLPaths.GAMEDIR.get() 编译+runData 通过) | +| V-13 | jspecify 在 port 构建链可用性 | #P15(M6 可选) | ✅ 已验证(多模块 compileOnly 使用通过) | +| V-14 | VillagerProfession 6 元 record / Potion 构造 | #P13 | ✅ 已验证(编译通过) | +| V-15 | RecipeSerializer 匿名类编译 | #P13 | ✅ 已验证(编译通过) | +| V-16 | District.color() int 打包颜色语义 | #P5 | 未验证(需 runClient 目测) | +| V-17 | GatherDataEvent includeClient/getPackOutput 签名 | #P3/#P6/#P14 | ✅ 已验证(#P3 runData、#P6 runData 通过) | +| V-18 | 根构建脚本模块注册方式对齐 | #P1–#P8 | ✅ 已验证(全部新模块注册并编译) | + +## 遗留登记(环境限制未完成的运行类验收) + +| 编号 | 事项 | 说明 | 复现/补验步骤 | +|------|------|------|----------------| +| L-1 | roseauCheck 运行验证(#P19) | 本机运行 roseauCheck 时 Gradle daemon 崩溃(疑似 roseau-cli 0.6.0 与本地 Java 21 环境兼容问题);任务可注册、配置阶段通过 | CI 或人工:`./gradlew :anvillib-<模块>:roseauCheck` | +| L-2 | RPC 行为验证(#P1/#P18 T8) | 远程调用往返、@CallableParam 编解码、tick 超时 | runClient/runServer,`-Danvillib.test.rpc.auto` 系统属性 | +| L-3 | sync 行为验证(#P2) | @Sync 字段同步、LazySync、processor 字节码注入生效 | runClient 启动日志 + 反编译核对注入点 | +| L-4 | explosion 行为验证(#P3/#P18 T9) | 分层球壳破坏、熔化替换、实体 hurt | runClient/runServer 执行测试命令 | +| L-5 | space-select 渲染(#P5,V-4/V-16) | 线框绘制颜色/平移语义、滚轮交互、网络收发 | runClient 目测 | +| L-6 | font 运行验证(#P6) | SDF 文字渲染、anvillib$text 扩展、FontConfigScreen | runClient | +| L-7 | rendering 运行验证(#P7a) | mixin 注入、AT 生效、mods.toml 加载 | runClient 启动日志 | +| L-8 | multiblock 行为验证(#P15) | 事件可取消回滚、快照复用、懒解析 | runServer | +| L-9 | wheel 运行验证(#P17) | 环形扇区效果、颜色配置、旧路径无回归 | runClient + T10 | +| L-10 | Iris 集成(#P7d,V-2/V-6) | #P7d 未执行,随后续批次 | — | +| L-11 | Compute(#P7e,V-8) | #P7e 未执行,随后续批次 | — | From a6544e6b4976a5eb7ef1ebfa835e2bb9f726aa3b Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 11 Aug 2026 02:47:08 +0800 Subject: [PATCH 23/38] =?UTF-8?q?feat(rendering):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=E6=B8=B2=E6=9F=93=E6=89=A7=E8=A1=8C=E5=B1=82=EF=BC=88?= =?UTF-8?q?#P7b=20=E5=90=8E=E5=A4=84=E7=90=86=20/=20#P7c=20SDF=20GUI=20/?= =?UTF-8?q?=20#P7d=20CachedBER=20/=20#P7e=20Compute=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回移植 dev/26.1 module.rendering 的四个执行层功能块,执行层从 26.1 GpuDevice/CommandEncoder/RenderPipeline/SubmitNodeStorage 体系重写到 1.21.1 RenderTarget/ShaderInstance/VertexBuffer/GuiGraphics 体系(7121bf6、b5f5420、 6bb3269、0e8290d、64bcf21、eccdc6d、6b5a544、494274e、27a8c79、05a1f6c、 e8ee3f9、d7a8f5d、6d5a97b、2751be4、cff1802、d121047、48bfe55、fba1027、 084c503、7d07f20)。 #P7b 后处理三件套:Bloom 5 级金字塔/GaussianBlur/Glitch 的算法与 GLSL 保留; 新增 postprocess/(GlUniformBuffer 自管 GL UBO、GlPostProcess 全屏 pass、 ALRPostEffectShaders 经 RegisterShadersEvent 注册);BloomSubmitNodeStorage 改 MultiBufferSource.BufferSource 重画;DeltaTracker → getTimer()。 #P7c SDF GUI:SdfGraphics 保留 9 种距离函数编排/旋转居中/collide/MAX_SDFS=256, _draw 改 GuiGraphics BufferSource + std140 UBO(glBindBufferRange 绑定点 2); SdfShaders 注册 sdf_graphics;shader GLSL #version 150 化。 #P7d CachedBER:chunk 表/编译上传队列/失效逻辑保留;GpuBuffer → VertexBuffer (照搬 1.21.1 renderSectionLayer 模式);CachedBlockEntityRenderer 改单阶段 render(...);Iris 集成经反射访问(Modrinth maven 停服,编译依赖注释占位)。 #P7e Compute:ALRComputePipeline.builder() API 形状保留,ShaderDefines → Map;GlComputePassBackend 独立自管状态机(glDispatchCompute/ glBindImageTexture/glBindBufferRange/glMemoryBarrier/glObjectLabel,LWJGL 全量); ALRComputeShaderManager 内联 GlslPreprocessor(#moj_import 走 ClientHooks)。 --- module.rendering/build.gradle | 11 + .../lib/v2/rendering/ALRPostEffects.java | 45 ++ .../lib/v2/rendering/AnvilLibRendering.java | 65 +- .../rendering/bloom/BloomParametersUbo.java | 36 ++ .../bloom/BloomPipelineParametersUbo.java | 39 ++ .../v2/rendering/bloom/BloomPostEffect.java | 335 +++++++++++ .../rendering/bloom/BloomRenderCallback.java | 17 + .../lib/v2/rendering/bloom/TransformsUbo.java | 31 + .../v2/rendering/blur/BlurParametersUbo.java | 38 ++ .../lib/v2/rendering/blur/GaussianBlur.java | 106 ++++ .../v2/rendering/cachedber/package-info.java | 7 + .../CachedBlockEntityRenderingPipeline.java | 203 +++++++ .../pipeline/CachedRenderingChunk.java | 275 +++++++++ .../cachedber/pipeline/RebuildTask.java | 72 +++ .../cachedber/pipeline/package-info.java | 7 + .../CachedBlockEntityRenderDispatcher.java | 77 +++ .../renderer/CachedBlockEntityRenderer.java | 24 + .../cachedber/renderer/package-info.java | 7 + .../blaze3d/compute/BufferSlice.java | 21 + .../compute/pipeline/ALRComputePass.java | 100 +++ .../pipeline/ALRComputePassBackend.java | 39 ++ .../compute/pipeline/ALRComputePipeline.java | 116 ++++ .../bindings/AtomicCounterBinding.java | 19 + .../bindings/ComputeBindingLayout.java | 12 + .../pipeline/bindings/ImageBinding.java | 34 ++ .../bindings/ShaderStorageBinding.java | 19 + .../pipeline/bindings/TextureBinding.java | 33 + .../bindings/UniformBlockBinding.java | 19 + .../pipeline/gl/GlComputePassBackend.java | 215 +++++++ .../shader/ALRComputeProgramInstance.java | 20 + .../shader/ALRComputeProgramInstanceKey.java | 13 + .../shader/ALRComputeShaderManager.java | 205 +++++++ .../compute/shader/ShaderResourceType.java | 9 + .../buffers/FullyBufferedBufferSource.java | 128 ++++ .../foundation/buffers/VertexBufferHost.java | 19 + .../rendering/glitch/GlitchParametersUbo.java | 32 + .../v2/rendering/glitch/GlitchPostEffect.java | 83 +++ .../v2/rendering/integration/IrisSupport.java | 156 +++++ ...chedBlockEntityRenderingPipelineMixin.java | 42 ++ .../integration/mixins/RebuildTaskMixin.java | 35 ++ .../v2/rendering/mixins/MinecraftMixin.java | 11 +- .../postprocess/ALRPostEffectShaders.java | 66 ++ .../rendering/postprocess/GlPostProcess.java | 67 +++ .../postprocess/GlUniformBuffer.java | 57 ++ .../lib/v2/rendering/sdf/SdfGraphics.java | 568 ++++++++++++++++++ .../lib/v2/rendering/sdf/SdfShaders.java | 46 ++ .../resources/META-INF/accesstransformer.cfg | 2 + .../resources/META-INF/neoforge.mods.toml | 4 + ...anvillib_rendering.integration.mixins.json | 17 + .../shaders/core/apply_bloom.json | 9 + .../anvillib_rendering/shaders/core/blur.json | 8 + .../shaders/core/down_sample.json | 8 + .../shaders/core/glitch.json | 8 + .../shaders/core/sdf_graphics.fsh | 2 +- .../shaders/core/sdf_graphics.json | 10 + .../shaders/core/sdf_graphics.vsh | 10 +- .../shaders/core/up_sample.json | 9 + 57 files changed, 3656 insertions(+), 10 deletions(-) create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRPostEffects.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomParametersUbo.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPipelineParametersUbo.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPostEffect.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomRenderCallback.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/TransformsUbo.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/BlurParametersUbo.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/GaussianBlur.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/package-info.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedBlockEntityRenderingPipeline.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedRenderingChunk.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/RebuildTask.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/package-info.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderDispatcher.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderer.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/package-info.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/BufferSlice.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePass.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePassBackend.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePipeline.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/AtomicCounterBinding.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ComputeBindingLayout.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ImageBinding.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ShaderStorageBinding.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/TextureBinding.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/UniformBlockBinding.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/gl/GlComputePassBackend.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstance.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstanceKey.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeShaderManager.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ShaderResourceType.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/FullyBufferedBufferSource.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/VertexBufferHost.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchParametersUbo.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchPostEffect.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/IrisSupport.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/CachedBlockEntityRenderingPipelineMixin.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/RebuildTaskMixin.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/ALRPostEffectShaders.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlPostProcess.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlUniformBuffer.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfGraphics.java create mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfShaders.java create mode 100644 module.rendering/src/main/resources/anvillib_rendering.integration.mixins.json create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.json create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.json create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.json create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.json create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.json create mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.json diff --git a/module.rendering/build.gradle b/module.rendering/build.gradle index 4f4eedc8..92fe77b1 100644 --- a/module.rendering/build.gradle +++ b/module.rendering/build.gradle @@ -116,6 +116,17 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { compileOnly "org.jspecify:jspecify:1.0.0" + + // #P7d: optional Iris integration (integration/IrisSupport.java + the two integration mixins). + // The dev/26.1 branch used compileOnly("maven.modrinth:YL57xq9U:...") (Iris) from the Modrinth + // maven; that maven has been shut down, and no other reachable mirror hosts Iris 1.8.x for 1.21.1 + // (the API was verified by downloading iris-neoforge-1.8.12+mc1.21.1.jar manually). The integration + // therefore accesses the Iris classes through reflection and needs no compile-time dependency; it + // activates at runtime only when Iris is loaded (gated by ALRIntegrationCompatMixinPlugin + the + // ModList check inside IrisSupport). To restore a compile-time dependency once a mirror is available: + // compileOnly("maven.modrinth:YL57xq9U:t3ruzodq") // iris 1.8.12+1.21.1-neoforge (needs the Modrinth maven repository) + // and revert IrisSupport to direct references (net.irisshaders.iris.api.v0.IrisApi / + // net.irisshaders.iris.vertices.ImmediateState). } tasks.register('runStd140LayoutRulesTest', JavaExec) { diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRPostEffects.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRPostEffects.java new file mode 100644 index 00000000..87a4e5ea --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRPostEffects.java @@ -0,0 +1,45 @@ +package dev.anvilcraft.lib.v2.rendering; + +import dev.anvilcraft.lib.v2.rendering.bloom.BloomPostEffect; +import dev.anvilcraft.lib.v2.rendering.event.MainTargetResizeEvent; +import dev.anvilcraft.lib.v2.rendering.glitch.GlitchPostEffect; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import org.jetbrains.annotations.ApiStatus; +import org.joml.Matrix4f; + +@Slf4j +@EventBusSubscriber +public class ALRPostEffects { + @Getter + private static BloomPostEffect bloomPostEffect; + @Getter + private static GlitchPostEffect glitchPostEffect; + + public static void createPostEffects() { + bloomPostEffect = new BloomPostEffect(); + glitchPostEffect = new GlitchPostEffect(); + } + + public static void runBloomDraws(Matrix4f mvMat) { + try { + bloomPostEffect.runBloomDraws(mvMat); + } catch (Exception e) { + log.error("runBloomDraws boom!", e); + } + } + + @ApiStatus.Internal + @SubscribeEvent + public static void on(MainTargetResizeEvent event) { + BloomPostEffect bloomPostEffect = ALRPostEffects.getBloomPostEffect(); + if (bloomPostEffect != null) { + bloomPostEffect.resize( + event.getNewWidth(), + event.getNewHeight() + ); + } + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/AnvilLibRendering.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/AnvilLibRendering.java index b4d10f0b..81ffe7c4 100644 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/AnvilLibRendering.java +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/AnvilLibRendering.java @@ -1,5 +1,10 @@ package dev.anvilcraft.lib.v2.rendering; +import dev.anvilcraft.lib.v2.rendering.cachedber.pipeline.CachedBlockEntityRenderingPipeline; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ALRComputeShaderManager; +import dev.anvilcraft.lib.v2.rendering.postprocess.ALRPostEffectShaders; +import dev.anvilcraft.lib.v2.rendering.sdf.SdfGraphics; +import dev.anvilcraft.lib.v2.rendering.sdf.SdfShaders; import lombok.extern.slf4j.Slf4j; import net.minecraft.resources.ResourceLocation; import net.neoforged.api.distmarker.Dist; @@ -8,6 +13,9 @@ import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.fml.common.Mod; import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.RegisterShadersEvent; +import net.neoforged.neoforge.client.event.RenderFrameEvent; +import net.neoforged.neoforge.client.event.RenderLevelStageEvent; import org.jetbrains.annotations.ApiStatus; @Slf4j @@ -25,8 +33,63 @@ public static ResourceLocation location(String path) { return ResourceLocation.fromNamespaceAndPath(MODID, path); } + @SubscribeEvent + public static void on(RegisterShadersEvent event) { + // #P7b: post-processing shaders (bloom downsample/upsample/apply, blur, glitch) + ALRPostEffectShaders.register(event); + // #P7c: SDF GUI shader (sdf_graphics) + SdfShaders.register(event); + } + @SubscribeEvent public static void on(RegisterClientReloadListenersEvent event) { - // #P7e: register ALRComputeShaderManager.INSTANCE via event.registerReloadListener(...) + // #P7e: compute shader manager (compiles and caches GL compute programs) + event.registerReloadListener(ALRComputeShaderManager.INSTANCE); + } + + @SubscribeEvent + public static void on(RenderFrameEvent.Pre event) { + if (ALRPostEffects.getBloomPostEffect() != null) { + ALRPostEffects.getBloomPostEffect().beginFrame(); + } + // #P7c: reset the per-frame SDF parameter slots (the 26.1 GuiRendererMixin flush equivalent). + SdfGraphics.flush(); + // #P7d: run the cached BER compile + upload queues on the render thread. + if (CachedBlockEntityRenderingPipeline.getInstance() != null) { + CachedBlockEntityRenderingPipeline.getInstance().runTasks(); + } + } + + @SubscribeEvent + public static void on(RenderLevelStageEvent event) { + if (event.getStage() == RenderLevelStageEvent.Stage.AFTER_PARTICLES) { + ALRPostEffects.runBloomDraws(event.getModelViewMatrix()); + } else if (event.getStage() == RenderLevelStageEvent.Stage.AFTER_LEVEL) { + try { + if (ALRPostEffects.getBloomPostEffect() != null) { + ALRPostEffects.getBloomPostEffect().process(); + } + } catch (Exception e) { + log.error("ALRPostEffects BloomPostEffect", e); + } + } + } + + /** + * #P7d: cached BER rendering. Opaque layers render after the opaque world (mapped from the 26.1 + * {@code AfterOpaqueBlocks}), translucent layers after the translucent world (26.1 + * {@code AfterTranslucentFeatures}); both exist as {@link RenderLevelStageEvent.Stage}s on 1.21.1. + */ + @SubscribeEvent + public static void onRenderCachedBlockEntities(RenderLevelStageEvent event) { + if (event.getStage() == RenderLevelStageEvent.Stage.AFTER_CUTOUT_BLOCKS) { + if (CachedBlockEntityRenderingPipeline.getInstance() != null) { + CachedBlockEntityRenderingPipeline.getInstance().render(event.getFrustum(), false); + } + } else if (event.getStage() == RenderLevelStageEvent.Stage.AFTER_TRANSLUCENT_BLOCKS) { + if (CachedBlockEntityRenderingPipeline.getInstance() != null) { + CachedBlockEntityRenderingPipeline.getInstance().render(event.getFrustum(), true); + } + } } } diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomParametersUbo.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomParametersUbo.java new file mode 100644 index 00000000..4ae7e6d5 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomParametersUbo.java @@ -0,0 +1,36 @@ +package dev.anvilcraft.lib.v2.rendering.bloom; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutEntry; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.ShaderBufferObjectUsage; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class BloomParametersUbo extends BufferObject { + + public static final BufferObjectLayoutDefinition DEFINITION = BufferObjectLayoutDefinition.create( + BufferObjectLayoutEntry.ofFloat().forGetter(BloomParametersUbo::getBloomIntensity).build(), + BufferObjectLayoutEntry.ofFloat().forGetter(BloomParametersUbo::getBloomBlendThreshold).build(), + BufferObjectLayoutEntry.ofFloat().forGetter(BloomParametersUbo::getLuminanceSensitivity).build() + ); + + private float bloomIntensity; + private float bloomBlendThreshold; + private float luminanceSensitivity; + + public BloomParametersUbo(float bloomIntensity, float bloomBlendThreshold, float luminanceSensitivity) { + super(BufferLayout.STD140, ShaderBufferObjectUsage.UBO); + this.bloomIntensity = bloomIntensity; + this.bloomBlendThreshold = bloomBlendThreshold; + this.luminanceSensitivity = luminanceSensitivity; + } + + @Override + protected BufferObjectLayoutDefinition getDefinition() { + return DEFINITION; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPipelineParametersUbo.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPipelineParametersUbo.java new file mode 100644 index 00000000..c3cdae08 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPipelineParametersUbo.java @@ -0,0 +1,39 @@ +package dev.anvilcraft.lib.v2.rendering.bloom; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutEntry; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.ShaderBufferObjectUsage; +import lombok.Getter; +import lombok.Setter; +import org.joml.Vector2f; + +@Getter +@Setter +public class BloomPipelineParametersUbo extends BufferObject { + + public static final BufferObjectLayoutDefinition DEFINITION = BufferObjectLayoutDefinition.create( + BufferObjectLayoutEntry.ofVec2f().forGetter(BloomPipelineParametersUbo::getResolution).build(), + BufferObjectLayoutEntry.ofInt().forGetter(BloomPipelineParametersUbo::getFrameIndex).build() + ); + + private final Vector2f resolution = new Vector2f(); + private int frameIndex; + + protected BloomPipelineParametersUbo() { + super(BufferLayout.STD140, ShaderBufferObjectUsage.UBO); + } + + public void setResolution(int width, int height) { + this.resolution.set( + 1.0f / Math.max(width, 1.0f), + 1.0f / Math.max(height, 1.0f) + ); + } + + @Override + protected BufferObjectLayoutDefinition getDefinition() { + return DEFINITION; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPostEffect.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPostEffect.java new file mode 100644 index 00000000..f1d5ec85 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPostEffect.java @@ -0,0 +1,335 @@ +package dev.anvilcraft.lib.v2.rendering.bloom; + +import com.mojang.blaze3d.pipeline.MainTarget; +import com.mojang.blaze3d.pipeline.RenderTarget; +import com.mojang.blaze3d.pipeline.TextureTarget; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.vertex.MeshData; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.compound.DirtyTracked; +import dev.anvilcraft.lib.v2.rendering.postprocess.ALRPostEffectShaders; +import dev.anvilcraft.lib.v2.rendering.postprocess.GlPostProcess; +import dev.anvilcraft.lib.v2.rendering.postprocess.GlUniformBuffer; +import lombok.Getter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.ShaderInstance; +import org.joml.Matrix4f; + +import java.util.ArrayList; +import java.util.List; + +/** + * Bloom post-processing effect: captures a re-rendered copy of the scene, runs it through a + * 5-level downsample/upsample Gaussian pyramid and composites the result back onto the main target. + *

+ * Ported from 26.1 (see #P7b): the {@code GpuDevice}/{@code CommandEncoder}/{@code RenderPass} + * execution layer becomes {@link RenderTarget} + {@link ShaderInstance} (registered via + * {@code RegisterShadersEvent}, see {@link ALRPostEffectShaders}) + fullscreen quad + * ({@code BufferUploader}/{@code Tesselator}); UBO uploads use {@link GlUniformBuffer}. + * The 26.1 {@code SubmitNodeStorage} redraw path is replaced by a {@link MultiBufferSource.BufferSource} + * redraw; {@code BloomSubmitNodeStorage} itself is not portable and is intentionally dropped. + */ +@SuppressWarnings({"FieldMayBeFinal", "SameParameterValue"}) +public class BloomPostEffect implements DirtyTracked { + public static final int UNIFORM_TRANSFORM_SIZE = TransformsUbo.DEFINITION.size(BufferLayout.STD140); + public static final int UNIFORM_BLOOM_SIZE = BloomParametersUbo.DEFINITION.size(BufferLayout.STD140); + public static final int UNIFORM_ENHANCED_BLOOM_SIZE = BloomPipelineParametersUbo.DEFINITION.size(BufferLayout.STD140); + + // todo: uses config or options + private static final int PASSES_AMOUNT = 5; + private static final int PASS_STEP = 1; + + @Getter + private final RenderTarget bloomInputTarget = new MainTarget(854, 480); + private final RenderTarget bloomTempTarget = new TextureTarget(854, 480, false, false); + + private final RenderTarget[] downsampleTargets = arrayInit("DownSample", PASSES_AMOUNT); + private final RenderTarget[] upsampleTargets = arrayInit("UpSample", PASSES_AMOUNT - 1); + + private final GlUniformBuffer transformUBO = new GlUniformBuffer(UNIFORM_TRANSFORM_SIZE); + private final GlUniformBuffer bloomUBO = new GlUniformBuffer(UNIFORM_BLOOM_SIZE); + private final GlUniformBuffer enhancedBloomParametersUBO = new GlUniformBuffer(UNIFORM_ENHANCED_BLOOM_SIZE); + + @Getter + private final BloomParametersUbo bloomParameters; + @Getter + private final BloomPipelineParametersUbo enhancedBloomParameters; + + private final TransformsUbo transformsUbo = new TransformsUbo(new Matrix4f()); + private final List bloomCalls = new ArrayList<>(); + + private int width; + private int height; + private MeshData quadMesh; + private boolean dirty = false; + + private int passes; + private int step; + + public BloomPostEffect() { + this(1.25f, 1.943f, 1.105f, 0.08f, 0.8f, PASSES_AMOUNT, PASS_STEP); + } + + public BloomPostEffect( + float bloomIntensity, + float sampleStepLength, + float colorMultiplier, + float bloomThreshold, + float bloomIntensityMultiplier, + int passes, + int step + ) { + RenderTarget mainRenderTarget = Minecraft.getInstance().getMainRenderTarget(); + + this.width = mainRenderTarget.width; + this.height = mainRenderTarget.height; + this.bloomParameters = new BloomParametersUbo(bloomIntensity, bloomThreshold, bloomIntensityMultiplier); + this.enhancedBloomParameters = new BloomPipelineParametersUbo(); + this.passes = passes; + this.step = step; + + resize(width, height); + } + + public void beginFrame() { + clearColorAndDepth(bloomInputTarget); + clearColorAndDepth(bloomTempTarget); + dirty = false; + bloomCalls.clear(); + } + + public void markDirty() { + dirty = true; + } + + public void drawBloomed(BloomRenderCallback runnable) { + bloomCalls.add(runnable); + markDirty(); + } + + public void beginBloomDraw() { + RenderTarget mainRenderTarget = Minecraft.getInstance().getMainRenderTarget(); + + this.guardSize(mainRenderTarget); + + bloomInputTarget.copyDepthFrom(mainRenderTarget); + + bloomInputTarget.bindWrite(true); + this.markDirty(); + } + + private void guardSize(RenderTarget mainRenderTarget) { + if (mainRenderTarget.width != this.bloomInputTarget.width || mainRenderTarget.height != this.bloomInputTarget.height) { + this.resize(mainRenderTarget.width, mainRenderTarget.height); + } + } + + public void endBloomDraw() { + // restore the level's framebuffer + Minecraft.getInstance().getMainRenderTarget().bindWrite(true); + } + + public void runBloomDraws(Matrix4f modelViewMatrix) { + if (!dirty) return; + + PoseStack poseStack = new PoseStack(); + try { + beginBloomDraw(); + if (!bloomCalls.isEmpty()) { + try (com.mojang.blaze3d.vertex.ByteBufferBuilder bufferBuilder = new com.mojang.blaze3d.vertex.ByteBufferBuilder(256)) { + MultiBufferSource.BufferSource bufferSource = MultiBufferSource.immediate(bufferBuilder); + for (BloomRenderCallback bloomCall : bloomCalls) { + bloomCall.render(bufferSource, poseStack); + } + bufferSource.endBatch(); + } + } + } finally { + endBloomDraw(); + } + } + + @SuppressWarnings("DataFlowIssue") + public void process() { + if (!dirty) return; + RenderTarget mainRenderTarget = Minecraft.getInstance().getMainRenderTarget(); + + this.guardSize(mainRenderTarget); + + transformUBO.upload(transformsUbo); + + this.doDownSample(); + this.doUpSample(); + + // backup depth texture + bloomInputTarget.copyDepthFrom(mainRenderTarget); + + clearColorAndDepth(bloomTempTarget); + applyBloom(this.upsampleTargets[0], mainRenderTarget, bloomTempTarget); + copyColor(bloomTempTarget, mainRenderTarget); + mainRenderTarget.copyDepthFrom(bloomInputTarget); + } + + private void clearColorAndDepth(RenderTarget rt) { + rt.clear(false); + } + + /** + * Copies the color attachment of {@code src} onto the color texture of {@code dst} through the + * GL read framebuffer. Replaces the 26.1 {@code CommandEncoder.copyTextureToTexture}. + */ + private void copyColor(RenderTarget src, RenderTarget dst) { + GlStateManager._activeTexture(33984); + GlStateManager._glBindFramebuffer(36008, src.frameBufferId); + GlStateManager._bindTexture(dst.getColorTextureId()); + GlStateManager._glCopyTexSubImage2D(3553, 0, 0, 0, 0, 0, this.width, this.height); + GlStateManager._bindTexture(0); + GlStateManager._glBindFramebuffer(36008, 0); + } + + private void applyBloom( + RenderTarget inputTarget, + RenderTarget gameTarget, + RenderTarget outputTarget + ) { + ShaderInstance shader = ALRPostEffectShaders.getApplyBloomShader(); + if (shader == null) return; + transformUBO.upload(transformsUbo); + bloomUBO.upload(bloomParameters); + GlPostProcess.beginTarget(outputTarget); + shader.setSampler("GameSampler", gameTarget); + shader.setSampler("DiffuseSampler", inputTarget); + GlPostProcess.draw(shader, quadMesh, s -> { + transformUBO.bindTo(s.getId(), "Transforms", 0); + bloomUBO.bindTo(s.getId(), "BloomParameters", 1); + }); + } + + private void doDownSample() { + + this.downSample( + this.bloomInputTarget, + this.downsampleTargets[0], + 0 + ); + + for (int i = 1; i < this.passes; i++) { + this.downSample( + this.downsampleTargets[i - 1], + this.downsampleTargets[i], + i + ); + } + + } + + private void downSample( + RenderTarget src, + RenderTarget dst, + int frameIndex + ) { + ShaderInstance shader = ALRPostEffectShaders.getDownSampleShader(); + if (shader == null) return; + this.enhancedBloomParameters.setFrameIndex(frameIndex); + this.enhancedBloomParameters.setResolution(src.width, src.height); + this.enhancedBloomParametersUBO.upload(enhancedBloomParameters); + + GlPostProcess.beginTarget(dst); + shader.setSampler("DiffuseSampler", src); + GlPostProcess.draw(shader, quadMesh, s -> { + transformUBO.bindTo(s.getId(), "Transforms", 0); + enhancedBloomParametersUBO.bindTo(s.getId(), "BloomParameters", 1); + }); + } + + private void doUpSample() { + var steps = this.passes; + + this.upSample( + this.downsampleTargets[steps - 2], + this.downsampleTargets[steps - 1], + this.upsampleTargets[steps - 2], + steps - 1 + ); + + for (int i = steps - 2; i > 0; i--) { + this.upSample( + this.downsampleTargets[i - 1], + this.upsampleTargets[i], + this.upsampleTargets[i - 1], + i + ); + } + } + + private void upSample( + RenderTarget curr, + RenderTarget prev, + RenderTarget dst, + int frameIndex + ) { + ShaderInstance shader = ALRPostEffectShaders.getUpSampleShader(); + if (shader == null) return; + this.enhancedBloomParameters.setFrameIndex(frameIndex); + this.enhancedBloomParameters.setResolution(curr.width, curr.height); + this.enhancedBloomParametersUBO.upload(enhancedBloomParameters); + + GlPostProcess.beginTarget(dst); + shader.setSampler("DiffuseSampler", curr); + shader.setSampler("PreviousSampler", prev); + GlPostProcess.draw(shader, quadMesh, s -> { + transformUBO.bindTo(s.getId(), "Transforms", 0); + enhancedBloomParametersUBO.bindTo(s.getId(), "BloomParameters", 1); + }); + } + + public void resize(int width, int height) { + this.width = width; + this.height = height; + bloomInputTarget.resize(width, height, false); + bloomTempTarget.resize(width, height, false); + transformsUbo.getProjMat().setOrtho( + 0, width, 0, height, -1, -10000f + ); + + this.quadMesh = GlPostProcess.buildQuad(width, height, false); + + this.passes = PASSES_AMOUNT; + + int pWidth = width; + int pHeight = height; + int step = this.step; + + for (int i = 0; i < PASSES_AMOUNT; i++) { + pWidth >>= step; + pHeight >>= step; + + if (pWidth == 0 || pHeight == 0) { + this.passes = i; + break; + } + + this.downsampleTargets[i].resize(pWidth, pHeight, false); + + if (i < PASSES_AMOUNT - 1) { + this.upsampleTargets[i].resize(pWidth, pHeight, false); + } + } + } + + private static RenderTarget[] arrayInit(String name, int size) { + RenderTarget[] targets = new RenderTarget[size]; + + for (int i = 0; i < size; i++) { + var target = new TextureTarget( + 854 >> i, 480 >> i, + false, false + ); + targets[i] = target; + } + + return targets; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomRenderCallback.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomRenderCallback.java new file mode 100644 index 00000000..5db41793 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomRenderCallback.java @@ -0,0 +1,17 @@ +package dev.anvilcraft.lib.v2.rendering.bloom; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.MultiBufferSource; + +/** + * Callback that re-renders part of the scene into the bloom input target. + *

+ * Ported from 26.1: the original received a {@code SubmitNodeCollector} and relied on + * {@code FeatureRenderDispatcher} to flush the re-rendered features; neither exists on 1.21.1, + * so the callback now receives a {@link MultiBufferSource.BufferSource} and is expected to submit + * geometry (typically with {@code ALRRenderTypeExtension.copyWithBloom} marked render types) which + * {@code BloomPostEffect} flushes while the bloom input target is bound. + */ +public interface BloomRenderCallback { + void render(MultiBufferSource.BufferSource bufferSource, PoseStack poseStack); +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/TransformsUbo.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/TransformsUbo.java new file mode 100644 index 00000000..12ce845b --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/TransformsUbo.java @@ -0,0 +1,31 @@ +package dev.anvilcraft.lib.v2.rendering.bloom; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutEntry; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.ShaderBufferObjectUsage; +import lombok.Getter; +import lombok.Setter; +import org.joml.Matrix4f; + +@Setter +@Getter +public class TransformsUbo extends BufferObject { + + public static final BufferObjectLayoutDefinition DEFINITION = BufferObjectLayoutDefinition.create( + BufferObjectLayoutEntry.ofMat4f().forGetter(TransformsUbo::getProjMat).build() + ); + + private Matrix4f projMat; + + public TransformsUbo(Matrix4f projMat) { + super(BufferLayout.STD140, ShaderBufferObjectUsage.UBO); + this.projMat = projMat; + } + + @Override + protected BufferObjectLayoutDefinition getDefinition() { + return DEFINITION; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/BlurParametersUbo.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/BlurParametersUbo.java new file mode 100644 index 00000000..8830ffa2 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/BlurParametersUbo.java @@ -0,0 +1,38 @@ +package dev.anvilcraft.lib.v2.rendering.blur; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutEntry; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.ShaderBufferObjectUsage; +import lombok.Getter; +import lombok.Setter; +import org.joml.Vector2f; + +@Setter +@Getter +public class BlurParametersUbo extends BufferObject { + + public static final BufferObjectLayoutDefinition DEFINITION = BufferObjectLayoutDefinition.create( + BufferObjectLayoutEntry.ofVec2f().forGetter(BlurParametersUbo::getDirection).build(), + BufferObjectLayoutEntry.ofFloat().forGetter(BlurParametersUbo::getSampleStepLength).build(), + BufferObjectLayoutEntry.ofFloat().forGetter(BlurParametersUbo::getColorMultiplier).build() + ); + + private float sampleStepLength; + + private float colorMultiplier; + private Vector2f direction; + + public BlurParametersUbo(float sampleStepLength, float colorMultiplier, Vector2f direction) { + super(BufferLayout.STD140, ShaderBufferObjectUsage.UBO); + this.sampleStepLength = sampleStepLength; + this.colorMultiplier = colorMultiplier; + this.direction = direction; + } + + @Override + protected BufferObjectLayoutDefinition getDefinition() { + return DEFINITION; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/GaussianBlur.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/GaussianBlur.java new file mode 100644 index 00000000..82d10f6b --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/GaussianBlur.java @@ -0,0 +1,106 @@ +package dev.anvilcraft.lib.v2.rendering.blur; + +import com.mojang.blaze3d.pipeline.MainTarget; +import com.mojang.blaze3d.pipeline.RenderTarget; +import com.mojang.blaze3d.pipeline.TextureTarget; +import com.mojang.blaze3d.platform.Window; +import com.mojang.blaze3d.vertex.MeshData; +import dev.anvilcraft.lib.v2.rendering.bloom.TransformsUbo; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.postprocess.ALRPostEffectShaders; +import dev.anvilcraft.lib.v2.rendering.postprocess.GlPostProcess; +import dev.anvilcraft.lib.v2.rendering.postprocess.GlUniformBuffer; +import lombok.Getter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ShaderInstance; +import org.joml.Matrix4f; +import org.joml.Vector2f; + +/** + * Two-pass (horizontal + vertical) Gaussian blur over a {@link RenderTarget}. + *

+ * Ported from 26.1 (see #P7b): the {@code GpuDevice}/{@code CommandEncoder}/{@code RenderPass} + * execution layer becomes {@link RenderTarget} + {@link ShaderInstance} + fullscreen quad; + * UBO uploads use {@link GlUniformBuffer}. The shader and its std140 layout are unchanged. + */ +public class GaussianBlur implements AutoCloseable { + private final RenderTarget inputTarget = new MainTarget(854, 480); + private final RenderTarget tempTarget = new TextureTarget(854, 480, false, false); + + private final GlUniformBuffer blurUBO = new GlUniformBuffer(BlurParametersUbo.DEFINITION.size(BufferLayout.STD140)); + private final GlUniformBuffer transformUBO = new GlUniformBuffer(TransformsUbo.DEFINITION.size(BufferLayout.STD140)); + + @Getter + private final BlurParametersUbo blurParameters; + + private final TransformsUbo transformsUbo = new TransformsUbo(new Matrix4f()); + + private int width; + private int height; + private MeshData quadMesh; + + public GaussianBlur(float sampleStepLength, float colorMultiplier) { + Window window = Minecraft.getInstance().getWindow(); + + this.width = window.getWidth(); + this.height = window.getHeight(); + this.blurParameters = new BlurParametersUbo(sampleStepLength, colorMultiplier, new Vector2f()); + + resize(width, height); + } + + /** + * Runs two horizontal and two vertical blur passes and returns the blurred target. + */ + public RenderTarget process(RenderTarget input) { + transformUBO.upload(transformsUbo); + + blurOnce(input, tempTarget, true); + blurOnce(tempTarget, inputTarget, true); + + blurOnce(inputTarget, tempTarget, false); + blurOnce(tempTarget, inputTarget, false); + + return inputTarget; + } + + private void blurOnce(RenderTarget inputTarget, RenderTarget outputTarget, boolean horizontal) { + ShaderInstance shader = ALRPostEffectShaders.getBlurShader(); + if (shader == null) return; + Vector2f direction; + if (horizontal) { + direction = new Vector2f(1f, 0f); + } else { + direction = new Vector2f(0f, 1f); + } + blurParameters.setDirection(direction); + blurUBO.upload(blurParameters); + + GlPostProcess.beginTarget(outputTarget); + shader.setSampler("DiffuseSampler", inputTarget); + GlPostProcess.draw(shader, quadMesh, s -> { + transformUBO.bindTo(s.getId(), "Transforms", 0); + blurUBO.bindTo(s.getId(), "BlurParameters", 1); + }); + } + + public void resize(int width, int height) { + this.width = width; + this.height = height; + inputTarget.resize(width, height, false); + tempTarget.resize(width, height, false); + transformsUbo.getProjMat().setOrtho( + 0, width, 0, height, -1, -10000f + ); + + this.quadMesh = GlPostProcess.buildQuad(width, height, false); + } + + @Override + public void close() { + blurUBO.close(); + transformUBO.close(); + inputTarget.destroyBuffers(); + tempTarget.destroyBuffers(); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/package-info.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/package-info.java new file mode 100644 index 00000000..4b8a7e5c --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package dev.anvilcraft.lib.v2.rendering.cachedber; + +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedBlockEntityRenderingPipeline.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedBlockEntityRenderingPipeline.java new file mode 100644 index 00000000..7fa4c115 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedBlockEntityRenderingPipeline.java @@ -0,0 +1,203 @@ +package dev.anvilcraft.lib.v2.rendering.cachedber.pipeline; + +import dev.anvilcraft.lib.v2.rendering.AnvilLibRendering; +import dev.anvilcraft.lib.v2.rendering.cachedber.renderer.CachedBlockEntityRenderDispatcher; +import lombok.Getter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.culling.Frustum; +import net.minecraft.core.BlockPos; +import net.minecraft.util.StringUtil; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.Vec3; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.RenderFrameEvent; +import net.neoforged.neoforge.client.event.RenderLevelStageEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GL46; + +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; + +/** + * Caches the render output of registered block entity renderers per 16x16 chunk as GPU vertex buffers, + * with frustum culling, translucent distance sorting and optional bloom double-drawing. + *

+ * Ported from 26.1 (see #P7d): the chunk table, compile/upload queues and invalidation logic + * ({@code blockRemoved}/{@code update}/{@code forcedUpdate}) are kept as-is; the execution layer moves + * from 26.1 {@code GpuBuffer}/{@code RenderPass} to 1.21.1 {@code VertexBuffer} + the classic + * {@code RenderSystem} path (see {@link CachedRenderingChunk#renderLayer}). {@code ChunkPos.containing} + * becomes {@code new ChunkPos}, and the 26.1 {@code RenderLevelStageEvent.AfterSky} camera tracking uses + * {@code event.getCamera().getPosition()} instead of {@code LevelRenderState}. + * + * @author ZhuRuoLing + */ +@SuppressWarnings("unused") +@EventBusSubscriber(modid = AnvilLibRendering.MODID, value = Dist.CLIENT) +public class CachedBlockEntityRenderingPipeline { + @Nullable + private static CachedBlockEntityRenderingPipeline instance; + @Getter + private final ClientLevel level; + private final Queue pendingCompiles = new ArrayDeque<>(); + private final Queue pendingUploads = new ArrayDeque<>(); + private final Map chunks = new HashMap<>(); + @Getter + private boolean valid = true; + private static Vec3 cameraOldPosition = null; + @Getter + private static boolean cameraMoved = true; + private static int glMaxLabelLength = 0; + + public static void create() { + if (GL.getCapabilities().GL_KHR_debug) { + glMaxLabelLength = GL46.glGetInteger(GL46.GL_MAX_LABEL_LENGTH); + glMaxLabelLength /= 2; + } + } + + public CachedRenderingChunk getRenderRegion(ChunkPos chunkPos) { + if (chunks.containsKey(chunkPos)) { + return chunks.get(chunkPos); + } + CachedRenderingChunk region = new CachedRenderingChunk(chunkPos, this); + chunks.put(chunkPos, region); + return region; + } + + public CachedBlockEntityRenderingPipeline(ClientLevel level) { + this.level = level; + } + + public void runTasks() { + while (!pendingCompiles.isEmpty() && valid) { + pendingCompiles.poll().run(); + } + while (!pendingUploads.isEmpty() && valid) { + pendingUploads.poll().run(); + } + } + + /** + * Updates the rendering pipeline instance with a new level context. + * + * @param level The new ClientLevel instance that the rendering pipeline should be updated to use. + */ + public static void updateLevel(@Nullable ClientLevel level) { + if (instance != null) { + instance.releaseBuffers(); + } + if (level == null) { + instance = null; + return; + } + instance = new CachedBlockEntityRenderingPipeline(level); + } + + /** + * Notifies the pipeline that a {@link BlockEntity} has been removed. + * This method will be automatically called when a {@link BlockEntity} has been removed. + * + * @param be The removed {@link BlockEntity} + */ + public void blockRemoved(BlockEntity be) { + if (!CachedBlockEntityRenderDispatcher.INSTANCE.hasRenderer(be)) return; + ChunkPos chunkPos = new ChunkPos(be.getBlockPos()); + getRenderRegion(chunkPos).blockRemoved(be); + } + + public void update(BlockEntity be) { + update(be, false); + } + + /** + * Notifies the pipeline that a {@link BlockEntity} has been updated and the cache should be rebuilt. + * + * @param be The updated {@link BlockEntity} + */ + public void update(BlockEntity be, boolean forced) { + if (!CachedBlockEntityRenderDispatcher.INSTANCE.hasRenderer(be)) return; + ChunkPos chunkPos = new ChunkPos(be.getBlockPos()); + getRenderRegion(chunkPos).update(be, forced); + } + + public void submitUploadTask(Runnable task) { + pendingUploads.add(task); + } + + public void submitCompileTask(Runnable task) { + pendingCompiles.add(task); + } + + /** + * Releases all buffers in use and mark current pipeline instance as invalid. + */ + public void releaseBuffers() { + chunks.values().forEach(CachedRenderingChunk::releaseBuffers); + valid = false; + } + + public void render(Frustum frustum, boolean translucent) { + for (CachedRenderingChunk value : chunks.values()) { + value.render(frustum, translucent); + } + } + + public String truncateName(String s) { + return StringUtil.truncateStringIfNecessary(s, glMaxLabelLength, true); + } + + /** + * Retrieves the current instance of the CacheableBERenderingPipeline. + * + * @return The current instance of the CacheableBERenderingPipeline, + * or null if there has no {@link ClientLevel} in current {@link Minecraft} client. + */ + @Nullable + public static CachedBlockEntityRenderingPipeline getInstance() { + return instance; + } + + public void forcedUpdate() { + for (CachedRenderingChunk value : chunks.values()) { + value.forcedUpdate(); + } + } + + public void forcedUpdate(BlockPos pos) { + getRenderRegion(new ChunkPos(pos)).forcedUpdate(); + } + + @ApiStatus.Internal + @SubscribeEvent + public static void on(RenderLevelStageEvent event) { + if (event.getStage() == RenderLevelStageEvent.Stage.AFTER_SKY) { + Vec3 pos = event.getCamera().getPosition(); + if (pos.equals(cameraOldPosition)) { + cameraMoved = false; + return; + } + cameraOldPosition = new Vec3(pos.x, pos.y, pos.z); + cameraMoved = true; + } + } + + @ApiStatus.Internal + @SubscribeEvent + public static void on(RenderFrameEvent.Pre event) { + if (instance != null) { + instance.handleIntegration(); + } + } + + private void handleIntegration() { + // intentionally empty; the Iris mixin injects the shader-toggle invalidation here. + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedRenderingChunk.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedRenderingChunk.java new file mode 100644 index 00000000..f6974199 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedRenderingChunk.java @@ -0,0 +1,275 @@ +package dev.anvilcraft.lib.v2.rendering.cachedber.pipeline; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.ByteBufferBuilder; +import com.mojang.blaze3d.vertex.MeshData; +import com.mojang.blaze3d.vertex.VertexBuffer; +import dev.anvilcraft.lib.v2.rendering.ALRPostEffects; +import dev.anvilcraft.lib.v2.rendering.bloom.BloomPostEffect; +import dev.anvilcraft.lib.v2.rendering.extension.ALRRenderTypeExtension; +import dev.anvilcraft.lib.v2.rendering.foundation.ALRMeshSorting; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.VertexBufferHost; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.ShaderInstance; +import net.minecraft.client.renderer.culling.Frustum; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.joml.Matrix4fStack; +import org.joml.Vector3f; +import org.jspecify.annotations.Nullable; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A 16x16 block region of cached BER content: one {@link VertexBuffer} per {@link RenderType}, rebuilt + * through {@link RebuildTask}s and drawn with frustum culling plus distance-based translucent sorting. + *

+ * Ported from 26.1: {@code GpuBuffer}/{@code RenderPass} become {@link VertexBuffer} + the classic + * {@code RenderSystem} draw path ({@code renderType.setupRenderState()} + {@code ShaderInstance} + + * {@code VertexBuffer.bind()/draw()} — the same pattern {@code LevelRenderer.renderSectionLayer} uses). + * The 26.1 per-type index {@code GpuBuffer}s are dropped: a 1.21.1 {@link VertexBuffer} owns its index + * storage internally ({@code uploadIndexBuffer}), which also holds the sorted indices between frames. + * + * @author ZhuRuoLing + */ +public class CachedRenderingChunk implements VertexBufferHost { + @Getter + private final ChunkPos chunkPos; + private final Map buffers = new HashMap<>(); + private final Map sortBuffers = new HashMap<>(); + @Getter + private final Set blockEntities = new HashSet<>(); + private final CachedBlockEntityRenderingPipeline pipeline; + private final Minecraft minecraft = Minecraft.getInstance(); + private final AABB renderingBB; + private Map meshSorting = new HashMap<>(); + private Reference2IntMap indexCountMap = new Reference2IntOpenHashMap<>(); + @Nullable + @Setter(AccessLevel.PACKAGE) + private RebuildTask lastRebuildTask; + @Getter + @Setter(AccessLevel.PACKAGE) + private boolean isEmpty = true; + private boolean isFreshMesh = true; + + public CachedRenderingChunk(ChunkPos chunkPos, CachedBlockEntityRenderingPipeline pipeline) { + this.chunkPos = chunkPos; + this.pipeline = pipeline; + this.renderingBB = new AABB( + this.chunkPos.getMinBlockX(), + -65, + this.chunkPos.getMinBlockZ(), + this.chunkPos.getMaxBlockX(), + 321, + this.chunkPos.getMaxBlockZ() + ); + } + + /** + * Updates the block entities collection and triggers a rebuild of the region. + * + * @param be The block entity to update. + * @see CachedBlockEntityRenderingPipeline#update(BlockEntity) + */ + public void update(BlockEntity be, boolean forced) { + if (lastRebuildTask != null) { + lastRebuildTask.cancel(); + } + boolean shouldRecompile = blockEntities.removeIf(BlockEntity::isRemoved); + if (be.isRemoved()) { + shouldRecompile |= blockEntities.remove(be); + if (shouldRecompile) { + pipeline.submitCompileTask(new RebuildTask(this)); + } + return; + } + shouldRecompile |= blockEntities.add(be); + if (shouldRecompile || forced) { + pipeline.submitCompileTask(new RebuildTask(this)); + } + } + + /** + * Handles the removal of a block entity from the system and initiates a cache rebuild. + * + * @param be The block entity that has been removed. + * @see CachedBlockEntityRenderingPipeline#blockRemoved(BlockEntity) + */ + public void blockRemoved(BlockEntity be) { + if (lastRebuildTask != null) { + lastRebuildTask.cancel(); + } + boolean removedAny = blockEntities.removeIf(BlockEntity::isRemoved) || blockEntities.remove(be); + if (removedAny) { + pipeline.submitCompileTask(new RebuildTask(this)); + } + } + + public void render(Frustum frustum, boolean translucent) { + if (!frustum.isVisible(renderingBB)) return; + renderInternal(buffers.keySet(), translucent); + } + + @Override + public VertexBuffer getVertexBuffer(RenderType renderType, long size) { + // 1.21.1's VertexBuffer has no size accessor and grows its GL storage on every upload, + // so the 26.1 "reallocate when too small" check is dropped; reusing the instance is enough. + return buffers.computeIfAbsent(renderType, key -> new VertexBuffer(VertexBuffer.Usage.STATIC)); + } + + @Override + public ByteBufferBuilder getSortingByteBufferBuilder(RenderType renderType) { + if (sortBuffers.containsKey(renderType)) { + return sortBuffers.get(renderType); + } + ByteBufferBuilder builder = new ByteBufferBuilder(4096); + sortBuffers.put(renderType, builder); + return builder; + } + + @Override + public void acceptUploadAction(Runnable runnable) { + this.pipeline.submitUploadTask(runnable); + } + + private void renderInternal(Collection renderTypes, boolean translucent) { + if (isEmpty) return; + + Vec3 cameraPosition = minecraft.gameRenderer.getMainCamera().getPosition(); + int renderDistance = Minecraft.getInstance().options.getEffectiveRenderDistance() * 16; + + if (cameraPosition.distanceTo(new Vec3( + chunkPos.x * 16, + cameraPosition.y, + chunkPos.z * 16 + )) > renderDistance) { + return; + } + + renderLayers(renderTypes, cameraPosition, translucent); + } + + private void renderLayers(Collection renderingOrders, Vec3 cameraPosition, boolean translucent) { + for (RenderType renderType : renderingOrders) { + if (renderType.sortOnUpload() != translucent) continue; + VertexBuffer vb = buffers.get(renderType); + if (vb == null) continue; + renderLayer(renderType, vb, cameraPosition); + if (ALRRenderTypeExtension.isRenderingBloomed(renderType)) { + BloomPostEffect bloomPostEffect = ALRPostEffects.getBloomPostEffect(); + if (bloomPostEffect != null) { + bloomPostEffect.beginBloomDraw(); + renderLayer(renderType, vb, cameraPosition); + bloomPostEffect.endBloomDraw(); + } + } + } + } + + public void releaseBuffers() { + buffers.values().forEach(VertexBuffer::close); + buffers.clear(); + sortBuffers.values().forEach(ByteBufferBuilder::close); + sortBuffers.clear(); + } + + private void renderLayer( + RenderType renderType, + VertexBuffer vertexBuffer, + Vec3 cameraPosition + ) { + MeshData.SortState sortState = this.meshSorting.get(renderType); + int indexCount = indexCountMap.getInt(renderType); + + if (indexCount <= 0) return; + + if (sortState != null) { + // Re-sort and re-upload the index buffer whenever the camera moved (or right after a + // compile, since the compile-time sort used an arbitrary sorting origin). + if (isFreshMesh || CachedBlockEntityRenderingPipeline.isCameraMoved()) { + if (isFreshMesh) { + isFreshMesh = false; + } + Vector3f relativePos = cameraPosition.toVector3f().sub( + chunkPos.getMinBlockX(), + 0, + chunkPos.getMinBlockZ() + ); + + ByteBufferBuilder builder = this.getSortingByteBufferBuilder(renderType); + ByteBufferBuilder.Result result = sortState.buildSortedIndexBuffer( + builder, + ALRMeshSorting.byDistance(relativePos) + ); + + if (result != null) { + vertexBuffer.bind(); + // uploadIndexBuffer() takes ownership of (and closes) the result. + vertexBuffer.uploadIndexBuffer(result); + builder.clear(); + } + } + } + + renderType.setupRenderState(); + ShaderInstance shader = RenderSystem.getShader(); + if (shader != null) { + Matrix4fStack modelViewStack = RenderSystem.getModelViewStack(); + modelViewStack.pushMatrix(); + modelViewStack.translate( + -(float) cameraPosition.x + chunkPos.getMinBlockX(), + -(float) cameraPosition.y, + -(float) cameraPosition.z + chunkPos.getMinBlockZ() + ); + RenderSystem.applyModelViewMatrix(); + + shader.setDefaultUniforms( + renderType.mode(), + RenderSystem.getModelViewMatrix(), + RenderSystem.getProjectionMatrix(), + this.minecraft.getWindow() + ); + shader.apply(); + vertexBuffer.bind(); + vertexBuffer.draw(); + shader.clear(); + VertexBuffer.unbind(); + + modelViewStack.popMatrix(); + RenderSystem.applyModelViewMatrix(); + } + renderType.clearRenderState(); + } + + public void forcedUpdate() { + pipeline.submitCompileTask(new RebuildTask(this)); + } + + public void addIfPossible(E blockEntity) { + if (!blockEntities.contains(blockEntity)) { + blockEntities.add(blockEntity); + pipeline.submitCompileTask(new RebuildTask(this)); + } + } + + public void replaceMeshData( + Map meshSorts, + Reference2IntMap indexCountMap + ) { + this.meshSorting = meshSorts; + this.indexCountMap = indexCountMap; + this.isFreshMesh = true; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/RebuildTask.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/RebuildTask.java new file mode 100644 index 00000000..19e35e34 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/RebuildTask.java @@ -0,0 +1,72 @@ +package dev.anvilcraft.lib.v2.rendering.cachedber.pipeline; + +import com.mojang.blaze3d.vertex.PoseStack; +import dev.anvilcraft.lib.v2.rendering.cachedber.renderer.CachedBlockEntityRenderDispatcher; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.FullyBufferedBufferSource; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.jetbrains.annotations.ApiStatus; + +import java.util.ArrayList; + +/** + * Compiles all block entities of a {@link CachedRenderingChunk} into a {@link FullyBufferedBufferSource} + * and uploads the resulting meshes to the chunk's {@link com.mojang.blaze3d.vertex.VertexBuffer}s. + *

+ * Ported from 26.1: the 26.1 feature pipeline ({@code SubmitNodeStorage} + {@code FeatureRenderDispatcher}) + * is replaced by a direct dispatch through {@link CachedBlockEntityRenderDispatcher} into the + * fully-buffered source, matching the single-stage 1.21.1 BER model. + */ +@ApiStatus.Internal +public class RebuildTask implements Runnable { + private final CachedRenderingChunk owner; + private boolean cancelled = false; + + public RebuildTask(CachedRenderingChunk owner) { + this.owner = owner; + } + + @Override + public void run() { + owner.setLastRebuildTask(this); + PoseStack poseStack = new PoseStack(); + owner.setEmpty(true); + FullyBufferedBufferSource bufferSource = new FullyBufferedBufferSource(); + Minecraft minecraft = Minecraft.getInstance(); + for (BlockEntity be : new ArrayList<>(owner.getBlockEntities())) { + if (cancelled) { + bufferSource.close(); + return; + } + + poseStack.pushPose(); + BlockPos pos = be.getBlockPos(); + ChunkPos chunkPos = owner.getChunkPos(); + poseStack.translate( + pos.getX() - chunkPos.getMinBlockX(), + pos.getY(), + pos.getZ() - chunkPos.getMinBlockZ() + ); + + CachedBlockEntityRenderDispatcher.INSTANCE.render( + be, + 0, + poseStack, + bufferSource + ); + + poseStack.popPose(); + } + + owner.setEmpty(bufferSource.isEmpty()); + bufferSource.upload(owner); + owner.replaceMeshData(bufferSource.getMeshSorts(), bufferSource.getIndexCountMap()); + owner.setLastRebuildTask(null); + } + + void cancel() { + cancelled = true; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/package-info.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/package-info.java new file mode 100644 index 00000000..adf26b37 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package dev.anvilcraft.lib.v2.rendering.cachedber.pipeline; + +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderDispatcher.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderDispatcher.java new file mode 100644 index 00000000..8b5c39ae --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderDispatcher.java @@ -0,0 +1,77 @@ +package dev.anvilcraft.lib.v2.rendering.cachedber.renderer; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.logging.LogUtils; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Dispatcher for the {@link CachedBlockEntityRenderer}s registered with the cached BER pipeline. + *

+ * Ported from 26.1: the two-phase {@code tryExtractRenderState}/{@code submit} entry points are replaced + * by a single {@link #render} that mirrors {@code BlockEntityRenderDispatcher} (computing packed light / + * overlay from the level, exactly like the vanilla {@code setupAndRender}). + */ +public class CachedBlockEntityRenderDispatcher { + public static final CachedBlockEntityRenderDispatcher INSTANCE = new CachedBlockEntityRenderDispatcher(); + + private final Map, CachedBlockEntityRenderer> renderers = new HashMap<>(); + private final Logger logger = LogUtils.getLogger(); + + public void registerRenderer( + BlockEntityType type, + CachedBlockEntityRenderer renderer + ) { + CachedBlockEntityRenderer old = renderers.put(type, renderer); + if (old != null) { + logger.warn("Replacing old CachedBlockEntityRenderer {} with {}", old, renderer); + } + } + + public void registerRenderer( + Supplier> type, + CachedBlockEntityRenderer renderer + ) { + registerRenderer(type.get(), renderer); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public void render( + E blockEntity, + float partialTick, + PoseStack poseStack, + MultiBufferSource bufferSource + ) { + CachedBlockEntityRenderer renderer = (CachedBlockEntityRenderer) renderers.get(blockEntity.getType()); + if (renderer == null) { + return; + } + Level level = blockEntity.getLevel(); + int packedLight; + if (level != null) { + BlockPos pos = blockEntity.getBlockPos(); + packedLight = LightTexture.pack( + level.getRawBrightness(pos, level.getSkyDarken()), + level.getRawBrightness(pos.above(), level.getSkyDarken()) + ); + } else { + packedLight = 15728880; + } + renderer.render(blockEntity, partialTick, poseStack, bufferSource, packedLight, OverlayTexture.NO_OVERLAY); + } + + public boolean hasRenderer(BlockEntity be) { + return renderers.containsKey(be.getType()); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderer.java new file mode 100644 index 00000000..6a0b3001 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderer.java @@ -0,0 +1,24 @@ +package dev.anvilcraft.lib.v2.rendering.cachedber.renderer; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.world.level.block.entity.BlockEntity; + +/** + * Single-phase renderer interface for the cached BER pipeline. + *

+ * Ported from 26.1: the original two-phase {@code extractRenderState}/{@code submit} model (driven by + * the 26.1 {@code SubmitNodeCollector} feature system) does not exist on 1.21.1, where block entities + * render through the single {@code BlockEntityRenderDispatcher.render(...)} stage. Renderers therefore + * draw directly into the {@link MultiBufferSource} handed out by {@link dev.anvilcraft.lib.v2.rendering.foundation.buffers.FullyBufferedBufferSource}. + * + * @param the block entity type + */ +public interface CachedBlockEntityRenderer { + /** + * Draws the block entity into {@code bufferSource}. Called once per compile of the containing + * chunk with a {@code partialTick} of 0 and a pose already translated to the block entity's + * position relative to the chunk origin. + */ + void render(T blockEntity, float partialTick, PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, int packedOverlay); +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/package-info.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/package-info.java new file mode 100644 index 00000000..a9665209 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package dev.anvilcraft.lib.v2.rendering.cachedber.renderer; + +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/BufferSlice.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/BufferSlice.java new file mode 100644 index 00000000..0e93f2ae --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/BufferSlice.java @@ -0,0 +1,21 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute; + +import org.jetbrains.annotations.ApiStatus; + +/** + * A slice of a GL buffer, described by the raw buffer handle, byte offset and byte length. + *

+ * Ported from 26.1: the original used {@code com.mojang.blaze3d.buffers.GpuBufferSlice}, which does + * not exist on 1.21.1. The buffer handle is the plain LWJGL buffer name (see + * {@code GlStateManager._glGenBuffers}). + */ +@ApiStatus.Internal +public record BufferSlice(int bufferId, long offset, long length) { + public static BufferSlice of(int bufferId, long length) { + return new BufferSlice(bufferId, 0L, length); + } + + public static BufferSlice of(int bufferId, long offset, long length) { + return new BufferSlice(bufferId, offset, length); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePass.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePass.java new file mode 100644 index 00000000..aee8282c --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePass.java @@ -0,0 +1,100 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline; + +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.BufferSlice; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.ComputeBindingLayout; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.TextureBinding; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.MemoryBarrierFlag; + +import java.util.List; + +/** + * A single compute dispatch session backed by an {@link ALRComputePassBackend}. + *

+ * Ported from 26.1: {@code GpuBufferSlice}/{@code GpuTexture} resources become + * {@link BufferSlice}/plain GL texture ids. + */ +public class ALRComputePass implements AutoCloseable { + private ALRComputePipeline pipeline; + private final ALRComputePassBackend backend; + + public ALRComputePass(ALRComputePassBackend backend) { + this.backend = backend; + } + + public void pushDebugGroup(String name) { + this.backend.pushDebugGroup(name); + } + + public void popDebugGroup(String name) { + this.backend.popDebugGroup(); + } + + public void memoryBarrier(MemoryBarrierFlag... flags) { + this.backend.memoryBarrier(flags); + } + + public void dispatchWorkgroups( + int groupCountX, + int groupCountY, + int groupCountZ + ) { + this.backend.pushDebugGroup("Compute " + pipeline.identifier()); + this.backend.dispatchWorkgroups(groupCountX, groupCountY, groupCountZ); + this.backend.popDebugGroup(); + } + + public void dispatchWorkgroupsIndirect( + BufferSlice buffer + ) { + this.backend.dispatchWorkgroupsIndirect(buffer); + } + + @Override + public void close() { + this.backend.close(); + } + + public void setPipeline(ALRComputePipeline pipeline) { + this.pipeline = pipeline; + this.backend.setPipeline(pipeline); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public void bindAll(List elements) { + int bindingPoint = 0; + for (ComputeBindingLayout binding : pipeline.bindings()) { + this.bind(bindingPoint++, binding, elements.get(bindingPoint - 1)); + } + } + + public void bind(int bindingPoint, ComputeBindingLayout layout, T resource) { + layout.apply(bindingPoint, resource, this); + } + + public void bindTexture(int bindingPoint, TextureBinding.SamplerAndTexture resource) { + this.backend.bindTexture(bindingPoint, resource); + } + + public void bindImage(int bindingPoint, int textureId, boolean read, boolean write) { + this.backend.bindImage(bindingPoint, textureId, read, write); + } + + /** + * Binds a texture as an image with an explicit internal format for {@code glBindImageTexture}. + */ + public void bindImage(int bindingPoint, int textureId, boolean read, boolean write, int internalFormat) { + this.backend.bindImage(bindingPoint, textureId, read, write, internalFormat); + } + + public void bindUniformBlock(int bindingPoint, BufferSlice resource) { + this.backend.bindUniformBlock(bindingPoint, resource); + } + + public void bindShaderStorage(int bindingPoint, BufferSlice resource) { + this.backend.bindShaderStorage(bindingPoint, resource); + } + + public void bindAtomicCounter(int bindingPoint, BufferSlice resource) { + this.backend.bindAtomicCounter(bindingPoint, resource); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePassBackend.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePassBackend.java new file mode 100644 index 00000000..e8732afe --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePassBackend.java @@ -0,0 +1,39 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline; + +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.BufferSlice; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.TextureBinding; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.MemoryBarrierFlag; + +/** + * Backend contract for {@link ALRComputePass}. + *

+ * Ported from 26.1: {@code GpuBufferSlice}/{@code GpuTexture} resources become + * {@link BufferSlice}/plain GL texture ids. + */ +public interface ALRComputePassBackend { + void setPipeline(ALRComputePipeline pipeline); + + void pushDebugGroup(String name); + + void popDebugGroup(); + + void dispatchWorkgroups(int groupCountX, int groupCountY, int groupCountZ); + + void dispatchWorkgroupsIndirect(BufferSlice buffer); + + void memoryBarrier(MemoryBarrierFlag... flags); + + void bindTexture(int bindingPoint, TextureBinding.SamplerAndTexture resource); + + void bindImage(int bindingPoint, int textureId, boolean read, boolean write); + + void bindImage(int bindingPoint, int textureId, boolean read, boolean write, int internalFormat); + + void bindUniformBlock(int bindingPoint, BufferSlice resource); + + void bindShaderStorage(int bindingPoint, BufferSlice resource); + + void bindAtomicCounter(int bindingPoint, BufferSlice resource); + + void close(); +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePipeline.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePipeline.java new file mode 100644 index 00000000..33277641 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePipeline.java @@ -0,0 +1,116 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline; + +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.AtomicCounterBinding; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.ComputeBindingLayout; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.ImageBinding; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.ShaderStorageBinding; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.TextureBinding; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.UniformBlockBinding; +import net.minecraft.resources.ResourceLocation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Describes a compute pipeline: its identifier, shader location, preprocessor defines and binding layout. + *

+ * Ported from 26.1: {@code Identifier} becomes {@link ResourceLocation} and the 26.1 + * {@code ShaderDefines} becomes a plain {@link Map}{@code } of preprocessor defines. + */ +public record ALRComputePipeline( + ResourceLocation identifier, + List> bindings, + ResourceLocation shaderLocation, + Map defines +) { + + public static Builder builder() { + return new Builder(); + } + + public static Builder builder(ALRComputePipeline pipeline) { + return builder().withPipeline(pipeline); + } + + public static class Builder { + private final List> bindings = new ArrayList<>(); + private ResourceLocation name; + private ResourceLocation shaderLocation; + private Map defines = Map.of(); + + public Builder withPipeline(ALRComputePipeline pipeline) { + this.bindings.clear(); + this.bindings.addAll(pipeline.bindings()); + this.shaderLocation = pipeline.shaderLocation(); + this.defines = pipeline.defines(); + return this; + } + + public Builder withShader(ResourceLocation shaderLocation) { + this.shaderLocation = shaderLocation; + return this; + } + + public Builder withName(ResourceLocation name) { + this.name = name; + return this; + } + + public Builder withShaderLocation(ResourceLocation shaderLocation) { + return this.withShader(shaderLocation); + } + + public Builder withDefines(Map defines) { + this.defines = defines; + return this; + } + + public Builder withBinding(ComputeBindingLayout binding) { + this.bindings.add(binding); + return this; + } + + public Builder withTexture(String name) { + return this.withBinding(new TextureBinding(name)); + } + + public Builder withImage(String name, boolean read, boolean write) { + return this.withBinding(new ImageBinding(name, read, write)); + } + + public Builder withReadOnlyImage(String name) { + return this.withImage(name, true, false); + } + + public Builder withWriteOnlyImage(String name) { + return this.withImage(name, false, true); + } + + public Builder withReadWriteImage(String name) { + return this.withImage(name, true, true); + } + + public Builder withUniformBlock(String name) { + return this.withBinding(new UniformBlockBinding(name)); + } + + public Builder withShaderStorage(String name) { + return this.withBinding(new ShaderStorageBinding(name)); + } + + public Builder withAtomicCounter(String name) { + return this.withBinding(new AtomicCounterBinding(name)); + } + + public ALRComputePipeline build() { + return new ALRComputePipeline( + Objects.requireNonNull(this.name, "name"), + this.bindings, + Objects.requireNonNull(this.shaderLocation, "shaderLocation"), + this.defines + ); + } + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/AtomicCounterBinding.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/AtomicCounterBinding.java new file mode 100644 index 00000000..33955530 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/AtomicCounterBinding.java @@ -0,0 +1,19 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings; + +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.BufferSlice; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ShaderResourceType; + +public record AtomicCounterBinding( + String name +) implements ComputeBindingLayout { + @Override + public ShaderResourceType type() { + return ShaderResourceType.ATOMIC_COUNTER; + } + + @Override + public void apply(int bindingPoint, BufferSlice resource, ALRComputePass computePass) { + computePass.bindAtomicCounter(bindingPoint, resource); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ComputeBindingLayout.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ComputeBindingLayout.java new file mode 100644 index 00000000..b9bbb054 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ComputeBindingLayout.java @@ -0,0 +1,12 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings; + +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ShaderResourceType; + +public interface ComputeBindingLayout { + ShaderResourceType type(); + + String name(); + + void apply(int bindingPoint, T resource, ALRComputePass computePass); +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ImageBinding.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ImageBinding.java new file mode 100644 index 00000000..be49104a --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ImageBinding.java @@ -0,0 +1,34 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings; + +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ShaderResourceType; + +/** + * Binds a GL texture (given by its texture name) as an image for read/write access. + *

+ * Ported from 26.1: {@code GpuTexture} becomes a plain GL texture id. The default internal format + * used for {@code glBindImageTexture} is {@code GL_RGBA8}; {@link ALRComputePass#bindImage} offers + * an overload accepting an explicit internal format. + */ +public record ImageBinding( + String name, + boolean read, + boolean write +) implements ComputeBindingLayout { + + public ImageBinding { + if (!read && !write) { + throw new IllegalArgumentException("ImageResource does not allow both read and write are false"); + } + } + + @Override + public ShaderResourceType type() { + return ShaderResourceType.IMAGE; + } + + @Override + public void apply(int bindingPoint, Integer resource, ALRComputePass computePass) { + computePass.bindImage(bindingPoint, resource, read, write); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ShaderStorageBinding.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ShaderStorageBinding.java new file mode 100644 index 00000000..4d5c8b1a --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ShaderStorageBinding.java @@ -0,0 +1,19 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings; + +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.BufferSlice; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ShaderResourceType; + +public record ShaderStorageBinding( + String name +) implements ComputeBindingLayout { + @Override + public ShaderResourceType type() { + return ShaderResourceType.SHADER_STORAGE; + } + + @Override + public void apply(int bindingPoint, BufferSlice resource, ALRComputePass computePass) { + computePass.bindShaderStorage(bindingPoint, resource); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/TextureBinding.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/TextureBinding.java new file mode 100644 index 00000000..4227f5c4 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/TextureBinding.java @@ -0,0 +1,33 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings; + +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ShaderResourceType; + +/** + * Binds a GL texture (by texture name) together with an optional GL sampler (0 = default sampler). + *

+ * Ported from 26.1: {@code GpuSampler}/{@code GpuTexture} become plain GL ids; the sampler is + * optional since 1.21.1's {@code GlStateManager} exposes no sampler management API. + */ +public record TextureBinding( + String name +) implements ComputeBindingLayout { + @Override + public ShaderResourceType type() { + return ShaderResourceType.TEXTURE; + } + + @Override + public void apply(int bindingPoint, SamplerAndTexture resource, ALRComputePass pass) { + pass.bindTexture(bindingPoint, resource); + } + + public record SamplerAndTexture( + int samplerId, + int textureId + ) { + public SamplerAndTexture(int textureId) { + this(0, textureId); + } + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/UniformBlockBinding.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/UniformBlockBinding.java new file mode 100644 index 00000000..e1d86638 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/UniformBlockBinding.java @@ -0,0 +1,19 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings; + +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.BufferSlice; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ShaderResourceType; + +public record UniformBlockBinding( + String name +) implements ComputeBindingLayout { + @Override + public ShaderResourceType type() { + return ShaderResourceType.UNIFORM_BLOCK; + } + + @Override + public void apply(int bindingPoint, BufferSlice resource, ALRComputePass computePass) { + computePass.bindUniformBlock(bindingPoint, resource); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/gl/GlComputePassBackend.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/gl/GlComputePassBackend.java new file mode 100644 index 00000000..d44beb72 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/gl/GlComputePassBackend.java @@ -0,0 +1,215 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.gl; + +import com.mojang.blaze3d.platform.GlStateManager; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.MemoryBarrierFlag; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.BufferSlice; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePassBackend; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePipeline; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.TextureBinding; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ALRComputeProgramInstance; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ALRComputeShaderManager; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import org.jetbrains.annotations.ApiStatus; +import org.lwjgl.opengl.ARBComputeShader; +import org.lwjgl.opengl.ARBShaderAtomicCounters; +import org.lwjgl.opengl.ARBShaderImageLoadStore; +import org.lwjgl.opengl.ARBShaderStorageBufferObject; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL31; +import org.lwjgl.opengl.GL33; +import org.lwjgl.opengl.KHRDebug; + +/** + * Standalone GL backend for {@link dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass}. + *

+ * Ported from 26.1 (see #P7e): the 26.1 mixin targets ({@code GpuDevice}/{@code CommandEncoder}/ + * {@code GlDevice}/{@code GlCommandEncoder}/{@code DirectStateAccess}/{@code GlDebugLabel}) do not + * exist on 1.21.1, so this class owns the program/buffer state machine directly. All GL capability + * entry points used here are available in the 1.21.1 LWJGL; the program compile/link logic lives in + * {@link ALRComputeShaderManager}. + */ +@ApiStatus.Internal +public class GlComputePassBackend implements ALRComputePassBackend { + private static final int DEFAULT_IMAGE_FORMAT = GL11.GL_RGBA8; + + private final Int2ObjectMap textureBindings = new Int2ObjectOpenHashMap<>(); + private final Int2ObjectMap imageBindings = new Int2ObjectOpenHashMap<>(); + private final Int2ObjectMap uniformBlockBindings = new Int2ObjectOpenHashMap<>(); + private final Int2ObjectMap shaderStorageBindings = new Int2ObjectOpenHashMap<>(); + private final Int2ObjectMap atomicCounterBindings = new Int2ObjectOpenHashMap<>(); + private ALRComputePipeline pipeline = null; + + public GlComputePassBackend() { + } + + @Override + public void setPipeline(ALRComputePipeline pipeline) { + this.pipeline = pipeline; + } + + @Override + public void pushDebugGroup(String name) { + if (org.lwjgl.opengl.GL.getCapabilities().GL_KHR_debug) { + KHRDebug.glPushDebugGroup(KHRDebug.GL_DEBUG_SOURCE_APPLICATION, 0, name); + } + } + + @Override + public void popDebugGroup() { + if (org.lwjgl.opengl.GL.getCapabilities().GL_KHR_debug) { + KHRDebug.glPopDebugGroup(); + } + } + + @Override + public void dispatchWorkgroups(int groupCountX, int groupCountY, int groupCountZ) { + this.setupState(); + ARBComputeShader.glDispatchCompute(groupCountX, groupCountY, groupCountZ); + } + + @Override + public void dispatchWorkgroupsIndirect(BufferSlice buffer) { + this.setupState(); + GlStateManager._glBindBuffer(ARBComputeShader.GL_DISPATCH_INDIRECT_BUFFER, buffer.bufferId()); + ARBComputeShader.glDispatchComputeIndirect(buffer.offset()); + } + + @Override + public void memoryBarrier(MemoryBarrierFlag... flags) { + ARBShaderImageLoadStore.glMemoryBarrier(MemoryBarrierFlag.compound(flags)); + } + + @Override + public void bindTexture(int bindingPoint, TextureBinding.SamplerAndTexture resource) { + this.textureBindings.put(bindingPoint, resource); + } + + @Override + public void bindImage(int bindingPoint, int textureId, boolean read, boolean write) { + this.bindImage(bindingPoint, textureId, read, write, DEFAULT_IMAGE_FORMAT); + } + + @Override + public void bindImage(int bindingPoint, int textureId, boolean read, boolean write, int internalFormat) { + if (!read && !write) { + throw new IllegalArgumentException("ImageResource does not allow both read and write are false"); + } + this.imageBindings.put(bindingPoint, new ImageState(textureId, read, write, internalFormat)); + } + + @Override + public void bindUniformBlock(int bindingPoint, BufferSlice resource) { + this.uniformBlockBindings.put(bindingPoint, resource); + } + + @Override + public void bindShaderStorage(int bindingPoint, BufferSlice resource) { + this.shaderStorageBindings.put(bindingPoint, resource); + } + + @Override + public void bindAtomicCounter(int bindingPoint, BufferSlice resource) { + this.atomicCounterBindings.put(bindingPoint, resource); + } + + @Override + public void close() { + + } + + private void setupState() { + ALRComputeProgramInstance program = ALRComputeShaderManager.INSTANCE.getShader(pipeline); + if (program == null || program == ALRComputeProgramInstance.INVALID) return; + GL20.glUseProgram(program.id()); + + for (Int2ObjectMap.Entry entry : this.textureBindings.int2ObjectEntrySet()) { + this.setupTexture(entry.getIntKey(), entry.getValue()); + } + for (Int2ObjectMap.Entry entry : this.imageBindings.int2ObjectEntrySet()) { + this.setupImage(entry.getIntKey(), entry.getValue()); + } + for (Int2ObjectMap.Entry entry : this.uniformBlockBindings.int2ObjectEntrySet()) { + this.setupUniformBlock(entry.getIntKey(), entry.getValue()); + } + for (Int2ObjectMap.Entry entry : this.shaderStorageBindings.int2ObjectEntrySet()) { + this.setupShaderStorage(entry.getIntKey(), entry.getValue()); + } + for (Int2ObjectMap.Entry entry : this.atomicCounterBindings.int2ObjectEntrySet()) { + this.setupAtomicCounter(entry.getIntKey(), entry.getValue()); + } + } + + private void setupTexture(int bindingPoint, TextureBinding.SamplerAndTexture resource) { + GlStateManager._activeTexture(GL13.GL_TEXTURE0 + bindingPoint); + GlStateManager._bindTexture(resource.textureId()); + if (resource.samplerId() != 0) { + GL33.glBindSampler(bindingPoint, resource.samplerId()); + } + } + + private void setupImage(int bindingPoint, ImageState state) { + int access = 0; + if (state.read() && state.write()) { + access = GL15.GL_READ_WRITE; + } else { + if (state.read()) { + access |= GL15.GL_READ_ONLY; + } + if (state.write()) { + access |= GL15.GL_WRITE_ONLY; + } + } + + ARBShaderImageLoadStore.glBindImageTexture( + bindingPoint, + state.textureId(), + 0, + false, + 0, + access, + state.internalFormat() + ); + } + + private void setupUniformBlock(int bindingPoint, BufferSlice resource) { + GL31.glBindBufferRange( + GL31.GL_UNIFORM_BUFFER, + bindingPoint, + resource.bufferId(), + resource.offset(), + resource.length() + ); + } + + private void setupShaderStorage(int bindingPoint, BufferSlice resource) { + GL31.glBindBufferRange( + ARBShaderStorageBufferObject.GL_SHADER_STORAGE_BUFFER, + bindingPoint, + resource.bufferId(), + resource.offset(), + resource.length() + ); + } + + private void setupAtomicCounter(int bindingPoint, BufferSlice resource) { + GL31.glBindBufferRange( + ARBShaderAtomicCounters.GL_ATOMIC_COUNTER_BUFFER, + bindingPoint, + resource.bufferId(), + resource.offset(), + resource.length() + ); + } + + private record ImageState( + int textureId, + boolean read, + boolean write, + int internalFormat + ) { + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstance.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstance.java new file mode 100644 index 00000000..2b0e9b24 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstance.java @@ -0,0 +1,20 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader; + +import dev.anvilcraft.lib.v2.rendering.AnvilLibRendering; +import net.minecraft.resources.ResourceLocation; + +import java.util.Map; + +/** + * A compiled GL compute program identified by its program id and the key it was compiled from. + */ +public record ALRComputeProgramInstance(int id, ALRComputeProgramInstanceKey key) { + public static final ALRComputeProgramInstance INVALID = new ALRComputeProgramInstance( + 0, + new ALRComputeProgramInstanceKey( + AnvilLibRendering.location("compute/invalid"), + Map.of() + ) + ); + +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstanceKey.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstanceKey.java new file mode 100644 index 00000000..e947c5ca --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstanceKey.java @@ -0,0 +1,13 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader; + +import net.minecraft.resources.ResourceLocation; + +import java.util.Map; + +/** + * Key identifying a compute program: shader location plus preprocessor defines. + *

+ * Ported from 26.1: the 26.1 {@code ShaderDefines} becomes a simple {@link Map}{@code }. + */ +public record ALRComputeProgramInstanceKey(ResourceLocation location, Map defines) { +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeShaderManager.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeShaderManager.java new file mode 100644 index 00000000..4f62c6a6 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeShaderManager.java @@ -0,0 +1,205 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader; + +import com.google.common.collect.ImmutableMap; +import com.mojang.blaze3d.preprocessor.GlslPreprocessor; +import com.mojang.logging.LogUtils; +import dev.anvilcraft.lib.v2.rendering.event.RegisterComputePipelinesEvent; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.ALRComputeCapabilities; +import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePipeline; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import net.neoforged.fml.ModLoader; +import org.apache.commons.io.IOUtils; +import org.jetbrains.annotations.UnknownNullability; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.lwjgl.opengl.ARBComputeShader; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.KHRDebug; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.Reader; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Loads compute shader sources from the resource manager, compiles them into GL compute programs + * and caches the results per pipeline. + *

+ * Ported from 26.1 (see #P7e): the reload mechanism ({@link SimplePreparableReloadListener}) and the + * resource scanning logic are preserved; registration happens through + * {@code RegisterClientReloadListenersEvent}. {@code ShaderManager.createPreprocessor} (26.1) is + * replaced by an inline {@link GlslPreprocessor} resolving {@code #moj_import} the same way the + * 1.21.1 core shader pipeline does. The 26.1 {@code GpuDevice}-backed compile/destroy logic is + * inlined with raw LWJGL calls. + */ +@Slf4j +public class ALRComputeShaderManager extends SimplePreparableReloadListener { + private static final Logger LOGGER = LogUtils.getLogger(); + public static final ALRComputeShaderManager INSTANCE = new ALRComputeShaderManager(); + + @UnknownNullability + private ComputeShaderSource source = null; + + private final Map shaderInstanceMap = new HashMap<>(); + private final Map pipelineToProgramMap = new HashMap<>(); + + @Override + protected @NonNull ComputeShaderSource prepare(ResourceManager manager, ProfilerFiller profiler) { + ImmutableMap.Builder sources = new ImmutableMap.Builder<>(); + + Map shaders = manager.listResources( + "shaders", + it -> it.getPath().endsWith(".csh") || it.getPath().endsWith(".compute") || it.getPath().endsWith(".glsl") + ); + + for (Map.Entry it : shaders.entrySet()) { + GlslPreprocessor preprocessor = createPreprocessor(manager, it.getKey()); + + try (Reader reader = it.getValue().openAsReader()) { + String source = IOUtils.toString(reader); + sources.put(it.getKey(), String.join("\n", preprocessor.process(source))); + } catch (IOException ex) { + log.error("Failed to load compute shader source at {}", it.getKey(), ex); + } + } + log.info("Loaded {} compute shader.", shaders.size()); + return new ComputeShaderSource(sources.build()); + } + + @Override + protected void apply(ComputeShaderSource preparations, ResourceManager manager, ProfilerFiller profiler) { + ALRComputeCapabilities.init(); + if (!ALRComputeCapabilities.isComputeSupported()) { + return; + } + for (ALRComputeProgramInstance value : shaderInstanceMap.values()) { + destroyComputeShader(value); + } + shaderInstanceMap.clear(); + pipelineToProgramMap.clear(); + + this.source = preparations; + RegisterComputePipelinesEvent event = new RegisterComputePipelinesEvent<>(); + ModLoader.postEvent(event); + + for (ALRComputePipeline pipeline : event.getPipelines()) { + ALRComputeProgramInstanceKey key = new ALRComputeProgramInstanceKey( + pipeline.shaderLocation(), + pipeline.defines() + ); + log.debug("Compiled COMPUTE shader {}", pipeline.shaderLocation()); + ALRComputeProgramInstance instance = compileComputeShader(key); + this.shaderInstanceMap.put(key, instance); + this.pipelineToProgramMap.put(pipeline, instance); + } + } + + public String getSource(ResourceLocation location) { + return source.source.get(location.withPrefix("shaders/")); + } + + @Nullable + public ALRComputeProgramInstance getShader(ALRComputeProgramInstanceKey location) { + return this.shaderInstanceMap.get(location); + } + + @Nullable + public ALRComputeProgramInstance getShader(ALRComputePipeline pipeline) { + return this.pipelineToProgramMap.get(pipeline); + } + + private void destroyComputeShader(ALRComputeProgramInstance instance) { + if (instance.id() != 0) { + GL20.glDeleteProgram(instance.id()); + } + } + + /** + * Compiles a GL compute program from the preprocessed source of the given key. + *

+ * The compile/link sequence is ported line by line from the 26.1 {@code GlDeviceMixin}; + * the debug label application replaces the 26.1 {@code GlDebugLabel} mixins. + */ + private ALRComputeProgramInstance compileComputeShader(ALRComputeProgramInstanceKey instanceKey) { + String source = getSource(instanceKey.location()); + if (source == null) { + LOGGER.error("Could not find COMPUTE shader source for {}", instanceKey.location()); + return ALRComputeProgramInstance.INVALID; + } + int shaderId = GL20.glCreateShader(ARBComputeShader.GL_COMPUTE_SHADER); + GL20.glShaderSource(shaderId, source); + GL20.glCompileShader(shaderId); + if (GL20.glGetShaderi(shaderId, GL20.GL_COMPILE_STATUS) == 0) { + String infoLog = GL20.glGetShaderInfoLog(shaderId); + LOGGER.error("Could not compile COMPUTE shader {}: {}", instanceKey.location(), infoLog); + GL20.glDeleteShader(shaderId); + return ALRComputeProgramInstance.INVALID; + } + int program = GL20.glCreateProgram(); + GL20.glAttachShader(program, shaderId); + GL20.glLinkProgram(program); + if (GL20.glGetProgrami(program, GL20.GL_LINK_STATUS) == 0) { + String infoLog = GL20.glGetProgramInfoLog(shaderId); + LOGGER.error("Could not link COMPUTE shader {}: {}", instanceKey.location(), infoLog); + GL20.glDeleteShader(shaderId); + GL20.glDeleteProgram(program); + return ALRComputeProgramInstance.INVALID; + } + GL20.glDeleteShader(shaderId); + ALRComputeProgramInstance instance = new ALRComputeProgramInstance(program, instanceKey); + applyDebugLabel(instance); + return instance; + } + + private void applyDebugLabel(ALRComputeProgramInstance shaderInstance) { + if (!GL.getCapabilities().GL_KHR_debug) { + return; + } + KHRDebug.glObjectLabel( + KHRDebug.GL_PROGRAM, + shaderInstance.id(), + shaderInstance.key().location().toString() + ); + } + + /** + * A minimal GLSL preprocessor for compute shaders: resolves {@code #moj_import<...>} the same + * way the 1.21.1 core shader pipeline does ({@code ClientHooks.getShaderImportLocation}). + */ + private static GlslPreprocessor createPreprocessor(ResourceManager manager, ResourceLocation base) { + return new GlslPreprocessor() { + private final Set importedPaths = new HashSet<>(); + + @Override + public String applyImport(boolean useFullPath, String directory) { + ResourceLocation resourcelocation = net.neoforged.neoforge.client.ClientHooks.getShaderImportLocation(base.toString(), useFullPath, directory); + if (!this.importedPaths.add(resourcelocation.toString())) { + return null; + } else { + try { + try (Reader reader = manager.openAsReader(resourcelocation)) { + return IOUtils.toString(reader); + } + } catch (IOException ioexception) { + LOGGER.error("Could not open GLSL import {}: {}", resourcelocation, ioexception.getMessage()); + return "#error " + ioexception.getMessage(); + } + } + } + }; + } + + public record ComputeShaderSource( + Map source + ) { + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ShaderResourceType.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ShaderResourceType.java new file mode 100644 index 00000000..e615402c --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ShaderResourceType.java @@ -0,0 +1,9 @@ +package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader; + +public enum ShaderResourceType { + TEXTURE, + IMAGE, + UNIFORM_BLOCK, + SHADER_STORAGE, + ATOMIC_COUNTER +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/FullyBufferedBufferSource.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/FullyBufferedBufferSource.java new file mode 100644 index 00000000..9cf4404f --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/FullyBufferedBufferSource.java @@ -0,0 +1,128 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.ByteBufferBuilder; +import com.mojang.blaze3d.vertex.MeshData; +import com.mojang.blaze3d.vertex.VertexBuffer; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import lombok.Getter; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.HashMap; +import java.util.Map; + +/** + * A {@link MultiBufferSource.BufferSource} that never draws: it accumulates one {@link BufferBuilder} + * per {@link RenderType} and hands the finished {@link MeshData} to a {@link VertexBufferHost} through + * {@link #upload(VertexBufferHost)}. + *

+ * Ported from 26.1: the meshes used to be copied into {@code GpuBuffer}s through a + * {@code CommandEncoder}; on 1.21.1 they are uploaded into {@link VertexBuffer}s on the render thread + * (the upload runs via {@link VertexBufferHost#acceptUploadAction}, executed by the cached-BER pipeline + * at {@code RenderFrameEvent.Pre}). The per-type {@code ByteBufferBuilder}s (786432 bytes each) must be + * large enough for the whole compiled chunk layer. + */ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +public class FullyBufferedBufferSource extends MultiBufferSource.BufferSource implements AutoCloseable { + private final Map byteBuffers = new HashMap<>(); + private final Map bufferBuilders = new HashMap<>(); + @Getter + private final Reference2IntMap indexCountMap = new Reference2IntOpenHashMap<>(); + @Getter + private final Map meshSorts = new HashMap<>(); + + public FullyBufferedBufferSource() { + // The parent's shared/fixed buffers are never touched: getBuffer/endBatch are fully overridden. + super(null, null); + } + + private ByteBufferBuilder getByteBuffer(RenderType renderType) { + return byteBuffers.computeIfAbsent(renderType, key -> new ByteBufferBuilder(786432)); + } + + @Override + public VertexConsumer getBuffer(RenderType renderType) { + return bufferBuilders.computeIfAbsent( + renderType, + it -> new BufferBuilder( + getByteBuffer(renderType), + it.mode(), + it.format() + ) + ); + } + + /** + * @return whether no render type was requested while compiling this batch. A render type whose + * builder ended up with zero vertices is not reported here (it yields an empty mesh and is + * skipped during upload), so an exact "no vertices anywhere" answer would require per-type + * vertex counters that 1.21.1's {@link BufferBuilder} does not expose. + */ + public boolean isEmpty() { + return bufferBuilders.isEmpty(); + } + + @Override + public void endBatch(RenderType renderType) { + } + + @Override + public void endLastBatch() { + } + + @Override + public void endBatch() { + } + + public void upload(VertexBufferHost host) { + for (RenderType renderType : bufferBuilders.keySet()) { + host.acceptUploadAction(() -> uploadNow(host, renderType)); + } + } + + @SuppressWarnings("resource") + private void uploadNow(VertexBufferHost host, RenderType renderType) { + BufferBuilder bufferBuilder = bufferBuilders.get(renderType); + ByteBufferBuilder byteBuffer = byteBuffers.get(renderType); + MeshData mesh = bufferBuilder.build(); + if (mesh != null) { + indexCountMap.put(renderType, mesh.drawState().indexCount()); + if (renderType.sortOnUpload()) { + MeshData.SortState sortState = mesh.sortQuads( + host.getSortingByteBufferBuilder(renderType), + RenderSystem.getVertexSorting() + ); + meshSorts.put(renderType, sortState); + } + + VertexBuffer vertexBuffer = host.getVertexBuffer( + renderType, + (long) mesh.drawState().vertexCount() * mesh.drawState().format().getVertexSize() + ); + vertexBuffer.bind(); + vertexBuffer.upload(mesh); + } else { + indexCountMap.put(renderType, 0); + meshSorts.remove(renderType); + } + byteBuffer.close(); + bufferBuilders.remove(renderType); + byteBuffers.remove(renderType); + } + + public void close(RenderType renderType) { + ByteBufferBuilder builder = byteBuffers.get(renderType); + builder.close(); + } + + public void close() { + byteBuffers.keySet().forEach(this::close); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/VertexBufferHost.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/VertexBufferHost.java new file mode 100644 index 00000000..9ee981d0 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/VertexBufferHost.java @@ -0,0 +1,19 @@ +package dev.anvilcraft.lib.v2.rendering.foundation.buffers; + +import com.mojang.blaze3d.vertex.ByteBufferBuilder; +import com.mojang.blaze3d.vertex.VertexBuffer; +import net.minecraft.client.renderer.RenderType; + +/** + * Receives the compiled meshes of a {@link FullyBufferedBufferSource} (see {@link #acceptUploadAction}). + *

+ * Ported from 26.1: the original returned {@code GpuBuffer}s; on 1.21.1 the host hands out + * {@link VertexBuffer}s, which own both the vertex storage and the (possibly sorted) index storage. + */ +public interface VertexBufferHost { + VertexBuffer getVertexBuffer(RenderType renderType, long size); + + ByteBufferBuilder getSortingByteBufferBuilder(RenderType renderType); + + void acceptUploadAction(Runnable runnable); +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchParametersUbo.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchParametersUbo.java new file mode 100644 index 00000000..b991a51b --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchParametersUbo.java @@ -0,0 +1,32 @@ +package dev.anvilcraft.lib.v2.rendering.glitch; + +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutEntry; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.ShaderBufferObjectUsage; +import lombok.Getter; +import lombok.Setter; +import org.joml.Vector2f; + +@Getter +@Setter +public class GlitchParametersUbo extends BufferObject { + + public static final BufferObjectLayoutDefinition DEFINITION = BufferObjectLayoutDefinition.create( + BufferObjectLayoutEntry.ofVec2f().forGetter(GlitchParametersUbo::getInSize).build(), + BufferObjectLayoutEntry.ofFloat().forGetter(GlitchParametersUbo::getGameTime).build() + ); + + private Vector2f inSize = new Vector2f(); + private float gameTime = 0; + + protected GlitchParametersUbo() { + super(BufferLayout.STD140, ShaderBufferObjectUsage.UBO); + } + + @Override + protected BufferObjectLayoutDefinition getDefinition() { + return DEFINITION; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchPostEffect.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchPostEffect.java new file mode 100644 index 00000000..657d6f45 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchPostEffect.java @@ -0,0 +1,83 @@ +package dev.anvilcraft.lib.v2.rendering.glitch; + +import com.mojang.blaze3d.pipeline.RenderTarget; +import com.mojang.blaze3d.pipeline.TextureTarget; +import com.mojang.blaze3d.vertex.MeshData; +import dev.anvilcraft.lib.v2.rendering.bloom.TransformsUbo; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.postprocess.ALRPostEffectShaders; +import dev.anvilcraft.lib.v2.rendering.postprocess.GlPostProcess; +import dev.anvilcraft.lib.v2.rendering.postprocess.GlUniformBuffer; +import lombok.Getter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ShaderInstance; +import org.joml.Matrix4f; + +/** + * Time-driven glitch post-processing effect. + *

+ * Ported from 26.1 (see #P7b): {@code process(GpuTextureView)} becomes + * {@code process(RenderTarget, int, int)} returning a {@link RenderTarget}; the 26.1 + * {@code DeltaTracker} is replaced by {@code Minecraft.getTimer().getGameTimeDeltaPartialTick(false)}. + * The shader and its std140 layout are unchanged. + */ +public class GlitchPostEffect { + public static final int UNIFORM_GLITCH_SIZE = GlitchParametersUbo.DEFINITION.size(BufferLayout.STD140); + public static final int UNIFORM_TRANSFORM_SIZE = TransformsUbo.DEFINITION.size(BufferLayout.STD140); + + @Getter + private final RenderTarget glitchOutputTarget = new TextureTarget(854, 480, false, false); + + private final GlUniformBuffer transformUBO = new GlUniformBuffer(UNIFORM_TRANSFORM_SIZE); + private final GlUniformBuffer glitchParameterUBO = new GlUniformBuffer(UNIFORM_GLITCH_SIZE); + + private final TransformsUbo transform = new TransformsUbo(new Matrix4f()); + private final GlitchParametersUbo glitchParameters = new GlitchParametersUbo(); + private int lastWidth = 0; + private int lastHeight = 0; + private MeshData quadMesh; + + public RenderTarget process(RenderTarget input, int width, int height) { + if (glitchOutputTarget.width < width || glitchOutputTarget.height < height) { + glitchOutputTarget.resize(width, height, false); + } + + Minecraft minecraft = Minecraft.getInstance(); + long gameTime = minecraft.level.getGameTime(); + float partialTick = minecraft.getTimer().getGameTimeDeltaPartialTick(false); + + if (lastHeight != height || lastWidth != width) { + this.lastWidth = width; + this.lastHeight = height; + this.quadMesh = GlPostProcess.buildQuad(width, height, true); + } + + transform.getProjMat().setOrtho( + 0, + glitchOutputTarget.width, + 0, + glitchOutputTarget.height, + -10000, + 10000 + ); + glitchParameters.getInSize().set(width, height); + float uGameTime = (float) (gameTime % 24000L) + partialTick; + uGameTime /= 10f; + glitchParameters.setGameTime(uGameTime); + + transformUBO.upload(transform); + glitchParameterUBO.upload(glitchParameters); + + ShaderInstance shader = ALRPostEffectShaders.getGlitchShader(); + if (shader == null) { + return glitchOutputTarget; + } + GlPostProcess.beginTarget(glitchOutputTarget); + shader.setSampler("DiffuseSampler", input); + GlPostProcess.draw(shader, quadMesh, s -> { + transformUBO.bindTo(s.getId(), "Transforms", 0); + glitchParameterUBO.bindTo(s.getId(), "GlitchParameters", 1); + }); + return glitchOutputTarget; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/IrisSupport.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/IrisSupport.java new file mode 100644 index 00000000..dccfe738 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/IrisSupport.java @@ -0,0 +1,156 @@ +package dev.anvilcraft.lib.v2.rendering.integration; + +import net.neoforged.fml.ModList; +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Stack; + +/** + * Optional integration with Iris (shader mod) for the cached BER pipeline. + *

+ * Ported from 26.1: the API shape ({@code net.irisshaders.iris.api.v0.IrisApi} + + * {@code net.irisshaders.iris.vertices.ImmediateState}) is identical in 1.21.1-era Iris (verified + * against 1.8.12+1.21.1-neoforge). The dev branch used a {@code compileOnly} dependency from the + * Modrinth maven, which has been shut down (the 1.21.1 Iris coordinate can no longer be resolved), so + * the Iris classes are accessed through reflection instead: the integration compiles without Iris and + * activates at runtime only when Iris is actually loaded. The "oculus" branch does not exist on 1.21.1 + * (kept for API parity, always false). + */ +public class IrisSupport { + private static final boolean IRIS_PRESENT; + private static final Stack irisStateStack = new Stack<>(); + + @Nullable private static Method isShaderPackInUseMethod; + @Nullable private static Object irisApiInstance; + @Nullable private static Field isRenderingLevelField; + @Nullable private static Field skipExtensionField; + + static { + boolean present = ModList.get().isLoaded("iris") || ModList.get().isLoaded("oculus"); + if (present) { + try { + Class irisApiClass = Class.forName("net.irisshaders.iris.api.v0.IrisApi"); + Method getInstance = irisApiClass.getMethod("getInstance"); + irisApiInstance = getInstance.invoke(null); + isShaderPackInUseMethod = irisApiClass.getMethod("isShaderPackInUse"); + + Class immediateStateClass = Class.forName("net.irisshaders.iris.vertices.ImmediateState"); + isRenderingLevelField = immediateStateClass.getField("isRenderingLevel"); + skipExtensionField = immediateStateClass.getField("skipExtension"); + } catch (ReflectiveOperationException e) { + // The loaded Iris version does not match the expected class shape: disable the + // integration instead of breaking the cached BER pipeline. + present = false; + irisApiInstance = null; + isShaderPackInUseMethod = null; + isRenderingLevelField = null; + skipExtensionField = null; + } + } + IRIS_PRESENT = present; + } + + public static boolean isIrisPresent() { + return IRIS_PRESENT; + } + + public static void pushIrisGlobalState() { + if (IRIS_PRESENT) { + _pushIrisGlobalState(); + } + } + + public static void popIrisGlobalState() { + if (IRIS_PRESENT) { + _popIrisGlobalState(); + } + } + + public static boolean isShaderEnabled() { + if (IRIS_PRESENT) { + return isShaderEnabledInternal(); + } + return false; + } + + private static void _pushIrisGlobalState() { + irisStateStack.push( + new IrisState( + getIsRenderingLevel(), + getSkipExtension() + ) + ); + setIsRenderingLevel(true); + setSkipExtension(false); + } + + private static void _popIrisGlobalState() { + IrisState peek = irisStateStack.peek(); + if (peek != null) { + irisStateStack.pop(); + setIsRenderingLevel(peek.isRenderingLevel); + setSkipExtension(peek.skipExtension); + } + } + + private static boolean isShaderEnabledInternal() { + try { + return (boolean) isShaderPackInUseMethod.invoke(irisApiInstance); + } catch (ReflectiveOperationException e) { + return false; + } + } + + private static boolean getIsRenderingLevel() { + try { + return isRenderingLevelField != null && (boolean) isRenderingLevelField.get(null); + } catch (IllegalAccessException e) { + return false; + } + } + + private static void setIsRenderingLevel(boolean value) { + try { + if (isRenderingLevelField != null) { + isRenderingLevelField.set(null, value); + } + } catch (IllegalAccessException e) { + // ignore + } + } + + private static boolean getSkipExtension() { + try { + if (skipExtensionField != null) { + @SuppressWarnings("unchecked") + ThreadLocal threadLocal = (ThreadLocal) skipExtensionField.get(null); + return threadLocal != null && threadLocal.get(); + } + } catch (IllegalAccessException e) { + // ignore + } + return false; + } + + private static void setSkipExtension(boolean value) { + try { + if (skipExtensionField != null) { + @SuppressWarnings("unchecked") + ThreadLocal threadLocal = (ThreadLocal) skipExtensionField.get(null); + if (threadLocal != null) { + threadLocal.set(value); + } + } + } catch (IllegalAccessException e) { + // ignore + } + } + + private record IrisState( + boolean isRenderingLevel, + boolean skipExtension + ) { + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/CachedBlockEntityRenderingPipelineMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/CachedBlockEntityRenderingPipelineMixin.java new file mode 100644 index 00000000..5a62d251 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/CachedBlockEntityRenderingPipelineMixin.java @@ -0,0 +1,42 @@ +package dev.anvilcraft.lib.v2.rendering.integration.mixins; + +import dev.anvilcraft.lib.v2.rendering.cachedber.pipeline.CachedBlockEntityRenderingPipeline; +import dev.anvilcraft.lib.v2.rendering.integration.IrisSupport; +import org.jetbrains.annotations.ApiStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Invalidates the cached BER buffers when the Iris shader pack enablement changes (e.g. a shader pack + * is toggled in-game), so the compiled meshes are recompiled under the new pipeline. + *

+ * Gated by {@link ALRIntegrationCompatMixinPlugin} on the "iris" mod being loaded. + */ +@Mixin(CachedBlockEntityRenderingPipeline.class) +@ApiStatus.Internal +public abstract class CachedBlockEntityRenderingPipelineMixin { + + @Shadow + public abstract void forcedUpdate(); + + @Unique + private boolean previousShaderEnabled = false; + + @Inject( + method = "handleIntegration", + at = @At("HEAD") + ) + void handleIris(CallbackInfo ci) { + boolean shaderEnabled = IrisSupport.isShaderEnabled(); + + if (previousShaderEnabled != shaderEnabled) { + this.forcedUpdate(); + } + + this.previousShaderEnabled = shaderEnabled; + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/RebuildTaskMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/RebuildTaskMixin.java new file mode 100644 index 00000000..c89b02ef --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/RebuildTaskMixin.java @@ -0,0 +1,35 @@ +package dev.anvilcraft.lib.v2.rendering.integration.mixins; + +import dev.anvilcraft.lib.v2.rendering.cachedber.pipeline.RebuildTask; +import dev.anvilcraft.lib.v2.rendering.integration.IrisSupport; +import org.jetbrains.annotations.ApiStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Marks the cached BER compile as a "level" Iris render (so Iris extensions such as extended vertex + * formats are skipped for the cached geometry) and restores the previous Iris state afterwards. + *

+ * Gated by {@link ALRIntegrationCompatMixinPlugin} on the "iris" mod being loaded. + */ +@Mixin(RebuildTask.class) +@ApiStatus.Internal +public class RebuildTaskMixin { + @Inject( + method = "run", + at = @At("HEAD") + ) + void handleBegin(CallbackInfo ci) { + IrisSupport.pushIrisGlobalState(); + } + + @Inject( + method = "run", + at = @At("RETURN") + ) + void handleEnd(CallbackInfo ci) { + IrisSupport.popIrisGlobalState(); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/MinecraftMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/MinecraftMixin.java index 85ca9d4e..a4a8ce63 100644 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/MinecraftMixin.java +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/MinecraftMixin.java @@ -1,5 +1,7 @@ package dev.anvilcraft.lib.v2.rendering.mixins; +import dev.anvilcraft.lib.v2.rendering.ALRPostEffects; +import dev.anvilcraft.lib.v2.rendering.cachedber.pipeline.CachedBlockEntityRenderingPipeline; import net.minecraft.client.Minecraft; import net.minecraft.client.main.GameConfig; import net.minecraft.client.multiplayer.ClientLevel; @@ -18,8 +20,10 @@ public class MinecraftMixin { at = @At("RETURN") ) private void onCreateInstance(GameConfig gameConfig, CallbackInfo ci) { - // #P7b: ALRPostEffects.createPostEffects(); - // #P7d: CachedBlockEntityRenderingPipeline.create(); + // #P7b: creates the post-processing effects (bloom/glitch) once the main render target exists. + ALRPostEffects.createPostEffects(); + // #P7d: initializes the cached BER pipeline (GL debug label support). + CachedBlockEntityRenderingPipeline.create(); } @Inject( @@ -27,6 +31,7 @@ private void onCreateInstance(GameConfig gameConfig, CallbackInfo ci) { at = @At("RETURN") ) private void onUpdateLevel(ClientLevel level, CallbackInfo ci) { - // #P7d: CachedBlockEntityRenderingPipeline.updateLevel(level); + // #P7d: swap the cached BER pipeline instance when the client level changes. + CachedBlockEntityRenderingPipeline.updateLevel(level); } } diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/ALRPostEffectShaders.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/ALRPostEffectShaders.java new file mode 100644 index 00000000..f4905b6e --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/ALRPostEffectShaders.java @@ -0,0 +1,66 @@ +package dev.anvilcraft.lib.v2.rendering.postprocess; + +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import dev.anvilcraft.lib.v2.rendering.AnvilLibRendering; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.client.renderer.ShaderInstance; +import net.neoforged.neoforge.client.event.RegisterShadersEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +/** + * Holder for the custom {@link ShaderInstance}s used by the post-processing pipeline + * (bloom downsample/upsample/apply, gaussian blur, glitch). + *

+ * Ported from 26.1: the original declared the shaders as {@code RenderPipeline}s through + * {@code RegisterRenderPipelinesEvent}; on 1.21.1 they are registered as core shaders through + * {@link RegisterShadersEvent} (same pattern as the wheel module's {@code LibShaders}). + * The std140 uniform blocks are bound manually through {@link GlUniformBuffer}. + */ +@ApiStatus.Internal +@Slf4j +public final class ALRPostEffectShaders { + @Getter + private static @Nullable ShaderInstance downSampleShader; + @Getter + private static @Nullable ShaderInstance upSampleShader; + @Getter + private static @Nullable ShaderInstance applyBloomShader; + @Getter + private static @Nullable ShaderInstance blurShader; + @Getter + private static @Nullable ShaderInstance glitchShader; + + private ALRPostEffectShaders() { + } + + public static void register(RegisterShadersEvent event) { + try { + event.registerShader( + new ShaderInstance(event.getResourceProvider(), AnvilLibRendering.location("down_sample"), DefaultVertexFormat.POSITION_TEX), + it -> downSampleShader = it + ); + event.registerShader( + new ShaderInstance(event.getResourceProvider(), AnvilLibRendering.location("up_sample"), DefaultVertexFormat.POSITION_TEX), + it -> upSampleShader = it + ); + event.registerShader( + new ShaderInstance(event.getResourceProvider(), AnvilLibRendering.location("apply_bloom"), DefaultVertexFormat.POSITION_TEX), + it -> applyBloomShader = it + ); + event.registerShader( + new ShaderInstance(event.getResourceProvider(), AnvilLibRendering.location("blur"), DefaultVertexFormat.POSITION_TEX), + it -> blurShader = it + ); + event.registerShader( + new ShaderInstance(event.getResourceProvider(), AnvilLibRendering.location("glitch"), DefaultVertexFormat.POSITION_TEX), + it -> glitchShader = it + ); + } catch (IOException e) { + log.error("Failed to register post-processing shaders", e); + } + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlPostProcess.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlPostProcess.java new file mode 100644 index 00000000..6df81094 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlPostProcess.java @@ -0,0 +1,67 @@ +package dev.anvilcraft.lib.v2.rendering.postprocess; + +import com.mojang.blaze3d.pipeline.RenderTarget; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.vertex.BufferUploader; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.MeshData; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; +import net.minecraft.client.renderer.ShaderInstance; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Consumer; + +/** + * Shared helpers for the 1.21.1 port of the post-processing pipeline. + *

+ * Replaces the 26.1 {@code GpuDevice}/{@code CommandEncoder}/{@code RenderPass} execution layer with the + * classic {@code RenderTarget} + {@code ShaderInstance} + fullscreen-quad ({@code BufferUploader}/{@code Tesselator}) + * path. The std140 uniform blocks declared by the GLSL are fed through {@link GlUniformBuffer}. + */ +@ApiStatus.Internal +public final class GlPostProcess { + private GlPostProcess() { + } + + /** + * Binds {@code target} as the current framebuffer, sets the viewport and clears its color attachment. + */ + public static void beginTarget(RenderTarget target) { + target.bindWrite(true); + GlStateManager._clearColor(0.0F, 0.0F, 0.0F, 1.0F); + GlStateManager._clear(16384, false); + } + + /** + * Applies the given shader (binding its samplers), runs {@code setup} (e.g. UBO binding), + * draws the fullscreen quad and clears the shader state again. + */ + public static void draw(ShaderInstance shader, MeshData quad, Consumer setup) { + shader.apply(); + setup.accept(shader); + BufferUploader.draw(quad); + shader.clear(); + } + + /** + * Builds a fullscreen quad mesh covering {@code [0,width]x[0,height]} in {@code POSITION_TEX} format. + * The {@code flipV} parameter controls the vertical texture coordinate orientation. + */ + public static MeshData buildQuad(int width, int height, boolean flipV) { + Tesselator tesselator = Tesselator.getInstance(); + var builder = tesselator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + if (flipV) { + builder.addVertex(0, 0, 100).setUv(0, 1); + builder.addVertex(0, height, 100).setUv(0, 0); + builder.addVertex(width, height, 100).setUv(1, 0); + builder.addVertex(width, 0, 100).setUv(1, 1); + } else { + builder.addVertex(0, 0, 100).setUv(0, 0); + builder.addVertex(0, height, 100).setUv(0, 1); + builder.addVertex(width, height, 100).setUv(1, 1); + builder.addVertex(width, 0, 100).setUv(1, 0); + } + return builder.buildOrThrow(); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlUniformBuffer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlUniformBuffer.java new file mode 100644 index 00000000..abe7d206 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlUniformBuffer.java @@ -0,0 +1,57 @@ +package dev.anvilcraft.lib.v2.rendering.postprocess; + +import com.mojang.blaze3d.platform.GlStateManager; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; +import org.lwjgl.opengl.GL31; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Self-managed GL uniform buffer object (UBO) used to feed {@code layout(std140) uniform} blocks + * of the post-processing shaders. + *

+ * Ported from 26.1: the original relied on {@code GpuDevice.createBuffer} + + * {@code CommandEncoder.writeToTexture}/{@code DynamicUniformStorage}, which do not exist on 1.21.1. + * The data is still produced by the shared {@link BufferObject}/{@code Std140Writer} layout system + * (see {@code foundation.buffers}), only the upload path now uses raw LWJGL buffer management. + */ +public final class GlUniformBuffer implements AutoCloseable { + private final int bufferId; + private final int size; + + public GlUniformBuffer(int size) { + this.size = size; + this.bufferId = GlStateManager._glGenBuffers(); + GlStateManager._glBindBuffer(GL31.GL_UNIFORM_BUFFER, this.bufferId); + GlStateManager._glBufferData(GL31.GL_UNIFORM_BUFFER, size, GL31.GL_STREAM_DRAW); + } + + /** + * Writes the given buffer object into this UBO using its std140 layout and uploads it. + */ + public void upload(BufferObject object) { + ByteBuffer buffer = ByteBuffer.allocate(this.size).order(ByteOrder.nativeOrder()); + object.write(buffer); + GlStateManager._glBindBuffer(GL31.GL_UNIFORM_BUFFER, this.bufferId); + GlStateManager._glBufferData(GL31.GL_UNIFORM_BUFFER, buffer, GL31.GL_STREAM_DRAW); + } + + /** + * Binds this UBO to the uniform block {@code blockName} of the given program at {@code bindingPoint}. + * The block index is looked up per program; if the program does not declare the block, nothing happens. + */ + public void bindTo(int programId, String blockName, int bindingPoint) { + int blockIndex = GL31.glGetUniformBlockIndex(programId, blockName); + if (blockIndex == GL31.GL_INVALID_INDEX) { + return; + } + GL31.glUniformBlockBinding(programId, blockIndex, bindingPoint); + GL31.glBindBufferRange(GL31.GL_UNIFORM_BUFFER, bindingPoint, this.bufferId, 0L, this.size); + } + + @Override + public void close() { + GlStateManager._glDeleteBuffers(this.bufferId); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfGraphics.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfGraphics.java new file mode 100644 index 00000000..08beeb6d --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfGraphics.java @@ -0,0 +1,568 @@ +package dev.anvilcraft.lib.v2.rendering.sdf; + +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; +import com.mojang.math.Axis; +import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; +import dev.anvilcraft.lib.v2.rendering.postprocess.GlUniformBuffer; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.RenderStateShard; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.ShaderInstance; +import net.minecraft.util.FastColor; +import net.minecraft.util.Mth; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.opengl.GL31; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Draws 2D shapes (circle/rect/rounded rect/...) on top of a {@link GuiGraphics} using the nine SDF + * distance functions declared in {@code sdf_graphics.fsh}. + *

+ * Ported from 26.1 (see #P7c): the original submitted {@code LibGuiElementRenderState}s through the + * 26.1 GUI render pipeline (with {@code Matrix3x2f} poses and a {@code GpuDevice} UBO); on 1.21.1 the + * shapes are appended as 4-vertex quads into the {@link GuiGraphics} {@code BufferSource} using a custom + * {@link RenderType} bound to the {@code sdf_graphics} {@link ShaderInstance} (registered through + * {@code RegisterShadersEvent}, see {@link SdfShaders}). Per-shape parameters are uploaded into a + * self-managed {@code layout(std140) uniform SDFParameters} UBO (one slot per quad, referenced through + * {@code UV1.x}); the UBO is bound through {@code glBindBufferRange} right before the batch is drawn. + *

+ * The {@code mixins/GuiRendererMixin} SDF injection of the 26.1 branch is not ported: its target + * class ({@code GuiRenderer}) does not exist on 1.21.1. {@link #flush()} is instead invoked automatically + * at the start of every frame (see {@code AnvilLibRendering}). + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class SdfGraphics { + private static final int MAX_SDF_AMOUNT = 256; + private static final int MAX_SHARED_SDF_AMOUNT = 64; + private static final int SDF_PARAMETER_SIZE = SdfParameters.DEFINITION.size(BufferLayout.STD140); + /** + * GL uniform buffer binding point used by the SDF parameters block. The post-processing pipeline + * ({@link GlUniformBuffer}) uses points 0 and 1, so 2 is free for the SDF UBO. + */ + private static final int UBO_BINDING_POINT = 2; + + @Getter + public static final SdfGraphics instance = new SdfGraphics(new SdfParameters()); + + private static boolean debug = false; + + private final SdfParameters[] shared = new SdfParameters[MAX_SHARED_SDF_AMOUNT]; + + private final SdfParameters parameters; + + /** Self-managed {@code SDFParameters} UBO (all 256 slots). Created lazily on the first draw. */ + private static int uboId = -1; + private static final ByteBuffer SLOT_BUFFER = ByteBuffer.allocateDirect(SDF_PARAMETER_SIZE).order(ByteOrder.nativeOrder()); + + private int shareCursor = 0; + + private float x, y, rotation; + private int color = -1; + private boolean centred = false; + + private static int index; + + private static final RenderType SDF_RENDER_TYPE = RenderType.create( + "anvillib_rendering:sdf_graphics", + DefaultVertexFormat.POSITION_COLOR_TEX_LIGHTMAP, + VertexFormat.Mode.QUADS, + 1536, + false, + false, + RenderType.CompositeState.builder() + .setShaderState(new RenderStateShard.ShaderStateShard(SdfGraphics::bindAndGetShader)) + .setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY) + .setDepthTestState(RenderStateShard.NO_DEPTH_TEST) + .setCullState(RenderStateShard.NO_CULL) + .setWriteMaskState(RenderStateShard.COLOR_WRITE) + .createCompositeState(false) + ); + + public SdfGraphics box(float x, float y, float width, float height) { + this.parameters.getRect().set(0, 0, width, height); + this.parameters.box(width, height); + this.x = x; + this.y = y; + + return this; + } + + public SdfGraphics circle(float x, float y, float radius) { + this.parameters.getRect().set(0, 0, radius * 2, radius * 2); + this.parameters.circle(radius); + this.x = x; + this.y = y; + + return this; + } + + public SdfGraphics arc(float x, float y, float sweep, float radius, float width) { + var scale = radius * 2 + width; + + this.parameters.getRect().set(0, 0, scale, scale); + this.parameters.arc(sweep, radius, width); + this.x = x; + this.y = y; + + return this; + } + + public SdfGraphics sector(float x, float y, float sweep, float radius, float width) { + this.parameters.getRect().set(0, 0, radius * 2, radius * 2); + this.parameters.sector(sweep, radius, width); + this.x = x; + this.y = y; + + return this; + } + + public SdfGraphics pie(float x, float y, float sweep, float radius) { + this.parameters.getRect().set(0, 0, radius * 2, radius * 2); + this.parameters.pie(sweep, radius); + this.x = x; + this.y = y; + + return this; + } + + public SdfGraphics capsule( + float x, float y, + float topRadius, float bottomRadius, + float height + ) { + var width = Math.max(topRadius, bottomRadius) * 2; + + this.parameters.getRect().set(0, 0, width, height + (topRadius + bottomRadius) * 3); + this.parameters.capsule(topRadius, bottomRadius, height); + this.x = x; + this.y = y; + + return this; + } + + public SdfGraphics egg( + float x, float y, + float topRadius, float bottomRadius, + float height + ) { + var width = Math.max(topRadius, bottomRadius) * 2; + + this.parameters.getRect().set(0, 0, width, height + (topRadius + bottomRadius) * 2); + this.parameters.egg(topRadius, bottomRadius, height); + this.x = x; + this.y = y; + + return this; + } + + public SdfGraphics segment( + float x0, float y0, + float x1, float y1 + ) { + var left = Math.min(x0, x1); + var top = Math.min(y0, y1); + var width = Math.abs(x1 - x0); + var height = Math.abs(y1 - y0); + + var halfWidth = (x1 - x0) * 0.5f; + var halfHeight = (y1 - y0) * 0.5f; + + this.parameters.getRect().set(left + width / 2, top + height / 2, width, height); + this.parameters.segment(-halfWidth, -halfHeight, +halfWidth, +halfHeight); + this.x = left + width / 2; + this.y = top + height / 2; + + return this; + } + + public SdfGraphics triangleEquilateral(float x, float y, float radius) { + var actual = radius * (1.0f / 1.2f); + + this.parameters.getRect().set(0, 0, radius * 2, radius * 2); + this.parameters.triangleEquilateral(actual); + this.x = x; + this.y = y; + + return this; + } + + public SdfGraphics triangleIsosceles(float x, float y, float width, float height) { + final var factor = 1.0f / 1.2f; + this.parameters.getRect().set(0, 0, width * 2.0f, height); + this.parameters.triangleIsosceles(width * factor, height * factor); + this.x = x; + this.y = y; + + return this; + } + + public SdfGraphics color(int color) { + this.color = color; + return this; + } + + public SdfGraphics color(float red, float green, float blue, float alpha) { + this.color = FastColor.ARGB32.color( + (int) (alpha * 255.0F), + (int) (red * 255.0F), + (int) (green * 255.0F), + (int) (blue * 255.0F) + ); + return this; + } + + public SdfGraphics color(int red, int green, int blue, int alpha) { + this.color = FastColor.ARGB32.color(alpha, red, green, blue); + return this; + } + + public SdfGraphics smooth(float radius) { + var v = Math.max(0.0f, radius); + this.parameters.smooth(v); + return this; + } + + public SdfGraphics round(float radius) { + var v = Math.max(0.0f, radius); + this.parameters.round(v); + return this; + } + + public SdfGraphics stroke(float width) { + var v = Math.max(0.0f, width * 0.5f); + this.parameters.stroke(v); + this.parameters.onion(width > 0.0f); + return this; + } + + public SdfGraphics rotate(float degrees) { + this.rotation = degrees; + return this; + } + + public SdfGraphics center(boolean center) { + this.centred = center; + return this; + } + + public SdfGraphics onion(boolean onion) { + this.parameters.onion(onion); + return this; + } + + public SdfGraphics fill() { + this.parameters.fill(); + return this; + } + + public SdfGraphics light(float radius) { + this.parameters.light(radius); + return this; + } + + public SdfGraphics draw( + @NotNull GuiGraphics graphics + ) { + _draw( + graphics, + this.parameters, + this.x, + this.y, + this.rotation, + this.color, + this.centred + ); + return this; + } + + public SdfGraphics draw( + @NotNull GuiGraphics graphics, + @NotNull SdfParameters parameters, + float x, + float y + ) { + + if (parameters.isShared()) { + _draw( + graphics, + parameters, + x, + y, + this.rotation, + this.color, + this.centred + ); + } + + return this; + } + + public SdfGraphics reset() { + this.parameters.reset(); + this.x = 0; + this.y = 0; + this.rotation = 0; + this.color = -1; + this.centred = false; + return this; + } + + public boolean collide(float x, float y, float threshold) { + return this.collide( + this.parameters, + x, + y, + this.x, + this.y, + threshold + ); + } + + public boolean collide( + @NotNull SdfParameters parameters, + float pointX, + float pointY, + float x, + float y, + float threshold + ) { + return Sdf2d.sd( + parameters, + pointX, + pointY, + x, + y, + this.rotation, + this.centred + ) < threshold; + } + + public @NotNull SdfParameters cache() { + return this.parameters.duplicate(); + } + + public @NotNull SdfParameters share() { + final var cache = this.cache(); + this.share(cache); + return cache; + } + + public void share(@NotNull SdfParameters cache) { + final var cursor = this.shareCursor; + if (cursor == MAX_SHARED_SDF_AMOUNT) { + return; + } + + this.shared[cursor] = cache; + cache.uboIndex = MAX_SDF_AMOUNT - cursor - 1; + cache.uploaded = false; + + do { + this.shareCursor++; + } while (this.shareCursor < MAX_SHARED_SDF_AMOUNT && this.shared[this.shareCursor] != null); + } + + public void unshare(@NotNull SdfParameters cache) { + var cursor = 0; + // find index + for (; cursor < MAX_SHARED_SDF_AMOUNT; cursor++) { + if (this.shared[cursor] == cache) { + cache.uploaded = false; + cache.uboIndex = -1; + break; + } + } + + if (cursor == MAX_SHARED_SDF_AMOUNT) { + return; + } + + this.shared[cursor] = null; + if (cursor < this.shareCursor) { + this.shareCursor = cursor; + } + } + + /** + * Resets the per-frame SDF slot cursor (and the singleton's parameters). Called automatically at the + * start of every frame; kept public for manual use inside screens. + */ + public static void flush() { + instance.parameters.reset(); + SdfGraphics.index = 0; + } + + public static void debug(boolean enable) { + SdfGraphics.debug = enable; + } + + /** + * Lazily creates the SDF parameters UBO and (re)binds it to the SDF uniform block of the current + * shader program. This runs as the {@code ShaderStateShard} supplier, i.e. right before the SDF + * batch is drawn, so the binding is guaranteed to be current at draw time even if another pipeline + * touched the binding point in between. + */ + private static ShaderInstance bindAndGetShader() { + ShaderInstance shader = SdfShaders.getSdfGraphicsShader(); + if (shader == null) { + return null; + } + if (uboId == -1) { + uboId = GL31.glGenBuffers(); + GL31.glBindBuffer(GL31.GL_UNIFORM_BUFFER, uboId); + GL31.glBufferData(GL31.GL_UNIFORM_BUFFER, (long) SDF_PARAMETER_SIZE * MAX_SDF_AMOUNT, GL31.GL_DYNAMIC_DRAW); + } + int programId = shader.getId(); + int blockIndex = GL31.glGetUniformBlockIndex(programId, "SDFParameters"); + if (blockIndex != GL31.GL_INVALID_INDEX) { + GL31.glUniformBlockBinding(programId, blockIndex, UBO_BINDING_POINT); + } + GL31.glBindBufferRange( + GL31.GL_UNIFORM_BUFFER, + UBO_BINDING_POINT, + uboId, + 0L, + (long) SDF_PARAMETER_SIZE * MAX_SDF_AMOUNT + ); + return shader; + } + + private static void _draw( + @NotNull GuiGraphics graphics, + @NotNull SdfParameters parameters, + float x, + float y, + float rotation, + int color, + boolean centred + ) { + var round = parameters.getRound(); + var smooth = parameters.getSmooth(); + var stroke = parameters.getStroke(); + + var rect = parameters.getRect(); + var z = rect.z; + var w = rect.w; + + var pose = graphics.pose(); + var ex = (round + smooth + stroke) * 2.0f; + var width = rect.z + ex; + var height = rect.w + ex; + final var hw = width * 0.5f; + final var hh = height * 0.5f; + + final var radian = rotation * Mth.DEG_TO_RAD; + final var cos = Mth.cos(radian); + final var sin = Mth.sin(radian); + + float cx; + float cy; + pose.pushPose(); + if (centred) { + pose.translate(x, y, 0.0f); + + cx = x; + cy = y; + } else { + pose.translate( + x + hw, + y + hh, + 0.0f + ); + + cx = x + hw - hw * cos + hh * sin; + cy = y + hh - hw * sin - hh * cos; + } + + if (rotation != 0.0f) { + pose.mulPose(Axis.ZP.rotationDegrees(rotation)); + } + + if (!centred) { + pose.translate(-hw, -hh, 0.0f); + } + + pose.scale(width, height, 1.0f); + + rect.z = width; + rect.w = height; + + var extX = Mth.abs(hw * cos) + Mth.abs(hh * sin) + 1.0f; + var extY = Mth.abs(hw * sin) + Mth.abs(hh * cos) + 1.0f; + + var x0 = cx - extX; + var x1 = cx + extX; + var y0 = cy - extY; + var y1 = cy + extY; + + var shared = parameters.isShared(); + var idx = shared ? parameters.uboIndex : index; + + // If the sdf_graphics shader failed to register, don't append quads: a RenderType whose + // shader supplier returns null would NPE inside BufferUploader at flush time. + if (SdfShaders.getSdfGraphicsShader() == null) { + pose.popPose(); + rect.z = z; + rect.w = w; + return; + } + + if (debug) { + graphics.renderOutline( + (int) x0, (int) y0, + (int) (x1 - x0), (int) (y1 - y0), + 0xFF0000FF + ); + } + + if (idx >= MAX_SDF_AMOUNT) { + pose.popPose(); + rect.z = z; + rect.w = w; + return; + } + + if (!shared || !parameters.uploaded) { + upload(parameters, idx); + parameters.uploaded = shared; + } + + var poseEntry = pose.last(); + VertexConsumer consumer = graphics.bufferSource().getBuffer(SDF_RENDER_TYPE); + consumer.addVertex(poseEntry, -0.5f, -0.5f, 0.0f).setUv(0, 0).setUv1(idx, 0).setColor(color); + consumer.addVertex(poseEntry, -0.5f, +0.5f, 0.0f).setUv(0, 1).setUv1(idx, 0).setColor(color); + consumer.addVertex(poseEntry, +0.5f, +0.5f, 0.0f).setUv(1, 1).setUv1(idx, 0).setColor(color); + consumer.addVertex(poseEntry, +0.5f, -0.5f, 0.0f).setUv(1, 0).setUv1(idx, 0).setColor(color); + + pose.popPose(); + rect.z = z; + rect.w = w; + + if (!shared) { + SdfGraphics.index++; + } + } + + private static void upload(@NotNull SdfParameters parameters, int slot) { + if (uboId == -1) { + return; + } + SLOT_BUFFER.clear(); + parameters.write(SLOT_BUFFER); + GL31.glBindBuffer(GL31.GL_UNIFORM_BUFFER, uboId); + GL31.glBufferSubData( + GL31.GL_UNIFORM_BUFFER, + (long) slot * SDF_PARAMETER_SIZE, + SLOT_BUFFER + ); + GL31.glBindBuffer(GL31.GL_UNIFORM_BUFFER, 0); + } +} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfShaders.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfShaders.java new file mode 100644 index 00000000..ab76d134 --- /dev/null +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfShaders.java @@ -0,0 +1,46 @@ +package dev.anvilcraft.lib.v2.rendering.sdf; + +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import dev.anvilcraft.lib.v2.rendering.AnvilLibRendering; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.client.renderer.ShaderInstance; +import net.neoforged.neoforge.client.event.RegisterShadersEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +/** + * Holder for the custom {@link ShaderInstance} used by {@link SdfGraphics}. + *

+ * Ported from 26.1: the original declared the SDF pipeline through {@code RenderPipeline} + * ({@code RegisterRenderPipelinesEvent}); on 1.21.1 it is registered as a core shader through + * {@link RegisterShadersEvent} (same pattern as the wheel module's {@code LibShaders}). + * The {@code layout(std140) uniform SDFParameters} block is fed by a self-managed GL UBO + * (see {@link SdfGraphics}), bound through {@code glBindBufferRange} at draw time. + */ +@ApiStatus.Internal +@Slf4j +public final class SdfShaders { + @Getter + private static @Nullable ShaderInstance sdfGraphicsShader; + + private SdfShaders() { + } + + public static void register(RegisterShadersEvent event) { + try { + event.registerShader( + new ShaderInstance( + event.getResourceProvider(), + AnvilLibRendering.location("sdf_graphics"), + DefaultVertexFormat.POSITION_COLOR_TEX_LIGHTMAP + ), + it -> sdfGraphicsShader = it + ); + } catch (IOException e) { + log.error("Failed to register sdf_graphics shader", e); + } + } +} diff --git a/module.rendering/src/main/resources/META-INF/accesstransformer.cfg b/module.rendering/src/main/resources/META-INF/accesstransformer.cfg index 28896b16..703b0d0f 100644 --- a/module.rendering/src/main/resources/META-INF/accesstransformer.cfg +++ b/module.rendering/src/main/resources/META-INF/accesstransformer.cfg @@ -1,3 +1,5 @@ # #P7a: exposes CompositeRenderType.state() for ALRRenderTypeExtension.copyWithBloom (RenderType.create(name, format, mode, bufferSize, state) on 1.21.1). public net.minecraft.client.renderer.RenderType$CompositeRenderType public net.minecraft.client.renderer.RenderType$CompositeRenderType state()Lnet/minecraft/client/renderer/RenderType$CompositeState; +# #P7c: makes ShaderStateShard constructible outside net.minecraft.client.renderer for the custom sdf_graphics RenderType used by SdfGraphics. +public net.minecraft.client.renderer.RenderStateShard$ShaderStateShard (Ljava/util/function/Supplier;)V diff --git a/module.rendering/src/main/resources/META-INF/neoforge.mods.toml b/module.rendering/src/main/resources/META-INF/neoforge.mods.toml index 27f7ba75..508ab566 100644 --- a/module.rendering/src/main/resources/META-INF/neoforge.mods.toml +++ b/module.rendering/src/main/resources/META-INF/neoforge.mods.toml @@ -45,6 +45,10 @@ description = '''${mod_description}''' [[mixins]] config = "${mod_id}.mixins.json" +# Iris-optional integration mixins for the cached BER pipeline (gated by ALRIntegrationCompatMixinPlugin). +[[mixins]] +config = "${mod_id}.integration.mixins.json" + # The [[accessTransformers]] block allows you to declare where your AT file is. # If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg #[[accessTransformers]] diff --git a/module.rendering/src/main/resources/anvillib_rendering.integration.mixins.json b/module.rendering/src/main/resources/anvillib_rendering.integration.mixins.json new file mode 100644 index 00000000..2622e009 --- /dev/null +++ b/module.rendering/src/main/resources/anvillib_rendering.integration.mixins.json @@ -0,0 +1,17 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.anvilcraft.lib.v2.rendering.integration.mixins", + "compatibilityLevel": "JAVA_8", + "refmap": "anvillib.refmap.json", + "plugin": "dev.anvilcraft.lib.v2.rendering.integration.mixins.ALRIntegrationCompatMixinPlugin", + "mixins": [ + ], + "client": [ + "CachedBlockEntityRenderingPipelineMixin", + "RebuildTaskMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.json b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.json new file mode 100644 index 00000000..038c2b8a --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.json @@ -0,0 +1,9 @@ +{ + "vertex": "anvillib_rendering:blit", + "fragment": "anvillib_rendering:apply_bloom", + "samplers": [ + { "name": "GameSampler" }, + { "name": "DiffuseSampler" } + ], + "uniforms": [] +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.json b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.json new file mode 100644 index 00000000..7a7e73d0 --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.json @@ -0,0 +1,8 @@ +{ + "vertex": "anvillib_rendering:blit", + "fragment": "anvillib_rendering:blur", + "samplers": [ + { "name": "DiffuseSampler" } + ], + "uniforms": [] +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.json b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.json new file mode 100644 index 00000000..15bf6c51 --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.json @@ -0,0 +1,8 @@ +{ + "vertex": "anvillib_rendering:blit", + "fragment": "anvillib_rendering:down_sample", + "samplers": [ + { "name": "DiffuseSampler" } + ], + "uniforms": [] +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.json b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.json new file mode 100644 index 00000000..fa67d802 --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.json @@ -0,0 +1,8 @@ +{ + "vertex": "anvillib_rendering:blit", + "fragment": "anvillib_rendering:glitch", + "samplers": [ + { "name": "DiffuseSampler" } + ], + "uniforms": [] +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh index 01d09ce0..76f5c52d 100644 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh @@ -1,4 +1,4 @@ -#version 330 +#version 150 #define MAX_SDFS 256 diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.json b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.json new file mode 100644 index 00000000..92512715 --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.json @@ -0,0 +1,10 @@ +{ + "vertex": "anvillib_rendering:sdf_graphics", + "fragment": "anvillib_rendering:sdf_graphics", + "samplers": [ + ], + "uniforms": [ + { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] } + ] +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.vsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.vsh index 97bf2577..0651a71b 100644 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.vsh +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.vsh @@ -1,7 +1,4 @@ -#version 330 - -#moj_import -#moj_import +#version 150 #define MAX_SDFS 256 @@ -21,6 +18,9 @@ in vec4 Color; in vec2 UV0; in ivec2 UV1; +uniform mat4 ModelViewMat; +uniform mat4 ProjMat; + out vec2 vPosition; out vec4 vColor; flat out int vIndex; @@ -32,4 +32,4 @@ void main() { vPosition = (UV0 - 0.5) * SDFs[index].Rect.zw; vColor = Color; vIndex = index; -} \ No newline at end of file +} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.json b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.json new file mode 100644 index 00000000..f20e8687 --- /dev/null +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.json @@ -0,0 +1,9 @@ +{ + "vertex": "anvillib_rendering:blit", + "fragment": "anvillib_rendering:up_sample", + "samplers": [ + { "name": "DiffuseSampler" }, + { "name": "PreviousSampler" } + ], + "uniforms": [] +} From a08c611827a2eb487f4bfb3f3604154423bf288b Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 11 Aug 2026 02:48:27 +0800 Subject: [PATCH 24/38] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E5=9B=9E?= =?UTF-8?q?=E7=A7=BB=E6=A4=8D=E6=89=A7=E8=A1=8C=E8=B7=9F=E8=B8=AA=E6=96=87?= =?UTF-8?q?=E6=A1=A3=EF=BC=88#P7=20=E5=85=A8=E6=89=B9=E6=AC=A1=E5=AE=8C?= =?UTF-8?q?=E6=88=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #P7 标记完成(#P7a 纯逻辑批 b0e179e8 + #P7b-e 执行层 a6544e6b) - 待验证销账:V-2/V-6(Iris 1.8.12 签名一致、Modrinth maven 停服改反射)、 V-8(ShaderDefines → Map 编译通过) - 遗留登记更新:L-10/L-11 已解决,新增 L-12(rendering 执行层运行验证) --- docs/migration-todo-port-1.21.1.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/migration-todo-port-1.21.1.md b/docs/migration-todo-port-1.21.1.md index d23e96a2..c63aa333 100644 --- a/docs/migration-todo-port-1.21.1.md +++ b/docs/migration-todo-port-1.21.1.md @@ -19,7 +19,7 @@ | #P4 | module.collision 整模块(3 文件) | 极低 | ✅ 已完成 | `c25dd465` | | #P5 | module.space-select 整模块(18 文件) | 中 | ✅ 已完成 | `1706feaa` | | #P6 | module.font 整模块(25 文件) | 中 | ✅ 已完成 | `91533730` | -| #P7 | module.rendering(#P7a–#P7e 分批) | 高 | ✅ 已完成(#P7a) | `b0e179e8` | +| #P7 | module.rendering(#P7a–#P7e 分批) | 高 | ✅ 已完成 | `b0e179e8` + `a6544e6b` | | #P8 | renderdoc-loader(2 文件) | 低 | ✅ 已完成 | `c62a262e` | | #P9 | module.config:group + TranslatableEnum + TOML 点分隔 | 低 | ✅ 已完成 | `65aa5e11` | | #P10 | module.integration:数据加载拆分 + meter 递增 | 低 | ✅ 已完成 | `adc23468` | @@ -191,13 +191,13 @@ | 编号 | 事项 | 涉及项 | 状态 | |------|------------------------------------------------------------------------------------|-----------------|--------| | V-1 | `Mth.roundToward`/`VertexSorting`/`CompactVectorArray`/`MeshData.sortQuads` 存在性 | #P7a | ✅ 已验证(roundToward 存在;CompactVectorArray 不存在改 VertexSorting.sort,V-1 计划修正) | -| V-2 | Iris 1.7.x `ImmediateState` 字段 | #P7d | 未验证(#P7d 未执行) | +| V-2 | Iris 1.7.x `ImmediateState` 字段 | #P7d | ✅ 已验证(Iris 1.8.12 签名一致,改反射访问) | | V-3 | `MultiBufferSource.BufferSource` 构造器参数 | #P7a | ✅ 已验证(super(null,null) 兼容) | | V-4 | `renderVoxelShape` 颜色语义 | #P5 | 未验证(需 runClient 目测) | | V-5 | port 侧 run 配置自建模式 | #P3/#P5/#P8 | ✅ 已验证(#P3 runData 通过、#P5 processResources 通过) | -| V-6 | Iris 1.21.1 Modrinth 坐标 | #P7d | 未验证(#P7d 未执行) | +| V-6 | Iris 1.21.1 Modrinth 坐标 | #P7d | ✅ 已验证(Modrinth maven 停服,改反射访问,坐标注释占位) | | V-7 | mods.toml 模板变量注入 | #P7 | ✅ 已验证(port 模板展开正常) | -| V-8 | ShaderDefines → Map | #P7e | 未验证(#P7e 未执行) | +| V-8 | ShaderDefines → Map | #P7e | ✅ 已验证(编译通过) | | V-9 | CoreMod jar-in-jar services 可见性 | #P2 | 未验证(需打包启动实测) | | V-10 | CoreMod GETSTATIC 类可解析性 | #P2 | 未验证(需启动日志核对) | | V-11 | `getSecureJar().getPath` 签名 | #P2 | ✅ 已验证(javap 确认返回 Path) | @@ -222,5 +222,6 @@ | L-7 | rendering 运行验证(#P7a) | mixin 注入、AT 生效、mods.toml 加载 | runClient 启动日志 | | L-8 | multiblock 行为验证(#P15) | 事件可取消回滚、快照复用、懒解析 | runServer | | L-9 | wheel 运行验证(#P17) | 环形扇区效果、颜色配置、旧路径无回归 | runClient + T10 | -| L-10 | Iris 集成(#P7d,V-2/V-6) | #P7d 未执行,随后续批次 | — | -| L-11 | Compute(#P7e,V-8) | #P7e 未执行,随后续批次 | — | +| L-10 | Iris 集成(#P7d,V-2/V-6) | ✅ 已解决:#P7d 完成,Iris 经反射访问(签名一致),运行时需装 Iris 客户端验证 | — | +| L-11 | Compute(#P7e,V-8) | ✅ 已解决:#P7e 完成(编译通过),dispatch/内存屏障运行验证待 runClient | — | +| L-12 | rendering 执行层运行验证(#P7b-e) | 后处理效果/SDF GUI/CachedBER/GLSL 编译链接需 runClient | 运行 runClient 核对 7.2 清单 | From 37a51c52054c568f361f9321b20351ceafd41e9e Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 11 Aug 2026 10:02:21 +0800 Subject: [PATCH 25/38] =?UTF-8?q?refactor(nullness):=20=E7=A9=BA=E6=B3=A8?= =?UTF-8?q?=E8=A7=A3=E4=BD=93=E7=B3=BB=E7=BB=9F=E4=B8=80=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E8=87=B3=20jsr305?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 包级 @NullMarked 替换为 @FieldsAreNonnullByDefault/@MethodsReturnNonnullByDefault/@ParametersAreNonnullByDefault 组合 - @Nullable/@NonNull 注解统一到 javax.annotation(jsr305),@NonNull 用法改为 @Nonnull - 限定类型与泛型类型实参位置的 @Nullable 调整为 jsr305 可表达的声明级注解 - 移除全部 12 个模块 build.gradle 的 org.jspecify:jspecify 编译期依赖 - 修正 module.test rpc 包 package-info 错误的包声明 --- .../java/dev/anvilcraft/lib/v2/codec/package-info.java | 4 +++- module.collision/build.gradle | 5 ----- .../dev/anvilcraft/lib/v2/collision/package-info.java | 9 +++++++-- .../java/dev/anvilcraft/lib/v2/config/package-info.java | 4 +++- .../dev/anvilcraft/lib/v2/config/util/package-info.java | 4 +++- module.explosion/build.gradle | 1 - .../anvilcraft/lib/v2/explosion/ExplosionExecutor.java | 2 +- .../anvilcraft/lib/v2/explosion/ExplosionSession.java | 2 +- .../anvilcraft/lib/v2/explosion/data/package-info.java | 9 +++++++-- .../lib/v2/explosion/data/provider/package-info.java | 9 +++++++-- .../dev/anvilcraft/lib/v2/explosion/package-info.java | 9 +++++++-- module.font/build.gradle | 1 - .../java/dev/anvilcraft/lib/v2/font/ALFPipelines.java | 2 +- .../dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java | 2 +- .../java/dev/anvilcraft/lib/v2/font/FontManager.java | 2 +- .../dev/anvilcraft/lib/v2/font/data/package-info.java | 9 +++++++-- .../v2/font/extension/GuiGraphicsExtractorExtension.java | 2 +- .../anvilcraft/lib/v2/font/extension/package-info.java | 9 +++++++-- .../dev/anvilcraft/lib/v2/font/mixin/package-info.java | 9 +++++++-- .../java/dev/anvilcraft/lib/v2/font/package-info.java | 9 +++++++-- .../anvilcraft/lib/v2/font/screen/FontConfigScreen.java | 8 ++++---- .../dev/anvilcraft/lib/v2/font/screen/package-info.java | 9 +++++++-- .../anvilcraft/lib/v2/font/screen/widget/Dropdown.java | 6 +++--- .../lib/v2/font/screen/widget/package-info.java | 9 +++++++-- .../dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java | 2 +- .../dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java | 2 +- .../dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java | 2 +- .../dev/anvilcraft/lib/v2/font/sdf/package-info.java | 9 +++++++-- .../anvilcraft/lib/v2/font/sdf/state/package-info.java | 9 +++++++-- .../dev/anvilcraft/lib/v2/integration/package-info.java | 4 +++- .../main/java/dev/anvilcraft/lib/v2/package-info.java | 4 +++- .../anvilcraft/lib/v2/piston/injection/package-info.java | 4 +++- .../dev/anvilcraft/lib/v2/piston/mixin/package-info.java | 4 +++- .../java/dev/anvilcraft/lib/v2/piston/package-info.java | 4 +++- module.multiblock/build.gradle | 1 - .../lib/v2/multiblock/data/provider/package-info.java | 4 +++- .../lib/v2/multiblock/dynamic/MultiblockState.java | 4 ++-- .../v2/multiblock/dynamic/controller/package-info.java | 4 +++- .../v2/multiblock/dynamic/definition/package-info.java | 4 +++- .../lib/v2/multiblock/dynamic/package-info.java | 4 +++- .../anvilcraft/lib/v2/multiblock/event/package-info.java | 4 +++- .../anvilcraft/lib/v2/multiblock/init/package-info.java | 4 +++- .../dev/anvilcraft/lib/v2/multiblock/package-info.java | 4 +++- .../anvilcraft/lib/v2/network/packet/package-info.java | 4 +++- .../anvilcraft/lib/v2/network/register/package-info.java | 4 +++- .../dev/anvilcraft/lib/v2/network/util/package-info.java | 4 +++- .../anvilcraft/lib/v2/recipe/builder/package-info.java | 4 +++- .../lib/v2/recipe/cache/item/operation/package-info.java | 4 +++- .../lib/v2/recipe/cache/item/package-info.java | 4 +++- .../dev/anvilcraft/lib/v2/recipe/cache/package-info.java | 4 +++- .../anvilcraft/lib/v2/recipe/component/package-info.java | 4 +++- .../data/advancement/predicate/item/package-info.java | 4 +++- .../anvilcraft/lib/v2/recipe/data/gen/package-info.java | 4 +++- .../lib/v2/recipe/data/gen/provider/package-info.java | 4 +++- .../dev/anvilcraft/lib/v2/recipe/init/package-info.java | 4 +++- .../lib/v2/recipe/init/recipe/package-info.java | 4 +++- .../dev/anvilcraft/lib/v2/recipe/mixin/package-info.java | 4 +++- .../lib/v2/recipe/outcome/function/package-info.java | 4 +++- .../anvilcraft/lib/v2/recipe/outcome/package-info.java | 4 +++- .../java/dev/anvilcraft/lib/v2/recipe/package-info.java | 4 +++- .../lib/v2/recipe/predicate/block/package-info.java | 4 +++- .../lib/v2/recipe/predicate/function/package-info.java | 4 +++- .../lib/v2/recipe/predicate/item/package-info.java | 4 +++- .../anvilcraft/lib/v2/recipe/predicate/package-info.java | 4 +++- .../anvilcraft/lib/v2/recipe/trigger/package-info.java | 4 +++- .../dev/anvilcraft/lib/v2/recipe/util/package-info.java | 4 +++- module.registrum/build.gradle | 1 - .../lib/v2/registrum/builders/data/package-info.java | 9 +++++++-- .../lib/v2/registrum/builders/modifier/package-info.java | 9 +++++++-- .../lib/v2/registrum/builders/recipe/package-info.java | 9 +++++++-- .../lib/v2/registrum/builders/self/package-info.java | 9 +++++++-- .../builders/villager/VillagerProfessionBuilder.java | 2 +- .../lib/v2/registrum/builders/villager/package-info.java | 9 +++++++-- .../v2/registrum/providers/RegistrumDataMapProvider.java | 4 ++-- .../lib/v2/registrum/util/entry/data/package-info.java | 9 +++++++-- .../v2/registrum/util/entry/modifier/package-info.java | 9 +++++++-- .../lib/v2/registrum/util/entry/recipe/package-info.java | 9 +++++++-- module.rendering/build.gradle | 1 - .../pipeline/CachedBlockEntityRenderingPipeline.java | 2 +- .../cachedber/pipeline/CachedRenderingChunk.java | 2 +- .../renderer/CachedBlockEntityRenderDispatcher.java | 2 +- .../blaze3d/compute/shader/ALRComputeShaderManager.java | 6 +++--- .../foundation/buffers/object/BufferObject.java | 4 ++-- .../lib/v2/rendering/integration/IrisSupport.java | 2 +- module.rpc/build.gradle | 1 - .../main/java/dev/anvilcraft/lib/v2/rpc/RpcMethods.java | 2 +- .../java/dev/anvilcraft/lib/v2/rpc/RpcPendingCalls.java | 2 +- .../main/java/dev/anvilcraft/lib/v2/rpc/RpcRegistry.java | 2 +- .../dev/anvilcraft/lib/v2/rpc/client/package-info.java | 9 +++++++-- .../dev/anvilcraft/lib/v2/rpc/config/package-info.java | 9 +++++++-- .../java/dev/anvilcraft/lib/v2/rpc/package-info.java | 9 +++++++-- module.space-select/build.gradle | 1 - .../anvilcraft/lib/v2/space_select/DistrictManager.java | 2 +- .../v2/space_select/client/ClientDistrictManager.java | 2 +- .../lib/v2/space_select/client/package-info.java | 9 +++++++-- .../lib/v2/space_select/event/handler/package-info.java | 9 +++++++-- .../lib/v2/space_select/event/package-info.java | 9 +++++++-- .../lib/v2/space_select/network/package-info.java | 9 +++++++-- .../v2/space_select/network/payload/package-info.java | 9 +++++++-- .../dev/anvilcraft/lib/v2/space_select/package-info.java | 9 +++++++-- module.sync/build.gradle | 1 - module.sync/processor/build.gradle | 4 ---- .../anvilcraft/lib/v2/sync/transform/package-info.java | 9 +++++++-- .../anvilcraft/lib/v2/sync/annotation/package-info.java | 9 +++++++-- .../dev/anvilcraft/lib/v2/sync/init/package-info.java | 9 +++++++-- .../lib/v2/sync/management/LazySyncManager.java | 2 +- .../lib/v2/sync/management/SyncConfigManager.java | 2 +- .../anvilcraft/lib/v2/sync/management/SyncManager.java | 2 +- .../dev/anvilcraft/lib/v2/sync/management/SyncProxy.java | 2 +- .../lib/v2/sync/management/SyncRegisterEntry.java | 2 +- .../anvilcraft/lib/v2/sync/management/package-info.java | 9 +++++++-- .../dev/anvilcraft/lib/v2/sync/network/package-info.java | 9 +++++++-- .../lib/v2/sync/network/payload/package-info.java | 9 +++++++-- .../java/dev/anvilcraft/lib/v2/sync/package-info.java | 9 +++++++-- .../java/dev/anvilcraft/lib/v2/sync/util/SideUtil.java | 2 +- .../dev/anvilcraft/lib/v2/sync/util/package-info.java | 9 +++++++-- module.test/build.gradle | 1 - .../dev/anvilcraft/lib/v2/test/command/package-info.java | 9 +++++++-- .../dev/anvilcraft/lib/v2/test/mixin/package-info.java | 9 +++++++-- .../lib/v2/test/multiblock/block/package-info.java | 4 +++- .../lib/v2/test/multiblock/init/package-info.java | 4 +++- .../java/dev/anvilcraft/lib/v2/test/package-info.java | 4 +++- .../anvilcraft/lib/v2/test/rpc/RpcIntegrationTest.java | 4 ++-- .../dev/anvilcraft/lib/v2/test/rpc/package-info.java | 9 +++++++-- .../lib/v2/test/wheel/data/lang/package-info.java | 4 +++- .../dev/anvilcraft/lib/v2/test/wheel/package-info.java | 4 +++- module.util/build.gradle | 1 - .../java/dev/anvilcraft/lib/v2/util/OutlineUtil.java | 2 +- .../anvilcraft/lib/v2/util/component/package-info.java | 4 +++- .../java/dev/anvilcraft/lib/v2/util/package-info.java | 4 +++- .../anvilcraft/lib/v2/util/predicate/package-info.java | 4 +++- .../dev/anvilcraft/lib/v2/util/stack/package-info.java | 4 +++- .../dev/anvilcraft/lib/v2/wheel/api/package-info.java | 4 +++- .../lib/v2/wheel/client/gui/component/package-info.java | 4 +++- .../lib/v2/wheel/client/gui/screen/package-info.java | 4 +++- .../lib/v2/wheel/client/init/package-info.java | 4 +++- .../lib/v2/wheel/client/input/package-info.java | 4 +++- .../java/dev/anvilcraft/lib/v2/wheel/package-info.java | 4 +++- 138 files changed, 483 insertions(+), 196 deletions(-) diff --git a/module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/package-info.java b/module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/package-info.java index 3da08152..aca1fab5 100644 --- a/module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/package-info.java +++ b/module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.codec; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.collision/build.gradle b/module.collision/build.gradle index a0854c6d..07308738 100644 --- a/module.collision/build.gradle +++ b/module.collision/build.gradle @@ -113,11 +113,6 @@ neoForge { // Include resources generated by data generators. sourceSets.main.resources { srcDir 'src/generated/resources' } - -dependencies { - compileOnly "org.jspecify:jspecify:1.0.0" -} - // This block of code expands all declared replace properties in the specified resource targets. // A missing property will result in an error. Properties are expanded using ${} Groovy notation. // When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. diff --git a/module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/package-info.java b/module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/package-info.java index 84cf6564..0409ca51 100644 --- a/module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/package-info.java +++ b/module.collision/src/main/java/dev/anvilcraft/lib/v2/collision/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.collision; -import org.jspecify.annotations.NullMarked; \ No newline at end of file +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/package-info.java b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/package-info.java index 7c4e5b7a..4443138a 100644 --- a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/package-info.java +++ b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.config; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/util/package-info.java b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/util/package-info.java index 6d03442f..e7ea2352 100644 --- a/module.config/src/main/java/dev/anvilcraft/lib/v2/config/util/package-info.java +++ b/module.config/src/main/java/dev/anvilcraft/lib/v2/config/util/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.config.util; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.explosion/build.gradle b/module.explosion/build.gradle index 4b09d3c8..87231b8c 100644 --- a/module.explosion/build.gradle +++ b/module.explosion/build.gradle @@ -115,7 +115,6 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { - compileOnly "org.jspecify:jspecify:1.0.0" if (System.getenv("NOT_DEV") == 'true') { jarJar(api("dev.anvilcraft.lib:anvillib-config-neoforge-1.21.1:latest.release")) diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionExecutor.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionExecutor.java index 346225e1..f53bd6e4 100644 --- a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionExecutor.java +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionExecutor.java @@ -10,7 +10,7 @@ import net.minecraft.world.level.block.Block; import net.neoforged.neoforge.common.Tags; import org.apache.logging.log4j.util.TriConsumer; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionSession.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionSession.java index e8a4792f..bb9ab5e9 100644 --- a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionSession.java +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/ExplosionSession.java @@ -18,7 +18,7 @@ import net.neoforged.neoforge.event.tick.ServerTickEvent; import org.apache.logging.log4j.util.TriConsumer; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/package-info.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/package-info.java index 11edb600..c758fcab 100644 --- a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/package-info.java +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.explosion.data; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/provider/package-info.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/provider/package-info.java index 00eef8b7..42b78d63 100644 --- a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/provider/package-info.java +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/data/provider/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.explosion.data.provider; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/package-info.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/package-info.java index 33a27026..af71303d 100644 --- a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/package-info.java +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.explosion; -import org.jspecify.annotations.NullMarked; \ No newline at end of file +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.font/build.gradle b/module.font/build.gradle index d7fa1e8a..c01e5e0e 100644 --- a/module.font/build.gradle +++ b/module.font/build.gradle @@ -120,7 +120,6 @@ dependencies { } else { jarJar(implementation project(":anvillib-rendering-neoforge-1.21.1")) } - compileOnly "org.jspecify:jspecify:1.0.0" } // This block of code expands all declared replace properties in the specified resource targets. diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java index b2604edf..139af899 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java @@ -10,7 +10,7 @@ import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.neoforge.client.event.RegisterShadersEvent; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.io.IOException; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java index 428ee183..1ffa027f 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java @@ -7,7 +7,7 @@ import lombok.extern.slf4j.Slf4j; import net.neoforged.fml.loading.FMLPaths; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.io.File; import java.io.FileReader; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java index 7ec0d56c..fd126517 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java @@ -5,7 +5,7 @@ import com.google.common.collect.MultimapBuilder; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.awt.Font; import java.awt.GraphicsEnvironment; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/package-info.java index dc9f983b..a5a28bf2 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/package-info.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.font.data; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java index b06ba290..863d4a4e 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java @@ -8,7 +8,7 @@ import net.minecraft.network.chat.FormattedText; import net.minecraft.util.FastColor; import net.minecraft.util.FormattedCharSequence; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.awt.Font; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/package-info.java index ebeb597f..b726412a 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/package-info.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.font.extension; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/package-info.java index 7bc2def2..ec5e1614 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/package-info.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.font.mixin; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/package-info.java index f314ae57..3e4dc59f 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/package-info.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.font; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java index 8d02e209..51b77ed4 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java @@ -9,7 +9,7 @@ import net.minecraft.network.chat.Component; import net.neoforged.fml.ModContainer; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Comparator; @@ -18,7 +18,7 @@ @ApiStatus.Internal public class FontConfigScreen extends Screen { protected final Screen lastScreen; - private Dropdown.@Nullable Shielding shielding; + private @Nullable Dropdown.Shielding shielding; @SuppressWarnings("FieldCanBeLocal") private @Nullable Dropdown familyDropdown; private @Nullable Dropdown fontDropdown; @@ -127,13 +127,13 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia guiGraphics.drawString(this.font, this.fontComponent, dropdownLabelX, fontDropdownLabelY, 0xFFFFFFFF); } - private void updateSelectedFamily(Dropdown.@Nullable DropdownEntry entry) { + private void updateSelectedFamily(@Nullable Dropdown.DropdownEntry entry) { this.selectedFamilyText = entry == null ? Component.literal("Current family: ") : Component.literal("Current family: " + entry.id()); } - private void updateSelectedFont(Dropdown.@Nullable DropdownEntry entry) { + private void updateSelectedFont(@Nullable Dropdown.DropdownEntry entry) { this.selectedFontText = entry == null ? Component.literal("Current font: ") : Component.literal("Current font: " + entry.id()); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/package-info.java index e3a64d76..5bacb848 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/package-info.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.font.screen; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java index 987634d9..e6c4bfc1 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java @@ -10,7 +10,7 @@ import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.network.chat.Component; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; @@ -25,7 +25,7 @@ public class Dropdown extends AbstractWidget { private @Nullable DropdownEntry value = null; private boolean expanded = false; @Setter - private Consumer<@Nullable DropdownEntry> onValueChanged = ignored -> { + private Consumer onValueChanged = ignored -> { }; @Setter private Consumer onShieldingAdd = ignored -> { @@ -34,7 +34,7 @@ public class Dropdown extends AbstractWidget { private Runnable onShieldingRemove = () -> { }; @Setter - private Supplier<@Nullable Shielding> shieldingGetter = () -> null; + private Supplier shieldingGetter = () -> null; private final int screenWidth; private final int screenHeight; private int scrollOffset = 0; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/package-info.java index e95c0cd3..3ee0438d 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/package-info.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.font.screen.widget; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java index 4cce9e98..449cce1b 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java @@ -2,7 +2,7 @@ import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.awt.Color; import java.awt.Font; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java index 4bdff3bd..b0b6566b 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java @@ -2,7 +2,7 @@ import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java index 9254f140..7675386d 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java @@ -16,7 +16,7 @@ import net.minecraft.network.chat.FormattedText; import net.minecraft.network.chat.Style; import net.minecraft.util.FormattedCharSequence; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.awt.Font; import java.util.List; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/package-info.java index 1fcf1894..24b99a53 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/package-info.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.font.sdf; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/package-info.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/package-info.java index 7830d14c..e9922755 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/package-info.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.font.sdf.state; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/package-info.java b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/package-info.java index 0d42d3c7..e59e85ee 100644 --- a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/package-info.java +++ b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.integration; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.main/src/main/java/dev/anvilcraft/lib/v2/package-info.java b/module.main/src/main/java/dev/anvilcraft/lib/v2/package-info.java index 5b0d0b07..e17e19b9 100644 --- a/module.main/src/main/java/dev/anvilcraft/lib/v2/package-info.java +++ b/module.main/src/main/java/dev/anvilcraft/lib/v2/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/injection/package-info.java b/module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/injection/package-info.java index 7afb0e57..3eee62fd 100644 --- a/module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/injection/package-info.java +++ b/module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/injection/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.piston.injection; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/mixin/package-info.java b/module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/mixin/package-info.java index 70d4f6c2..bfc0d851 100644 --- a/module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/mixin/package-info.java +++ b/module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/mixin/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.piston.mixin; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/package-info.java b/module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/package-info.java index b10f83fc..f0aa1419 100644 --- a/module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/package-info.java +++ b/module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.piston; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.multiblock/build.gradle b/module.multiblock/build.gradle index b2a7aed8..c4bfd404 100644 --- a/module.multiblock/build.gradle +++ b/module.multiblock/build.gradle @@ -115,7 +115,6 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { - compileOnly "org.jspecify:jspecify:1.0.0" if (System.getenv("NOT_DEV") == 'true') { jarJar(api("dev.anvilcraft.lib:anvillib-config-neoforge-1.21.1:latest.release")) diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/data/provider/package-info.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/data/provider/package-info.java index 80ab4e69..b8c79c0b 100644 --- a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/data/provider/package-info.java +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/data/provider/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.multiblock.data.provider; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockState.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockState.java index 42e3ebd5..6f15e1e1 100644 --- a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockState.java +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockState.java @@ -17,7 +17,7 @@ import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceKey; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.Map; @@ -40,7 +40,7 @@ public class MultiblockState { private final ResourceKey definitionKey; @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) - private Holder.@Nullable Reference definition; + private @Nullable Holder.Reference definition; private boolean formed; private MultiblockCheckSnapshot snapshot; diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/controller/package-info.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/controller/package-info.java index 5371e9b2..56c65c58 100644 --- a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/controller/package-info.java +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/controller/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.multiblock.dynamic.controller; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/definition/package-info.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/definition/package-info.java index 70f938f5..daa4f1fa 100644 --- a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/definition/package-info.java +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/definition/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.multiblock.dynamic.definition; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/package-info.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/package-info.java index 5c5a03a1..95697a09 100644 --- a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/package-info.java +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/package-info.java @@ -8,10 +8,12 @@ *

  • 控制器相关接口/记录用于自定义多方块控制器行为(onFormed/onUnformed)。
  • * */ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.multiblock.dynamic; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/event/package-info.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/event/package-info.java index ab5aacd8..f74ac1e9 100644 --- a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/event/package-info.java +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/event/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.multiblock.event; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/init/package-info.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/init/package-info.java index 790b69ef..814ce38c 100644 --- a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/init/package-info.java +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/init/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.multiblock.init; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/package-info.java b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/package-info.java index 45bd281d..5c330db0 100644 --- a/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/package-info.java +++ b/module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.multiblock; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/package-info.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/package-info.java index 15ec89c8..fffc9b26 100644 --- a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/package-info.java +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.network.packet; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/package-info.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/package-info.java index 692698a1..1c6dbf9d 100644 --- a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/package-info.java +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.network.register; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/util/package-info.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/util/package-info.java index 765a8d77..f5ab4079 100644 --- a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/util/package-info.java +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/util/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.network.util; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/package-info.java index 808fd0ac..2791c1da 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.builder; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/operation/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/operation/package-info.java index 2d02847f..df0a22ec 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/operation/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/operation/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.cache.item.operation; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/package-info.java index eaf20fe7..3728baa9 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.cache.item; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/package-info.java index 7481d3c9..97c4590e 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.cache; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/component/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/component/package-info.java index 5d8c540d..aa979f30 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/component/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/component/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.component; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/advancement/predicate/item/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/advancement/predicate/item/package-info.java index 95ce952a..34ac99b2 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/advancement/predicate/item/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/advancement/predicate/item/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.data.advancement.predicate.item; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/gen/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/gen/package-info.java index 90db3869..ee65b443 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/gen/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/gen/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.data.gen; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/gen/provider/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/gen/provider/package-info.java index 2ae429d7..0ca2f060 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/gen/provider/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/gen/provider/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.data.gen.provider; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/init/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/init/package-info.java index 9432e79c..717b8263 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/init/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/init/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.init; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/init/recipe/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/init/recipe/package-info.java index 00b0dc87..042fe908 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/init/recipe/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/init/recipe/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.init.recipe; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/mixin/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/mixin/package-info.java index b2af0b05..76c02fbd 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/mixin/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/mixin/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.mixin; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/function/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/function/package-info.java index 1a6848ec..4dbbd6ae 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/function/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/function/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.outcome.function; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/package-info.java index 28f4ef96..7e931446 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.outcome; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/package-info.java index be7a477b..f98e1c18 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/package-info.java index bc8ca1a4..ff7d4d80 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.predicate.block; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/function/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/function/package-info.java index 63486cba..717b5324 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/function/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/function/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.predicate.function; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/package-info.java index c13ee1ec..b4245681 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.predicate.item; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/package-info.java index 3d9a0583..435d1f47 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.predicate; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/trigger/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/trigger/package-info.java index 62109eb1..8ed6adc8 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/trigger/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/trigger/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.trigger; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/util/package-info.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/util/package-info.java index 169adb65..d7f01740 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/util/package-info.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/util/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.recipe.util; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.registrum/build.gradle b/module.registrum/build.gradle index 77e10aa2..29bec520 100644 --- a/module.registrum/build.gradle +++ b/module.registrum/build.gradle @@ -115,7 +115,6 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { - compileOnly "org.jspecify:jspecify:1.0.0" if (System.getenv("NOT_DEV") == 'true') { jarJar(api("dev.anvilcraft.lib:anvillib-util-neoforge-1.21.1:latest.release")) } else { diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/package-info.java index ec0b37cd..1729a063 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/package-info.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/data/package-info.java @@ -11,7 +11,12 @@ * */ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.registrum.builders.data; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/package-info.java index bc72532c..902461a2 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/package-info.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/modifier/package-info.java @@ -10,7 +10,12 @@ * * Original File: https://github.com/tterrag1098/Registrate/blob/1.21.5/dev/src/main/java/com/tterrag/registrate/builders/package-info.java * */ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.registrum.builders.modifier; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/package-info.java index 3c5a14f3..c90321f4 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/package-info.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/recipe/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.registrum.builders.recipe; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/package-info.java index f59332d5..a476738a 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/package-info.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/self/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.registrum.builders.self; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/VillagerProfessionBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/VillagerProfessionBuilder.java index e5e1088b..d1fe51ed 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/VillagerProfessionBuilder.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/VillagerProfessionBuilder.java @@ -12,7 +12,7 @@ import net.minecraft.world.entity.npc.VillagerProfession; import net.minecraft.world.item.Item; import net.minecraft.world.level.block.Block; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.function.Predicate; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/package-info.java index 4138dbb1..e92abeef 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/package-info.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/villager/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.registrum.builders.villager; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/providers/RegistrumDataMapProvider.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/providers/RegistrumDataMapProvider.java index 40798978..57b164c0 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/providers/RegistrumDataMapProvider.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/providers/RegistrumDataMapProvider.java @@ -17,7 +17,7 @@ import net.minecraft.data.PackOutput; import net.neoforged.fml.LogicalSide; import net.neoforged.neoforge.common.data.DataMapProvider; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.concurrent.CompletableFuture; @@ -25,7 +25,7 @@ public class RegistrumDataMapProvider extends DataMapProvider implements Registr private final AbstractRegistrum parent; - private HolderLookup.@Nullable Provider provider; + private @Nullable HolderLookup.Provider provider; protected RegistrumDataMapProvider(AbstractRegistrum parent, PackOutput output, CompletableFuture pvd) { super(output, pvd); diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/package-info.java index 88a69741..024aa62c 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/package-info.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/data/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.registrum.util.entry.data; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/package-info.java index 31fbe90d..02c91049 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/package-info.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/modifier/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.registrum.util.entry.modifier; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/package-info.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/package-info.java index 43ab4c8a..702532d9 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/package-info.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/util/entry/recipe/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.registrum.util.entry.recipe; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.rendering/build.gradle b/module.rendering/build.gradle index 92fe77b1..f778e94e 100644 --- a/module.rendering/build.gradle +++ b/module.rendering/build.gradle @@ -115,7 +115,6 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { - compileOnly "org.jspecify:jspecify:1.0.0" // #P7d: optional Iris integration (integration/IrisSupport.java + the two integration mixins). // The dev/26.1 branch used compileOnly("maven.modrinth:YL57xq9U:...") (Iris) from the Modrinth diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedBlockEntityRenderingPipeline.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedBlockEntityRenderingPipeline.java index 7fa4c115..d1bc8f73 100644 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedBlockEntityRenderingPipeline.java +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedBlockEntityRenderingPipeline.java @@ -17,7 +17,7 @@ import net.neoforged.neoforge.client.event.RenderFrameEvent; import net.neoforged.neoforge.client.event.RenderLevelStageEvent; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import org.lwjgl.opengl.GL; import org.lwjgl.opengl.GL46; diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedRenderingChunk.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedRenderingChunk.java index f6974199..416a034b 100644 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedRenderingChunk.java +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedRenderingChunk.java @@ -24,7 +24,7 @@ import net.minecraft.world.phys.Vec3; import org.joml.Matrix4fStack; import org.joml.Vector3f; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.Collection; import java.util.HashMap; diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderDispatcher.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderDispatcher.java index 8b5c39ae..ef0d19d3 100644 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderDispatcher.java +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderDispatcher.java @@ -9,7 +9,7 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import org.slf4j.Logger; import java.util.HashMap; diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeShaderManager.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeShaderManager.java index 4f62c6a6..2e964cb3 100644 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeShaderManager.java +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeShaderManager.java @@ -15,8 +15,8 @@ import net.neoforged.fml.ModLoader; import org.apache.commons.io.IOUtils; import org.jetbrains.annotations.UnknownNullability; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.lwjgl.opengl.ARBComputeShader; import org.lwjgl.opengl.GL; import org.lwjgl.opengl.GL20; @@ -53,7 +53,7 @@ public class ALRComputeShaderManager extends SimplePreparableReloadListener pipelineToProgramMap = new HashMap<>(); @Override - protected @NonNull ComputeShaderSource prepare(ResourceManager manager, ProfilerFiller profiler) { + protected @Nonnull ComputeShaderSource prepare(ResourceManager manager, ProfilerFiller profiler) { ImmutableMap.Builder sources = new ImmutableMap.Builder<>(); Map shaders = manager.listResources( diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObject.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObject.java index 43952839..b11b885f 100644 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObject.java +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObject.java @@ -1,7 +1,7 @@ package dev.anvilcraft.lib.v2.rendering.foundation.buffers.object; import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import org.jspecify.annotations.NonNull; +import javax.annotation.Nonnull; import java.nio.ByteBuffer; @@ -25,7 +25,7 @@ protected BufferObject(BufferLayout layout, ShaderBufferObjectUsage usage) { * user-provided buffer instead. */ @SuppressWarnings("unchecked") - public void write(@NonNull ByteBuffer buffer) { + public void write(@Nonnull ByteBuffer buffer) { getDefinition().write(buffer, (T) this, this.layout); } } diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/IrisSupport.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/IrisSupport.java index dccfe738..f2e8d561 100644 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/IrisSupport.java +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/IrisSupport.java @@ -1,7 +1,7 @@ package dev.anvilcraft.lib.v2.rendering.integration; import net.neoforged.fml.ModList; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.lang.reflect.Field; import java.lang.reflect.Method; diff --git a/module.rpc/build.gradle b/module.rpc/build.gradle index a8457d91..ee7f4f39 100644 --- a/module.rpc/build.gradle +++ b/module.rpc/build.gradle @@ -115,7 +115,6 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { - compileOnly "org.jspecify:jspecify:1.0.0" if (System.getenv("NOT_DEV") == 'true') { jarJar(api("dev.anvilcraft.lib:anvillib-network-neoforge-1.21.1:latest.release")) diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcMethods.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcMethods.java index 2bd58b54..629695b3 100644 --- a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcMethods.java +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcMethods.java @@ -10,7 +10,7 @@ import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import net.neoforged.neoforge.network.handling.IPayloadContext; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.lang.reflect.Field; import java.lang.reflect.Method; diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcPendingCalls.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcPendingCalls.java index 899e4e33..cfe13564 100644 --- a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcPendingCalls.java +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcPendingCalls.java @@ -1,7 +1,7 @@ package dev.anvilcraft.lib.v2.rpc; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.lang.reflect.Method; import java.util.Iterator; diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcRegistry.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcRegistry.java index ad15a860..5e17777d 100644 --- a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcRegistry.java +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/RpcRegistry.java @@ -5,7 +5,7 @@ import net.neoforged.fml.loading.moddiscovery.ModFileInfo; import net.neoforged.neoforgespi.language.ModFileScanData; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.lang.annotation.ElementType; import java.lang.reflect.Method; diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/client/package-info.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/client/package-info.java index 8a6ca921..e76b5e46 100644 --- a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/client/package-info.java +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/client/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.rpc.client; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/package-info.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/package-info.java index 4f5e749f..05bb04d9 100644 --- a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/package-info.java +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/config/package-info.java @@ -1,7 +1,12 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault @Network(protocol = PacketProtocol.CONFIGURATION) package dev.anvilcraft.lib.v2.rpc.config; import dev.anvilcraft.lib.v2.network.register.Network; import dev.anvilcraft.lib.v2.network.register.PacketProtocol; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/package-info.java b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/package-info.java index e535f4d1..1ea2c6e7 100644 --- a/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/package-info.java +++ b/module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/package-info.java @@ -1,7 +1,12 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault @Network(protocol = PacketProtocol.PLAY) package dev.anvilcraft.lib.v2.rpc; import dev.anvilcraft.lib.v2.network.register.Network; import dev.anvilcraft.lib.v2.network.register.PacketProtocol; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.space-select/build.gradle b/module.space-select/build.gradle index a8457d91..ee7f4f39 100644 --- a/module.space-select/build.gradle +++ b/module.space-select/build.gradle @@ -115,7 +115,6 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { - compileOnly "org.jspecify:jspecify:1.0.0" if (System.getenv("NOT_DEV") == 'true') { jarJar(api("dev.anvilcraft.lib:anvillib-network-neoforge-1.21.1:latest.release")) diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/DistrictManager.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/DistrictManager.java index f3f92eee..0eb13a80 100644 --- a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/DistrictManager.java +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/DistrictManager.java @@ -4,7 +4,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/ClientDistrictManager.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/ClientDistrictManager.java index 90d4c503..3c7f3eb6 100644 --- a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/ClientDistrictManager.java +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/ClientDistrictManager.java @@ -12,7 +12,7 @@ import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; import net.neoforged.neoforge.network.PacketDistributor; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/package-info.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/package-info.java index 629a077e..bb728588 100644 --- a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/package-info.java +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/client/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.space_select.client; -import org.jspecify.annotations.NullMarked; \ No newline at end of file +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/handler/package-info.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/handler/package-info.java index ecc5f1fa..b702fac6 100644 --- a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/handler/package-info.java +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/handler/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.space_select.event.handler; -import org.jspecify.annotations.NullMarked; \ No newline at end of file +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/package-info.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/package-info.java index 2554d1f3..435b5a93 100644 --- a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/package-info.java +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/event/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.space_select.event; -import org.jspecify.annotations.NullMarked; \ No newline at end of file +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/package-info.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/package-info.java index db309f4b..9d3893e1 100644 --- a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/package-info.java +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.space_select.network; -import org.jspecify.annotations.NullMarked; \ No newline at end of file +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/payload/package-info.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/payload/package-info.java index db3afb53..4e728284 100644 --- a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/payload/package-info.java +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/network/payload/package-info.java @@ -1,6 +1,11 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault @Network package dev.anvilcraft.lib.v2.space_select.network.payload; import dev.anvilcraft.lib.v2.network.register.Network; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/package-info.java b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/package-info.java index 1a2ec58b..5f622c79 100644 --- a/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/package-info.java +++ b/module.space-select/src/main/java/dev/anvilcraft/lib/v2/space_select/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.space_select; -import org.jspecify.annotations.NullMarked; \ No newline at end of file +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.sync/build.gradle b/module.sync/build.gradle index 561b5876..c85dca6c 100644 --- a/module.sync/build.gradle +++ b/module.sync/build.gradle @@ -115,7 +115,6 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { - compileOnly "org.jspecify:jspecify:1.0.0" if (System.getenv("NOT_DEV") == 'true') { jarJar(api("dev.anvilcraft.lib:anvillib-sync-processor-neoforge-1.21.1:latest.release")) diff --git a/module.sync/processor/build.gradle b/module.sync/processor/build.gradle index 8bf87f17..6f2f8be6 100644 --- a/module.sync/processor/build.gradle +++ b/module.sync/processor/build.gradle @@ -27,10 +27,6 @@ neoForge { version = libs.versions.neoForge.get() } -dependencies { - compileOnly "org.jspecify:jspecify:1.0.0" -} - java { toolchain { languageVersion = JavaLanguageVersion.of(21) diff --git a/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/package-info.java b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/package-info.java index f1bbd2ef..a94f8298 100644 --- a/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/package-info.java +++ b/module.sync/processor/src/main/java/dev/anvilcraft/lib/v2/sync/transform/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.sync.transform; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/package-info.java index 80ad80f8..192d799a 100644 --- a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/package-info.java +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.sync.annotation; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/package-info.java index 004ae1f8..fac333b3 100644 --- a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/package-info.java +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/init/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.sync.init; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/LazySyncManager.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/LazySyncManager.java index 4269746c..a6a696c5 100644 --- a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/LazySyncManager.java +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/LazySyncManager.java @@ -14,7 +14,7 @@ import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.lang.reflect.Field; import java.lang.reflect.Modifier; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncConfigManager.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncConfigManager.java index db879108..cfc2a4ff 100644 --- a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncConfigManager.java +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncConfigManager.java @@ -9,7 +9,7 @@ import net.neoforged.neoforgespi.language.ModFileScanData; import net.neoforged.neoforgespi.locating.IModFile; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncManager.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncManager.java index 7c949be0..e2fe6abb 100644 --- a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncManager.java +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncManager.java @@ -6,7 +6,7 @@ import lombok.extern.slf4j.Slf4j; import net.minecraft.resources.ResourceKey; import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncProxy.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncProxy.java index 0ab097ad..170ebb12 100644 --- a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncProxy.java +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncProxy.java @@ -24,7 +24,7 @@ import org.jetbrains.annotations.ApiStatus; import org.joml.Quaternionfc; import org.joml.Vector3fc; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.lang.reflect.Field; import java.lang.reflect.Modifier; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncRegisterEntry.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncRegisterEntry.java index 425ab659..21a53ff4 100644 --- a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncRegisterEntry.java +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncRegisterEntry.java @@ -5,7 +5,7 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.Level; import net.neoforged.neoforge.network.handling.IPayloadContext; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.function.Function; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/package-info.java index a7fc9061..3cbce42f 100644 --- a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/package-info.java +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.sync.management; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/package-info.java index f65681b2..3ee2150a 100644 --- a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/package-info.java +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.sync.network; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/package-info.java index aa10c6ea..595fd528 100644 --- a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/package-info.java +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/network/payload/package-info.java @@ -1,7 +1,12 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault @Network(protocol = PacketProtocol.COMMON) package dev.anvilcraft.lib.v2.sync.network.payload; import dev.anvilcraft.lib.v2.network.register.Network; import dev.anvilcraft.lib.v2.network.register.PacketProtocol; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/package-info.java index 42c1ddfb..e5521346 100644 --- a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/package-info.java +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.sync; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SideUtil.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SideUtil.java index e96e72ff..23e4ae3d 100644 --- a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SideUtil.java +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SideUtil.java @@ -23,7 +23,7 @@ import net.neoforged.neoforge.network.connection.ConnectionType; import net.neoforged.neoforge.network.handling.IPayloadContext; import net.neoforged.neoforge.server.ServerLifecycleHooks; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.UUID; import java.util.function.Function; diff --git a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/package-info.java b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/package-info.java index 62bdfb93..6d866150 100644 --- a/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/package-info.java +++ b/module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.sync.util; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.test/build.gradle b/module.test/build.gradle index 993885d4..0d8bcca4 100644 --- a/module.test/build.gradle +++ b/module.test/build.gradle @@ -113,7 +113,6 @@ dependencies { implementation project(":anvillib-codec-neoforge-1.21.1") implementation project(":anvillib-explosion-neoforge-1.21.1") implementation project(":anvillib-rpc-neoforge-1.21.1") - compileOnly "org.jspecify:jspecify:1.0.0" implementation project(":anvillib-multiblock-neoforge-1.21.1") implementation project(":anvillib-integration-neoforge-1.21.1") interfaceInjectionData(implementation project(":anvillib-moveable-entity-block-neoforge-1.21.1")) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/command/package-info.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/command/package-info.java index 766f71f8..787ed01c 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/command/package-info.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/command/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.test.command; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/mixin/package-info.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/mixin/package-info.java index 278ef4ed..b9fd8b43 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/mixin/package-info.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/mixin/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.test.mixin; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/block/package-info.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/block/package-info.java index fb443b2f..4d475d12 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/block/package-info.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/block/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.test.multiblock.block; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/init/package-info.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/init/package-info.java index dd0ee18b..280fd357 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/init/package-info.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/init/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.test.multiblock.init; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/package-info.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/package-info.java index 56c74823..ac8ff0d9 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/package-info.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.test; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcIntegrationTest.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcIntegrationTest.java index 1e6578ca..b27ec608 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcIntegrationTest.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/RpcIntegrationTest.java @@ -8,7 +8,7 @@ import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.neoforge.event.entity.player.PlayerEvent; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -237,7 +237,7 @@ private static void check(String name, boolean condition, String message) { /** * future 异常处理器:记录失败并吞掉异常,避免污染后续回调链。 */ - private static Function logFailure(String name) { + private static Function logFailure(String name) { return ex -> { LOGGER.error("RPC call FAILED [{}]: {}", name, ex.getMessage(), ex); return null; diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/package-info.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/package-info.java index cd49ce65..350eadb0 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/package-info.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/rpc/package-info.java @@ -1,4 +1,9 @@ -@NullMarked +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.test.rpc; -import org.jspecify.annotations.NullMarked; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/data/lang/package-info.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/data/lang/package-info.java index 0c334edf..bf8afca0 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/data/lang/package-info.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/data/lang/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.test.wheel.data.lang; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/package-info.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/package-info.java index b0fcde8f..0bf72eb3 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/package-info.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.test.wheel; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.util/build.gradle b/module.util/build.gradle index 33a6277d..1363f35d 100644 --- a/module.util/build.gradle +++ b/module.util/build.gradle @@ -115,7 +115,6 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { - compileOnly "org.jspecify:jspecify:1.0.0" if (System.getenv("NOT_DEV") == 'true') { jarJar(api("dev.anvilcraft.lib:anvillib-codec-neoforge-1.21.1:latest.release")) diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/OutlineUtil.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/OutlineUtil.java index b73ea8b6..f7efa5ef 100644 --- a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/OutlineUtil.java +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/OutlineUtil.java @@ -3,7 +3,7 @@ import dev.anvilcraft.lib.v2.util.client.Line; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.BitSet; diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/component/package-info.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/component/package-info.java index 3854bb0f..9ebf8ec5 100644 --- a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/component/package-info.java +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/component/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.util.component; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/package-info.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/package-info.java index 5b27ce6c..3f07dc21 100644 --- a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/package-info.java +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.util; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/package-info.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/package-info.java index f4ba3df4..8c765df0 100644 --- a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/package-info.java +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.util.predicate; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/stack/package-info.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/stack/package-info.java index ed55ee0a..40fc6855 100644 --- a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/stack/package-info.java +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/stack/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.util.stack; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/package-info.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/package-info.java index 31324d72..cf574937 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/package-info.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.wheel.api; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/package-info.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/package-info.java index 53c687ac..3809009a 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/package-info.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.wheel.client.gui.component; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/package-info.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/package-info.java index b83c8086..86e4be80 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/package-info.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.wheel.client.gui.screen; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/package-info.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/package-info.java index 9c4598bf..f34e997b 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/package-info.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.wheel.client.init; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/input/package-info.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/input/package-info.java index 647b5738..c4654dc3 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/input/package-info.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/input/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.wheel.client.input; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/package-info.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/package-info.java index c78eb4cb..d5582a73 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/package-info.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/package-info.java @@ -1,7 +1,9 @@ +@FieldsAreNonnullByDefault @MethodsReturnNonnullByDefault @ParametersAreNonnullByDefault package dev.anvilcraft.lib.v2.wheel; -import net.minecraft.MethodsReturnNonnullByDefault; +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; import javax.annotation.ParametersAreNonnullByDefault; From 386adc8d1a966b2859427c63a658df3a7124b3c7 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 11 Aug 2026 10:03:43 +0800 Subject: [PATCH 26/38] =?UTF-8?q?Revert=20"feat(integration):=20=E5=9B=9E?= =?UTF-8?q?=E7=A7=BB=E6=A4=8D=E6=95=B0=E6=8D=AE=E5=8A=A0=E8=BD=BD=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF/=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=8B=86?= =?UTF-8?q?=E5=88=86=E4=B8=8E=20meter=20=E9=80=92=E5=A2=9E"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit adc23468727627741edcb6bb87898648a68887e1. --- .../v2/integration/IntegrationInstance.java | 31 +++++-------------- .../v2/integration/IntegrationManager.java | 31 ++++--------------- .../lib/v2/integration/IntegrationType.java | 3 +- 3 files changed, 15 insertions(+), 50 deletions(-) diff --git a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationInstance.java b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationInstance.java index 262c9819..4892ad47 100644 --- a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationInstance.java +++ b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationInstance.java @@ -24,8 +24,7 @@ public final class IntegrationInstance { private MethodHandle constructor; private MethodHandle loader; private MethodHandle clientLoader; - private MethodHandle clientDataLoader; - private MethodHandle serverDataLoader; + private MethodHandle dataLoader; @SneakyThrows public IntegrationInstance(String modid, ModVersionRange versionRange, String className, List type) { @@ -55,18 +54,12 @@ public void newInstance() { } this.clientLoader = loader; try { - loader = lookup.findVirtual(clazz, "applyClientData", MethodType.methodType(void.class)); + loader = lookup.findVirtual(clazz, "applyData", MethodType.methodType(void.class)); } catch (Throwable e) { loader = null; } - this.clientDataLoader = loader; - try { - loader = lookup.findVirtual(clazz, "applyServerData", MethodType.methodType(void.class)); - } catch (Throwable e) { - loader = null; - } - this.serverDataLoader = loader; - if (this.loader == null && this.clientLoader == null && this.clientDataLoader == null && this.serverDataLoader == null) { + this.dataLoader = loader; + if (this.loader == null && this.clientLoader == null && this.dataLoader == null) { log.warn("Integration {} does not declare any loader method.", className); } } @@ -90,16 +83,9 @@ public void invokeClient() { } @SneakyThrows - public void invokeClientData() { - if (clientDataLoader != null) { - clientDataLoader.invoke(instance); - } - } - - @SneakyThrows - public void invokeServerData() { - if (serverDataLoader != null) { - serverDataLoader.invoke(instance); + public void invokeData() { + if (dataLoader != null) { + dataLoader.invoke(instance); } } @@ -119,8 +105,7 @@ public String toString() { + "instance=" + instance + ", " + "loader=" + loader + ", " + "clientLoader=" + clientLoader + ", " - + "clientDataLoader=" + clientDataLoader + ", " - + "serverDataLoader=" + serverDataLoader + + "dataLoader=" + dataLoader + ']'; } } diff --git a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java index 2748ad55..300121d4 100644 --- a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java +++ b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java @@ -51,15 +51,13 @@ public void compileContent() { type = typeHolders.stream().map(holder -> switch (holder.value()) { case "DEDICATED_SERVER" -> IntegrationType.DEDICATED_SERVER; case "CLIENT" -> IntegrationType.CLIENT; - case "CLIENT_DATA" -> IntegrationType.CLIENT_DATA; - case "SERVER_DATA" -> IntegrationType.SERVER_DATA; + case "DATA" -> IntegrationType.DATA; default -> throw new IllegalArgumentException("Unknown integration type: " + holder.value()); }).toList(); } log.info("Considering integration {} for {id:{}, version:{}}", annotation.memberName(), modid, version); IntegrationInstance instance = new IntegrationInstance(modid, ModVersionRange.of(version), annotation.memberName(), type); this.instances.put(modid, instance); - meter.increment(); } } StartupNotificationManager.popBar(meter); @@ -87,22 +85,12 @@ public void loadClient(String modid, ModInfo info) { } @SuppressWarnings("DataFlowIssue") - public void loadClientData(String modid, ModInfo info) { + public void loadData(String modid, ModInfo info) { for (IntegrationInstance instance : instances.get(modid)) { if (!instance.is(info)) continue; instance.newInstance(); - log.info("Loading client data integration {} for {}.", instance.getInstance(), modid); - instance.invokeClientData(); - } - } - - @SuppressWarnings("DataFlowIssue") - public void loadServerData(String modid, ModInfo info) { - for (IntegrationInstance instance : instances.get(modid)) { - if (!instance.is(info)) continue; - instance.newInstance(); - log.info("Loading server data integration {} for {}.", instance.getInstance(), modid); - instance.invokeServerData(); + log.info("Loading data integration {} for {}.", instance.getInstance(), modid); + instance.invokeData(); } } @@ -120,17 +108,10 @@ public void loadAllClientIntegrations() { } } - public void loadAllClientDataIntegrations() { - for (String key : instances.keySet()) { - Optional info = LoadingModList.get().getMods().stream().filter(it -> it.getModId().equals(key)).findFirst(); - info.ifPresent(modInfo -> loadClientData(key, modInfo)); - } - } - - public void loadAllServerDataIntegrations() { + public void loadAllDataIntegrations() { for (String key : instances.keySet()) { Optional info = LoadingModList.get().getMods().stream().filter(it -> it.getModId().equals(key)).findFirst(); - info.ifPresent(modInfo -> loadServerData(key, modInfo)); + info.ifPresent(modInfo -> loadData(key, modInfo)); } } } diff --git a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationType.java b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationType.java index d440f618..2fac43af 100644 --- a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationType.java +++ b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationType.java @@ -3,6 +3,5 @@ public enum IntegrationType { CLIENT, DEDICATED_SERVER, - CLIENT_DATA, - SERVER_DATA + DATA } From dff907b65a301b4c4063df4ca39ef3d10ce519f8 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 11 Aug 2026 10:05:06 +0800 Subject: [PATCH 27/38] =?UTF-8?q?1feat(integration):=20=E5=9B=9E=E7=A7=BB?= =?UTF-8?q?=E6=A4=8D=20meter=20=E9=80=92=E5=A2=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - meter 递增(e8efbd4):compileContent 循环内 meter.increment() --- .../dev/anvilcraft/lib/v2/integration/IntegrationManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java index 300121d4..9e41bc98 100644 --- a/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java +++ b/module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java @@ -58,6 +58,7 @@ public void compileContent() { log.info("Considering integration {} for {id:{}, version:{}}", annotation.memberName(), modid, version); IntegrationInstance instance = new IntegrationInstance(modid, ModVersionRange.of(version), annotation.memberName(), type); this.instances.put(modid, instance); + meter.increment(); } } StartupNotificationManager.popBar(meter); From b57530359b4d818615464940dbfcecc9522ef301 Mon Sep 17 00:00:00 2001 From: QiuShui1012 Date: Tue, 11 Aug 2026 11:21:30 +0800 Subject: [PATCH 28/38] port(unlimited): port the new UnlimitedItemStack from 26.1 --- .../lib/v2/util/stack/UnlimitedItemStack.java | 440 +++++++++++++----- 1 file changed, 329 insertions(+), 111 deletions(-) diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/stack/UnlimitedItemStack.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/stack/UnlimitedItemStack.java index 05dfa6a6..35413177 100644 --- a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/stack/UnlimitedItemStack.java +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/stack/UnlimitedItemStack.java @@ -5,47 +5,67 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; -import dev.anvilcraft.lib.v2.util.Util; import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.EncoderException; import lombok.Getter; import lombok.Setter; import net.minecraft.core.Holder; -import net.minecraft.core.HolderLookup; +import net.minecraft.core.HolderSet; +import net.minecraft.core.NonNullList; +import net.minecraft.core.component.DataComponentHolder; +import net.minecraft.core.component.DataComponentMap; import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.component.TypedDataComponent; import net.minecraft.core.registries.Registries; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.NbtOps; -import net.minecraft.nbt.Tag; import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceKey; import net.minecraft.tags.TagKey; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.component.TooltipProvider; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.item.enchantment.ItemEnchantments; import net.minecraft.world.level.ItemLike; -import net.neoforged.neoforge.common.util.INBTSerializable; +import net.neoforged.neoforge.common.ItemAbility; +import net.neoforged.neoforge.common.MutableDataComponentHolder; +import net.neoforged.neoforge.registries.datamaps.DataMapType; +import net.neoforged.neoforge.registries.datamaps.IWithData; import org.slf4j.Logger; import java.util.List; -import java.util.Optional; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import javax.annotation.Nullable; @Getter @Setter -public class UnlimitedItemStack implements INBTSerializable { +public class UnlimitedItemStack implements DataComponentHolder, MutableDataComponentHolder, IWithData { private static final Logger LOGGER = LogUtils.getLogger(); public static final UnlimitedItemStack EMPTY = new UnlimitedItemStack(ItemStack.EMPTY, 0); - public static final MapCodec MAP_CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group( - ItemStack.ITEM_NON_AIR_CODEC - .fieldOf("id") - .forGetter(stack -> stack.getStack().getItemHolder()), - Codec.INT - .fieldOf("count") - .forGetter(UnlimitedItemStack::getCount), - DataComponentPatch.CODEC - .optionalFieldOf("components", DataComponentPatch.EMPTY) - .forGetter(stack -> stack.getStack().getComponentsPatch()) - ).apply(inst, UnlimitedItemStack::new)); + public static final MapCodec MAP_CODEC = MapCodec.recursive( + "UnlimitedItemStack", + ignored -> RecordCodecBuilder.mapCodec(inst -> inst.group( + ItemStack.ITEM_NON_AIR_CODEC + .fieldOf("id") + .forGetter(stack -> stack.getStack().getItemHolder()), + Codec.INT + .fieldOf("count") + .forGetter(UnlimitedItemStack::getCount), + DataComponentPatch.CODEC + .optionalFieldOf("components", DataComponentPatch.EMPTY) + .forGetter(stack -> stack.getStack().getComponentsPatch()) + ).apply(inst, UnlimitedItemStack::new)) + ); public static final Codec CODEC = Codec.lazyInitialized(MAP_CODEC::codec); public static final StreamCodec OPTIONAL_STREAM_CODEC = new StreamCodec<>() { private static final StreamCodec> ITEM_STREAM_CODEC = ByteBufCodecs.holderRegistry( @@ -82,15 +102,15 @@ public void encode(RegistryFriendlyByteBuf buf, UnlimitedItemStack stack) { UnlimitedItemStack.OPTIONAL_STREAM_CODEC.encode(buf, stack); } }; - private ItemStack stack; + public static final StreamCodec> OPTIONAL_LIST_STREAM_CODEC = OPTIONAL_STREAM_CODEC + .apply(ByteBufCodecs.collection(NonNullList::createWithCapacity)); + private ItemStack stack = ItemStack.EMPTY; private int count; public UnlimitedItemStack(ItemStack stack, int count) { this.count = Math.max(count, 0); - if (this.count == 0) { - this.stack = ItemStack.EMPTY; - } else { - this.stack = stack.copyWithCount(1); + if (this.count != 0) { + this.setStack(stack); } } @@ -103,64 +123,70 @@ public UnlimitedItemStack(ItemStack stack) { } public boolean isEmpty() { - return this.stack.isEmpty() || this.count == 0; + return this.getStack().isEmpty() || this.count <= 0; } - public UnlimitedItemStack copy() { - return new UnlimitedItemStack(this.stack, this.count); + public UnlimitedItemStack split(int amount) { + int realAmount = Math.min(amount, this.getCount()); + UnlimitedItemStack result = this.copyWithCount(realAmount); + this.shrink(realAmount); + return result; } - public UnlimitedItemStack copyWithCount(int count) { - return new UnlimitedItemStack(this.stack, count); + public UnlimitedItemStack copyAndClear() { + if (this.isEmpty()) { + return UnlimitedItemStack.EMPTY; + } else { + UnlimitedItemStack result = this.copy(); + this.setCount(0); + return result; + } } - @Override - public CompoundTag serializeNBT(HolderLookup.Provider provider) { - return Util.cast(CODEC.encodeStart(provider.createSerializationContext(NbtOps.INSTANCE), this).getOrThrow()); + public Item getItem() { + return this.typeHolder().value(); } - @Override - public void deserializeNBT(HolderLookup.Provider provider, CompoundTag nbt) { - UnlimitedItemStack.parse(provider, nbt).ifPresent(this::copyFrom); + @SuppressWarnings("deprecation") + public Holder getItemHolder() { + return this.isEmpty() ? Items.AIR.builtInRegistryHolder() : this.getStack().getItemHolder(); } - public static Optional parse(HolderLookup.Provider provider, Tag tag) { - return CODEC.parse(provider.createSerializationContext(NbtOps.INSTANCE), tag) - .resultOrPartial(info -> LOGGER.error("Tried to load invalid item: '{}'", info)); + @SuppressWarnings("deprecation") + public Holder typeHolder() { + return this.isEmpty() ? Items.AIR.builtInRegistryHolder() : this.getStack().getItemHolder(); } - /** - * 该方法不会设置数量。请使用 {@link UnlimitedItemStack#setCount(int)} 设置数量。 - * - * @param stack 提供物品和数据组件的 {@link ItemStack} - */ - public void setStack(ItemStack stack) { - this.stack = stack.copyWithCount(1); + public Stream> tags() { + return this.typeHolder().tags(); } - public void grow(int count) { - this.setCount(this.count + count); + public boolean is(TagKey tag) { + return this.typeHolder().is(tag); } - public void copyFrom(UnlimitedItemStack stack) { - this.setStack(stack.getStack()); - this.setCount(stack.getCount()); + public boolean is(HolderSet set) { + return set.contains(this.typeHolder()); } - public Item getItem() { - return this.stack.getItem(); + public boolean is(Item rawType) { + return this.typeHolder().value() == rawType; + } + + public boolean is(Holder type) { + return this.is(type.value()); } - public boolean is(Item item) { - return this.stack.is(item); + public boolean is(ResourceKey type) { + return this.typeHolder().is(type); } public boolean is(ItemLike item) { - return this.stack.is(item.asItem()); + return this.is(item.asItem()); } - public boolean is(TagKey item) { - return this.stack.is(item); + public boolean is(Predicate> item) { + return item.test(this.typeHolder()); } public boolean isAny(ItemLike... items) { @@ -170,28 +196,106 @@ public boolean isAny(ItemLike... items) { return false; } - public Holder getItemHolder() { - return this.stack.getItemHolder(); + @Override + public @Nullable T getData(DataMapType type) { + return this.typeHolder().getData(type); } - public DataComponentPatch getComponentsPatch() { - return this.stack.getComponentsPatch(); + public int getMaxStackSize() { + return this.getItem().getMaxStackSize(this.toStack()); + } + + public boolean isStackable() { + return this.getMaxStackSize() > 1 && (!this.isDamageableItem() || !this.isDamaged()); + } + + public boolean isDamageableItem() { + return this.getStack().isDamageableItem(); + } + + public boolean isDamaged() { + return this.getStack().isDamaged(); + } + + public UnlimitedItemStack copy() { + return new UnlimitedItemStack(this.getStack(), this.getCount()); + } + + public UnlimitedItemStack copyWithCount(int count) { + return new UnlimitedItemStack(this.getStack(), count); + } + + public UnlimitedItemStack transmuteCopy(ItemLike newItem) { + return this.transmuteCopy(newItem, this.getCount()); + } + + public UnlimitedItemStack transmuteCopy(ItemLike newItem, int newCount) { + return this.isEmpty() ? UnlimitedItemStack.EMPTY : this.transmuteCopyIgnoreEmpty(newItem, newCount); + } + + @SuppressWarnings("deprecation") + private UnlimitedItemStack transmuteCopyIgnoreEmpty(ItemLike newItem, int newCount) { + return new UnlimitedItemStack(newItem.asItem().builtInRegistryHolder(), newCount, this.getComponentsPatch()); + } + + public boolean matches(ItemStack stack) { + return ItemStack.matches(this.toStack(), stack); + } + + public boolean matches(UnlimitedItemStack stack) { + if (this == stack) { + return true; + } + return this.getCount() == stack.getCount() && this.isSameItemSameComponents(stack); + } + + public boolean isSameItem(ItemStack stack) { + return ItemStack.isSameItem(this.toStack(), stack); + } + + public boolean isSameItem(UnlimitedItemStack stack) { + return this.is(stack.getItem()); } public boolean isSameItemSameComponents(ItemStack stack) { return ItemStack.isSameItemSameComponents(this.getStack(), stack); } - public boolean isSameItemSameComponents(UnlimitedItemStack stack) { - return this.isSameItemSameComponents(stack.getStack()); + public boolean isSameItemSameComponents(@Nullable UnlimitedItemStack stack) { + return stack != null && this.isSameItemSameComponents(stack.getStack()); } - public static boolean listMatches(List list, List other) { - if (list.size() != other.size()) return false; - for (int i = 0; i < list.size(); i++) { - if (!list.get(i).isSameItemSameComponents(other.get(i))) return false; + public boolean matchesIgnoringComponents(ItemStack other, Predicate> ignoredPredicate) { + ItemStack stack = this.toStack(); + if (stack == other) { + return true; + } else if (stack.getCount() != other.getCount()) { + return false; + } else if (!stack.is(other.getItem())) { + return false; + } else if (stack.isEmpty() && other.isEmpty()) { + return true; + } else if (stack.getComponents().size() != other.getComponents().size()) { + return false; + } else { + for (DataComponentType type : stack.getComponents().keySet()) { + Object componentA = stack.getComponents().get(type); + Object componentB = other.getComponents().get(type); + if (componentA == null || componentB == null) { + return false; + } + + if (!Objects.equals(componentA, componentB) && !ignoredPredicate.test(type)) { + return false; + } + } + + return true; } - return true; + } + + public boolean matchesIgnoringComponents(UnlimitedItemStack stack, Predicate> ignoredPredicate) { + return this.matchesIgnoringComponents(stack.toStack(), ignoredPredicate); } public int hashItemAndComponents() { @@ -206,55 +310,165 @@ public static int hashStackList(List list) { return i; } - public UnlimitedItemStack splitUnlimited(int amount) { - if (amount > this.stack.getMaxStackSize()) { - throw new IllegalArgumentException("Cannot split amount that bigger than max stack size."); - } - this.count -= amount; - return this.copyWithCount(amount); + public @Nullable TypedDataComponent getTyped(DataComponentType type) { + T value = this.get(type); + return value != null ? new TypedDataComponent<>(type, value) : null; + } + + @Override + public @Nullable T set(DataComponentType type, @Nullable T value) { + return this.getStack().set(type, value); + } + + public @Nullable T set(TypedDataComponent value) { + return this.getStack().set(value.type(), value.value()); + } + + @Override + public void copyFrom(DataComponentHolder src, DataComponentType... componentTypes) { + this.getStack().copyFrom(src, componentTypes); } - public ItemStack split(int amount) { - if (amount > this.stack.getMaxStackSize()) { - throw new IllegalArgumentException("Cannot split amount that bigger than max stack size."); + public void copyFrom(UnlimitedItemStack stack) { + this.setStack(stack.getStack()); + this.setCount(stack.getCount()); + } + + @Override + public @Nullable T update(DataComponentType type, T defaultValue, U value, BiFunction combiner) { + return this.getStack().update(type, defaultValue, value, combiner); + } + + @Override + public @Nullable T update(DataComponentType type, T defaultValue, UnaryOperator function) { + return this.getStack().update(type, defaultValue, function); + } + + @Override + public @Nullable T remove(DataComponentType type) { + return this.getStack().remove(type); + } + + @Override + public void applyComponents(DataComponentPatch patch) { + this.getStack().applyComponents(patch); + } + + @Override + public void applyComponents(DataComponentMap components) { + this.getStack().applyComponents(components); + } + + @Override + public void addToTooltip( + DataComponentType type, + Item.TooltipContext context, + Consumer adder, + TooltipFlag flag + ) { + this.toStack().addToTooltip(type, context, adder, flag); + } + + public boolean canPerformAction(ItemAbility ability) { + return this.typeHolder().value().canPerformAction(this.toStack(), ability); + } + + public ItemEnchantments getTagEnchantments() { + return this.getStack().getTagEnchantments(); + } + + public int getEnchantmentLevel(Holder enchantment) { + return this.getStack().getEnchantmentLevel(enchantment); + } + + public int getCount() { + return this.isEmpty() ? 0 : this.count; + } + + public int count() { + return this.getCount(); + } + + public void grow(int amount) { + this.setCount(this.getCount() + amount); + } + + public void shrink(int amount) { + this.grow(-amount); + } + + /// 将内部存储的物品栈替换为指定物品栈 + /// + ///

    注意:该方法不会设置数量。请使用 {@link UnlimitedItemStack#setCount(int)} 设置数量

    + /// + /// @param stack 提供物品和数据组件的 {@link ItemStack} + /// + /// @see UnlimitedItemStack#setCount(int) + public void setStack(ItemStack stack) { + this.stack = stack.copyWithCount(1); + } + + @Override + public DataComponentMap getComponents() { + return this.getStack().getComponents(); + } + + public DataComponentPatch getComponentsPatch() { + return this.getStack().getComponentsPatch(); + } + + public ItemStack getCraftingRemainingItem() { + return this.getStack().getCraftingRemainingItem(); + } + + public ItemStack getCraftingRemainder() { + return this.getCraftingRemainingItem(); + } + + public static boolean listMatches(List list, List other) { + if (list.size() != other.size()) return false; + for (int i = 0; i < list.size(); i++) { + if (!list.get(i).isSameItemSameComponents(other.get(i))) return false; } - this.count -= amount; - return this.getStack().copyWithCount(amount); - } - - /** - * 将本Stack转为一个{@link ItemStack}。
    - * 数量可能大于{@link ItemStack}允许的最大数量。
    - * 若需要数量安全的{@link ItemStack},请查看{@link UnlimitedItemStack#toStacks()} - * - * @return 一个与本Stack数据完全相同的ItemStack - * @see UnlimitedItemStack#toStacks() - */ + return true; + } + + /// 将本物品栈转为一个 {@link ItemStack}。
    + /// 数量可能大于 {@link ItemStack} 允许的最大数量。
    + /// 若需要数量安全的 {@link ItemStack},请查看{@link UnlimitedItemStack#toStacks()} + /// + /// @return 一个与本物品栈数据完全相同的 {@link ItemStack} + /// @see UnlimitedItemStack#toStacks() public ItemStack toStack() { - return this.stack.copyWithCount(this.count); - } - - /** - * 将本Stack按存储的{@link ItemStack}允许的最大数量转为一个{@link ItemStack}列表。 - * - * @return 一个ItemStack列表。
    - * 每个ItemStack都有相同的物品和数据组件。
    - * 除最后一个外,每个ItemStack的数量都为其允许的最大数量;
    - * 最后一个ItemStack的数量为{@code totalCount - [(size - 1) * maxCount]} - * @see UnlimitedItemStack#toStack() - */ + return this.getStack().copyWithCount(this.getCount()); + } + + /// 将本物品栈按存储的 {@link ItemStack} 允许的最大数量转为一个物品栈列表。 + /// + /// @return 一个物品栈列表。
    + /// 每个物品栈都有相同的物品和数据组件。
    + /// 将本物品栈的数量 {@code count} 按存储的物品栈允许的最大数量 {@code max} 分割为 {@code n} 份,
    + /// 前 {@code n - 1} 份物品栈的数量都为 {@code max},
    + /// 最后一份物品栈的数量为 {@code count - [(n - 1) * max]} + /// @see UnlimitedItemStack#toStack() public List toStacks() { - int maxCount = this.stack.getMaxStackSize(); - if (this.count <= maxCount) return List.of(this.stack.copyWithCount(count)); + ItemStack stack = this.getStack(); + int count = this.getCount(); + int maxCount = stack.getMaxStackSize(); + if (count <= maxCount) { + return List.of(stack.copyWithCount(count)); + } - int fullStacks = this.count / maxCount; + int fullStacks = count / maxCount; ImmutableList.Builder stacksBuilder = ImmutableList.builder(); for (int i = 0; i < fullStacks; i++) { - stacksBuilder.add(this.stack.copyWithCount(maxCount)); + stacksBuilder.add(stack.copyWithCount(maxCount)); } - int remain = this.count % maxCount; - if (remain != 0) stacksBuilder.add(this.stack.copyWithCount(remain)); + int remain = count % maxCount; + if (remain != 0) { + stacksBuilder.add(stack.copyWithCount(remain)); + } return stacksBuilder.build(); } @@ -262,12 +476,16 @@ public List toStacks() { @Override public boolean equals(Object o) { if (!(o instanceof UnlimitedItemStack stack1)) return false; - return this.isSameItemSameComponents(stack1) - && this.getCount() == stack1.getCount(); + return this.matches(stack1); } @Override public int hashCode() { - return ((this.stack.getItem().hashCode() + 31) * 31 + Integer.hashCode(this.count)) * 31 + this.stack.getComponents().hashCode(); + return Objects.hash(this.getStack().getItem(), this.getCount(), this.getStack().getComponents()); + } + + @Override + public String toString() { + return this.getCount() + " " + this.getItem(); } -} \ No newline at end of file +} From ec7eb41fea9eb0933fc63352bb011953407eafa4 Mon Sep 17 00:00:00 2001 From: QiuShui1012 Date: Tue, 11 Aug 2026 15:31:24 +0800 Subject: [PATCH 29/38] =?UTF-8?q?revert(util):=20=E8=BF=98=E5=8E=9F?= =?UTF-8?q?=E8=B0=93=E8=AF=8D=20HolderGetter=20=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 该重构是为了适配 26.1 对注册表进行的修改而进行的,1.21.1无需移植 --- docs/migration-plan-port-1.21.1.md | 44 +++++---- .../recipe/builder/InWorldRecipeBuilder.java | 93 ++++++++----------- .../v2/recipe/predicate/block/HasBlock.java | 10 +- .../predicate/block/HasBlockIngredient.java | 10 +- .../lib/v2/recipe/predicate/item/HasItem.java | 10 +- .../predicate/item/HasItemIngredient.java | 10 +- .../util/predicate/BlockStatePredicate.java | 13 +-- .../predicate/ItemIngredientPredicate.java | 29 ++---- .../lib/v2/util/predicate/ItemPredicate.java | 12 +-- 9 files changed, 95 insertions(+), 136 deletions(-) diff --git a/docs/migration-plan-port-1.21.1.md b/docs/migration-plan-port-1.21.1.md index ea05a560..c425ea27 100644 --- a/docs/migration-plan-port-1.21.1.md +++ b/docs/migration-plan-port-1.21.1.md @@ -53,27 +53,27 @@ AnvilLib 的两个长期分支在 merge-base `81c5a9c` 之后完全分叉:`dev ### 1.4 回移植项一览(结论先行) -| #P | 内容 | 难度 | 一句话结论 | -|-----|------------------------------------------------------------------------------|-----------|------------------------------------------------------------------------------------------------| -| #P1 | `module.rpc` 整模块(注解式远程调用,25 文件) | 低 | 整模块拷贝 + 3 处机械 API 反替换(Identifier / FMLLoader / accessFlags) | -| #P2 | `module.sync` + processor(同步框架,31 文件) | 中-高 | 约 50/56 文件直接搬;processor 的 26.1 SPI 需 CoreMod(ICoreMod + ITransformer)重写 | -| #P3 | `module.explosion` 整模块(10 文件) | 低 | 6 处 26.1 API 反替换,其余逐字可搬;依赖 port module.config | -| #P4 | `module.collision` 整模块(纯数学,3 文件) | 极低 | 零 MC/NeoForge API 依赖,逐字搬移 | -| #P5 | `module.space-select` 整模块(18 文件) | 中 | 唯一实现重写点是 DistrictRenderer 线框路径(ShapeRenderer → LevelRenderer.renderVoxelShape) | -| #P6 | `module.font` 整模块(25 文件) | 中 | 约 60% 纯 CPU 逻辑直接搬;GPU 上传/绘制层与 GUI 按 1.21.1 改写;依赖 #P7 | -| #P7 | `module.rendering`(113 文件,按功能块分 5 批) | 高(分批)| 纯逻辑 19 项直接搬;后处理 / SDF GUI / CachedBER / Compute 四块执行层按 1.21.1 重写 | -| #P8 | renderdoc-loader(Java Agent,2 文件) | 低 | 源码逐字搬;构建脚本按 port 侧(Java 21、无 jreleaser 别名)改写 | -| #P9 | `module.config`:group 分组 + TranslatableEnum + TOML key 点分隔 | 低 | 三处纯逻辑/配置演进,1.21.1 加载链路原生支持子目录文件名 | -| #P10 | `module.integration`:数据加载客户端/服务端拆分 + meter 递增 | 低 | 纯反射逻辑,可移植;破坏性 API(applyData 改名)需提示下游 | -| #P11 | `module.network`:PacketData 泛型检查修复 + NetworkUtil Included 方法 + PacketProtocol public | 低 | 三处纯逻辑/修饰符改动,无平台依赖 | -| #P12 | `module.util`:OutlineUtil + predicate HolderGetter 重构 + withCount + @EqualsAndHashCode | 中 | 四处功能演进;HolderGetter 重构连带 module.recipe 四个调用方 | -| #P13 | `module.registrum`:14 种新注册表 builder/entry | 低 | 绝大多数仅 Identifier 改名或个别签名微调;VillagerProfession 需按 1.21.1 6 元 record 适配 | -| #P14 | `module.registrum` datagen 增强(RecipeProvider public 化 + dataMap(provider))| 中 | 方法 public 化改「公开包装内调静态方法」;Runner / 模型生成器不迁 | -| #P15 | `module.multiblock`:DynamicMultiblockEvent + 快照复用 + BlockPos 键化 + 懒解析 | 中 | 四项修复/演进均可移植;Config.group 依赖 #P9 | -| #P16 | `module.recipe`:SpawnItem 零数量守卫 + SetBlock nbt 默认值 | 低 | 两处纯逻辑修复(含一个潜在 NPE),与 dev 侧其它改动无关 | -| #P17 | `module.wheel`:环形扇区选择效果(WheelSelectionEffect + fsh + 渲染适配) | 中-高 | 纯 UI 功能可移植;渲染必须落在 port 旧 Tesselator/ShaderInstance 体系,切断 dev 渲染依赖 | -| #P18 | `module.test`:可选集成测试(T2 / T8 / T9 / T10) | 低-中 | 仅带回不依赖 dev 新渲染模块的测试;随 #P1/#P3/#P9/#P17 落地 | -| #P19 | 构建体系:roseauCheck API 兼容检查(+ module.gradle 集中化可选) | 中 | roseau-cli 无 MC 绑定直接搬;集中化需裁剪 26.1 特有内容 | +| #P | 内容 | 难度 | 一句话结论 | +|------|-----------------------------------------------------------------------------------------------|------------|-------------------------------------------------------------------------------------------------| +| #P1 | `module.rpc` 整模块(注解式远程调用,25 文件) | 低 | 整模块拷贝 + 3 处机械 API 反替换(Identifier / FMLLoader / accessFlags) | +| #P2 | `module.sync` + processor(同步框架,31 文件) | 中-高 | 约 50/56 文件直接搬;processor 的 26.1 SPI 需 CoreMod(ICoreMod + ITransformer)重写 | +| #P3 | `module.explosion` 整模块(10 文件) | 低 | 6 处 26.1 API 反替换,其余逐字可搬;依赖 port module.config | +| #P4 | `module.collision` 整模块(纯数学,3 文件) | 极低 | 零 MC/NeoForge API 依赖,逐字搬移 | +| #P5 | `module.space-select` 整模块(18 文件) | 中 | 唯一实现重写点是 DistrictRenderer 线框路径(ShapeRenderer → LevelRenderer.renderVoxelShape) | +| #P6 | `module.font` 整模块(25 文件) | 中 | 约 60% 纯 CPU 逻辑直接搬;GPU 上传/绘制层与 GUI 按 1.21.1 改写;依赖 #P7 | +| #P7 | `module.rendering`(113 文件,按功能块分 5 批) | 高(分批) | 纯逻辑 19 项直接搬;后处理 / SDF GUI / CachedBER / Compute 四块执行层按 1.21.1 重写 | +| #P8 | renderdoc-loader(Java Agent,2 文件) | 低 | 源码逐字搬;构建脚本按 port 侧(Java 21、无 jreleaser 别名)改写 | +| #P9 | `module.config`:group 分组 + TranslatableEnum + TOML key 点分隔 | 低 | 三处纯逻辑/配置演进,1.21.1 加载链路原生支持子目录文件名 | +| #P10 | `module.integration`:数据加载客户端/服务端拆分 + meter 递增 | 低 | 纯反射逻辑,可移植;破坏性 API(applyData 改名)需提示下游 | +| #P11 | `module.network`:PacketData 泛型检查修复 + NetworkUtil Included 方法 + PacketProtocol public | 低 | 三处纯逻辑/修饰符改动,无平台依赖 | +| #P12 | `module.util`:OutlineUtil + withCount + @EqualsAndHashCode | 中 | 四处功能演进; | +| #P13 | `module.registrum`:14 种新注册表 builder/entry | 低 | 绝大多数仅 Identifier 改名或个别签名微调;VillagerProfession 需按 1.21.1 6 元 record 适配 | +| #P14 | `module.registrum` datagen 增强(RecipeProvider public 化 + dataMap(provider)) | 中 | 方法 public 化改「公开包装内调静态方法」;Runner / 模型生成器不迁 | +| #P15 | `module.multiblock`:DynamicMultiblockEvent + 快照复用 + BlockPos 键化 + 懒解析 | 中 | 四项修复/演进均可移植;Config.group 依赖 #P9 | +| #P16 | `module.recipe`:SpawnItem 零数量守卫 + SetBlock nbt 默认值 | 低 | 两处纯逻辑修复(含一个潜在 NPE),与 dev 侧其它改动无关 | +| #P17 | `module.wheel`:环形扇区选择效果(WheelSelectionEffect + fsh + 渲染适配) | 中-高 | 纯 UI 功能可移植;渲染必须落在 port 旧 Tesselator/ShaderInstance 体系,切断 dev 渲染依赖 | +| #P18 | `module.test`:可选集成测试(T2 / T8 / T9 / T10) | 低-中 | 仅带回不依赖 dev 新渲染模块的测试;随 #P1/#P3/#P9/#P17 落地 | +| #P19 | 构建体系:roseauCheck API 兼容检查(+ module.gradle 集中化可选) | 中 | roseau-cli 无 MC 绑定直接搬;集中化需裁剪 26.1 特有内容 | --- @@ -1113,8 +1113,6 @@ done | 不迁移内容 | 理由 | |---|---| -| 新版 `UnlimitedItemStack`(util 包,`fd18fe9` #55,445 行) | 实现 `ItemInstance`/`MutableDataComponentHolder`,方法签名依赖 `ItemStackTemplate`/`DataComponentGetter`/`TooltipDisplay`/`Item.CODEC_WITH_BOUND_COMPONENTS`/`Item.STREAM_CODEC`——1.21.1 mappings 核实全部不存在;其功能(不限数量 ItemStack)由 port 侧已有的 `stack/UnlimitedItemStack`(NBT 序列化版)承担 | -| 旧 `stack/UnlimitedItemStack` 的 `@Deprecated(forRemoval)`、`typeHolder()` 改名、去 `INBTSerializable` | 均为 26.1 序列化体系适配(`b1484bc` #86 的构造器优化已在 port 侧存在);port 侧无替代新类,不应加弃用标记 | | `ClientTickRecorder` 修复(`6b4d6bd` #61,删除 level 判空与 LoggingOut 归零) | dev 的修复针对 26.1 事件触发时机变化(主菜单 tick 行为);1.21.1 的 `ClientTickEvent.Pre` 行为与 port 现状一致,照搬会破坏计时器正确性(反向移植引入 bug) | | `ChanceItemStack`/`ChanceBlockState`/`WeightedChanceBlockStates`/`NbtPredicate` 的 26.1 版 | `ItemStackTemplate` 化、`ContextMap/ContextKeySet` LootParams 构造、`TagValueOutput`+`ProblemReporter`——均为 26.1 专属;port 侧 ItemStack 版/LootParams 旧构造器实现完整可用 | | package-info `@NullMarked` 迁移 | 同 5.5,注解风格迁移无功能价值 | diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java index 056f5524..447a68bc 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java @@ -19,7 +19,6 @@ import net.minecraft.advancements.AdvancementRewards; import net.minecraft.advancements.Criterion; import net.minecraft.advancements.critereon.RecipeUnlockedTrigger; -import net.minecraft.core.HolderGetter; import net.minecraft.core.NonNullList; import net.minecraft.data.recipes.RecipeBuilder; import net.minecraft.data.recipes.RecipeOutput; @@ -292,24 +291,22 @@ public T hasItem(double x, double y, double z, ItemLike... items) { /** * 添加物品谓词 * - * @param items 物品注册表访问器 - * @param tag 物品标签 + * @param items 物品标签 * @return 当前构建器实例 */ - public T hasItem(HolderGetter items, TagKey tag) { - return this.with(HasItem.builder().of(items, tag).offset(this.offset).build()); + public T hasItem(TagKey items) { + return this.with(HasItem.builder().of(items).offset(this.offset).build()); } /** * 添加物品谓词 * * @param offset 偏移向量 - * @param items 物品注册表访问器 - * @param tag 物品标签 + * @param items 物品标签 * @return 当前构建器实例 */ - public T hasItem(Vec3 offset, HolderGetter items, TagKey tag) { - return this.with(HasItem.builder().of(items, tag).offset(offset).build()); + public T hasItem(Vec3 offset, TagKey items) { + return this.with(HasItem.builder().offset(offset).build()); } /** @@ -318,12 +315,11 @@ public T hasItem(Vec3 offset, HolderGetter items, TagKey tag) { * @param x X轴偏移量 * @param y Y轴偏移量 * @param z Z轴偏移量 - * @param items 物品注册表访问器 - * @param tag 物品标签 + * @param items 物品标签 * @return 当前构建器实例 */ - public T hasItem(double x, double y, double z, HolderGetter items, TagKey tag) { - return this.with(HasItem.builder().of(items, tag).offset(x, y, z).build()); + public T hasItem(double x, double y, double z, TagKey items) { + return this.with(HasItem.builder().offset(x, y, z).build()); } /** @@ -376,24 +372,22 @@ public T hasItemIngredient(double x, double y, double z, ItemLike... items) { /** * 添加物品原料谓词 * - * @param items 物品注册表访问器 - * @param tag 物品标签 + * @param items 物品标签 * @return 当前构建器实例 */ - public T hasItemIngredient(HolderGetter items, TagKey tag) { - return this.with(HasItemIngredient.builder().of(items, tag).offset(this.offset).build()); + public T hasItemIngredient(TagKey items) { + return this.with(HasItemIngredient.builder().of(items).offset(this.offset).build()); } /** * 添加物品原料谓词 * * @param offset 偏移向量 - * @param items 物品注册表访问器 - * @param tag 物品标签 + * @param items 物品标签 * @return 当前构建器实例 */ - public T hasItemIngredient(Vec3 offset, HolderGetter items, TagKey tag) { - return this.with(HasItemIngredient.builder().of(items, tag).offset(offset).build()); + public T hasItemIngredient(Vec3 offset, TagKey items) { + return this.with(HasItemIngredient.builder().of(items).offset(offset).build()); } /** @@ -402,12 +396,11 @@ public T hasItemIngredient(Vec3 offset, HolderGetter items, TagKey t * @param x X轴偏移量 * @param y Y轴偏移量 * @param z Z轴偏移量 - * @param items 物品注册表访问器 - * @param tag 物品标签 + * @param items 物品标签 * @return 当前构建器实例 */ - public T hasItemIngredient(double x, double y, double z, HolderGetter items, TagKey tag) { - return this.with(HasItemIngredient.builder().of(items, tag).offset(x, y, z).build()); + public T hasItemIngredient(double x, double y, double z, TagKey items) { + return this.with(HasItemIngredient.builder().of(items).offset(x, y, z).build()); } /** @@ -494,38 +487,35 @@ public T hasBlock(double x, double y, double z, Collection blocks) { /** * 添加方块谓词 * - * @param blocks 方块注册表访问器 - * @param tag 方块标签 + * @param tag 方块标签 * @return 当前构建器实例 */ - public T hasBlock(HolderGetter blocks, TagKey tag) { - return this.with(HasBlock.builder().of(blocks, tag).offset(this.offset).build()); + public T hasBlock(TagKey tag) { + return this.with(HasBlock.builder().of(tag).offset(this.offset).build()); } /** * 添加方块谓词 * * @param offset 偏移向量 - * @param blocks 方块注册表访问器 * @param tag 方块标签 * @return 当前构建器实例 */ - public T hasBlock(Vec3 offset, HolderGetter blocks, TagKey tag) { - return this.with(HasBlock.builder().of(blocks, tag).offset(offset).build()); + public T hasBlock(Vec3 offset, TagKey tag) { + return this.with(HasBlock.builder().of(tag).offset(offset).build()); } /** * 添加方块谓词 * - * @param x X轴偏移量 - * @param y Y轴偏移量 - * @param z Z轴偏移量 - * @param blocks 方块注册表访问器 - * @param tag 方块标签 + * @param x X轴偏移量 + * @param y Y轴偏移量 + * @param z Z轴偏移量 + * @param tag 方块标签 * @return 当前构建器实例 */ - public T hasBlock(double x, double y, double z, HolderGetter blocks, TagKey tag) { - return this.with(HasBlock.builder().of(blocks, tag).offset(x, y, z).build()); + public T hasBlock(double x, double y, double z, TagKey tag) { + return this.with(HasBlock.builder().of(tag).offset(x, y, z).build()); } /** @@ -659,38 +649,35 @@ public T hasBlockIngredient(double x, double y, double z, Collection bloc /** * 添加方块原料谓词 * - * @param blocks 方块注册表访问器 - * @param tag 方块标签 + * @param tag 方块标签 * @return 当前构建器实例 */ - public T hasBlockIngredient(HolderGetter blocks, TagKey tag) { - return this.with(HasBlockIngredient.builder().of(blocks, tag).offset(this.offset).build()); + public T hasBlockIngredient(TagKey tag) { + return this.with(HasBlockIngredient.builder().of(tag).offset(this.offset).build()); } /** * 添加方块原料谓词 * * @param offset 偏移向量 - * @param blocks 方块注册表访问器 * @param tag 方块标签 * @return 当前构建器实例 */ - public T hasBlockIngredient(Vec3 offset, HolderGetter blocks, TagKey tag) { - return this.with(HasBlockIngredient.builder().of(blocks, tag).offset(offset).build()); + public T hasBlockIngredient(Vec3 offset, TagKey tag) { + return this.with(HasBlockIngredient.builder().of(tag).offset(offset).build()); } /** * 添加方块原料谓词 * - * @param x X轴偏移量 - * @param y Y轴偏移量 - * @param z Z轴偏移量 - * @param blocks 方块注册表访问器 - * @param tag 方块标签 + * @param x X轴偏移量 + * @param y Y轴偏移量 + * @param z Z轴偏移量 + * @param tag 方块标签 * @return 当前构建器实例 */ - public T hasBlockIngredient(double x, double y, double z, HolderGetter blocks, TagKey tag) { - return this.with(HasBlockIngredient.builder().of(blocks, tag).offset(new Vec3(x, y, z)).build()); + public T hasBlockIngredient(double x, double y, double z, TagKey tag) { + return this.with(HasBlockIngredient.builder().of(tag).offset(new Vec3(x, y, z)).build()); } /** diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlock.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlock.java index e101fac2..52c003ba 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlock.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlock.java @@ -3,7 +3,6 @@ import dev.anvilcraft.lib.v2.recipe.init.recipe.LibRecipePredicateTypes; import dev.anvilcraft.lib.v2.util.predicate.BlockStatePredicate; import lombok.Getter; -import net.minecraft.core.HolderGetter; import net.minecraft.tags.TagKey; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.properties.Property; @@ -160,12 +159,11 @@ public Builder of(Collection blocks) { /** * 设置方块标签 * - * @param blocks 方块注册表访问器 - * @param tag 方块标签 + * @param tag 方块标签 * @return 构建器实例 */ - public Builder of(HolderGetter blocks, TagKey tag) { - this.predicate.of(blocks, tag); + public Builder of(TagKey tag) { + this.predicate.of(tag); return this; } @@ -287,4 +285,4 @@ public HasBlock build() { return new HasBlock(offset, predicate.build()); } } -} \ No newline at end of file +} diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlockIngredient.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlockIngredient.java index b2417921..349276da 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlockIngredient.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/block/HasBlockIngredient.java @@ -6,7 +6,6 @@ import dev.anvilcraft.lib.v2.recipe.util.InWorldRecipeContext; import lombok.Getter; import net.minecraft.core.BlockPos; -import net.minecraft.core.HolderGetter; import net.minecraft.tags.TagKey; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.properties.Property; @@ -171,12 +170,11 @@ public Builder of(Collection blocks) { /** * 设置方块标签 * - * @param blocks 方块注册表访问器 - * @param tag 方块标签 + * @param tag 方块标签 * @return 构建器实例 */ - public Builder of(HolderGetter blocks, TagKey tag) { - this.predicate.of(blocks, tag); + public Builder of(TagKey tag) { + this.predicate.of(tag); return this; } @@ -298,4 +296,4 @@ public HasBlockIngredient build() { return new HasBlockIngredient(offset, predicate.build()); } } -} \ No newline at end of file +} diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItem.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItem.java index 484ded68..8813c7a4 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItem.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItem.java @@ -9,7 +9,6 @@ import lombok.Getter; import net.minecraft.advancements.critereon.ItemSubPredicate; import net.minecraft.advancements.critereon.MinMaxBounds; -import net.minecraft.core.HolderGetter; import net.minecraft.core.component.DataComponentPredicate; import net.minecraft.core.component.DataComponentType; import net.minecraft.network.RegistryFriendlyByteBuf; @@ -200,12 +199,11 @@ public Builder of(ItemLike... items) { /** * 设置物品标签 * - * @param items 物品注册表访问器 - * @param tag 物品标签 + * @param tag 物品标签 * @return 构建器实例 */ - public Builder of(HolderGetter items, TagKey tag) { - this.item.of(items, tag); + public Builder of(TagKey tag) { + this.item.of(tag); return this; } @@ -296,4 +294,4 @@ public HasItem build() { return new HasItem(offset, range, item.build(), functions); } } -} \ No newline at end of file +} diff --git a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItemIngredient.java b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItemIngredient.java index 285c6e9c..46da6be8 100644 --- a/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItemIngredient.java +++ b/module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItemIngredient.java @@ -13,7 +13,6 @@ import net.minecraft.advancements.critereon.ItemSubPredicate; import net.minecraft.core.component.DataComponentPredicate; import net.minecraft.core.component.DataComponentType; -import net.minecraft.core.HolderGetter; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; @@ -181,12 +180,11 @@ public Builder of(ItemLike... items) { /** * 设置物品标签 * - * @param items 物品注册表访问器 - * @param tag 物品标签 + * @param tag 物品标签 * @return 构建器实例 */ - public Builder of(HolderGetter items, TagKey tag) { - this.item.of(items, tag); + public Builder of(TagKey tag) { + this.item.of(tag); return this; } @@ -304,4 +302,4 @@ public HasItemIngredient build() { return new HasItemIngredient(offset, range, item.build(), functions); } } -} \ No newline at end of file +} diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java index 4f93262e..af41bb53 100644 --- a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java @@ -6,12 +6,11 @@ import dev.anvilcraft.lib.v2.util.AnvilLibUtil; import dev.anvilcraft.lib.v2.util.Util; import io.netty.buffer.ByteBuf; -import lombok.EqualsAndHashCode; import lombok.Getter; import net.minecraft.core.Holder; -import net.minecraft.core.HolderGetter; import net.minecraft.core.HolderSet; import net.minecraft.core.RegistryCodecs; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.RegistryFriendlyByteBuf; @@ -47,7 +46,6 @@ *

    */ @Getter -@EqualsAndHashCode public class BlockStatePredicate { /** * 空 ResourceLocation @@ -317,12 +315,11 @@ public Builder of(Collection blocks) { /** * 设置方块标签 * - * @param blocks 方块注册表访问器 - * @param tag 方块标签 + * @param tag 方块标签 * @return 构建器实例 */ - public Builder of(HolderGetter blocks, TagKey tag) { - this.blocks = blocks.getOrThrow(tag); + public Builder of(TagKey tag) { + this.blocks = BuiltInRegistries.BLOCK.getOrCreateTag(tag); return this; } @@ -671,4 +668,4 @@ public interface ValueMatcher { */ , S extends StateHolder> List applyToState(S state, Property property); } -} \ No newline at end of file +} diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemIngredientPredicate.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemIngredientPredicate.java index c6edd2c6..12eccaf0 100644 --- a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemIngredientPredicate.java +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemIngredientPredicate.java @@ -7,12 +7,12 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import net.minecraft.advancements.critereon.ItemSubPredicate; -import net.minecraft.core.HolderGetter; import net.minecraft.core.HolderSet; import net.minecraft.core.RegistryCodecs; import net.minecraft.core.component.DataComponentPredicate; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.component.TypedDataComponent; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; @@ -24,7 +24,6 @@ import java.util.Map; import java.util.Optional; -import java.util.function.Function; /** * 物品原料谓词 @@ -74,22 +73,11 @@ public static Builder of(ItemLike... items) { /** * 创建一个标签构建器 * - * @param items 物品注册表访问器 - * @param tag 物品标签 + * @param tag 物品标签 * @return 构建器实例 */ - public static Builder of(HolderGetter items, TagKey tag) { - return new Builder().of(items, tag); - } - - /** - * 返回一个指定数量的新实例,不修改原实例。 - * - * @param count 数量 - * @return 新实例 - */ - public ItemIngredientPredicate withCount(int count) { - return new ItemIngredientPredicate(this.items, count, this.components, this.subPredicates); + public static Builder of(TagKey tag) { + return new Builder().of(tag); } @Override @@ -175,12 +163,11 @@ public Builder of(ItemLike... items) { /** * 设置物品标签 * - * @param items 物品注册表访问器 - * @param tag 物品标签 + * @param tag 物品标签 * @return 构建器实例 */ - public Builder of(HolderGetter items, TagKey tag) { - this.items = items.get(tag).map(Function.identity()); + public Builder of(TagKey tag) { + this.items = Optional.of(BuiltInRegistries.ITEM.getOrCreateTag(tag)); return this; } @@ -249,4 +236,4 @@ public ItemIngredientPredicate build() { return new ItemIngredientPredicate(this.items, this.count, this.components, this.subPredicates.build()); } } -} \ No newline at end of file +} diff --git a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java index 237ad20d..7ddaa503 100644 --- a/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java +++ b/module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java @@ -6,10 +6,10 @@ import dev.anvilcraft.lib.v2.codec.StreamCodecUtil; import net.minecraft.advancements.critereon.ItemSubPredicate; import net.minecraft.advancements.critereon.MinMaxBounds; -import net.minecraft.core.HolderGetter; import net.minecraft.core.HolderSet; import net.minecraft.core.RegistryCodecs; import net.minecraft.core.component.DataComponentPredicate; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; @@ -20,7 +20,6 @@ import java.util.Map; import java.util.Optional; -import java.util.function.Function; /** * 物品谓词 @@ -118,12 +117,11 @@ public Builder of(ItemLike... items) { /** * 设置物品标签 * - * @param items 物品注册表访问器 - * @param tag 物品标签 + * @param tag 物品标签 * @return 构建器实例 */ - public Builder of(HolderGetter items, TagKey tag) { - this.items = items.get(tag).map(Function.identity()); + public Builder of(TagKey tag) { + this.items = Optional.of(BuiltInRegistries.ITEM.getOrCreateTag(tag)); return this; } @@ -171,4 +169,4 @@ public ItemPredicate build() { return new ItemPredicate(this.items, this.count, this.components, this.subPredicates.build()); } } -} \ No newline at end of file +} From f7f8db54d5b8cb8e7cb324dc70d1fb5bb39abc33 Mon Sep 17 00:00:00 2001 From: QiuShui1012 Date: Tue, 11 Aug 2026 15:50:29 +0800 Subject: [PATCH 30/38] =?UTF-8?q?revert(registrum):=20=E8=BF=98=E5=8E=9FBl?= =?UTF-8?q?ockBuilder.item()=20=E9=BB=98=E8=AE=A4=20lang=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 该修复是为了适配 26.1 对方块物品翻译键(从共用翻译键转为独立翻译键)进行的修改而进行的,1.21.1无需移植 --- .../dev/anvilcraft/lib/v2/registrum/builders/BlockBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/BlockBuilder.java b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/BlockBuilder.java index de01a48f..e44e0958 100644 --- a/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/BlockBuilder.java +++ b/module.registrum/src/main/java/dev/anvilcraft/lib/v2/registrum/builders/BlockBuilder.java @@ -192,7 +192,7 @@ protected void registerLayers(T entry) { * @see #item() */ public BlockBuilder simpleItem() { - return item().defaultLang().build(); + return item().build(); } /** @@ -220,6 +220,7 @@ public ItemBuilder> item() { public ItemBuilder> item(NonNullBiFunction factory) { final var sup = asSupplier(); return getOwner().> item(this, getName(), p -> factory.apply(getEntry(), p)) + .setData(ProviderType.LANG, NonNullBiConsumer.noop()) // FIXME Need a beetter API for "unsetting" providers .model((ctx, prov) -> { Optional model = getOwner().getDataProvider(ProviderType.BLOCKSTATE) .flatMap(p -> p.getExistingVariantBuilder(getEntry())) From 12fa69d6784db57bbbda56d113d11991ce89c9f6 Mon Sep 17 00:00:00 2001 From: QiuShui1012 Date: Tue, 11 Aug 2026 17:44:26 +0800 Subject: [PATCH 31/38] =?UTF-8?q?feat(ci):=20=E8=A1=A5=E5=85=85=20CI=20?= =?UTF-8?q?=E5=92=8C=E4=B8=BB=E4=BA=A7=E7=89=A9=E5=86=85=E5=B5=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 318 +++++++++++++++++++++++------ .github/workflows/pull_request.yml | 130 +++++++++--- .github/workflows/release.yml | 282 ++++++++++++++++++++++--- module.main/build.gradle | 22 +- 4 files changed, 632 insertions(+), 120 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b71d1f27..46b1bb56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,56 @@ on: - settings.gradle jobs: + codec: + uses: ./.github/workflows/build_and_test.yml + with: + module: codec + module_id: anvillib-codec + mod_id: anvillib_codec + ci_build: true + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + codec-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - codec + with: + module: codec + module_id: anvillib-codec + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} + collision: + uses: ./.github/workflows/build_and_test.yml + with: + module: collision + module_id: anvillib-collision + mod_id: anvillib_collision + ci_build: true + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + collision-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - collision + with: + module: collision + module_id: anvillib-collision + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} config: uses: ./.github/workflows/build_and_test.yml with: @@ -39,55 +89,54 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} - codec: + explosion: uses: ./.github/workflows/build_and_test.yml + needs: + - config with: - module: codec - module_id: anvillib-codec - mod_id: anvillib_codec + module: explosion + module_id: anvillib-explosion + mod_id: anvillib_explosion ci_build: true pr_build: false secrets: maven_url: ${{ secrets.MAVEN_URL }} maven_user: ${{ secrets.MAVEN_USER }} maven_pass: ${{ secrets.MAVEN_PASS }} - codec-maven-central-deploy: + explosion-maven-central-deploy: uses: ./.github/workflows/publish_maven_central.yml needs: - - codec + - explosion with: - module: codec - module_id: anvillib-codec + module: explosion + module_id: anvillib-explosion secrets: maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} - multiblock: + font: uses: ./.github/workflows/build_and_test.yml needs: - - config - - codec - - network - - util + - rendering with: - module: multiblock - module_id: anvillib-multiblock - mod_id: anvillib_multiblock + module: font + module_id: anvillib-font + mod_id: anvillib_font ci_build: true pr_build: false secrets: maven_url: ${{ secrets.MAVEN_URL }} maven_user: ${{ secrets.MAVEN_USER }} maven_pass: ${{ secrets.MAVEN_PASS }} - multiblock-maven-central-deploy: + font-maven-central-deploy: uses: ./.github/workflows/publish_maven_central.yml needs: - - multiblock + - font with: - module: multiblock - module_id: anvillib-multiblock + module: font + module_id: anvillib-font secrets: maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} @@ -119,6 +168,53 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} + main: + uses: ./.github/workflows/build_and_test.yml + needs: + - codec + - collision + - config + - explosion + - font + - integration + - moveable-entity-block + - multiblock + - network + - recipe + - registrum + - rendering + - rpc + - space-select + - sync + - util + - wheel + - yukkuri + with: + module: main + module_id: anvillib + mod_id: anvillib + ci_build: true + pr_build: false + gametest: true + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + curseforge_token: ${{ secrets.CURSEFORGE_TOKEN }} + modrinth_token: ${{ secrets.MODRINTH_TOKEN }} + main-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - main + with: + module: main + module_id: anvillib + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} moveable-entity-block: uses: ./.github/workflows/build_and_test.yml with: @@ -144,6 +240,36 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} + multiblock: + uses: ./.github/workflows/build_and_test.yml + needs: + - codec + - config + - network + - util + with: + module: multiblock + module_id: anvillib-multiblock + mod_id: anvillib_multiblock + ci_build: true + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + multiblock-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - multiblock + with: + module: multiblock + module_id: anvillib-multiblock + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} network: uses: ./.github/workflows/build_and_test.yml with: @@ -170,11 +296,11 @@ jobs: signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} recipe: + uses: ./.github/workflows/build_and_test.yml needs: - - config - codec + - config - util - uses: ./.github/workflows/build_and_test.yml with: module: recipe module_id: anvillib-recipe @@ -225,6 +351,114 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} + rendering: + uses: ./.github/workflows/build_and_test.yml + with: + module: rendering + module_id: anvillib-rendering + mod_id: anvillib_rendering + ci_build: true + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + rendering-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - rendering + with: + module: rendering + module_id: anvillib-rendering + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} + rpc: + uses: ./.github/workflows/build_and_test.yml + needs: + - network + with: + module: rpc + module_id: anvillib-rpc + mod_id: anvillib_rpc + ci_build: true + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + rpc-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - rpc + with: + module: rpc + module_id: anvillib-rpc + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} + space-select: + uses: ./.github/workflows/build_and_test.yml + needs: + - network + with: + module: space-select + module_id: anvillib-space-select + mod_id: anvillib_space_select + ci_build: true + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + space-select-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - space-select + with: + module: space-select + module_id: anvillib-space-select + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} + sync: + uses: ./.github/workflows/build_and_test.yml + needs: + - codec + - network + - util + with: + module: sync + module_id: anvillib-sync + mod_id: anvillib_sync + ci_build: true + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + sync-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - sync + with: + module: sync + module_id: anvillib-sync + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} util: uses: ./.github/workflows/build_and_test.yml needs: @@ -304,43 +538,3 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} - main: - needs: - - config - - codec - - integration - - moveable-entity-block - - multiblock - - network - - recipe - - registrum - - util - - wheel - - yukkuri - uses: ./.github/workflows/build_and_test.yml - with: - module: main - module_id: anvillib - mod_id: anvillib - ci_build: true - pr_build: false - gametest: true - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - curseforge_token: ${{ secrets.CURSEFORGE_TOKEN }} - modrinth_token: ${{ secrets.MODRINTH_TOKEN }} - main-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - main - with: - module: main - module_id: anvillib - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index cc611f7a..47397393 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -14,6 +14,22 @@ on: - settings.gradle jobs: + codec: + uses: ./.github/workflows/build_and_test.yml + with: + module: codec + module_id: anvillib-codec + mod_id: anvillib_codec + ci_build: true + pr_build: true + collision: + uses: ./.github/workflows/build_and_test.yml + with: + module: collision + module_id: anvillib-collision + mod_id: anvillib_collision + ci_build: true + pr_build: true config: uses: ./.github/workflows/build_and_test.yml with: @@ -22,12 +38,24 @@ jobs: mod_id: anvillib_config ci_build: true pr_build: true - codec: + explosion: uses: ./.github/workflows/build_and_test.yml + needs: + - config with: - module: codec - module_id: anvillib-codec - mod_id: anvillib_codec + module: explosion + module_id: anvillib-explosion + mod_id: anvillib_explosion + ci_build: true + pr_build: true + font: + uses: ./.github/workflows/build_and_test.yml + needs: + - rendering + with: + module: font + module_id: anvillib-font + mod_id: anvillib_font ci_build: true pr_build: true integration: @@ -38,6 +66,34 @@ jobs: mod_id: anvillib_integration ci_build: true pr_build: true + main: + uses: ./.github/workflows/build_and_test.yml + needs: + - codec + - collision + - config + - explosion + - font + - integration + - moveable-entity-block + - multiblock + - network + - recipe + - registrum + - rendering + - rpc + - space-select + - sync + - util + - wheel + - yukkuri + with: + module: main + module_id: anvillib + mod_id: anvillib + ci_build: true + pr_build: true + gametest: true moveable-entity-block: uses: ./.github/workflows/build_and_test.yml with: @@ -49,8 +105,8 @@ jobs: multiblock: uses: ./.github/workflows/build_and_test.yml needs: - - config - codec + - config - network - util with: @@ -68,11 +124,11 @@ jobs: ci_build: true pr_build: true recipe: + uses: ./.github/workflows/build_and_test.yml needs: - - config - codec + - config - util - uses: ./.github/workflows/build_and_test.yml with: module: recipe module_id: anvillib-recipe @@ -89,6 +145,46 @@ jobs: mod_id: anvillib_registrum ci_build: true pr_build: true + rendering: + uses: ./.github/workflows/build_and_test.yml + with: + module: rendering + module_id: anvillib-rendering + mod_id: anvillib_rendering + ci_build: true + pr_build: true + rpc: + uses: ./.github/workflows/build_and_test.yml + needs: + - network + with: + module: rpc + module_id: anvillib-rpc + mod_id: anvillib_rpc + ci_build: true + pr_build: true + space-select: + uses: ./.github/workflows/build_and_test.yml + needs: + - network + with: + module: space-select + module_id: anvillib-space-select + mod_id: anvillib_space_select + ci_build: true + pr_build: true + sync: + uses: ./.github/workflows/build_and_test.yml + needs: + - codec + - network + - util + with: + module: sync + module_id: anvillib-sync + mod_id: anvillib_sync + ci_build: true + pr_build: true util: uses: ./.github/workflows/build_and_test.yml needs: @@ -109,23 +205,11 @@ jobs: mod_id: anvillib_wheel ci_build: true pr_build: true - main: - needs: - - config - - codec - - integration - - moveable-entity-block - - multiblock - - network - - recipe - - registrum - - util - - wheel + yukkuri: uses: ./.github/workflows/build_and_test.yml with: - module: main - module_id: anvillib - mod_id: anvillib + module: yukkuri + module_id: anvillib-yukkuri + mod_id: anvillib_yukkuri ci_build: true pr_build: true - gametest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc8b4d03..c4c7255e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,56 @@ on: - settings.gradle jobs: + codec: + uses: ./.github/workflows/build_and_test.yml + with: + module: codec + module_id: anvillib-codec + mod_id: anvillib_codec + ci_build: false + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + codec-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - codec + with: + module: codec + module_id: anvillib-codec + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} + collision: + uses: ./.github/workflows/build_and_test.yml + with: + module: collision + module_id: anvillib-collision + mod_id: anvillib_collision + ci_build: false + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + collision-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - collision + with: + module: collision + module_id: anvillib-collision + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} config: uses: ./.github/workflows/build_and_test.yml with: @@ -39,25 +89,54 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} - codec: + explosion: uses: ./.github/workflows/build_and_test.yml + needs: + - config with: - module: codec - module_id: anvillib-codec - mod_id: anvillib_codec + module: explosion + module_id: anvillib-explosion + mod_id: anvillib_explosion ci_build: false pr_build: false secrets: maven_url: ${{ secrets.MAVEN_URL }} maven_user: ${{ secrets.MAVEN_USER }} maven_pass: ${{ secrets.MAVEN_PASS }} - codec-maven-central-deploy: + explosion-maven-central-deploy: uses: ./.github/workflows/publish_maven_central.yml needs: - - codec + - explosion with: - module: codec - module_id: anvillib-codec + module: explosion + module_id: anvillib-explosion + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} + font: + uses: ./.github/workflows/build_and_test.yml + needs: + - rendering + with: + module: font + module_id: anvillib-font + mod_id: anvillib_font + ci_build: false + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + font-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - font + with: + module: font + module_id: anvillib-font secrets: maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} @@ -89,6 +168,50 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} + main: + uses: ./.github/workflows/build_and_test.yml + needs: + - codec + - collision + - config + - explosion + - font + - integration + - moveable-entity-block + - multiblock + - network + - recipe + - registrum + - rendering + - rpc + - space-select + - sync + - util + - wheel + - yukkuri + with: + module: main + module_id: anvillib + mod_id: anvillib + ci_build: false + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + main-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - main + with: + module: main + module_id: anvillib + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} moveable-entity-block: uses: ./.github/workflows/build_and_test.yml with: @@ -116,9 +239,9 @@ jobs: signing_password: ${{ secrets.SIGNING_PASSWORD }} multiblock: uses: ./.github/workflows/build_and_test.yml - needs: - - config + needs: - codec + - config - network - util with: @@ -170,11 +293,11 @@ jobs: signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} recipe: + uses: ./.github/workflows/build_and_test.yml needs: - - config - codec + - config - util - uses: ./.github/workflows/build_and_test.yml with: module: recipe module_id: anvillib-recipe @@ -225,6 +348,114 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} + rendering: + uses: ./.github/workflows/build_and_test.yml + with: + module: rendering + module_id: anvillib-rendering + mod_id: anvillib_rendering + ci_build: false + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + rendering-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - rendering + with: + module: rendering + module_id: anvillib-rendering + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} + rpc: + uses: ./.github/workflows/build_and_test.yml + needs: + - network + with: + module: rpc + module_id: anvillib-rpc + mod_id: anvillib_rpc + ci_build: false + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + rpc-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - rpc + with: + module: rpc + module_id: anvillib-rpc + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} + space-select: + uses: ./.github/workflows/build_and_test.yml + needs: + - network + with: + module: space-select + module_id: anvillib-space-select + mod_id: anvillib_space_select + ci_build: false + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + space-select-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - space-select + with: + module: space-select + module_id: anvillib-space-select + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} + sync: + uses: ./.github/workflows/build_and_test.yml + needs: + - codec + - network + - util + with: + module: sync + module_id: anvillib-sync + mod_id: anvillib_sync + ci_build: false + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + sync-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - sync + with: + module: sync + module_id: anvillib-sync + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} util: uses: ./.github/workflows/build_and_test.yml needs: @@ -279,36 +510,25 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} - main: - needs: - - config - - codec - - integration - - moveable-entity-block - - multiblock - - network - - recipe - - registrum - - util - - wheel + yukkuri: uses: ./.github/workflows/build_and_test.yml with: - module: main - module_id: anvillib - mod_id: anvillib + module: yukkuri + module_id: anvillib-yukkuri + mod_id: anvillib_yukkuri ci_build: false pr_build: false secrets: maven_url: ${{ secrets.MAVEN_URL }} maven_user: ${{ secrets.MAVEN_USER }} maven_pass: ${{ secrets.MAVEN_PASS }} - main-maven-central-deploy: + yukkuri-maven-central-deploy: uses: ./.github/workflows/publish_maven_central.yml needs: - - main + - yukkuri with: - module: main - module_id: anvillib + module: yukkuri + module_id: anvillib-yukkuri secrets: maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} diff --git a/module.main/build.gradle b/module.main/build.gradle index 641096de..a888d6c6 100644 --- a/module.main/build.gradle +++ b/module.main/build.gradle @@ -126,30 +126,44 @@ sourceSets.main.resources { srcDir 'src/generated/resources' } dependencies { if (System.getenv("NOT_DEV") == 'true') { - jarJar(api("dev.anvilcraft.lib:anvillib-config-neoforge-1.21.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-codec-neoforge-1.21.1:latest.release")) - jarJar(api("dev.anvilcraft.lib:anvillib-multiblock-neoforge-1.21.1:latest.release")) + jarJar(api("dev.anvilcraft.lib:anvillib-collision-neoforge-1.21.1:latest.release")) + jarJar(api("dev.anvilcraft.lib:anvillib-config-neoforge-1.21.1:latest.release")) + jarJar(api("dev.anvilcraft.lib:anvillib-explosion-neoforge-1.21.1:latest.release")) + jarJar(api("dev.anvilcraft.lib:anvillib-font-neoforge-1.21.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-integration-neoforge-1.21.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-moveable-entity-block-neoforge-1.21.1:latest.release")) interfaceInjectionData("dev.anvilcraft.lib:anvillib-moveable-entity-block-neoforge-1.21.1:latest.release") + jarJar(api("dev.anvilcraft.lib:anvillib-multiblock-neoforge-1.21.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-network-neoforge-1.21.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-recipe-neoforge-1.21.1:latest.release")) interfaceInjectionData("dev.anvilcraft.lib:anvillib-recipe-neoforge-1.21.1:latest.release") jarJar(api("dev.anvilcraft.lib:anvillib-registrum-neoforge-1.21.1:latest.release")) + jarJar(api("dev.anvilcraft.lib:anvillib-rendering-neoforge-1.21.1:latest.release")) + jarJar(api("dev.anvilcraft.lib:anvillib-rpc-neoforge-1.21.1:latest.release")) + jarJar(api("dev.anvilcraft.lib:anvillib-space-select-neoforge-1.21.1:latest.release")) + jarJar(api("dev.anvilcraft.lib:anvillib-sync-neoforge-1.21.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-util-neoforge-1.21.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-wheel-neoforge-1.21.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-yukkuri-neoforge-1.21.1:latest.release")) } else { - jarJar(api project(":anvillib-config-neoforge-1.21.1")) jarJar(api project(":anvillib-codec-neoforge-1.21.1")) - jarJar(api project(":anvillib-multiblock-neoforge-1.21.1")) + jarJar(api project(":anvillib-collision-neoforge-1.21.1")) + jarJar(api project(":anvillib-config-neoforge-1.21.1")) + jarJar(api project(":anvillib-explosion-neoforge-1.21.1")) + jarJar(api project(":anvillib-font-neoforge-1.21.1")) jarJar(api project(":anvillib-integration-neoforge-1.21.1")) jarJar(api project(":anvillib-moveable-entity-block-neoforge-1.21.1")) interfaceInjectionData project(":anvillib-moveable-entity-block-neoforge-1.21.1") + jarJar(api project(":anvillib-multiblock-neoforge-1.21.1")) jarJar(api project(":anvillib-network-neoforge-1.21.1")) jarJar(api project(":anvillib-recipe-neoforge-1.21.1")) interfaceInjectionData project(":anvillib-recipe-neoforge-1.21.1") jarJar(api project(":anvillib-registrum-neoforge-1.21.1")) + jarJar(api project(":anvillib-rendering-neoforge-1.21.1")) + jarJar(api project(":anvillib-rpc-neoforge-1.21.1")) + jarJar(api project(":anvillib-space-select-neoforge-1.21.1")) + jarJar(api project(":anvillib-sync-neoforge-1.21.1")) jarJar(api project(":anvillib-util-neoforge-1.21.1")) jarJar(api project(":anvillib-wheel-neoforge-1.21.1")) jarJar(api project(":anvillib-yukkuri-neoforge-1.21.1")) From 05235aa280e2d31f4ac9b180cc9388d6fd3902f1 Mon Sep 17 00:00:00 2001 From: QiuShui1012 Date: Tue, 11 Aug 2026 18:18:55 +0800 Subject: [PATCH 32/38] feat(codec): add portable methods for creating Codec and MapCodec with varying parameters --- docs/branch-comparison.md | 2 +- docs/migration-todo-port-1.21.1.md | 14 +- .../anvilcraft/lib/v2/codec/CodecUtil.java | 1312 +++++++++++++++++ 3 files changed, 1326 insertions(+), 2 deletions(-) diff --git a/docs/branch-comparison.md b/docs/branch-comparison.md index 2c94b196..a9fdaa3b 100644 --- a/docs/branch-comparison.md +++ b/docs/branch-comparison.md @@ -375,7 +375,7 @@ port 侧独有、dev 侧缺失的内容;标注【dev 超集/同源】的模块 | 模块 | 结论 | 关键依据 | 回迁项 | |--------------------------------|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------| -| `module.codec` | **dev 超集** | port 侧 `0b56109`/`50aeac7`/`6a0e8a9` 均有 dev 同主题提交(`3b5dfea`/`2e1b537`/`922d523`);dev 另有 `173a454`(portable Codec/MapCodec 方法)、`52c3f8b`/`cf75837`(PositionPredicate AT 补充)、`3b5dfea` 等独有内容 | 无 | +| `module.codec` | **dev 超集** | port 侧 `0b56109`/`50aeac7`/`6a0e8a9` 均有 dev 同主题提交(`3b5dfea`/`2e1b537`/`922d523`);dev 另有 `52c3f8b`/`cf75837`(PositionPredicate AT 补充)、`3b5dfea` 等独有内容 | `173a454`(portable Codec/MapCodec 方法,2026-08-11 已回移植,落点待提交) | | `module.network` | **dev 超集** | port 侧网络提交与 dev 早期提交同主题镜像(附录 C);dev 侧继续演进:`20721b1`(泛型检查优化)、`5733a9d`(发包工具)、`6220427`(PacketProtocol 公开)、`395495d`(双向注册 handler 参数) | 无 | | `module.config` | **dev 演进** | dev 侧独有:`3d50d76`(Config 注解 group 属性)、`c0a619b`(配置管理器模组容器获取修复)、`946938f`/`1ddfd24`(配置管理增强与文档);port 侧无独有内容 | 无 | | `module.integration` | **dev 演进** | dev 侧独有:`dc4d583`(data loader 重命名与类型处理)、`e8efbd4`(实例计数器自增);port 侧无独有内容 | 无 | diff --git a/docs/migration-todo-port-1.21.1.md b/docs/migration-todo-port-1.21.1.md index c63aa333..ef9f2e63 100644 --- a/docs/migration-todo-port-1.21.1.md +++ b/docs/migration-todo-port-1.21.1.md @@ -1,6 +1,6 @@ # AnvilLib 回移植执行跟踪:`dev/26.1` → `port/1.21.1` -> 本文件是 `docs/migration-plan-port-1.21.1.md`(#P1–#P19)的执行跟踪清单。每完成一项,将对应状态更新为「✅ +> 本文件是 `docs/migration-plan-port-1.21.1.md`(#P1–#P19)的执行跟踪清单(另有计划外追加项 #P20,见「逐项日志」末尾)。每完成一项,将对应状态更新为「✅ > 已完成」并记录提交号;未完成项保持「⬜ 未开始 / 🔄 进行中」。待验证事项(附录 B 的 V-1…V-18)在验证后同步销账。 - 基线:`port/1.21.1`(执行起点 `96c3a34f`,计划基线 `bd25229`) @@ -32,6 +32,7 @@ | #P17 | module.wheel:环形扇区选择效果 | 中-高 | ✅ 已完成 | `142e3296` | | #P18 | module.test:T2/T8/T9/T10 可选测试 | 低-中 | ✅ 已完成 | `a4ab3acd` | | #P19 | 构建体系:roseauCheck | 中 | ✅ 已完成(运行待补验) | `544c9eb0` | +| #P20 | module.codec:CodecUtil 便携 create/mapCodec(计划外追加) | 低 | ✅ 已完成(待提交) | 来源 `173a454` | ## 执行顺序(计划第 6 章) @@ -184,6 +185,17 @@ - [ ] `gradlew roseauCheck` 运行通过 - [ ] 提交 +### #P20 module.codec:CodecUtil 便携 create/mapCodec(计划外追加 · 2026-08-11) + +> 来源:`dev/26.1` 提交 `173a454`(feat(codec): add portable methods for creating Codec and MapCodec with varying parameters) +> 落点:`module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/CodecUtil.java` + +- [x] 提取改动:16 个 `create`(Codec)+ 16 个 `mapCodec`(MapCodec)重载 + `App`/`Function3–16`/`StreamCodec` 导入 +- [x] 冲突消解:26.1 `Identifier` → 1.21.1 `ResourceLocation`;保留 port 侧既有 `zomListMap`/`encodeStart` 并追加在其后 +- [x] API 核实:1.21.1 DFU 6.0.8 含 `Function3–16`(javap 实测),无需反向替换 +- [x] `gradlew :anvillib-codec-neoforge-1.21.1:compileJava --rerun-tasks` 通过 +- [ ] 提交(conventional commit) + --- ## 待验证事项销账(计划附录 B) diff --git a/module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/CodecUtil.java b/module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/CodecUtil.java index 740a1474..d554da40 100644 --- a/module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/CodecUtil.java +++ b/module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/CodecUtil.java @@ -1,6 +1,21 @@ package dev.anvilcraft.lib.v2.codec; +import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.util.Either; +import com.mojang.datafixers.util.Function10; +import com.mojang.datafixers.util.Function11; +import com.mojang.datafixers.util.Function12; +import com.mojang.datafixers.util.Function13; +import com.mojang.datafixers.util.Function14; +import com.mojang.datafixers.util.Function15; +import com.mojang.datafixers.util.Function16; +import com.mojang.datafixers.util.Function3; +import com.mojang.datafixers.util.Function4; +import com.mojang.datafixers.util.Function5; +import com.mojang.datafixers.util.Function6; +import com.mojang.datafixers.util.Function7; +import com.mojang.datafixers.util.Function8; +import com.mojang.datafixers.util.Function9; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; @@ -13,6 +28,7 @@ import lombok.NoArgsConstructor; import net.minecraft.core.NonNullList; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.EntityType; import net.minecraft.world.item.Item; @@ -373,4 +389,1300 @@ public static MapCodec> zomListMap(Codec codec, String name) { public static DataResult encodeStart(MapCodec codec, DynamicOps ops, T input) { return codec.encode(input, ops, codec.compressedBuilder(ops)).build(ops.emptyMap()); } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + Function factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + BiFunction factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + Function3 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + Function4 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3, t4).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + Function5 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3, t4, t5).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + Function6 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3, t4, t5, t6).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + Function7 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + Function8 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + Function9 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + Function10 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param t11 第十一个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// @param 第十一个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + App, T11> t11, + Function11 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param t11 第十一个参数 + /// @param t12 第十二个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// @param 第十一个参数的类型 + /// @param 第十二个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + App, T11> t11, + App, T12> t12, + Function12 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param t11 第十一个参数 + /// @param t12 第十二个参数 + /// @param t13 第十三个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// @param 第十一个参数的类型 + /// @param 第十二个参数的类型 + /// @param 第十三个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + App, T11> t11, + App, T12> t12, + App, T13> t13, + Function13 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param t11 第十一个参数 + /// @param t12 第十二个参数 + /// @param t13 第十三个参数 + /// @param t14 第十四个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// @param 第十一个参数的类型 + /// @param 第十二个参数的类型 + /// @param 第十三个参数的类型 + /// @param 第十四个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + App, T11> t11, + App, T12> t12, + App, T13> t13, + App, T14> t14, + Function14 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param t11 第十一个参数 + /// @param t12 第十二个参数 + /// @param t13 第十三个参数 + /// @param t14 第十四个参数 + /// @param t15 第十五个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// @param 第十一个参数的类型 + /// @param 第十二个参数的类型 + /// @param 第十三个参数的类型 + /// @param 第十四个参数的类型 + /// @param 第十五个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + App, T11> t11, + App, T12> t12, + App, T13> t13, + App, T14> t14, + App, T15> t15, + Function15 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15).apply(inst, factory)); + } + + /// 创建 {@link Codec} 的便携方法,对 {@link RecordCodecBuilder#create(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param t11 第十一个参数 + /// @param t12 第十二个参数 + /// @param t13 第十三个参数 + /// @param t14 第十四个参数 + /// @param t15 第十五个参数 + /// @param t16 第十六个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// @param 第十一个参数的类型 + /// @param 第十二个参数的类型 + /// @param 第十三个参数的类型 + /// @param 第十四个参数的类型 + /// @param 第十五个参数的类型 + /// @param 第十六个参数的类型 + /// + /// @return 结果的 {@link Codec} + public static Codec create( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + App, T11> t11, + App, T12> t12, + App, T13> t13, + App, T14> t14, + App, T15> t15, + App, T16> t16, + Function16 factory + ) { + return RecordCodecBuilder.create(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + Function factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + BiFunction factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + Function3 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + Function4 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3, t4).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + Function5 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3, t4, t5).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + Function6 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3, t4, t5, t6).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + Function7 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + Function8 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + Function9 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + Function10 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param t11 第十一个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// @param 第十一个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + App, T11> t11, + Function11 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param t11 第十一个参数 + /// @param t12 第十二个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// @param 第十一个参数的类型 + /// @param 第十二个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + App, T11> t11, + App, T12> t12, + Function12 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param t11 第十一个参数 + /// @param t12 第十二个参数 + /// @param t13 第十三个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// @param 第十一个参数的类型 + /// @param 第十二个参数的类型 + /// @param 第十三个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + App, T11> t11, + App, T12> t12, + App, T13> t13, + Function13 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param t11 第十一个参数 + /// @param t12 第十二个参数 + /// @param t13 第十三个参数 + /// @param t14 第十四个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// @param 第十一个参数的类型 + /// @param 第十二个参数的类型 + /// @param 第十三个参数的类型 + /// @param 第十四个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + App, T11> t11, + App, T12> t12, + App, T13> t13, + App, T14> t14, + Function14 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param t11 第十一个参数 + /// @param t12 第十二个参数 + /// @param t13 第十三个参数 + /// @param t14 第十四个参数 + /// @param t15 第十五个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// @param 第十一个参数的类型 + /// @param 第十二个参数的类型 + /// @param 第十三个参数的类型 + /// @param 第十四个参数的类型 + /// @param 第十五个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + App, T11> t11, + App, T12> t12, + App, T13> t13, + App, T14> t14, + App, T15> t15, + Function15 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15).apply(inst, factory)); + } + + /// 创建 {@link MapCodec} 的便携方法,对 {@link RecordCodecBuilder#mapCodec(Function)} 的代理 + /// + ///

    参考了 {@link net.minecraft.network.codec.StreamCodec#composite(StreamCodec, Function, Function)} 的设计

    + /// + /// @param t1 第一个参数 + /// @param t2 第二个参数 + /// @param t3 第三个参数 + /// @param t4 第四个参数 + /// @param t5 第五个参数 + /// @param t6 第六个参数 + /// @param t7 第七个参数 + /// @param t8 第八个参数 + /// @param t9 第九个参数 + /// @param t10 第十个参数 + /// @param t11 第十一个参数 + /// @param t12 第十二个参数 + /// @param t13 第十三个参数 + /// @param t14 第十四个参数 + /// @param t15 第十五个参数 + /// @param t16 第十六个参数 + /// @param factory 使用所有参数创建结果的工厂 + /// + /// @param 结果的类型 + /// @param 第一个参数的类型 + /// @param 第二个参数的类型 + /// @param 第三个参数的类型 + /// @param 第四个参数的类型 + /// @param 第五个参数的类型 + /// @param 第六个参数的类型 + /// @param 第七个参数的类型 + /// @param 第八个参数的类型 + /// @param 第九个参数的类型 + /// @param 第十个参数的类型 + /// @param 第十一个参数的类型 + /// @param 第十二个参数的类型 + /// @param 第十三个参数的类型 + /// @param 第十四个参数的类型 + /// @param 第十五个参数的类型 + /// @param 第十六个参数的类型 + /// + /// @return 结果的 {@link MapCodec} + public static MapCodec mapCodec( + App, T1> t1, + App, T2> t2, + App, T3> t3, + App, T4> t4, + App, T5> t5, + App, T6> t6, + App, T7> t7, + App, T8> t8, + App, T9> t9, + App, T10> t10, + App, T11> t11, + App, T12> t12, + App, T13> t13, + App, T14> t14, + App, T15> t15, + App, T16> t16, + Function16 factory + ) { + return RecordCodecBuilder.mapCodec(inst -> inst.group(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16).apply(inst, factory)); + } } From cc0d934885a32c5c897b538418d3a4a956a23bcf Mon Sep 17 00:00:00 2001 From: QiuShui1012 Date: Wed, 12 Aug 2026 07:12:09 +0800 Subject: [PATCH 33/38] fix(font): fix crashing --- .../resources/assets/anvillib_font/shaders/core/sdf_text.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.json b/module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.json index 9de877d8..72820298 100644 --- a/module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.json +++ b/module.font/src/main/resources/assets/anvillib_font/shaders/core/sdf_text.json @@ -1,6 +1,6 @@ { - "vertex": "sdf_text", - "fragment": "sdf_text", + "vertex": "anvillib_font:sdf_text", + "fragment": "anvillib_font:sdf_text", "samplers": [ { "name": "Sampler0" } ], From 819199e9e818a05f06442d7efbdfb08dc342df83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AB=B9=E8=8B=A5=E6=B3=A0?= <98583550+zhuruoling@users.noreply.github.com> Date: Wed, 12 Aug 2026 08:02:27 +0800 Subject: [PATCH 34/38] remove(rendering): remove rendering module It's unnecessary --- .github/workflows/ci.yml | 28 - .github/workflows/pull_request.yml | 11 - .github/workflows/release.yml | 28 - docs/branch-comparison.md | 2 + docs/migration-plan-port-1.21.1.md | 8 + docs/migration-todo-port-1.21.1.md | 14 +- module.font/build.gradle | 9 - module.main/build.gradle | 2 - module.rendering/build.gradle | 297 --------- module.rendering/gradle.properties | 4 - .../lib/v2/rendering/ALRPostEffects.java | 45 -- .../lib/v2/rendering/ALRSharedMath.java | 8 - .../lib/v2/rendering/AnvilLibRendering.java | 95 --- .../rendering/bloom/BloomParametersUbo.java | 36 -- .../bloom/BloomPipelineParametersUbo.java | 39 -- .../v2/rendering/bloom/BloomPostEffect.java | 335 ----------- .../rendering/bloom/BloomRenderCallback.java | 17 - .../lib/v2/rendering/bloom/TransformsUbo.java | 31 - .../v2/rendering/blur/BlurParametersUbo.java | 38 -- .../lib/v2/rendering/blur/GaussianBlur.java | 106 ---- .../v2/rendering/cachedber/package-info.java | 7 - .../CachedBlockEntityRenderingPipeline.java | 203 ------- .../pipeline/CachedRenderingChunk.java | 275 --------- .../cachedber/pipeline/RebuildTask.java | 72 --- .../cachedber/pipeline/package-info.java | 7 - .../CachedBlockEntityRenderDispatcher.java | 77 --- .../renderer/CachedBlockEntityRenderer.java | 24 - .../cachedber/renderer/package-info.java | 7 - .../event/MainTargetResizeEvent.java | 16 - .../event/RegisterComputePipelinesEvent.java | 27 - .../extension/ALRRenderTypeExtension.java | 37 -- .../extension/blaze3d/MemoryBarrierFlag.java | 39 -- .../compute/ALRComputeCapabilities.java | 15 - .../blaze3d/compute/BufferSlice.java | 21 - .../compute/pipeline/ALRComputePass.java | 100 --- .../pipeline/ALRComputePassBackend.java | 39 -- .../compute/pipeline/ALRComputePipeline.java | 116 ---- .../bindings/AtomicCounterBinding.java | 19 - .../bindings/ComputeBindingLayout.java | 12 - .../pipeline/bindings/ImageBinding.java | 34 -- .../bindings/ShaderStorageBinding.java | 19 - .../pipeline/bindings/TextureBinding.java | 33 - .../bindings/UniformBlockBinding.java | 19 - .../pipeline/gl/GlComputePassBackend.java | 215 ------- .../shader/ALRComputeProgramInstance.java | 20 - .../shader/ALRComputeProgramInstanceKey.java | 13 - .../shader/ALRComputeShaderManager.java | 205 ------- .../compute/shader/ShaderResourceType.java | 9 - .../rendering/foundation/ALRMeshSorting.java | 44 -- .../foundation/buffers/EmptyBufferSource.java | 34 -- .../buffers/EmptyOutlineBufferSource.java | 20 - .../buffers/EmptyVertexConsumer.java | 43 -- .../buffers/FullyBufferedBufferSource.java | 128 ---- .../buffers/GpuBufferConstants.java | 6 - .../TransformingVertexConsumerWrapper.java | 50 -- .../foundation/buffers/VertexBufferHost.java | 19 - .../buffers/layout/BufferLayout.java | 38 -- .../buffers/layout/BufferSizeCalculator.java | 27 - .../buffers/layout/BufferWriter.java | 42 -- .../layout/std140/Std140SizeCalculator.java | 76 --- .../buffers/layout/std140/Std140Writer.java | 132 ---- .../layout/std430/Std430SizeCalculator.java | 75 --- .../buffers/layout/std430/Std430Writer.java | 134 ----- .../buffers/object/BufferObject.java | 31 - .../object/BufferObjectLayoutDefinition.java | 51 -- .../object/BufferObjectLayoutEntry.java | 87 --- .../object/BufferObjectLayoutEntryType.java | 173 ------ .../object/ShaderBufferObjectUsage.java | 6 - .../foundation/compound/DirtyTracked.java | 5 - .../rendering/glitch/GlitchParametersUbo.java | 32 - .../v2/rendering/glitch/GlitchPostEffect.java | 83 --- .../v2/rendering/integration/IrisSupport.java | 156 ----- .../ALRIntegrationCompatMixinPlugin.java | 63 -- ...chedBlockEntityRenderingPipelineMixin.java | 42 -- .../integration/mixins/RebuildTaskMixin.java | 35 -- .../rendering/mixins/GameRendererMixin.java | 23 - .../v2/rendering/mixins/MinecraftMixin.java | 37 -- .../v2/rendering/mixins/RenderTypeMixin.java | 23 - .../postprocess/ALRPostEffectShaders.java | 66 -- .../rendering/postprocess/GlPostProcess.java | 67 --- .../postprocess/GlUniformBuffer.java | 57 -- .../lib/v2/rendering/sdf/Sdf2d.java | 379 ------------ .../lib/v2/rendering/sdf/SdfGraphics.java | 568 ------------------ .../lib/v2/rendering/sdf/SdfParameters.java | 202 ------- .../lib/v2/rendering/sdf/SdfPassType.java | 6 - .../lib/v2/rendering/sdf/SdfRenderType.java | 22 - .../lib/v2/rendering/sdf/SdfShaders.java | 46 -- .../lib/v2/rendering/util/Timer.java | 9 - .../resources/META-INF/accesstransformer.cfg | 5 - .../resources/META-INF/neoforge.mods.toml | 90 --- ...anvillib_rendering.integration.mixins.json | 17 - .../resources/anvillib_rendering.mixins.json | 17 - .../shaders/core/apply_bloom.fsh | 23 - .../shaders/core/apply_bloom.json | 9 - .../anvillib_rendering/shaders/core/blit.vsh | 16 - .../anvillib_rendering/shaders/core/blur.fsh | 36 -- .../anvillib_rendering/shaders/core/blur.json | 8 - .../shaders/core/down_sample.fsh | 26 - .../shaders/core/down_sample.json | 8 - .../shaders/core/glitch.fsh | 51 -- .../shaders/core/glitch.json | 8 - .../shaders/core/sdf_graphics.fsh | 194 ------ .../shaders/core/sdf_graphics.json | 10 - .../shaders/core/sdf_graphics.vsh | 35 -- .../shaders/core/up_sample.fsh | 61 -- .../shaders/core/up_sample.json | 9 - .../shaders/include/util.glsl | 13 - module.rendering/src/main/resources/icon.png | Bin 1089 -> 0 bytes .../buffers/layout/Std140LayoutRulesTest.java | 139 ----- renderdoc-loader/build.gradle | 120 ---- renderdoc-loader/gradle.properties | 4 - .../lib/renderdoc/loader/RenderDocAgent.java | 99 --- .../lib/renderdoc/loader/package-info.java | 16 - settings.gradle | 36 +- 114 files changed, 36 insertions(+), 6984 deletions(-) delete mode 100644 module.rendering/build.gradle delete mode 100644 module.rendering/gradle.properties delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRPostEffects.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRSharedMath.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/AnvilLibRendering.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomParametersUbo.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPipelineParametersUbo.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPostEffect.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomRenderCallback.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/TransformsUbo.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/BlurParametersUbo.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/GaussianBlur.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/package-info.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedBlockEntityRenderingPipeline.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedRenderingChunk.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/RebuildTask.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/package-info.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderDispatcher.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderer.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/package-info.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/MainTargetResizeEvent.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/RegisterComputePipelinesEvent.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/ALRRenderTypeExtension.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/MemoryBarrierFlag.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/ALRComputeCapabilities.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/BufferSlice.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePass.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePassBackend.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePipeline.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/AtomicCounterBinding.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ComputeBindingLayout.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ImageBinding.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ShaderStorageBinding.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/TextureBinding.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/UniformBlockBinding.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/gl/GlComputePassBackend.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstance.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstanceKey.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeShaderManager.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ShaderResourceType.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/ALRMeshSorting.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyBufferSource.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyOutlineBufferSource.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyVertexConsumer.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/FullyBufferedBufferSource.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/GpuBufferConstants.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/TransformingVertexConsumerWrapper.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/VertexBufferHost.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferLayout.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferSizeCalculator.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferWriter.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140SizeCalculator.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140Writer.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430SizeCalculator.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430Writer.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObject.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutDefinition.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntry.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntryType.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/ShaderBufferObjectUsage.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/compound/DirtyTracked.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchParametersUbo.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchPostEffect.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/IrisSupport.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/ALRIntegrationCompatMixinPlugin.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/CachedBlockEntityRenderingPipelineMixin.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/RebuildTaskMixin.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/GameRendererMixin.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/MinecraftMixin.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/RenderTypeMixin.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/ALRPostEffectShaders.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlPostProcess.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlUniformBuffer.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/Sdf2d.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfGraphics.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfParameters.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfPassType.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfRenderType.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfShaders.java delete mode 100644 module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/util/Timer.java delete mode 100644 module.rendering/src/main/resources/META-INF/accesstransformer.cfg delete mode 100644 module.rendering/src/main/resources/META-INF/neoforge.mods.toml delete mode 100644 module.rendering/src/main/resources/anvillib_rendering.integration.mixins.json delete mode 100644 module.rendering/src/main/resources/anvillib_rendering.mixins.json delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.fsh delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.json delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blit.vsh delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.fsh delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.json delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.fsh delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.json delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.fsh delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.json delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.json delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.vsh delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.fsh delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.json delete mode 100644 module.rendering/src/main/resources/assets/anvillib_rendering/shaders/include/util.glsl delete mode 100644 module.rendering/src/main/resources/icon.png delete mode 100644 module.rendering/src/test/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/Std140LayoutRulesTest.java delete mode 100644 renderdoc-loader/build.gradle delete mode 100644 renderdoc-loader/gradle.properties delete mode 100644 renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/RenderDocAgent.java delete mode 100644 renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/package-info.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46b1bb56..0f6b6213 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,8 +118,6 @@ jobs: signing_password: ${{ secrets.SIGNING_PASSWORD }} font: uses: ./.github/workflows/build_and_test.yml - needs: - - rendering with: module: font module_id: anvillib-font @@ -182,7 +180,6 @@ jobs: - network - recipe - registrum - - rendering - rpc - space-select - sync @@ -351,31 +348,6 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} - rendering: - uses: ./.github/workflows/build_and_test.yml - with: - module: rendering - module_id: anvillib-rendering - mod_id: anvillib_rendering - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - rendering-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - rendering - with: - module: rendering - module_id: anvillib-rendering - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} rpc: uses: ./.github/workflows/build_and_test.yml needs: diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 47397393..77ddcac8 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -50,8 +50,6 @@ jobs: pr_build: true font: uses: ./.github/workflows/build_and_test.yml - needs: - - rendering with: module: font module_id: anvillib-font @@ -80,7 +78,6 @@ jobs: - network - recipe - registrum - - rendering - rpc - space-select - sync @@ -145,14 +142,6 @@ jobs: mod_id: anvillib_registrum ci_build: true pr_build: true - rendering: - uses: ./.github/workflows/build_and_test.yml - with: - module: rendering - module_id: anvillib-rendering - mod_id: anvillib_rendering - ci_build: true - pr_build: true rpc: uses: ./.github/workflows/build_and_test.yml needs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4c7255e..55d4b258 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -118,8 +118,6 @@ jobs: signing_password: ${{ secrets.SIGNING_PASSWORD }} font: uses: ./.github/workflows/build_and_test.yml - needs: - - rendering with: module: font module_id: anvillib-font @@ -182,7 +180,6 @@ jobs: - network - recipe - registrum - - rendering - rpc - space-select - sync @@ -348,31 +345,6 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} - rendering: - uses: ./.github/workflows/build_and_test.yml - with: - module: rendering - module_id: anvillib-rendering - mod_id: anvillib_rendering - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - rendering-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - rendering - with: - module: rendering - module_id: anvillib-rendering - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} rpc: uses: ./.github/workflows/build_and_test.yml needs: diff --git a/docs/branch-comparison.md b/docs/branch-comparison.md index a9fdaa3b..0b18e9ba 100644 --- a/docs/branch-comparison.md +++ b/docs/branch-comparison.md @@ -1,5 +1,7 @@ # AnvilLib 分支对比报告:`port/1.21.1` ↔ `dev/26.1` +> ⚠️ 状态更新(2026-08-12):`module.rendering` 与 `renderdoc-loader` 已从 `port/1.21.1` 分支移除(构建依赖、CI 任务与文档引用已同步清理)。本文为 2026-08-10 的快照分析,正文中 dev/26.1 侧的模块描述仍为历史事实。 + > 文档信息 > > - 生成日期:2026-08-10 diff --git a/docs/migration-plan-port-1.21.1.md b/docs/migration-plan-port-1.21.1.md index c425ea27..ff170035 100644 --- a/docs/migration-plan-port-1.21.1.md +++ b/docs/migration-plan-port-1.21.1.md @@ -1,5 +1,7 @@ # AnvilLib 回移植计划:`dev/26.1` → `port/1.21.1`(功能回移植执行方案) +> ⚠️ 状态更新(2026-08-12):#P7(`module.rendering`)与 #P8(`renderdoc-loader`)已从 `port/1.21.1` 分支整体移除;#P6(`module.font`)与 `module.main` 的渲染 jarJar 依赖、CI 工作流(ci / release / pull_request)中的渲染任务亦已同步清理。本文为历史计划文档,相关章节仅作记录保留。 + > 文档信息 > > - 生成日期:2026-08-10 @@ -423,6 +425,8 @@ done ### 4.6 回移植项 #P6:module.font 整模块(中) +> ⚠️ 2026-08-12:module.font 已落地为独立模块,其与 module.rendering 的依赖已随渲染模块移除而删除(build.gradle 不再声明 anvillib-rendering-neoforge-1.21.1)。 + #### 4.6.1 目标 把 dev 侧 SDF 字体模块 `module.font`(25 文件,AWT 字体 → CPU 生成 SDF 图集 → GPU 采样渲染)整体回移植,作为新模块 `anvillib-font-neoforge-1.21.1`;约 60% 纯 CPU 逻辑直接搬,GPU 上传/绘制层与 GUI 屏幕按 1.21.1 API 改写。**依赖 module.rendering 的 1.21.1 版(#P7)**(build.gradle jarJar 依赖 `anvillib-rendering-neoforge-1.21.1`;纯逻辑部分可在 #P7a 完成后先行落地)。 @@ -475,6 +479,8 @@ done ### 4.7 回移植项 #P7:module.rendering(高 · 按功能块分批) +> ⚠️ 2026-08-12:module.rendering 已从 port/1.21.1 整体移除,本节内容仅为历史计划记录。 + #### 4.7.0 总体说明与拆批依据 `module.rendering`(约 113 文件)在 port/1.21.1 上完全不存在(`git diff --name-status` 全部为 `A`)。其功能层深度构建在 26.1 全新渲染架构之上(`GpuDevice/CommandEncoder/RenderPass/GpuBuffer`、`RenderPipeline/RenderState`、`SubmitNodeStorage/FeatureRenderDispatcher/GameRenderState`、`GuiGraphicsExtractor`),**不能整模块移植**;1.21.1 等价物为 `RegisterShadersEvent + ShaderInstance`、`RenderTarget`、`VertexBuffer`、`GuiGraphics`、`MultiBufferSource`。按功能块拆为 **#P7a–#P7e** 五批,每批独立步骤与验收,作为单一模块(`anvillib-rendering-neoforge-1.21.1`)落地。 @@ -599,6 +605,8 @@ done ### 4.8 回移植项 #P8:renderdoc-loader(低) +> ⚠️ 2026-08-12:renderdoc-loader 已随 module.rendering 一并从 port/1.21.1 移除,本节内容仅为历史计划记录。 + #### 4.8.1 目标 把 dev 侧 RenderDoc 调试用 Java Agent 模块 `renderdoc-loader`(2 文件)回移植,作为 `renderdoc-loader-internal`,实现 `premain`/`agentmain` 加载 RenderDoc DLL。 diff --git a/docs/migration-todo-port-1.21.1.md b/docs/migration-todo-port-1.21.1.md index ef9f2e63..08384387 100644 --- a/docs/migration-todo-port-1.21.1.md +++ b/docs/migration-todo-port-1.21.1.md @@ -19,8 +19,8 @@ | #P4 | module.collision 整模块(3 文件) | 极低 | ✅ 已完成 | `c25dd465` | | #P5 | module.space-select 整模块(18 文件) | 中 | ✅ 已完成 | `1706feaa` | | #P6 | module.font 整模块(25 文件) | 中 | ✅ 已完成 | `91533730` | -| #P7 | module.rendering(#P7a–#P7e 分批) | 高 | ✅ 已完成 | `b0e179e8` + `a6544e6b` | -| #P8 | renderdoc-loader(2 文件) | 低 | ✅ 已完成 | `c62a262e` | +| #P7 | module.rendering(#P7a–#P7e 分批) | 高 | ✅ 已完成(已移除)| `b0e179e8` + `a6544e6b` | +| #P8 | renderdoc-loader(2 文件) | 低 | ✅ 已完成(已移除)| `c62a262e` | | #P9 | module.config:group + TranslatableEnum + TOML 点分隔 | 低 | ✅ 已完成 | `65aa5e11` | | #P10 | module.integration:数据加载拆分 + meter 递增 | 低 | ✅ 已完成 | `adc23468` | | #P11 | module.network:泛型修复 + Included + public | 低 | ✅ 已完成 | `20934ac4` | @@ -34,6 +34,8 @@ | #P19 | 构建体系:roseauCheck | 中 | ✅ 已完成(运行待补验) | `544c9eb0` | | #P20 | module.codec:CodecUtil 便携 create/mapCodec(计划外追加) | 低 | ✅ 已完成(待提交) | 来源 `173a454` | +> ⚠️ 2026-08-12 状态更新:#P7(module.rendering)与 #P8(renderdoc-loader)已从 `port/1.21.1` 分支整体移除;同时清理了 `module.font` / `module.main` 的渲染依赖、CI 工作流(ci / release / pull_request)中的渲染任务与相关文档引用。 + ## 执行顺序(计划第 6 章) ``` @@ -60,6 +62,8 @@ ### #P7 module.rendering(高 · 分批) +> ⚠️ 2026-08-12:该模块已从 `port/1.21.1` 移除,本清单仅保留历史记录。 + - [ ] #P7a 纯逻辑 19 项 + 模块骨架 - [ ] #P7b 后处理三件套(Bloom/GaussianBlur/Glitch) - [ ] #P7c SDF GUI @@ -95,6 +99,8 @@ ### #P8 renderdoc-loader(低) +> ⚠️ 2026-08-12:该模块已从 `port/1.21.1` 移除,本清单仅保留历史记录。 + - [ ] 2 文件提取 + 构建脚本改写(Java 21) - [ ] 提交 @@ -231,9 +237,9 @@ | L-4 | explosion 行为验证(#P3/#P18 T9) | 分层球壳破坏、熔化替换、实体 hurt | runClient/runServer 执行测试命令 | | L-5 | space-select 渲染(#P5,V-4/V-16) | 线框绘制颜色/平移语义、滚轮交互、网络收发 | runClient 目测 | | L-6 | font 运行验证(#P6) | SDF 文字渲染、anvillib$text 扩展、FontConfigScreen | runClient | -| L-7 | rendering 运行验证(#P7a) | mixin 注入、AT 生效、mods.toml 加载 | runClient 启动日志 | +| L-7 | rendering 运行验证(#P7a) | ⚠️ 2026-08-12:module.rendering 已移除,不再适用 | — | | L-8 | multiblock 行为验证(#P15) | 事件可取消回滚、快照复用、懒解析 | runServer | | L-9 | wheel 运行验证(#P17) | 环形扇区效果、颜色配置、旧路径无回归 | runClient + T10 | | L-10 | Iris 集成(#P7d,V-2/V-6) | ✅ 已解决:#P7d 完成,Iris 经反射访问(签名一致),运行时需装 Iris 客户端验证 | — | | L-11 | Compute(#P7e,V-8) | ✅ 已解决:#P7e 完成(编译通过),dispatch/内存屏障运行验证待 runClient | — | -| L-12 | rendering 执行层运行验证(#P7b-e) | 后处理效果/SDF GUI/CachedBER/GLSL 编译链接需 runClient | 运行 runClient 核对 7.2 清单 | +| L-12 | rendering 执行层运行验证(#P7b-e) | ⚠️ 2026-08-12:module.rendering 已移除,不再适用 | — | diff --git a/module.font/build.gradle b/module.font/build.gradle index c01e5e0e..1fbbbf23 100644 --- a/module.font/build.gradle +++ b/module.font/build.gradle @@ -113,15 +113,6 @@ neoForge { // Include resources generated by data generators. sourceSets.main.resources { srcDir 'src/generated/resources' } - -dependencies { - if (System.getenv("NOT_DEV") == 'true') { - jarJar(api("dev.anvilcraft.lib:anvillib-rendering-neoforge-1.21.1:latest.release")) - } else { - jarJar(implementation project(":anvillib-rendering-neoforge-1.21.1")) - } -} - // This block of code expands all declared replace properties in the specified resource targets. // A missing property will result in an error. Properties are expanded using ${} Groovy notation. // When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. diff --git a/module.main/build.gradle b/module.main/build.gradle index a888d6c6..d3d61a53 100644 --- a/module.main/build.gradle +++ b/module.main/build.gradle @@ -139,7 +139,6 @@ dependencies { jarJar(api("dev.anvilcraft.lib:anvillib-recipe-neoforge-1.21.1:latest.release")) interfaceInjectionData("dev.anvilcraft.lib:anvillib-recipe-neoforge-1.21.1:latest.release") jarJar(api("dev.anvilcraft.lib:anvillib-registrum-neoforge-1.21.1:latest.release")) - jarJar(api("dev.anvilcraft.lib:anvillib-rendering-neoforge-1.21.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-rpc-neoforge-1.21.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-space-select-neoforge-1.21.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-sync-neoforge-1.21.1:latest.release")) @@ -160,7 +159,6 @@ dependencies { jarJar(api project(":anvillib-recipe-neoforge-1.21.1")) interfaceInjectionData project(":anvillib-recipe-neoforge-1.21.1") jarJar(api project(":anvillib-registrum-neoforge-1.21.1")) - jarJar(api project(":anvillib-rendering-neoforge-1.21.1")) jarJar(api project(":anvillib-rpc-neoforge-1.21.1")) jarJar(api project(":anvillib-space-select-neoforge-1.21.1")) jarJar(api project(":anvillib-sync-neoforge-1.21.1")) diff --git a/module.rendering/build.gradle b/module.rendering/build.gradle deleted file mode 100644 index f778e94e..00000000 --- a/module.rendering/build.gradle +++ /dev/null @@ -1,297 +0,0 @@ -plugins { - id 'java-library' - id 'eclipse' - id 'idea' - id 'maven-publish' - alias libs.plugins.modDevGradle - alias libs.plugins.lombok - alias libs.plugins.machete - id 'org.jreleaser' version '1.23.0' - id "me.modmuss50.mod-publish-plugin" version "1.1.0" -} - -String buildType = 'snapshot' -String buildNumber -if (System.getenv("CI_BUILD") == 'false') buildNumber = null -else { - if (System.getenv("PR_BUILD") != 'false') buildType = 'pr' - buildNumber = System.getenv("GITHUB_RUN_NUMBER") -} -version = "${mod_version}" + (buildNumber != null ? "+${buildType}.${buildNumber}" : "") -group = mod_group_id - -base { - archivesName = "${project.mod_id}-neoforge-${libs.versions.minecraft.get()}" -} - -repositories { - maven { url = "https://server.cjsah.net:1002/maven/" } - mavenLocal() - mavenCentral() -} - -java.toolchain.languageVersion = JavaLanguageVersion.of(21) - -neoForge { - // Specify the version of NeoForge to use. - version = libs.versions.neoForge.get() - - parchment { - mappingsVersion = project.parchment_mappings_version - minecraftVersion = project.parchment_minecraft_version - } - - // This line is optional. Access Transformers are automatically detected - // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') - - interfaceInjectionData { - // from 'src/main/resources/interface_injections.json' - // publish file('src/main/resources/interface_injections.json') - } - - // Default run configurations. - // These can be tweaked, removed, or duplicated as needed. - runs { - client { - client() - - // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. - systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id - } - - server { - server() - programArgument '--nogui' - systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id - } - - // This run config launches GameTestServer and runs all registered gametests, then exits. - // By default, the server will crash when no gametests are provided. - // The gametest system is also enabled by default for other run configs under the /test command. - gameTestServer { - type = "gameTestServer" - systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id - } - - data { - data() - - // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it - // gameDirectory = project.file('run-data') - - // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. - programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() - } - - // applies to all the run configs above - configureEach { - // Recommended logging data for a userdev environment - // The markers can be added/remove as needed separated by commas. - // "SCAN": For mods scan. - // "REGISTRIES": For firing of registry events. - // "REGISTRYDUMP": For getting the contents of all registries. - systemProperty 'forge.logging.markers', 'REGISTRIES' - - // Recommended logging level for the console - // You can set various levels here. - // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels - logLevel = org.slf4j.event.Level.DEBUG - } - } - - mods { - // define mod <-> source bindings - // these are used to tell the game which sources are for which mod - // mostly optional in a single mod project - // but multi mod projects should define one per mod - "${mod_id}" { - sourceSet(sourceSets.main) - } - } -} - -// Include resources generated by data generators. -sourceSets.main.resources { srcDir 'src/generated/resources' } - - -dependencies { - - // #P7d: optional Iris integration (integration/IrisSupport.java + the two integration mixins). - // The dev/26.1 branch used compileOnly("maven.modrinth:YL57xq9U:...") (Iris) from the Modrinth - // maven; that maven has been shut down, and no other reachable mirror hosts Iris 1.8.x for 1.21.1 - // (the API was verified by downloading iris-neoforge-1.8.12+mc1.21.1.jar manually). The integration - // therefore accesses the Iris classes through reflection and needs no compile-time dependency; it - // activates at runtime only when Iris is loaded (gated by ALRIntegrationCompatMixinPlugin + the - // ModList check inside IrisSupport). To restore a compile-time dependency once a mirror is available: - // compileOnly("maven.modrinth:YL57xq9U:t3ruzodq") // iris 1.8.12+1.21.1-neoforge (needs the Modrinth maven repository) - // and revert IrisSupport to direct references (net.irisshaders.iris.api.v0.IrisApi / - // net.irisshaders.iris.vertices.ImmediateState). -} - -tasks.register('runStd140LayoutRulesTest', JavaExec) { - group = 'verification' - description = 'Runs lightweight std140 layout rule checks.' - classpath = sourceSets.test.runtimeClasspath + sourceSets.main.compileClasspath - mainClass = 'dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.Std140LayoutRulesTest' -} - -tasks.named('test') { - // Ensure the Std140 layout rule checks actually run under the :test task (pure JVM verification, no game needed). - dependsOn tasks.named('runStd140LayoutRulesTest') -} - -tasks.named('check') { - dependsOn tasks.named('runStd140LayoutRulesTest') -} - -// This block of code expands all declared replace properties in the specified resource targets. -// A missing property will result in an error. Properties are expanded using ${} Groovy notation. -// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. -// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html -tasks.withType(ProcessResources).configureEach { - var replaceProperties = [minecraft_version : libs.versions.minecraft.get(), - minecraft_version_range: minecraft_version_range, - neo_version : libs.versions.neoForge.get(), - neo_version_range : neo_version_range, - loader_version_range : loader_version_range, - mod_id : mod_id, - mod_name : mod_name, - mod_license : mod_license, - mod_version : version, - mod_authors : mod_authors, - mod_description : mod_description] - inputs.properties replaceProperties - - filesMatching(['META-INF/neoforge.mods.toml']) { - expand replaceProperties - } -} - -tasks.register('sourcesJar', Jar) { - from delombok.outputs.files - dependsOn delombok - archiveClassifier = "sources" -} - -tasks.register('javadocJar', Jar) { - from javadoc.destinationDir - dependsOn javadoc - archiveClassifier = "javadoc" -} - -javadoc { - options.encoding = 'UTF-8' - options.addStringOption('Xdoclint:none', '-quiet') -} - -jar { - from(rootProject.file("LICENSE")) - from(rootProject.file("ASSETS_LICENSE")) - from(rootProject.file("README.md")) - from(rootProject.file("README.en.md")) -} - -artifacts { - archives tasks.jar - archives tasks.sourcesJar - archives tasks.javadocJar -} - -def this_name = base.archivesName.get() -def this_description = mod_description - -publishing { - publications { - register('mavenJava', MavenPublication) { - from components.java - - artifact sourcesJar - artifact javadocJar - - versionMapping { - allVariants { - fromResolutionResult() - } - } - - pom { - name = this_name - description = this_description - url = 'https://github.com/Anvil-Dev/AnvilLib' - - licenses { - license { - name = 'MIT License' - url = 'https://opensource.org/license/mit' - } - } - developers { - developer { - id = 'Anvil-Dev' - name = 'AnvilCraft Dev' - email = 'admin@anvilcraft.dev' - } - } - scm { - url = 'https://github.com/Anvil-Dev/AnvilLib' - connection = 'https://github.com/Anvil-Dev/AnvilLib.git' - developerConnection = 'https://github.com/Anvil-Dev/AnvilLib.git' - } - } - } - } - repositories { - // Add repositories to publish to here. - def MAVEN_URL = System.getenv("MAVEN_URL") - if (MAVEN_URL != null) { - maven { - url MAVEN_URL - credentials { - username System.getenv("MAVEN_USERNAME") - password System.getenv("MAVEN_PASSWORD") - } - } - } - mavenLocal() - maven { - name = "staging" - url = layout.buildDirectory.dir("staging-deploy") - } - } -} - -jreleaser { - signing { - active = 'ALWAYS' - armored = true - } - - deploy { - maven { - mavenCentral { - sonatype { - active = 'ALWAYS' - url = 'https://central.sonatype.com/api/v1/publisher' - stagingRepository('build/staging-deploy') - } - } - } - } -} - -// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. -idea { - module { - downloadSources = true - downloadJavadoc = true - } -} - -machete { - // disable machete locally for faster builds - enabled = false -} - -lombok { - version = "1.18.34" -} diff --git a/module.rendering/gradle.properties b/module.rendering/gradle.properties deleted file mode 100644 index 4a0508f2..00000000 --- a/module.rendering/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -## Mod Properties -mod_id=anvillib_rendering -mod_name=AnvilLib-Rendering -mod_description=Rendering utilities diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRPostEffects.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRPostEffects.java deleted file mode 100644 index 87a4e5ea..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRPostEffects.java +++ /dev/null @@ -1,45 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering; - -import dev.anvilcraft.lib.v2.rendering.bloom.BloomPostEffect; -import dev.anvilcraft.lib.v2.rendering.event.MainTargetResizeEvent; -import dev.anvilcraft.lib.v2.rendering.glitch.GlitchPostEffect; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.neoforged.bus.api.SubscribeEvent; -import net.neoforged.fml.common.EventBusSubscriber; -import org.jetbrains.annotations.ApiStatus; -import org.joml.Matrix4f; - -@Slf4j -@EventBusSubscriber -public class ALRPostEffects { - @Getter - private static BloomPostEffect bloomPostEffect; - @Getter - private static GlitchPostEffect glitchPostEffect; - - public static void createPostEffects() { - bloomPostEffect = new BloomPostEffect(); - glitchPostEffect = new GlitchPostEffect(); - } - - public static void runBloomDraws(Matrix4f mvMat) { - try { - bloomPostEffect.runBloomDraws(mvMat); - } catch (Exception e) { - log.error("runBloomDraws boom!", e); - } - } - - @ApiStatus.Internal - @SubscribeEvent - public static void on(MainTargetResizeEvent event) { - BloomPostEffect bloomPostEffect = ALRPostEffects.getBloomPostEffect(); - if (bloomPostEffect != null) { - bloomPostEffect.resize( - event.getNewWidth(), - event.getNewHeight() - ); - } - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRSharedMath.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRSharedMath.java deleted file mode 100644 index 96313b9a..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/ALRSharedMath.java +++ /dev/null @@ -1,8 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering; - -import com.mojang.blaze3d.vertex.PoseStack; - -public class ALRSharedMath { - public static final PoseStack.Pose IDENTITY_POSE_3D = new PoseStack().last(); - public static final float SQRT_2 = 1.4142135623730950488016887242097f; -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/AnvilLibRendering.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/AnvilLibRendering.java deleted file mode 100644 index 81ffe7c4..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/AnvilLibRendering.java +++ /dev/null @@ -1,95 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering; - -import dev.anvilcraft.lib.v2.rendering.cachedber.pipeline.CachedBlockEntityRenderingPipeline; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ALRComputeShaderManager; -import dev.anvilcraft.lib.v2.rendering.postprocess.ALRPostEffectShaders; -import dev.anvilcraft.lib.v2.rendering.sdf.SdfGraphics; -import dev.anvilcraft.lib.v2.rendering.sdf.SdfShaders; -import lombok.extern.slf4j.Slf4j; -import net.minecraft.resources.ResourceLocation; -import net.neoforged.api.distmarker.Dist; -import net.neoforged.bus.api.IEventBus; -import net.neoforged.bus.api.SubscribeEvent; -import net.neoforged.fml.common.EventBusSubscriber; -import net.neoforged.fml.common.Mod; -import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; -import net.neoforged.neoforge.client.event.RegisterShadersEvent; -import net.neoforged.neoforge.client.event.RenderFrameEvent; -import net.neoforged.neoforge.client.event.RenderLevelStageEvent; -import org.jetbrains.annotations.ApiStatus; - -@Slf4j -@Mod(value = AnvilLibRendering.MODID, dist = Dist.CLIENT) -@EventBusSubscriber(Dist.CLIENT) -@ApiStatus.Internal -public class AnvilLibRendering { - public static final boolean DEBUG = System.getProperty("anvillib.rendering.debugMode") != null; - public static final String MODID = "anvillib_rendering"; - - public AnvilLibRendering(IEventBus modBus) { - } - - public static ResourceLocation location(String path) { - return ResourceLocation.fromNamespaceAndPath(MODID, path); - } - - @SubscribeEvent - public static void on(RegisterShadersEvent event) { - // #P7b: post-processing shaders (bloom downsample/upsample/apply, blur, glitch) - ALRPostEffectShaders.register(event); - // #P7c: SDF GUI shader (sdf_graphics) - SdfShaders.register(event); - } - - @SubscribeEvent - public static void on(RegisterClientReloadListenersEvent event) { - // #P7e: compute shader manager (compiles and caches GL compute programs) - event.registerReloadListener(ALRComputeShaderManager.INSTANCE); - } - - @SubscribeEvent - public static void on(RenderFrameEvent.Pre event) { - if (ALRPostEffects.getBloomPostEffect() != null) { - ALRPostEffects.getBloomPostEffect().beginFrame(); - } - // #P7c: reset the per-frame SDF parameter slots (the 26.1 GuiRendererMixin flush equivalent). - SdfGraphics.flush(); - // #P7d: run the cached BER compile + upload queues on the render thread. - if (CachedBlockEntityRenderingPipeline.getInstance() != null) { - CachedBlockEntityRenderingPipeline.getInstance().runTasks(); - } - } - - @SubscribeEvent - public static void on(RenderLevelStageEvent event) { - if (event.getStage() == RenderLevelStageEvent.Stage.AFTER_PARTICLES) { - ALRPostEffects.runBloomDraws(event.getModelViewMatrix()); - } else if (event.getStage() == RenderLevelStageEvent.Stage.AFTER_LEVEL) { - try { - if (ALRPostEffects.getBloomPostEffect() != null) { - ALRPostEffects.getBloomPostEffect().process(); - } - } catch (Exception e) { - log.error("ALRPostEffects BloomPostEffect", e); - } - } - } - - /** - * #P7d: cached BER rendering. Opaque layers render after the opaque world (mapped from the 26.1 - * {@code AfterOpaqueBlocks}), translucent layers after the translucent world (26.1 - * {@code AfterTranslucentFeatures}); both exist as {@link RenderLevelStageEvent.Stage}s on 1.21.1. - */ - @SubscribeEvent - public static void onRenderCachedBlockEntities(RenderLevelStageEvent event) { - if (event.getStage() == RenderLevelStageEvent.Stage.AFTER_CUTOUT_BLOCKS) { - if (CachedBlockEntityRenderingPipeline.getInstance() != null) { - CachedBlockEntityRenderingPipeline.getInstance().render(event.getFrustum(), false); - } - } else if (event.getStage() == RenderLevelStageEvent.Stage.AFTER_TRANSLUCENT_BLOCKS) { - if (CachedBlockEntityRenderingPipeline.getInstance() != null) { - CachedBlockEntityRenderingPipeline.getInstance().render(event.getFrustum(), true); - } - } - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomParametersUbo.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomParametersUbo.java deleted file mode 100644 index 4ae7e6d5..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomParametersUbo.java +++ /dev/null @@ -1,36 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.bloom; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutEntry; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.ShaderBufferObjectUsage; -import lombok.Getter; -import lombok.Setter; - -@Setter -@Getter -public class BloomParametersUbo extends BufferObject { - - public static final BufferObjectLayoutDefinition DEFINITION = BufferObjectLayoutDefinition.create( - BufferObjectLayoutEntry.ofFloat().forGetter(BloomParametersUbo::getBloomIntensity).build(), - BufferObjectLayoutEntry.ofFloat().forGetter(BloomParametersUbo::getBloomBlendThreshold).build(), - BufferObjectLayoutEntry.ofFloat().forGetter(BloomParametersUbo::getLuminanceSensitivity).build() - ); - - private float bloomIntensity; - private float bloomBlendThreshold; - private float luminanceSensitivity; - - public BloomParametersUbo(float bloomIntensity, float bloomBlendThreshold, float luminanceSensitivity) { - super(BufferLayout.STD140, ShaderBufferObjectUsage.UBO); - this.bloomIntensity = bloomIntensity; - this.bloomBlendThreshold = bloomBlendThreshold; - this.luminanceSensitivity = luminanceSensitivity; - } - - @Override - protected BufferObjectLayoutDefinition getDefinition() { - return DEFINITION; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPipelineParametersUbo.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPipelineParametersUbo.java deleted file mode 100644 index c3cdae08..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPipelineParametersUbo.java +++ /dev/null @@ -1,39 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.bloom; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutEntry; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.ShaderBufferObjectUsage; -import lombok.Getter; -import lombok.Setter; -import org.joml.Vector2f; - -@Getter -@Setter -public class BloomPipelineParametersUbo extends BufferObject { - - public static final BufferObjectLayoutDefinition DEFINITION = BufferObjectLayoutDefinition.create( - BufferObjectLayoutEntry.ofVec2f().forGetter(BloomPipelineParametersUbo::getResolution).build(), - BufferObjectLayoutEntry.ofInt().forGetter(BloomPipelineParametersUbo::getFrameIndex).build() - ); - - private final Vector2f resolution = new Vector2f(); - private int frameIndex; - - protected BloomPipelineParametersUbo() { - super(BufferLayout.STD140, ShaderBufferObjectUsage.UBO); - } - - public void setResolution(int width, int height) { - this.resolution.set( - 1.0f / Math.max(width, 1.0f), - 1.0f / Math.max(height, 1.0f) - ); - } - - @Override - protected BufferObjectLayoutDefinition getDefinition() { - return DEFINITION; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPostEffect.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPostEffect.java deleted file mode 100644 index f1d5ec85..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomPostEffect.java +++ /dev/null @@ -1,335 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.bloom; - -import com.mojang.blaze3d.pipeline.MainTarget; -import com.mojang.blaze3d.pipeline.RenderTarget; -import com.mojang.blaze3d.pipeline.TextureTarget; -import com.mojang.blaze3d.platform.GlStateManager; -import com.mojang.blaze3d.vertex.MeshData; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.compound.DirtyTracked; -import dev.anvilcraft.lib.v2.rendering.postprocess.ALRPostEffectShaders; -import dev.anvilcraft.lib.v2.rendering.postprocess.GlPostProcess; -import dev.anvilcraft.lib.v2.rendering.postprocess.GlUniformBuffer; -import lombok.Getter; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.ShaderInstance; -import org.joml.Matrix4f; - -import java.util.ArrayList; -import java.util.List; - -/** - * Bloom post-processing effect: captures a re-rendered copy of the scene, runs it through a - * 5-level downsample/upsample Gaussian pyramid and composites the result back onto the main target. - *

    - * Ported from 26.1 (see #P7b): the {@code GpuDevice}/{@code CommandEncoder}/{@code RenderPass} - * execution layer becomes {@link RenderTarget} + {@link ShaderInstance} (registered via - * {@code RegisterShadersEvent}, see {@link ALRPostEffectShaders}) + fullscreen quad - * ({@code BufferUploader}/{@code Tesselator}); UBO uploads use {@link GlUniformBuffer}. - * The 26.1 {@code SubmitNodeStorage} redraw path is replaced by a {@link MultiBufferSource.BufferSource} - * redraw; {@code BloomSubmitNodeStorage} itself is not portable and is intentionally dropped. - */ -@SuppressWarnings({"FieldMayBeFinal", "SameParameterValue"}) -public class BloomPostEffect implements DirtyTracked { - public static final int UNIFORM_TRANSFORM_SIZE = TransformsUbo.DEFINITION.size(BufferLayout.STD140); - public static final int UNIFORM_BLOOM_SIZE = BloomParametersUbo.DEFINITION.size(BufferLayout.STD140); - public static final int UNIFORM_ENHANCED_BLOOM_SIZE = BloomPipelineParametersUbo.DEFINITION.size(BufferLayout.STD140); - - // todo: uses config or options - private static final int PASSES_AMOUNT = 5; - private static final int PASS_STEP = 1; - - @Getter - private final RenderTarget bloomInputTarget = new MainTarget(854, 480); - private final RenderTarget bloomTempTarget = new TextureTarget(854, 480, false, false); - - private final RenderTarget[] downsampleTargets = arrayInit("DownSample", PASSES_AMOUNT); - private final RenderTarget[] upsampleTargets = arrayInit("UpSample", PASSES_AMOUNT - 1); - - private final GlUniformBuffer transformUBO = new GlUniformBuffer(UNIFORM_TRANSFORM_SIZE); - private final GlUniformBuffer bloomUBO = new GlUniformBuffer(UNIFORM_BLOOM_SIZE); - private final GlUniformBuffer enhancedBloomParametersUBO = new GlUniformBuffer(UNIFORM_ENHANCED_BLOOM_SIZE); - - @Getter - private final BloomParametersUbo bloomParameters; - @Getter - private final BloomPipelineParametersUbo enhancedBloomParameters; - - private final TransformsUbo transformsUbo = new TransformsUbo(new Matrix4f()); - private final List bloomCalls = new ArrayList<>(); - - private int width; - private int height; - private MeshData quadMesh; - private boolean dirty = false; - - private int passes; - private int step; - - public BloomPostEffect() { - this(1.25f, 1.943f, 1.105f, 0.08f, 0.8f, PASSES_AMOUNT, PASS_STEP); - } - - public BloomPostEffect( - float bloomIntensity, - float sampleStepLength, - float colorMultiplier, - float bloomThreshold, - float bloomIntensityMultiplier, - int passes, - int step - ) { - RenderTarget mainRenderTarget = Minecraft.getInstance().getMainRenderTarget(); - - this.width = mainRenderTarget.width; - this.height = mainRenderTarget.height; - this.bloomParameters = new BloomParametersUbo(bloomIntensity, bloomThreshold, bloomIntensityMultiplier); - this.enhancedBloomParameters = new BloomPipelineParametersUbo(); - this.passes = passes; - this.step = step; - - resize(width, height); - } - - public void beginFrame() { - clearColorAndDepth(bloomInputTarget); - clearColorAndDepth(bloomTempTarget); - dirty = false; - bloomCalls.clear(); - } - - public void markDirty() { - dirty = true; - } - - public void drawBloomed(BloomRenderCallback runnable) { - bloomCalls.add(runnable); - markDirty(); - } - - public void beginBloomDraw() { - RenderTarget mainRenderTarget = Minecraft.getInstance().getMainRenderTarget(); - - this.guardSize(mainRenderTarget); - - bloomInputTarget.copyDepthFrom(mainRenderTarget); - - bloomInputTarget.bindWrite(true); - this.markDirty(); - } - - private void guardSize(RenderTarget mainRenderTarget) { - if (mainRenderTarget.width != this.bloomInputTarget.width || mainRenderTarget.height != this.bloomInputTarget.height) { - this.resize(mainRenderTarget.width, mainRenderTarget.height); - } - } - - public void endBloomDraw() { - // restore the level's framebuffer - Minecraft.getInstance().getMainRenderTarget().bindWrite(true); - } - - public void runBloomDraws(Matrix4f modelViewMatrix) { - if (!dirty) return; - - PoseStack poseStack = new PoseStack(); - try { - beginBloomDraw(); - if (!bloomCalls.isEmpty()) { - try (com.mojang.blaze3d.vertex.ByteBufferBuilder bufferBuilder = new com.mojang.blaze3d.vertex.ByteBufferBuilder(256)) { - MultiBufferSource.BufferSource bufferSource = MultiBufferSource.immediate(bufferBuilder); - for (BloomRenderCallback bloomCall : bloomCalls) { - bloomCall.render(bufferSource, poseStack); - } - bufferSource.endBatch(); - } - } - } finally { - endBloomDraw(); - } - } - - @SuppressWarnings("DataFlowIssue") - public void process() { - if (!dirty) return; - RenderTarget mainRenderTarget = Minecraft.getInstance().getMainRenderTarget(); - - this.guardSize(mainRenderTarget); - - transformUBO.upload(transformsUbo); - - this.doDownSample(); - this.doUpSample(); - - // backup depth texture - bloomInputTarget.copyDepthFrom(mainRenderTarget); - - clearColorAndDepth(bloomTempTarget); - applyBloom(this.upsampleTargets[0], mainRenderTarget, bloomTempTarget); - copyColor(bloomTempTarget, mainRenderTarget); - mainRenderTarget.copyDepthFrom(bloomInputTarget); - } - - private void clearColorAndDepth(RenderTarget rt) { - rt.clear(false); - } - - /** - * Copies the color attachment of {@code src} onto the color texture of {@code dst} through the - * GL read framebuffer. Replaces the 26.1 {@code CommandEncoder.copyTextureToTexture}. - */ - private void copyColor(RenderTarget src, RenderTarget dst) { - GlStateManager._activeTexture(33984); - GlStateManager._glBindFramebuffer(36008, src.frameBufferId); - GlStateManager._bindTexture(dst.getColorTextureId()); - GlStateManager._glCopyTexSubImage2D(3553, 0, 0, 0, 0, 0, this.width, this.height); - GlStateManager._bindTexture(0); - GlStateManager._glBindFramebuffer(36008, 0); - } - - private void applyBloom( - RenderTarget inputTarget, - RenderTarget gameTarget, - RenderTarget outputTarget - ) { - ShaderInstance shader = ALRPostEffectShaders.getApplyBloomShader(); - if (shader == null) return; - transformUBO.upload(transformsUbo); - bloomUBO.upload(bloomParameters); - GlPostProcess.beginTarget(outputTarget); - shader.setSampler("GameSampler", gameTarget); - shader.setSampler("DiffuseSampler", inputTarget); - GlPostProcess.draw(shader, quadMesh, s -> { - transformUBO.bindTo(s.getId(), "Transforms", 0); - bloomUBO.bindTo(s.getId(), "BloomParameters", 1); - }); - } - - private void doDownSample() { - - this.downSample( - this.bloomInputTarget, - this.downsampleTargets[0], - 0 - ); - - for (int i = 1; i < this.passes; i++) { - this.downSample( - this.downsampleTargets[i - 1], - this.downsampleTargets[i], - i - ); - } - - } - - private void downSample( - RenderTarget src, - RenderTarget dst, - int frameIndex - ) { - ShaderInstance shader = ALRPostEffectShaders.getDownSampleShader(); - if (shader == null) return; - this.enhancedBloomParameters.setFrameIndex(frameIndex); - this.enhancedBloomParameters.setResolution(src.width, src.height); - this.enhancedBloomParametersUBO.upload(enhancedBloomParameters); - - GlPostProcess.beginTarget(dst); - shader.setSampler("DiffuseSampler", src); - GlPostProcess.draw(shader, quadMesh, s -> { - transformUBO.bindTo(s.getId(), "Transforms", 0); - enhancedBloomParametersUBO.bindTo(s.getId(), "BloomParameters", 1); - }); - } - - private void doUpSample() { - var steps = this.passes; - - this.upSample( - this.downsampleTargets[steps - 2], - this.downsampleTargets[steps - 1], - this.upsampleTargets[steps - 2], - steps - 1 - ); - - for (int i = steps - 2; i > 0; i--) { - this.upSample( - this.downsampleTargets[i - 1], - this.upsampleTargets[i], - this.upsampleTargets[i - 1], - i - ); - } - } - - private void upSample( - RenderTarget curr, - RenderTarget prev, - RenderTarget dst, - int frameIndex - ) { - ShaderInstance shader = ALRPostEffectShaders.getUpSampleShader(); - if (shader == null) return; - this.enhancedBloomParameters.setFrameIndex(frameIndex); - this.enhancedBloomParameters.setResolution(curr.width, curr.height); - this.enhancedBloomParametersUBO.upload(enhancedBloomParameters); - - GlPostProcess.beginTarget(dst); - shader.setSampler("DiffuseSampler", curr); - shader.setSampler("PreviousSampler", prev); - GlPostProcess.draw(shader, quadMesh, s -> { - transformUBO.bindTo(s.getId(), "Transforms", 0); - enhancedBloomParametersUBO.bindTo(s.getId(), "BloomParameters", 1); - }); - } - - public void resize(int width, int height) { - this.width = width; - this.height = height; - bloomInputTarget.resize(width, height, false); - bloomTempTarget.resize(width, height, false); - transformsUbo.getProjMat().setOrtho( - 0, width, 0, height, -1, -10000f - ); - - this.quadMesh = GlPostProcess.buildQuad(width, height, false); - - this.passes = PASSES_AMOUNT; - - int pWidth = width; - int pHeight = height; - int step = this.step; - - for (int i = 0; i < PASSES_AMOUNT; i++) { - pWidth >>= step; - pHeight >>= step; - - if (pWidth == 0 || pHeight == 0) { - this.passes = i; - break; - } - - this.downsampleTargets[i].resize(pWidth, pHeight, false); - - if (i < PASSES_AMOUNT - 1) { - this.upsampleTargets[i].resize(pWidth, pHeight, false); - } - } - } - - private static RenderTarget[] arrayInit(String name, int size) { - RenderTarget[] targets = new RenderTarget[size]; - - for (int i = 0; i < size; i++) { - var target = new TextureTarget( - 854 >> i, 480 >> i, - false, false - ); - targets[i] = target; - } - - return targets; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomRenderCallback.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomRenderCallback.java deleted file mode 100644 index 5db41793..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/BloomRenderCallback.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.bloom; - -import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.renderer.MultiBufferSource; - -/** - * Callback that re-renders part of the scene into the bloom input target. - *

    - * Ported from 26.1: the original received a {@code SubmitNodeCollector} and relied on - * {@code FeatureRenderDispatcher} to flush the re-rendered features; neither exists on 1.21.1, - * so the callback now receives a {@link MultiBufferSource.BufferSource} and is expected to submit - * geometry (typically with {@code ALRRenderTypeExtension.copyWithBloom} marked render types) which - * {@code BloomPostEffect} flushes while the bloom input target is bound. - */ -public interface BloomRenderCallback { - void render(MultiBufferSource.BufferSource bufferSource, PoseStack poseStack); -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/TransformsUbo.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/TransformsUbo.java deleted file mode 100644 index 12ce845b..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/bloom/TransformsUbo.java +++ /dev/null @@ -1,31 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.bloom; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutEntry; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.ShaderBufferObjectUsage; -import lombok.Getter; -import lombok.Setter; -import org.joml.Matrix4f; - -@Setter -@Getter -public class TransformsUbo extends BufferObject { - - public static final BufferObjectLayoutDefinition DEFINITION = BufferObjectLayoutDefinition.create( - BufferObjectLayoutEntry.ofMat4f().forGetter(TransformsUbo::getProjMat).build() - ); - - private Matrix4f projMat; - - public TransformsUbo(Matrix4f projMat) { - super(BufferLayout.STD140, ShaderBufferObjectUsage.UBO); - this.projMat = projMat; - } - - @Override - protected BufferObjectLayoutDefinition getDefinition() { - return DEFINITION; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/BlurParametersUbo.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/BlurParametersUbo.java deleted file mode 100644 index 8830ffa2..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/BlurParametersUbo.java +++ /dev/null @@ -1,38 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.blur; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutEntry; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.ShaderBufferObjectUsage; -import lombok.Getter; -import lombok.Setter; -import org.joml.Vector2f; - -@Setter -@Getter -public class BlurParametersUbo extends BufferObject { - - public static final BufferObjectLayoutDefinition DEFINITION = BufferObjectLayoutDefinition.create( - BufferObjectLayoutEntry.ofVec2f().forGetter(BlurParametersUbo::getDirection).build(), - BufferObjectLayoutEntry.ofFloat().forGetter(BlurParametersUbo::getSampleStepLength).build(), - BufferObjectLayoutEntry.ofFloat().forGetter(BlurParametersUbo::getColorMultiplier).build() - ); - - private float sampleStepLength; - - private float colorMultiplier; - private Vector2f direction; - - public BlurParametersUbo(float sampleStepLength, float colorMultiplier, Vector2f direction) { - super(BufferLayout.STD140, ShaderBufferObjectUsage.UBO); - this.sampleStepLength = sampleStepLength; - this.colorMultiplier = colorMultiplier; - this.direction = direction; - } - - @Override - protected BufferObjectLayoutDefinition getDefinition() { - return DEFINITION; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/GaussianBlur.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/GaussianBlur.java deleted file mode 100644 index 82d10f6b..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/blur/GaussianBlur.java +++ /dev/null @@ -1,106 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.blur; - -import com.mojang.blaze3d.pipeline.MainTarget; -import com.mojang.blaze3d.pipeline.RenderTarget; -import com.mojang.blaze3d.pipeline.TextureTarget; -import com.mojang.blaze3d.platform.Window; -import com.mojang.blaze3d.vertex.MeshData; -import dev.anvilcraft.lib.v2.rendering.bloom.TransformsUbo; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.postprocess.ALRPostEffectShaders; -import dev.anvilcraft.lib.v2.rendering.postprocess.GlPostProcess; -import dev.anvilcraft.lib.v2.rendering.postprocess.GlUniformBuffer; -import lombok.Getter; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.ShaderInstance; -import org.joml.Matrix4f; -import org.joml.Vector2f; - -/** - * Two-pass (horizontal + vertical) Gaussian blur over a {@link RenderTarget}. - *

    - * Ported from 26.1 (see #P7b): the {@code GpuDevice}/{@code CommandEncoder}/{@code RenderPass} - * execution layer becomes {@link RenderTarget} + {@link ShaderInstance} + fullscreen quad; - * UBO uploads use {@link GlUniformBuffer}. The shader and its std140 layout are unchanged. - */ -public class GaussianBlur implements AutoCloseable { - private final RenderTarget inputTarget = new MainTarget(854, 480); - private final RenderTarget tempTarget = new TextureTarget(854, 480, false, false); - - private final GlUniformBuffer blurUBO = new GlUniformBuffer(BlurParametersUbo.DEFINITION.size(BufferLayout.STD140)); - private final GlUniformBuffer transformUBO = new GlUniformBuffer(TransformsUbo.DEFINITION.size(BufferLayout.STD140)); - - @Getter - private final BlurParametersUbo blurParameters; - - private final TransformsUbo transformsUbo = new TransformsUbo(new Matrix4f()); - - private int width; - private int height; - private MeshData quadMesh; - - public GaussianBlur(float sampleStepLength, float colorMultiplier) { - Window window = Minecraft.getInstance().getWindow(); - - this.width = window.getWidth(); - this.height = window.getHeight(); - this.blurParameters = new BlurParametersUbo(sampleStepLength, colorMultiplier, new Vector2f()); - - resize(width, height); - } - - /** - * Runs two horizontal and two vertical blur passes and returns the blurred target. - */ - public RenderTarget process(RenderTarget input) { - transformUBO.upload(transformsUbo); - - blurOnce(input, tempTarget, true); - blurOnce(tempTarget, inputTarget, true); - - blurOnce(inputTarget, tempTarget, false); - blurOnce(tempTarget, inputTarget, false); - - return inputTarget; - } - - private void blurOnce(RenderTarget inputTarget, RenderTarget outputTarget, boolean horizontal) { - ShaderInstance shader = ALRPostEffectShaders.getBlurShader(); - if (shader == null) return; - Vector2f direction; - if (horizontal) { - direction = new Vector2f(1f, 0f); - } else { - direction = new Vector2f(0f, 1f); - } - blurParameters.setDirection(direction); - blurUBO.upload(blurParameters); - - GlPostProcess.beginTarget(outputTarget); - shader.setSampler("DiffuseSampler", inputTarget); - GlPostProcess.draw(shader, quadMesh, s -> { - transformUBO.bindTo(s.getId(), "Transforms", 0); - blurUBO.bindTo(s.getId(), "BlurParameters", 1); - }); - } - - public void resize(int width, int height) { - this.width = width; - this.height = height; - inputTarget.resize(width, height, false); - tempTarget.resize(width, height, false); - transformsUbo.getProjMat().setOrtho( - 0, width, 0, height, -1, -10000f - ); - - this.quadMesh = GlPostProcess.buildQuad(width, height, false); - } - - @Override - public void close() { - blurUBO.close(); - transformUBO.close(); - inputTarget.destroyBuffers(); - tempTarget.destroyBuffers(); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/package-info.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/package-info.java deleted file mode 100644 index 4b8a7e5c..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -@ParametersAreNonnullByDefault -@MethodsReturnNonnullByDefault -package dev.anvilcraft.lib.v2.rendering.cachedber; - -import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; - -import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedBlockEntityRenderingPipeline.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedBlockEntityRenderingPipeline.java deleted file mode 100644 index d1bc8f73..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedBlockEntityRenderingPipeline.java +++ /dev/null @@ -1,203 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.cachedber.pipeline; - -import dev.anvilcraft.lib.v2.rendering.AnvilLibRendering; -import dev.anvilcraft.lib.v2.rendering.cachedber.renderer.CachedBlockEntityRenderDispatcher; -import lombok.Getter; -import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.ClientLevel; -import net.minecraft.client.renderer.culling.Frustum; -import net.minecraft.core.BlockPos; -import net.minecraft.util.StringUtil; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.phys.Vec3; -import net.neoforged.api.distmarker.Dist; -import net.neoforged.bus.api.SubscribeEvent; -import net.neoforged.fml.common.EventBusSubscriber; -import net.neoforged.neoforge.client.event.RenderFrameEvent; -import net.neoforged.neoforge.client.event.RenderLevelStageEvent; -import org.jetbrains.annotations.ApiStatus; -import javax.annotation.Nullable; -import org.lwjgl.opengl.GL; -import org.lwjgl.opengl.GL46; - -import java.util.ArrayDeque; -import java.util.HashMap; -import java.util.Map; -import java.util.Queue; - -/** - * Caches the render output of registered block entity renderers per 16x16 chunk as GPU vertex buffers, - * with frustum culling, translucent distance sorting and optional bloom double-drawing. - *

    - * Ported from 26.1 (see #P7d): the chunk table, compile/upload queues and invalidation logic - * ({@code blockRemoved}/{@code update}/{@code forcedUpdate}) are kept as-is; the execution layer moves - * from 26.1 {@code GpuBuffer}/{@code RenderPass} to 1.21.1 {@code VertexBuffer} + the classic - * {@code RenderSystem} path (see {@link CachedRenderingChunk#renderLayer}). {@code ChunkPos.containing} - * becomes {@code new ChunkPos}, and the 26.1 {@code RenderLevelStageEvent.AfterSky} camera tracking uses - * {@code event.getCamera().getPosition()} instead of {@code LevelRenderState}. - * - * @author ZhuRuoLing - */ -@SuppressWarnings("unused") -@EventBusSubscriber(modid = AnvilLibRendering.MODID, value = Dist.CLIENT) -public class CachedBlockEntityRenderingPipeline { - @Nullable - private static CachedBlockEntityRenderingPipeline instance; - @Getter - private final ClientLevel level; - private final Queue pendingCompiles = new ArrayDeque<>(); - private final Queue pendingUploads = new ArrayDeque<>(); - private final Map chunks = new HashMap<>(); - @Getter - private boolean valid = true; - private static Vec3 cameraOldPosition = null; - @Getter - private static boolean cameraMoved = true; - private static int glMaxLabelLength = 0; - - public static void create() { - if (GL.getCapabilities().GL_KHR_debug) { - glMaxLabelLength = GL46.glGetInteger(GL46.GL_MAX_LABEL_LENGTH); - glMaxLabelLength /= 2; - } - } - - public CachedRenderingChunk getRenderRegion(ChunkPos chunkPos) { - if (chunks.containsKey(chunkPos)) { - return chunks.get(chunkPos); - } - CachedRenderingChunk region = new CachedRenderingChunk(chunkPos, this); - chunks.put(chunkPos, region); - return region; - } - - public CachedBlockEntityRenderingPipeline(ClientLevel level) { - this.level = level; - } - - public void runTasks() { - while (!pendingCompiles.isEmpty() && valid) { - pendingCompiles.poll().run(); - } - while (!pendingUploads.isEmpty() && valid) { - pendingUploads.poll().run(); - } - } - - /** - * Updates the rendering pipeline instance with a new level context. - * - * @param level The new ClientLevel instance that the rendering pipeline should be updated to use. - */ - public static void updateLevel(@Nullable ClientLevel level) { - if (instance != null) { - instance.releaseBuffers(); - } - if (level == null) { - instance = null; - return; - } - instance = new CachedBlockEntityRenderingPipeline(level); - } - - /** - * Notifies the pipeline that a {@link BlockEntity} has been removed. - * This method will be automatically called when a {@link BlockEntity} has been removed. - * - * @param be The removed {@link BlockEntity} - */ - public void blockRemoved(BlockEntity be) { - if (!CachedBlockEntityRenderDispatcher.INSTANCE.hasRenderer(be)) return; - ChunkPos chunkPos = new ChunkPos(be.getBlockPos()); - getRenderRegion(chunkPos).blockRemoved(be); - } - - public void update(BlockEntity be) { - update(be, false); - } - - /** - * Notifies the pipeline that a {@link BlockEntity} has been updated and the cache should be rebuilt. - * - * @param be The updated {@link BlockEntity} - */ - public void update(BlockEntity be, boolean forced) { - if (!CachedBlockEntityRenderDispatcher.INSTANCE.hasRenderer(be)) return; - ChunkPos chunkPos = new ChunkPos(be.getBlockPos()); - getRenderRegion(chunkPos).update(be, forced); - } - - public void submitUploadTask(Runnable task) { - pendingUploads.add(task); - } - - public void submitCompileTask(Runnable task) { - pendingCompiles.add(task); - } - - /** - * Releases all buffers in use and mark current pipeline instance as invalid. - */ - public void releaseBuffers() { - chunks.values().forEach(CachedRenderingChunk::releaseBuffers); - valid = false; - } - - public void render(Frustum frustum, boolean translucent) { - for (CachedRenderingChunk value : chunks.values()) { - value.render(frustum, translucent); - } - } - - public String truncateName(String s) { - return StringUtil.truncateStringIfNecessary(s, glMaxLabelLength, true); - } - - /** - * Retrieves the current instance of the CacheableBERenderingPipeline. - * - * @return The current instance of the CacheableBERenderingPipeline, - * or null if there has no {@link ClientLevel} in current {@link Minecraft} client. - */ - @Nullable - public static CachedBlockEntityRenderingPipeline getInstance() { - return instance; - } - - public void forcedUpdate() { - for (CachedRenderingChunk value : chunks.values()) { - value.forcedUpdate(); - } - } - - public void forcedUpdate(BlockPos pos) { - getRenderRegion(new ChunkPos(pos)).forcedUpdate(); - } - - @ApiStatus.Internal - @SubscribeEvent - public static void on(RenderLevelStageEvent event) { - if (event.getStage() == RenderLevelStageEvent.Stage.AFTER_SKY) { - Vec3 pos = event.getCamera().getPosition(); - if (pos.equals(cameraOldPosition)) { - cameraMoved = false; - return; - } - cameraOldPosition = new Vec3(pos.x, pos.y, pos.z); - cameraMoved = true; - } - } - - @ApiStatus.Internal - @SubscribeEvent - public static void on(RenderFrameEvent.Pre event) { - if (instance != null) { - instance.handleIntegration(); - } - } - - private void handleIntegration() { - // intentionally empty; the Iris mixin injects the shader-toggle invalidation here. - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedRenderingChunk.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedRenderingChunk.java deleted file mode 100644 index 416a034b..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/CachedRenderingChunk.java +++ /dev/null @@ -1,275 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.cachedber.pipeline; - -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.ByteBufferBuilder; -import com.mojang.blaze3d.vertex.MeshData; -import com.mojang.blaze3d.vertex.VertexBuffer; -import dev.anvilcraft.lib.v2.rendering.ALRPostEffects; -import dev.anvilcraft.lib.v2.rendering.bloom.BloomPostEffect; -import dev.anvilcraft.lib.v2.rendering.extension.ALRRenderTypeExtension; -import dev.anvilcraft.lib.v2.rendering.foundation.ALRMeshSorting; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.VertexBufferHost; -import it.unimi.dsi.fastutil.objects.Reference2IntMap; -import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.Setter; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.RenderType; -import net.minecraft.client.renderer.ShaderInstance; -import net.minecraft.client.renderer.culling.Frustum; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.Vec3; -import org.joml.Matrix4fStack; -import org.joml.Vector3f; -import javax.annotation.Nullable; - -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * A 16x16 block region of cached BER content: one {@link VertexBuffer} per {@link RenderType}, rebuilt - * through {@link RebuildTask}s and drawn with frustum culling plus distance-based translucent sorting. - *

    - * Ported from 26.1: {@code GpuBuffer}/{@code RenderPass} become {@link VertexBuffer} + the classic - * {@code RenderSystem} draw path ({@code renderType.setupRenderState()} + {@code ShaderInstance} + - * {@code VertexBuffer.bind()/draw()} — the same pattern {@code LevelRenderer.renderSectionLayer} uses). - * The 26.1 per-type index {@code GpuBuffer}s are dropped: a 1.21.1 {@link VertexBuffer} owns its index - * storage internally ({@code uploadIndexBuffer}), which also holds the sorted indices between frames. - * - * @author ZhuRuoLing - */ -public class CachedRenderingChunk implements VertexBufferHost { - @Getter - private final ChunkPos chunkPos; - private final Map buffers = new HashMap<>(); - private final Map sortBuffers = new HashMap<>(); - @Getter - private final Set blockEntities = new HashSet<>(); - private final CachedBlockEntityRenderingPipeline pipeline; - private final Minecraft minecraft = Minecraft.getInstance(); - private final AABB renderingBB; - private Map meshSorting = new HashMap<>(); - private Reference2IntMap indexCountMap = new Reference2IntOpenHashMap<>(); - @Nullable - @Setter(AccessLevel.PACKAGE) - private RebuildTask lastRebuildTask; - @Getter - @Setter(AccessLevel.PACKAGE) - private boolean isEmpty = true; - private boolean isFreshMesh = true; - - public CachedRenderingChunk(ChunkPos chunkPos, CachedBlockEntityRenderingPipeline pipeline) { - this.chunkPos = chunkPos; - this.pipeline = pipeline; - this.renderingBB = new AABB( - this.chunkPos.getMinBlockX(), - -65, - this.chunkPos.getMinBlockZ(), - this.chunkPos.getMaxBlockX(), - 321, - this.chunkPos.getMaxBlockZ() - ); - } - - /** - * Updates the block entities collection and triggers a rebuild of the region. - * - * @param be The block entity to update. - * @see CachedBlockEntityRenderingPipeline#update(BlockEntity) - */ - public void update(BlockEntity be, boolean forced) { - if (lastRebuildTask != null) { - lastRebuildTask.cancel(); - } - boolean shouldRecompile = blockEntities.removeIf(BlockEntity::isRemoved); - if (be.isRemoved()) { - shouldRecompile |= blockEntities.remove(be); - if (shouldRecompile) { - pipeline.submitCompileTask(new RebuildTask(this)); - } - return; - } - shouldRecompile |= blockEntities.add(be); - if (shouldRecompile || forced) { - pipeline.submitCompileTask(new RebuildTask(this)); - } - } - - /** - * Handles the removal of a block entity from the system and initiates a cache rebuild. - * - * @param be The block entity that has been removed. - * @see CachedBlockEntityRenderingPipeline#blockRemoved(BlockEntity) - */ - public void blockRemoved(BlockEntity be) { - if (lastRebuildTask != null) { - lastRebuildTask.cancel(); - } - boolean removedAny = blockEntities.removeIf(BlockEntity::isRemoved) || blockEntities.remove(be); - if (removedAny) { - pipeline.submitCompileTask(new RebuildTask(this)); - } - } - - public void render(Frustum frustum, boolean translucent) { - if (!frustum.isVisible(renderingBB)) return; - renderInternal(buffers.keySet(), translucent); - } - - @Override - public VertexBuffer getVertexBuffer(RenderType renderType, long size) { - // 1.21.1's VertexBuffer has no size accessor and grows its GL storage on every upload, - // so the 26.1 "reallocate when too small" check is dropped; reusing the instance is enough. - return buffers.computeIfAbsent(renderType, key -> new VertexBuffer(VertexBuffer.Usage.STATIC)); - } - - @Override - public ByteBufferBuilder getSortingByteBufferBuilder(RenderType renderType) { - if (sortBuffers.containsKey(renderType)) { - return sortBuffers.get(renderType); - } - ByteBufferBuilder builder = new ByteBufferBuilder(4096); - sortBuffers.put(renderType, builder); - return builder; - } - - @Override - public void acceptUploadAction(Runnable runnable) { - this.pipeline.submitUploadTask(runnable); - } - - private void renderInternal(Collection renderTypes, boolean translucent) { - if (isEmpty) return; - - Vec3 cameraPosition = minecraft.gameRenderer.getMainCamera().getPosition(); - int renderDistance = Minecraft.getInstance().options.getEffectiveRenderDistance() * 16; - - if (cameraPosition.distanceTo(new Vec3( - chunkPos.x * 16, - cameraPosition.y, - chunkPos.z * 16 - )) > renderDistance) { - return; - } - - renderLayers(renderTypes, cameraPosition, translucent); - } - - private void renderLayers(Collection renderingOrders, Vec3 cameraPosition, boolean translucent) { - for (RenderType renderType : renderingOrders) { - if (renderType.sortOnUpload() != translucent) continue; - VertexBuffer vb = buffers.get(renderType); - if (vb == null) continue; - renderLayer(renderType, vb, cameraPosition); - if (ALRRenderTypeExtension.isRenderingBloomed(renderType)) { - BloomPostEffect bloomPostEffect = ALRPostEffects.getBloomPostEffect(); - if (bloomPostEffect != null) { - bloomPostEffect.beginBloomDraw(); - renderLayer(renderType, vb, cameraPosition); - bloomPostEffect.endBloomDraw(); - } - } - } - } - - public void releaseBuffers() { - buffers.values().forEach(VertexBuffer::close); - buffers.clear(); - sortBuffers.values().forEach(ByteBufferBuilder::close); - sortBuffers.clear(); - } - - private void renderLayer( - RenderType renderType, - VertexBuffer vertexBuffer, - Vec3 cameraPosition - ) { - MeshData.SortState sortState = this.meshSorting.get(renderType); - int indexCount = indexCountMap.getInt(renderType); - - if (indexCount <= 0) return; - - if (sortState != null) { - // Re-sort and re-upload the index buffer whenever the camera moved (or right after a - // compile, since the compile-time sort used an arbitrary sorting origin). - if (isFreshMesh || CachedBlockEntityRenderingPipeline.isCameraMoved()) { - if (isFreshMesh) { - isFreshMesh = false; - } - Vector3f relativePos = cameraPosition.toVector3f().sub( - chunkPos.getMinBlockX(), - 0, - chunkPos.getMinBlockZ() - ); - - ByteBufferBuilder builder = this.getSortingByteBufferBuilder(renderType); - ByteBufferBuilder.Result result = sortState.buildSortedIndexBuffer( - builder, - ALRMeshSorting.byDistance(relativePos) - ); - - if (result != null) { - vertexBuffer.bind(); - // uploadIndexBuffer() takes ownership of (and closes) the result. - vertexBuffer.uploadIndexBuffer(result); - builder.clear(); - } - } - } - - renderType.setupRenderState(); - ShaderInstance shader = RenderSystem.getShader(); - if (shader != null) { - Matrix4fStack modelViewStack = RenderSystem.getModelViewStack(); - modelViewStack.pushMatrix(); - modelViewStack.translate( - -(float) cameraPosition.x + chunkPos.getMinBlockX(), - -(float) cameraPosition.y, - -(float) cameraPosition.z + chunkPos.getMinBlockZ() - ); - RenderSystem.applyModelViewMatrix(); - - shader.setDefaultUniforms( - renderType.mode(), - RenderSystem.getModelViewMatrix(), - RenderSystem.getProjectionMatrix(), - this.minecraft.getWindow() - ); - shader.apply(); - vertexBuffer.bind(); - vertexBuffer.draw(); - shader.clear(); - VertexBuffer.unbind(); - - modelViewStack.popMatrix(); - RenderSystem.applyModelViewMatrix(); - } - renderType.clearRenderState(); - } - - public void forcedUpdate() { - pipeline.submitCompileTask(new RebuildTask(this)); - } - - public void addIfPossible(E blockEntity) { - if (!blockEntities.contains(blockEntity)) { - blockEntities.add(blockEntity); - pipeline.submitCompileTask(new RebuildTask(this)); - } - } - - public void replaceMeshData( - Map meshSorts, - Reference2IntMap indexCountMap - ) { - this.meshSorting = meshSorts; - this.indexCountMap = indexCountMap; - this.isFreshMesh = true; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/RebuildTask.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/RebuildTask.java deleted file mode 100644 index 19e35e34..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/RebuildTask.java +++ /dev/null @@ -1,72 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.cachedber.pipeline; - -import com.mojang.blaze3d.vertex.PoseStack; -import dev.anvilcraft.lib.v2.rendering.cachedber.renderer.CachedBlockEntityRenderDispatcher; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.FullyBufferedBufferSource; -import net.minecraft.client.Minecraft; -import net.minecraft.core.BlockPos; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.block.entity.BlockEntity; -import org.jetbrains.annotations.ApiStatus; - -import java.util.ArrayList; - -/** - * Compiles all block entities of a {@link CachedRenderingChunk} into a {@link FullyBufferedBufferSource} - * and uploads the resulting meshes to the chunk's {@link com.mojang.blaze3d.vertex.VertexBuffer}s. - *

    - * Ported from 26.1: the 26.1 feature pipeline ({@code SubmitNodeStorage} + {@code FeatureRenderDispatcher}) - * is replaced by a direct dispatch through {@link CachedBlockEntityRenderDispatcher} into the - * fully-buffered source, matching the single-stage 1.21.1 BER model. - */ -@ApiStatus.Internal -public class RebuildTask implements Runnable { - private final CachedRenderingChunk owner; - private boolean cancelled = false; - - public RebuildTask(CachedRenderingChunk owner) { - this.owner = owner; - } - - @Override - public void run() { - owner.setLastRebuildTask(this); - PoseStack poseStack = new PoseStack(); - owner.setEmpty(true); - FullyBufferedBufferSource bufferSource = new FullyBufferedBufferSource(); - Minecraft minecraft = Minecraft.getInstance(); - for (BlockEntity be : new ArrayList<>(owner.getBlockEntities())) { - if (cancelled) { - bufferSource.close(); - return; - } - - poseStack.pushPose(); - BlockPos pos = be.getBlockPos(); - ChunkPos chunkPos = owner.getChunkPos(); - poseStack.translate( - pos.getX() - chunkPos.getMinBlockX(), - pos.getY(), - pos.getZ() - chunkPos.getMinBlockZ() - ); - - CachedBlockEntityRenderDispatcher.INSTANCE.render( - be, - 0, - poseStack, - bufferSource - ); - - poseStack.popPose(); - } - - owner.setEmpty(bufferSource.isEmpty()); - bufferSource.upload(owner); - owner.replaceMeshData(bufferSource.getMeshSorts(), bufferSource.getIndexCountMap()); - owner.setLastRebuildTask(null); - } - - void cancel() { - cancelled = true; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/package-info.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/package-info.java deleted file mode 100644 index adf26b37..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/pipeline/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -@ParametersAreNonnullByDefault -@MethodsReturnNonnullByDefault -package dev.anvilcraft.lib.v2.rendering.cachedber.pipeline; - -import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; - -import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderDispatcher.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderDispatcher.java deleted file mode 100644 index ef0d19d3..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderDispatcher.java +++ /dev/null @@ -1,77 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.cachedber.renderer; - -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.logging.LogUtils; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.LightTexture; -import net.minecraft.client.renderer.texture.OverlayTexture; -import net.minecraft.core.BlockPos; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.entity.BlockEntityType; -import javax.annotation.Nullable; -import org.slf4j.Logger; - -import java.util.HashMap; -import java.util.Map; -import java.util.function.Supplier; - -/** - * Dispatcher for the {@link CachedBlockEntityRenderer}s registered with the cached BER pipeline. - *

    - * Ported from 26.1: the two-phase {@code tryExtractRenderState}/{@code submit} entry points are replaced - * by a single {@link #render} that mirrors {@code BlockEntityRenderDispatcher} (computing packed light / - * overlay from the level, exactly like the vanilla {@code setupAndRender}). - */ -public class CachedBlockEntityRenderDispatcher { - public static final CachedBlockEntityRenderDispatcher INSTANCE = new CachedBlockEntityRenderDispatcher(); - - private final Map, CachedBlockEntityRenderer> renderers = new HashMap<>(); - private final Logger logger = LogUtils.getLogger(); - - public void registerRenderer( - BlockEntityType type, - CachedBlockEntityRenderer renderer - ) { - CachedBlockEntityRenderer old = renderers.put(type, renderer); - if (old != null) { - logger.warn("Replacing old CachedBlockEntityRenderer {} with {}", old, renderer); - } - } - - public void registerRenderer( - Supplier> type, - CachedBlockEntityRenderer renderer - ) { - registerRenderer(type.get(), renderer); - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - public void render( - E blockEntity, - float partialTick, - PoseStack poseStack, - MultiBufferSource bufferSource - ) { - CachedBlockEntityRenderer renderer = (CachedBlockEntityRenderer) renderers.get(blockEntity.getType()); - if (renderer == null) { - return; - } - Level level = blockEntity.getLevel(); - int packedLight; - if (level != null) { - BlockPos pos = blockEntity.getBlockPos(); - packedLight = LightTexture.pack( - level.getRawBrightness(pos, level.getSkyDarken()), - level.getRawBrightness(pos.above(), level.getSkyDarken()) - ); - } else { - packedLight = 15728880; - } - renderer.render(blockEntity, partialTick, poseStack, bufferSource, packedLight, OverlayTexture.NO_OVERLAY); - } - - public boolean hasRenderer(BlockEntity be) { - return renderers.containsKey(be.getType()); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderer.java deleted file mode 100644 index 6a0b3001..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/CachedBlockEntityRenderer.java +++ /dev/null @@ -1,24 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.cachedber.renderer; - -import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.world.level.block.entity.BlockEntity; - -/** - * Single-phase renderer interface for the cached BER pipeline. - *

    - * Ported from 26.1: the original two-phase {@code extractRenderState}/{@code submit} model (driven by - * the 26.1 {@code SubmitNodeCollector} feature system) does not exist on 1.21.1, where block entities - * render through the single {@code BlockEntityRenderDispatcher.render(...)} stage. Renderers therefore - * draw directly into the {@link MultiBufferSource} handed out by {@link dev.anvilcraft.lib.v2.rendering.foundation.buffers.FullyBufferedBufferSource}. - * - * @param the block entity type - */ -public interface CachedBlockEntityRenderer { - /** - * Draws the block entity into {@code bufferSource}. Called once per compile of the containing - * chunk with a {@code partialTick} of 0 and a pose already translated to the block entity's - * position relative to the chunk origin. - */ - void render(T blockEntity, float partialTick, PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, int packedOverlay); -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/package-info.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/package-info.java deleted file mode 100644 index a9665209..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/cachedber/renderer/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -@ParametersAreNonnullByDefault -@MethodsReturnNonnullByDefault -package dev.anvilcraft.lib.v2.rendering.cachedber.renderer; - -import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; - -import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/MainTargetResizeEvent.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/MainTargetResizeEvent.java deleted file mode 100644 index cb3c0d9c..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/MainTargetResizeEvent.java +++ /dev/null @@ -1,16 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.event; - -import lombok.Getter; -import net.neoforged.bus.api.Event; -import net.neoforged.fml.event.IModBusEvent; - -@Getter -public class MainTargetResizeEvent extends Event implements IModBusEvent { - private final int newWidth; - private final int newHeight; - - public MainTargetResizeEvent(int newWidth, int newHeight) { - this.newWidth = newWidth; - this.newHeight = newHeight; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/RegisterComputePipelinesEvent.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/RegisterComputePipelinesEvent.java deleted file mode 100644 index a0c02631..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/event/RegisterComputePipelinesEvent.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.event; - -import net.neoforged.bus.api.Event; -import net.neoforged.fml.event.IModBusEvent; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Mod-bus event for registering compute pipelines. - *

    - * Ported from 26.1: the original referenced {@code ALRComputePipeline} directly, which belongs to the - * #P7e (GPU compute pipeline extension) port batch. To keep this batch independent, the pipeline type is - * generic here; #P7e instantiates {@code RegisterComputePipelinesEvent}. - */ -public class RegisterComputePipelinesEvent extends Event implements IModBusEvent { - private final List pipelines = new ArrayList<>(); - - public void registerPipeline(T pipeline) { - this.pipelines.add(pipeline); - } - - public List getPipelines() { - return Collections.unmodifiableList(this.pipelines); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/ALRRenderTypeExtension.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/ALRRenderTypeExtension.java deleted file mode 100644 index 36427d31..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/ALRRenderTypeExtension.java +++ /dev/null @@ -1,37 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension; - -import net.minecraft.client.renderer.RenderType; - -public interface ALRRenderTypeExtension { - boolean anvillib_rendering$bloomRendering(); - - void anvillib_rendering$setBloomRendering(boolean value); - - static ALRRenderTypeExtension of(RenderType thiz) { - return ((ALRRenderTypeExtension) thiz); - } - - static void setRenderingBloomed(RenderType thiz, boolean value) { - of(thiz).anvillib_rendering$setBloomRendering(value); - } - - static boolean isRenderingBloomed(RenderType thiz) { - return of(thiz).anvillib_rendering$bloomRendering(); - } - - /** - * Creates a structurally identical copy of {@code that} marked as bloom-rendered. - *

    - * Ported from 26.1: {@code RenderType.create(that.name, that.state)} (26.1 took a {@code RenderState}) - * becomes {@code RenderType.create(name, format, mode, bufferSize, state)} on 1.21.1; the - * {@code CompositeState} is exposed via the access transformer (see META-INF/accesstransformer.cfg). - */ - static RenderType copyWithBloom(RenderType that) { - if (that instanceof RenderType.CompositeRenderType composite) { - RenderType newValue = RenderType.create(that.name, that.format(), that.mode(), that.bufferSize(), composite.state()); - setRenderingBloomed(newValue, true); - return newValue; - } - return that; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/MemoryBarrierFlag.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/MemoryBarrierFlag.java deleted file mode 100644 index 85d08c3f..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/MemoryBarrierFlag.java +++ /dev/null @@ -1,39 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d; - -import org.lwjgl.opengl.GL42; -import org.lwjgl.opengl.GL43; - -public enum MemoryBarrierFlag { - VERTEX_ATTRIB_ARRAY_BARRIER(GL42.GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT), - ELEMENT_ARRAY_BARRIER(GL42.GL_ELEMENT_ARRAY_BARRIER_BIT), - UNIFORM_BARRIER(GL42.GL_UNIFORM_BARRIER_BIT), - TEXTURE_FETCH_BARRIER(GL42.GL_TEXTURE_FETCH_BARRIER_BIT), - SHADER_IMAGE_ACCESS_BARRIER(GL42.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT), - COMMAND_BARRIER(GL42.GL_COMMAND_BARRIER_BIT), - PIXEL_BUFFER_BARRIER(GL42.GL_PIXEL_BUFFER_BARRIER_BIT), - TEXTURE_UPDATE_BARRIER(GL42.GL_TEXTURE_UPDATE_BARRIER_BIT), - BUFFER_UPDATE_BARRIER(GL42.GL_BUFFER_UPDATE_BARRIER_BIT), - FRAMEBUFFER_BARRIER(GL42.GL_FRAMEBUFFER_BARRIER_BIT), - TRANSFORM_FEEDBACK_BARRIER(GL42.GL_TRANSFORM_FEEDBACK_BARRIER_BIT), - ATOMIC_COUNTER_BARRIER(GL42.GL_ATOMIC_COUNTER_BARRIER_BIT), - SHADER_STORAGE_BARRIER(GL43.GL_SHADER_STORAGE_BARRIER_BIT), - ALL_BARRIER(GL42.GL_ALL_BARRIER_BITS); - - private final int glEnum; - - MemoryBarrierFlag(int glEnum) { - this.glEnum = glEnum; - } - - public int glEnum() { - return glEnum; - } - - public static int compound(MemoryBarrierFlag... flags){ - int i = 0; - for (MemoryBarrierFlag flag : flags) { - i |= flag.glEnum; - } - return i; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/ALRComputeCapabilities.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/ALRComputeCapabilities.java deleted file mode 100644 index ab178600..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/ALRComputeCapabilities.java +++ /dev/null @@ -1,15 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute; - -import org.lwjgl.opengl.GL; - -public class ALRComputeCapabilities { - private static boolean COMPUTE_SUPPORTED; - - public static void init() { - COMPUTE_SUPPORTED = GL.getCapabilities().GL_ARB_compute_shader; - } - - public static boolean isComputeSupported() { - return COMPUTE_SUPPORTED; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/BufferSlice.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/BufferSlice.java deleted file mode 100644 index 0e93f2ae..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/BufferSlice.java +++ /dev/null @@ -1,21 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute; - -import org.jetbrains.annotations.ApiStatus; - -/** - * A slice of a GL buffer, described by the raw buffer handle, byte offset and byte length. - *

    - * Ported from 26.1: the original used {@code com.mojang.blaze3d.buffers.GpuBufferSlice}, which does - * not exist on 1.21.1. The buffer handle is the plain LWJGL buffer name (see - * {@code GlStateManager._glGenBuffers}). - */ -@ApiStatus.Internal -public record BufferSlice(int bufferId, long offset, long length) { - public static BufferSlice of(int bufferId, long length) { - return new BufferSlice(bufferId, 0L, length); - } - - public static BufferSlice of(int bufferId, long offset, long length) { - return new BufferSlice(bufferId, offset, length); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePass.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePass.java deleted file mode 100644 index aee8282c..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePass.java +++ /dev/null @@ -1,100 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline; - -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.BufferSlice; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.ComputeBindingLayout; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.TextureBinding; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.MemoryBarrierFlag; - -import java.util.List; - -/** - * A single compute dispatch session backed by an {@link ALRComputePassBackend}. - *

    - * Ported from 26.1: {@code GpuBufferSlice}/{@code GpuTexture} resources become - * {@link BufferSlice}/plain GL texture ids. - */ -public class ALRComputePass implements AutoCloseable { - private ALRComputePipeline pipeline; - private final ALRComputePassBackend backend; - - public ALRComputePass(ALRComputePassBackend backend) { - this.backend = backend; - } - - public void pushDebugGroup(String name) { - this.backend.pushDebugGroup(name); - } - - public void popDebugGroup(String name) { - this.backend.popDebugGroup(); - } - - public void memoryBarrier(MemoryBarrierFlag... flags) { - this.backend.memoryBarrier(flags); - } - - public void dispatchWorkgroups( - int groupCountX, - int groupCountY, - int groupCountZ - ) { - this.backend.pushDebugGroup("Compute " + pipeline.identifier()); - this.backend.dispatchWorkgroups(groupCountX, groupCountY, groupCountZ); - this.backend.popDebugGroup(); - } - - public void dispatchWorkgroupsIndirect( - BufferSlice buffer - ) { - this.backend.dispatchWorkgroupsIndirect(buffer); - } - - @Override - public void close() { - this.backend.close(); - } - - public void setPipeline(ALRComputePipeline pipeline) { - this.pipeline = pipeline; - this.backend.setPipeline(pipeline); - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - public void bindAll(List elements) { - int bindingPoint = 0; - for (ComputeBindingLayout binding : pipeline.bindings()) { - this.bind(bindingPoint++, binding, elements.get(bindingPoint - 1)); - } - } - - public void bind(int bindingPoint, ComputeBindingLayout layout, T resource) { - layout.apply(bindingPoint, resource, this); - } - - public void bindTexture(int bindingPoint, TextureBinding.SamplerAndTexture resource) { - this.backend.bindTexture(bindingPoint, resource); - } - - public void bindImage(int bindingPoint, int textureId, boolean read, boolean write) { - this.backend.bindImage(bindingPoint, textureId, read, write); - } - - /** - * Binds a texture as an image with an explicit internal format for {@code glBindImageTexture}. - */ - public void bindImage(int bindingPoint, int textureId, boolean read, boolean write, int internalFormat) { - this.backend.bindImage(bindingPoint, textureId, read, write, internalFormat); - } - - public void bindUniformBlock(int bindingPoint, BufferSlice resource) { - this.backend.bindUniformBlock(bindingPoint, resource); - } - - public void bindShaderStorage(int bindingPoint, BufferSlice resource) { - this.backend.bindShaderStorage(bindingPoint, resource); - } - - public void bindAtomicCounter(int bindingPoint, BufferSlice resource) { - this.backend.bindAtomicCounter(bindingPoint, resource); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePassBackend.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePassBackend.java deleted file mode 100644 index e8732afe..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePassBackend.java +++ /dev/null @@ -1,39 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline; - -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.BufferSlice; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.TextureBinding; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.MemoryBarrierFlag; - -/** - * Backend contract for {@link ALRComputePass}. - *

    - * Ported from 26.1: {@code GpuBufferSlice}/{@code GpuTexture} resources become - * {@link BufferSlice}/plain GL texture ids. - */ -public interface ALRComputePassBackend { - void setPipeline(ALRComputePipeline pipeline); - - void pushDebugGroup(String name); - - void popDebugGroup(); - - void dispatchWorkgroups(int groupCountX, int groupCountY, int groupCountZ); - - void dispatchWorkgroupsIndirect(BufferSlice buffer); - - void memoryBarrier(MemoryBarrierFlag... flags); - - void bindTexture(int bindingPoint, TextureBinding.SamplerAndTexture resource); - - void bindImage(int bindingPoint, int textureId, boolean read, boolean write); - - void bindImage(int bindingPoint, int textureId, boolean read, boolean write, int internalFormat); - - void bindUniformBlock(int bindingPoint, BufferSlice resource); - - void bindShaderStorage(int bindingPoint, BufferSlice resource); - - void bindAtomicCounter(int bindingPoint, BufferSlice resource); - - void close(); -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePipeline.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePipeline.java deleted file mode 100644 index 33277641..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/ALRComputePipeline.java +++ /dev/null @@ -1,116 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline; - -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.AtomicCounterBinding; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.ComputeBindingLayout; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.ImageBinding; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.ShaderStorageBinding; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.TextureBinding; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.UniformBlockBinding; -import net.minecraft.resources.ResourceLocation; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * Describes a compute pipeline: its identifier, shader location, preprocessor defines and binding layout. - *

    - * Ported from 26.1: {@code Identifier} becomes {@link ResourceLocation} and the 26.1 - * {@code ShaderDefines} becomes a plain {@link Map}{@code } of preprocessor defines. - */ -public record ALRComputePipeline( - ResourceLocation identifier, - List> bindings, - ResourceLocation shaderLocation, - Map defines -) { - - public static Builder builder() { - return new Builder(); - } - - public static Builder builder(ALRComputePipeline pipeline) { - return builder().withPipeline(pipeline); - } - - public static class Builder { - private final List> bindings = new ArrayList<>(); - private ResourceLocation name; - private ResourceLocation shaderLocation; - private Map defines = Map.of(); - - public Builder withPipeline(ALRComputePipeline pipeline) { - this.bindings.clear(); - this.bindings.addAll(pipeline.bindings()); - this.shaderLocation = pipeline.shaderLocation(); - this.defines = pipeline.defines(); - return this; - } - - public Builder withShader(ResourceLocation shaderLocation) { - this.shaderLocation = shaderLocation; - return this; - } - - public Builder withName(ResourceLocation name) { - this.name = name; - return this; - } - - public Builder withShaderLocation(ResourceLocation shaderLocation) { - return this.withShader(shaderLocation); - } - - public Builder withDefines(Map defines) { - this.defines = defines; - return this; - } - - public Builder withBinding(ComputeBindingLayout binding) { - this.bindings.add(binding); - return this; - } - - public Builder withTexture(String name) { - return this.withBinding(new TextureBinding(name)); - } - - public Builder withImage(String name, boolean read, boolean write) { - return this.withBinding(new ImageBinding(name, read, write)); - } - - public Builder withReadOnlyImage(String name) { - return this.withImage(name, true, false); - } - - public Builder withWriteOnlyImage(String name) { - return this.withImage(name, false, true); - } - - public Builder withReadWriteImage(String name) { - return this.withImage(name, true, true); - } - - public Builder withUniformBlock(String name) { - return this.withBinding(new UniformBlockBinding(name)); - } - - public Builder withShaderStorage(String name) { - return this.withBinding(new ShaderStorageBinding(name)); - } - - public Builder withAtomicCounter(String name) { - return this.withBinding(new AtomicCounterBinding(name)); - } - - public ALRComputePipeline build() { - return new ALRComputePipeline( - Objects.requireNonNull(this.name, "name"), - this.bindings, - Objects.requireNonNull(this.shaderLocation, "shaderLocation"), - this.defines - ); - } - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/AtomicCounterBinding.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/AtomicCounterBinding.java deleted file mode 100644 index 33955530..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/AtomicCounterBinding.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings; - -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.BufferSlice; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ShaderResourceType; - -public record AtomicCounterBinding( - String name -) implements ComputeBindingLayout { - @Override - public ShaderResourceType type() { - return ShaderResourceType.ATOMIC_COUNTER; - } - - @Override - public void apply(int bindingPoint, BufferSlice resource, ALRComputePass computePass) { - computePass.bindAtomicCounter(bindingPoint, resource); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ComputeBindingLayout.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ComputeBindingLayout.java deleted file mode 100644 index b9bbb054..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ComputeBindingLayout.java +++ /dev/null @@ -1,12 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings; - -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ShaderResourceType; - -public interface ComputeBindingLayout { - ShaderResourceType type(); - - String name(); - - void apply(int bindingPoint, T resource, ALRComputePass computePass); -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ImageBinding.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ImageBinding.java deleted file mode 100644 index be49104a..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ImageBinding.java +++ /dev/null @@ -1,34 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings; - -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ShaderResourceType; - -/** - * Binds a GL texture (given by its texture name) as an image for read/write access. - *

    - * Ported from 26.1: {@code GpuTexture} becomes a plain GL texture id. The default internal format - * used for {@code glBindImageTexture} is {@code GL_RGBA8}; {@link ALRComputePass#bindImage} offers - * an overload accepting an explicit internal format. - */ -public record ImageBinding( - String name, - boolean read, - boolean write -) implements ComputeBindingLayout { - - public ImageBinding { - if (!read && !write) { - throw new IllegalArgumentException("ImageResource does not allow both read and write are false"); - } - } - - @Override - public ShaderResourceType type() { - return ShaderResourceType.IMAGE; - } - - @Override - public void apply(int bindingPoint, Integer resource, ALRComputePass computePass) { - computePass.bindImage(bindingPoint, resource, read, write); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ShaderStorageBinding.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ShaderStorageBinding.java deleted file mode 100644 index 4d5c8b1a..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/ShaderStorageBinding.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings; - -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.BufferSlice; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ShaderResourceType; - -public record ShaderStorageBinding( - String name -) implements ComputeBindingLayout { - @Override - public ShaderResourceType type() { - return ShaderResourceType.SHADER_STORAGE; - } - - @Override - public void apply(int bindingPoint, BufferSlice resource, ALRComputePass computePass) { - computePass.bindShaderStorage(bindingPoint, resource); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/TextureBinding.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/TextureBinding.java deleted file mode 100644 index 4227f5c4..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/TextureBinding.java +++ /dev/null @@ -1,33 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings; - -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ShaderResourceType; - -/** - * Binds a GL texture (by texture name) together with an optional GL sampler (0 = default sampler). - *

    - * Ported from 26.1: {@code GpuSampler}/{@code GpuTexture} become plain GL ids; the sampler is - * optional since 1.21.1's {@code GlStateManager} exposes no sampler management API. - */ -public record TextureBinding( - String name -) implements ComputeBindingLayout { - @Override - public ShaderResourceType type() { - return ShaderResourceType.TEXTURE; - } - - @Override - public void apply(int bindingPoint, SamplerAndTexture resource, ALRComputePass pass) { - pass.bindTexture(bindingPoint, resource); - } - - public record SamplerAndTexture( - int samplerId, - int textureId - ) { - public SamplerAndTexture(int textureId) { - this(0, textureId); - } - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/UniformBlockBinding.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/UniformBlockBinding.java deleted file mode 100644 index e1d86638..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/bindings/UniformBlockBinding.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings; - -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.BufferSlice; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ShaderResourceType; - -public record UniformBlockBinding( - String name -) implements ComputeBindingLayout { - @Override - public ShaderResourceType type() { - return ShaderResourceType.UNIFORM_BLOCK; - } - - @Override - public void apply(int bindingPoint, BufferSlice resource, ALRComputePass computePass) { - computePass.bindUniformBlock(bindingPoint, resource); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/gl/GlComputePassBackend.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/gl/GlComputePassBackend.java deleted file mode 100644 index d44beb72..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/pipeline/gl/GlComputePassBackend.java +++ /dev/null @@ -1,215 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.gl; - -import com.mojang.blaze3d.platform.GlStateManager; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.MemoryBarrierFlag; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.BufferSlice; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePassBackend; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePipeline; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.bindings.TextureBinding; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ALRComputeProgramInstance; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader.ALRComputeShaderManager; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import org.jetbrains.annotations.ApiStatus; -import org.lwjgl.opengl.ARBComputeShader; -import org.lwjgl.opengl.ARBShaderAtomicCounters; -import org.lwjgl.opengl.ARBShaderImageLoadStore; -import org.lwjgl.opengl.ARBShaderStorageBufferObject; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL13; -import org.lwjgl.opengl.GL15; -import org.lwjgl.opengl.GL20; -import org.lwjgl.opengl.GL31; -import org.lwjgl.opengl.GL33; -import org.lwjgl.opengl.KHRDebug; - -/** - * Standalone GL backend for {@link dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePass}. - *

    - * Ported from 26.1 (see #P7e): the 26.1 mixin targets ({@code GpuDevice}/{@code CommandEncoder}/ - * {@code GlDevice}/{@code GlCommandEncoder}/{@code DirectStateAccess}/{@code GlDebugLabel}) do not - * exist on 1.21.1, so this class owns the program/buffer state machine directly. All GL capability - * entry points used here are available in the 1.21.1 LWJGL; the program compile/link logic lives in - * {@link ALRComputeShaderManager}. - */ -@ApiStatus.Internal -public class GlComputePassBackend implements ALRComputePassBackend { - private static final int DEFAULT_IMAGE_FORMAT = GL11.GL_RGBA8; - - private final Int2ObjectMap textureBindings = new Int2ObjectOpenHashMap<>(); - private final Int2ObjectMap imageBindings = new Int2ObjectOpenHashMap<>(); - private final Int2ObjectMap uniformBlockBindings = new Int2ObjectOpenHashMap<>(); - private final Int2ObjectMap shaderStorageBindings = new Int2ObjectOpenHashMap<>(); - private final Int2ObjectMap atomicCounterBindings = new Int2ObjectOpenHashMap<>(); - private ALRComputePipeline pipeline = null; - - public GlComputePassBackend() { - } - - @Override - public void setPipeline(ALRComputePipeline pipeline) { - this.pipeline = pipeline; - } - - @Override - public void pushDebugGroup(String name) { - if (org.lwjgl.opengl.GL.getCapabilities().GL_KHR_debug) { - KHRDebug.glPushDebugGroup(KHRDebug.GL_DEBUG_SOURCE_APPLICATION, 0, name); - } - } - - @Override - public void popDebugGroup() { - if (org.lwjgl.opengl.GL.getCapabilities().GL_KHR_debug) { - KHRDebug.glPopDebugGroup(); - } - } - - @Override - public void dispatchWorkgroups(int groupCountX, int groupCountY, int groupCountZ) { - this.setupState(); - ARBComputeShader.glDispatchCompute(groupCountX, groupCountY, groupCountZ); - } - - @Override - public void dispatchWorkgroupsIndirect(BufferSlice buffer) { - this.setupState(); - GlStateManager._glBindBuffer(ARBComputeShader.GL_DISPATCH_INDIRECT_BUFFER, buffer.bufferId()); - ARBComputeShader.glDispatchComputeIndirect(buffer.offset()); - } - - @Override - public void memoryBarrier(MemoryBarrierFlag... flags) { - ARBShaderImageLoadStore.glMemoryBarrier(MemoryBarrierFlag.compound(flags)); - } - - @Override - public void bindTexture(int bindingPoint, TextureBinding.SamplerAndTexture resource) { - this.textureBindings.put(bindingPoint, resource); - } - - @Override - public void bindImage(int bindingPoint, int textureId, boolean read, boolean write) { - this.bindImage(bindingPoint, textureId, read, write, DEFAULT_IMAGE_FORMAT); - } - - @Override - public void bindImage(int bindingPoint, int textureId, boolean read, boolean write, int internalFormat) { - if (!read && !write) { - throw new IllegalArgumentException("ImageResource does not allow both read and write are false"); - } - this.imageBindings.put(bindingPoint, new ImageState(textureId, read, write, internalFormat)); - } - - @Override - public void bindUniformBlock(int bindingPoint, BufferSlice resource) { - this.uniformBlockBindings.put(bindingPoint, resource); - } - - @Override - public void bindShaderStorage(int bindingPoint, BufferSlice resource) { - this.shaderStorageBindings.put(bindingPoint, resource); - } - - @Override - public void bindAtomicCounter(int bindingPoint, BufferSlice resource) { - this.atomicCounterBindings.put(bindingPoint, resource); - } - - @Override - public void close() { - - } - - private void setupState() { - ALRComputeProgramInstance program = ALRComputeShaderManager.INSTANCE.getShader(pipeline); - if (program == null || program == ALRComputeProgramInstance.INVALID) return; - GL20.glUseProgram(program.id()); - - for (Int2ObjectMap.Entry entry : this.textureBindings.int2ObjectEntrySet()) { - this.setupTexture(entry.getIntKey(), entry.getValue()); - } - for (Int2ObjectMap.Entry entry : this.imageBindings.int2ObjectEntrySet()) { - this.setupImage(entry.getIntKey(), entry.getValue()); - } - for (Int2ObjectMap.Entry entry : this.uniformBlockBindings.int2ObjectEntrySet()) { - this.setupUniformBlock(entry.getIntKey(), entry.getValue()); - } - for (Int2ObjectMap.Entry entry : this.shaderStorageBindings.int2ObjectEntrySet()) { - this.setupShaderStorage(entry.getIntKey(), entry.getValue()); - } - for (Int2ObjectMap.Entry entry : this.atomicCounterBindings.int2ObjectEntrySet()) { - this.setupAtomicCounter(entry.getIntKey(), entry.getValue()); - } - } - - private void setupTexture(int bindingPoint, TextureBinding.SamplerAndTexture resource) { - GlStateManager._activeTexture(GL13.GL_TEXTURE0 + bindingPoint); - GlStateManager._bindTexture(resource.textureId()); - if (resource.samplerId() != 0) { - GL33.glBindSampler(bindingPoint, resource.samplerId()); - } - } - - private void setupImage(int bindingPoint, ImageState state) { - int access = 0; - if (state.read() && state.write()) { - access = GL15.GL_READ_WRITE; - } else { - if (state.read()) { - access |= GL15.GL_READ_ONLY; - } - if (state.write()) { - access |= GL15.GL_WRITE_ONLY; - } - } - - ARBShaderImageLoadStore.glBindImageTexture( - bindingPoint, - state.textureId(), - 0, - false, - 0, - access, - state.internalFormat() - ); - } - - private void setupUniformBlock(int bindingPoint, BufferSlice resource) { - GL31.glBindBufferRange( - GL31.GL_UNIFORM_BUFFER, - bindingPoint, - resource.bufferId(), - resource.offset(), - resource.length() - ); - } - - private void setupShaderStorage(int bindingPoint, BufferSlice resource) { - GL31.glBindBufferRange( - ARBShaderStorageBufferObject.GL_SHADER_STORAGE_BUFFER, - bindingPoint, - resource.bufferId(), - resource.offset(), - resource.length() - ); - } - - private void setupAtomicCounter(int bindingPoint, BufferSlice resource) { - GL31.glBindBufferRange( - ARBShaderAtomicCounters.GL_ATOMIC_COUNTER_BUFFER, - bindingPoint, - resource.bufferId(), - resource.offset(), - resource.length() - ); - } - - private record ImageState( - int textureId, - boolean read, - boolean write, - int internalFormat - ) { - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstance.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstance.java deleted file mode 100644 index 2b0e9b24..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstance.java +++ /dev/null @@ -1,20 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader; - -import dev.anvilcraft.lib.v2.rendering.AnvilLibRendering; -import net.minecraft.resources.ResourceLocation; - -import java.util.Map; - -/** - * A compiled GL compute program identified by its program id and the key it was compiled from. - */ -public record ALRComputeProgramInstance(int id, ALRComputeProgramInstanceKey key) { - public static final ALRComputeProgramInstance INVALID = new ALRComputeProgramInstance( - 0, - new ALRComputeProgramInstanceKey( - AnvilLibRendering.location("compute/invalid"), - Map.of() - ) - ); - -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstanceKey.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstanceKey.java deleted file mode 100644 index e947c5ca..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeProgramInstanceKey.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader; - -import net.minecraft.resources.ResourceLocation; - -import java.util.Map; - -/** - * Key identifying a compute program: shader location plus preprocessor defines. - *

    - * Ported from 26.1: the 26.1 {@code ShaderDefines} becomes a simple {@link Map}{@code }. - */ -public record ALRComputeProgramInstanceKey(ResourceLocation location, Map defines) { -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeShaderManager.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeShaderManager.java deleted file mode 100644 index 2e964cb3..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ALRComputeShaderManager.java +++ /dev/null @@ -1,205 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader; - -import com.google.common.collect.ImmutableMap; -import com.mojang.blaze3d.preprocessor.GlslPreprocessor; -import com.mojang.logging.LogUtils; -import dev.anvilcraft.lib.v2.rendering.event.RegisterComputePipelinesEvent; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.ALRComputeCapabilities; -import dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.pipeline.ALRComputePipeline; -import lombok.extern.slf4j.Slf4j; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.packs.resources.Resource; -import net.minecraft.server.packs.resources.ResourceManager; -import net.minecraft.server.packs.resources.SimplePreparableReloadListener; -import net.minecraft.util.profiling.ProfilerFiller; -import net.neoforged.fml.ModLoader; -import org.apache.commons.io.IOUtils; -import org.jetbrains.annotations.UnknownNullability; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.lwjgl.opengl.ARBComputeShader; -import org.lwjgl.opengl.GL; -import org.lwjgl.opengl.GL20; -import org.lwjgl.opengl.KHRDebug; -import org.slf4j.Logger; - -import java.io.IOException; -import java.io.Reader; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * Loads compute shader sources from the resource manager, compiles them into GL compute programs - * and caches the results per pipeline. - *

    - * Ported from 26.1 (see #P7e): the reload mechanism ({@link SimplePreparableReloadListener}) and the - * resource scanning logic are preserved; registration happens through - * {@code RegisterClientReloadListenersEvent}. {@code ShaderManager.createPreprocessor} (26.1) is - * replaced by an inline {@link GlslPreprocessor} resolving {@code #moj_import} the same way the - * 1.21.1 core shader pipeline does. The 26.1 {@code GpuDevice}-backed compile/destroy logic is - * inlined with raw LWJGL calls. - */ -@Slf4j -public class ALRComputeShaderManager extends SimplePreparableReloadListener { - private static final Logger LOGGER = LogUtils.getLogger(); - public static final ALRComputeShaderManager INSTANCE = new ALRComputeShaderManager(); - - @UnknownNullability - private ComputeShaderSource source = null; - - private final Map shaderInstanceMap = new HashMap<>(); - private final Map pipelineToProgramMap = new HashMap<>(); - - @Override - protected @Nonnull ComputeShaderSource prepare(ResourceManager manager, ProfilerFiller profiler) { - ImmutableMap.Builder sources = new ImmutableMap.Builder<>(); - - Map shaders = manager.listResources( - "shaders", - it -> it.getPath().endsWith(".csh") || it.getPath().endsWith(".compute") || it.getPath().endsWith(".glsl") - ); - - for (Map.Entry it : shaders.entrySet()) { - GlslPreprocessor preprocessor = createPreprocessor(manager, it.getKey()); - - try (Reader reader = it.getValue().openAsReader()) { - String source = IOUtils.toString(reader); - sources.put(it.getKey(), String.join("\n", preprocessor.process(source))); - } catch (IOException ex) { - log.error("Failed to load compute shader source at {}", it.getKey(), ex); - } - } - log.info("Loaded {} compute shader.", shaders.size()); - return new ComputeShaderSource(sources.build()); - } - - @Override - protected void apply(ComputeShaderSource preparations, ResourceManager manager, ProfilerFiller profiler) { - ALRComputeCapabilities.init(); - if (!ALRComputeCapabilities.isComputeSupported()) { - return; - } - for (ALRComputeProgramInstance value : shaderInstanceMap.values()) { - destroyComputeShader(value); - } - shaderInstanceMap.clear(); - pipelineToProgramMap.clear(); - - this.source = preparations; - RegisterComputePipelinesEvent event = new RegisterComputePipelinesEvent<>(); - ModLoader.postEvent(event); - - for (ALRComputePipeline pipeline : event.getPipelines()) { - ALRComputeProgramInstanceKey key = new ALRComputeProgramInstanceKey( - pipeline.shaderLocation(), - pipeline.defines() - ); - log.debug("Compiled COMPUTE shader {}", pipeline.shaderLocation()); - ALRComputeProgramInstance instance = compileComputeShader(key); - this.shaderInstanceMap.put(key, instance); - this.pipelineToProgramMap.put(pipeline, instance); - } - } - - public String getSource(ResourceLocation location) { - return source.source.get(location.withPrefix("shaders/")); - } - - @Nullable - public ALRComputeProgramInstance getShader(ALRComputeProgramInstanceKey location) { - return this.shaderInstanceMap.get(location); - } - - @Nullable - public ALRComputeProgramInstance getShader(ALRComputePipeline pipeline) { - return this.pipelineToProgramMap.get(pipeline); - } - - private void destroyComputeShader(ALRComputeProgramInstance instance) { - if (instance.id() != 0) { - GL20.glDeleteProgram(instance.id()); - } - } - - /** - * Compiles a GL compute program from the preprocessed source of the given key. - *

    - * The compile/link sequence is ported line by line from the 26.1 {@code GlDeviceMixin}; - * the debug label application replaces the 26.1 {@code GlDebugLabel} mixins. - */ - private ALRComputeProgramInstance compileComputeShader(ALRComputeProgramInstanceKey instanceKey) { - String source = getSource(instanceKey.location()); - if (source == null) { - LOGGER.error("Could not find COMPUTE shader source for {}", instanceKey.location()); - return ALRComputeProgramInstance.INVALID; - } - int shaderId = GL20.glCreateShader(ARBComputeShader.GL_COMPUTE_SHADER); - GL20.glShaderSource(shaderId, source); - GL20.glCompileShader(shaderId); - if (GL20.glGetShaderi(shaderId, GL20.GL_COMPILE_STATUS) == 0) { - String infoLog = GL20.glGetShaderInfoLog(shaderId); - LOGGER.error("Could not compile COMPUTE shader {}: {}", instanceKey.location(), infoLog); - GL20.glDeleteShader(shaderId); - return ALRComputeProgramInstance.INVALID; - } - int program = GL20.glCreateProgram(); - GL20.glAttachShader(program, shaderId); - GL20.glLinkProgram(program); - if (GL20.glGetProgrami(program, GL20.GL_LINK_STATUS) == 0) { - String infoLog = GL20.glGetProgramInfoLog(shaderId); - LOGGER.error("Could not link COMPUTE shader {}: {}", instanceKey.location(), infoLog); - GL20.glDeleteShader(shaderId); - GL20.glDeleteProgram(program); - return ALRComputeProgramInstance.INVALID; - } - GL20.glDeleteShader(shaderId); - ALRComputeProgramInstance instance = new ALRComputeProgramInstance(program, instanceKey); - applyDebugLabel(instance); - return instance; - } - - private void applyDebugLabel(ALRComputeProgramInstance shaderInstance) { - if (!GL.getCapabilities().GL_KHR_debug) { - return; - } - KHRDebug.glObjectLabel( - KHRDebug.GL_PROGRAM, - shaderInstance.id(), - shaderInstance.key().location().toString() - ); - } - - /** - * A minimal GLSL preprocessor for compute shaders: resolves {@code #moj_import<...>} the same - * way the 1.21.1 core shader pipeline does ({@code ClientHooks.getShaderImportLocation}). - */ - private static GlslPreprocessor createPreprocessor(ResourceManager manager, ResourceLocation base) { - return new GlslPreprocessor() { - private final Set importedPaths = new HashSet<>(); - - @Override - public String applyImport(boolean useFullPath, String directory) { - ResourceLocation resourcelocation = net.neoforged.neoforge.client.ClientHooks.getShaderImportLocation(base.toString(), useFullPath, directory); - if (!this.importedPaths.add(resourcelocation.toString())) { - return null; - } else { - try { - try (Reader reader = manager.openAsReader(resourcelocation)) { - return IOUtils.toString(reader); - } - } catch (IOException ioexception) { - LOGGER.error("Could not open GLSL import {}: {}", resourcelocation, ioexception.getMessage()); - return "#error " + ioexception.getMessage(); - } - } - } - }; - } - - public record ComputeShaderSource( - Map source - ) { - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ShaderResourceType.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ShaderResourceType.java deleted file mode 100644 index e615402c..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/extension/blaze3d/compute/shader/ShaderResourceType.java +++ /dev/null @@ -1,9 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.extension.blaze3d.compute.shader; - -public enum ShaderResourceType { - TEXTURE, - IMAGE, - UNIFORM_BLOCK, - SHADER_STORAGE, - ATOMIC_COUNTER -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/ALRMeshSorting.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/ALRMeshSorting.java deleted file mode 100644 index 8b5d97eb..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/ALRMeshSorting.java +++ /dev/null @@ -1,44 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation; - -import com.mojang.blaze3d.vertex.VertexSorting; -import it.unimi.dsi.fastutil.ints.IntArrays; -import org.joml.Vector3f; - -public class ALRMeshSorting { - - public static VertexSorting byDistance(Vector3f point) { - return new VertexSortingDistanceToPoint(point); - } - - public static class VertexSortingDistanceToPoint implements VertexSorting { - - private final Vector3f origin; - - private VertexSortingDistanceToPoint(Vector3f origin) { - this.origin = origin; - } - - @Override - public int[] sort(Vector3f[] points) { - int[] keys = new int[points.length]; - int[] indices = new int[points.length]; - - for (int i = 0; i < points.length; i++) { - keys[i] = floatToSortableInt(origin.distanceSquared(points[i])); - indices[i] = i; - } - - if (points.length <= 128) { - IntArrays.quickSortIndirect(indices, keys); - } else { - IntArrays.radixSortIndirect(indices, keys, true); - } - return indices; - } - } - - public static int floatToSortableInt(float f) { - int b = Float.floatToRawIntBits(f); - return b ^ ((b >> 31) | 0x7fffffff); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyBufferSource.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyBufferSource.java deleted file mode 100644 index 5017de54..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyBufferSource.java +++ /dev/null @@ -1,34 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers; - -import com.mojang.blaze3d.vertex.VertexConsumer; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.RenderType; - -public class EmptyBufferSource extends MultiBufferSource.BufferSource { - - public static final EmptyBufferSource INSTANCE = new EmptyBufferSource(); - - protected EmptyBufferSource() { - super(null, null); - } - - @Override - public VertexConsumer getBuffer(RenderType renderType) { - return EmptyVertexConsumer.INSTANCE; - } - - @Override - public void endBatch() { - - } - - @Override - public void endBatch(RenderType type) { - - } - - @Override - public void endLastBatch() { - - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyOutlineBufferSource.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyOutlineBufferSource.java deleted file mode 100644 index 527c1115..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyOutlineBufferSource.java +++ /dev/null @@ -1,20 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers; - -import com.mojang.blaze3d.vertex.VertexConsumer; -import net.minecraft.client.renderer.OutlineBufferSource; -import net.minecraft.client.renderer.RenderType; - -public class EmptyOutlineBufferSource extends OutlineBufferSource { - - public static final EmptyOutlineBufferSource INSTANCE = new EmptyOutlineBufferSource(); - - public EmptyOutlineBufferSource() { - // The delegate BufferSource is never used: getBuffer is fully overridden below. - super(null); - } - - @Override - public VertexConsumer getBuffer(RenderType renderType) { - return EmptyVertexConsumer.INSTANCE; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyVertexConsumer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyVertexConsumer.java deleted file mode 100644 index 0849c5f1..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/EmptyVertexConsumer.java +++ /dev/null @@ -1,43 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers; - -import com.mojang.blaze3d.vertex.VertexConsumer; - -public class EmptyVertexConsumer implements VertexConsumer { - - public static final EmptyVertexConsumer INSTANCE = new EmptyVertexConsumer(); - - @Override - public VertexConsumer addVertex(float x, float y, float z) { - return this; - } - - @Override - public VertexConsumer setColor(int r, int g, int b, int a) { - return this; - } - - @Override - public VertexConsumer setColor(int color) { - return this; - } - - @Override - public VertexConsumer setUv(float u, float v) { - return this; - } - - @Override - public VertexConsumer setUv1(int u, int v) { - return this; - } - - @Override - public VertexConsumer setUv2(int u, int v) { - return this; - } - - @Override - public VertexConsumer setNormal(float x, float y, float z) { - return this; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/FullyBufferedBufferSource.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/FullyBufferedBufferSource.java deleted file mode 100644 index 9cf4404f..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/FullyBufferedBufferSource.java +++ /dev/null @@ -1,128 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers; - -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.BufferBuilder; -import com.mojang.blaze3d.vertex.ByteBufferBuilder; -import com.mojang.blaze3d.vertex.MeshData; -import com.mojang.blaze3d.vertex.VertexBuffer; -import com.mojang.blaze3d.vertex.VertexConsumer; -import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; -import it.unimi.dsi.fastutil.objects.Reference2IntMap; -import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; -import lombok.Getter; -import net.minecraft.client.renderer.MultiBufferSource; -import net.minecraft.client.renderer.RenderType; - -import javax.annotation.ParametersAreNonnullByDefault; -import java.util.HashMap; -import java.util.Map; - -/** - * A {@link MultiBufferSource.BufferSource} that never draws: it accumulates one {@link BufferBuilder} - * per {@link RenderType} and hands the finished {@link MeshData} to a {@link VertexBufferHost} through - * {@link #upload(VertexBufferHost)}. - *

    - * Ported from 26.1: the meshes used to be copied into {@code GpuBuffer}s through a - * {@code CommandEncoder}; on 1.21.1 they are uploaded into {@link VertexBuffer}s on the render thread - * (the upload runs via {@link VertexBufferHost#acceptUploadAction}, executed by the cached-BER pipeline - * at {@code RenderFrameEvent.Pre}). The per-type {@code ByteBufferBuilder}s (786432 bytes each) must be - * large enough for the whole compiled chunk layer. - */ -@ParametersAreNonnullByDefault -@MethodsReturnNonnullByDefault -public class FullyBufferedBufferSource extends MultiBufferSource.BufferSource implements AutoCloseable { - private final Map byteBuffers = new HashMap<>(); - private final Map bufferBuilders = new HashMap<>(); - @Getter - private final Reference2IntMap indexCountMap = new Reference2IntOpenHashMap<>(); - @Getter - private final Map meshSorts = new HashMap<>(); - - public FullyBufferedBufferSource() { - // The parent's shared/fixed buffers are never touched: getBuffer/endBatch are fully overridden. - super(null, null); - } - - private ByteBufferBuilder getByteBuffer(RenderType renderType) { - return byteBuffers.computeIfAbsent(renderType, key -> new ByteBufferBuilder(786432)); - } - - @Override - public VertexConsumer getBuffer(RenderType renderType) { - return bufferBuilders.computeIfAbsent( - renderType, - it -> new BufferBuilder( - getByteBuffer(renderType), - it.mode(), - it.format() - ) - ); - } - - /** - * @return whether no render type was requested while compiling this batch. A render type whose - * builder ended up with zero vertices is not reported here (it yields an empty mesh and is - * skipped during upload), so an exact "no vertices anywhere" answer would require per-type - * vertex counters that 1.21.1's {@link BufferBuilder} does not expose. - */ - public boolean isEmpty() { - return bufferBuilders.isEmpty(); - } - - @Override - public void endBatch(RenderType renderType) { - } - - @Override - public void endLastBatch() { - } - - @Override - public void endBatch() { - } - - public void upload(VertexBufferHost host) { - for (RenderType renderType : bufferBuilders.keySet()) { - host.acceptUploadAction(() -> uploadNow(host, renderType)); - } - } - - @SuppressWarnings("resource") - private void uploadNow(VertexBufferHost host, RenderType renderType) { - BufferBuilder bufferBuilder = bufferBuilders.get(renderType); - ByteBufferBuilder byteBuffer = byteBuffers.get(renderType); - MeshData mesh = bufferBuilder.build(); - if (mesh != null) { - indexCountMap.put(renderType, mesh.drawState().indexCount()); - if (renderType.sortOnUpload()) { - MeshData.SortState sortState = mesh.sortQuads( - host.getSortingByteBufferBuilder(renderType), - RenderSystem.getVertexSorting() - ); - meshSorts.put(renderType, sortState); - } - - VertexBuffer vertexBuffer = host.getVertexBuffer( - renderType, - (long) mesh.drawState().vertexCount() * mesh.drawState().format().getVertexSize() - ); - vertexBuffer.bind(); - vertexBuffer.upload(mesh); - } else { - indexCountMap.put(renderType, 0); - meshSorts.remove(renderType); - } - byteBuffer.close(); - bufferBuilders.remove(renderType); - byteBuffers.remove(renderType); - } - - public void close(RenderType renderType) { - ByteBufferBuilder builder = byteBuffers.get(renderType); - builder.close(); - } - - public void close() { - byteBuffers.keySet().forEach(this::close); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/GpuBufferConstants.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/GpuBufferConstants.java deleted file mode 100644 index 0c94b2d5..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/GpuBufferConstants.java +++ /dev/null @@ -1,6 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers; - -public class GpuBufferConstants { - public static final int USAGE_SHADER_STORAGE = 512; - public static final int USAGE_ATOMIC_COUNTER = 1024; -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/TransformingVertexConsumerWrapper.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/TransformingVertexConsumerWrapper.java deleted file mode 100644 index e907bb00..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/TransformingVertexConsumerWrapper.java +++ /dev/null @@ -1,50 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers; - -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; -import org.joml.Vector3f; - -public class TransformingVertexConsumerWrapper implements VertexConsumer { - private final PoseStack.Pose pose; - private final VertexConsumer delegate; - - public TransformingVertexConsumerWrapper(PoseStack.Pose pose, VertexConsumer delegate) { - this.pose = pose; - this.delegate = delegate; - } - - @Override - public VertexConsumer addVertex(float x, float y, float z) { - return delegate.addVertex(pose.pose().transformPosition(x, y, z, new Vector3f())); - } - - @Override - public VertexConsumer setColor(int r, int g, int b, int a) { - return delegate.setColor(r, g, b, a); - } - - @Override - public VertexConsumer setColor(int color) { - return delegate.setColor(color); - } - - @Override - public VertexConsumer setUv(float u, float v) { - return delegate.setUv(u, v); - } - - @Override - public VertexConsumer setUv1(int u, int v) { - return delegate.setUv1(u, v); - } - - @Override - public VertexConsumer setUv2(int u, int v) { - return delegate.setUv2(u, v); - } - - @Override - public VertexConsumer setNormal(float x, float y, float z) { - return delegate.setNormal(pose, x, y, z); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/VertexBufferHost.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/VertexBufferHost.java deleted file mode 100644 index 9ee981d0..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/VertexBufferHost.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers; - -import com.mojang.blaze3d.vertex.ByteBufferBuilder; -import com.mojang.blaze3d.vertex.VertexBuffer; -import net.minecraft.client.renderer.RenderType; - -/** - * Receives the compiled meshes of a {@link FullyBufferedBufferSource} (see {@link #acceptUploadAction}). - *

    - * Ported from 26.1: the original returned {@code GpuBuffer}s; on 1.21.1 the host hands out - * {@link VertexBuffer}s, which own both the vertex storage and the (possibly sorted) index storage. - */ -public interface VertexBufferHost { - VertexBuffer getVertexBuffer(RenderType renderType, long size); - - ByteBufferBuilder getSortingByteBufferBuilder(RenderType renderType); - - void acceptUploadAction(Runnable runnable); -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferLayout.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferLayout.java deleted file mode 100644 index 3a95b3d2..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferLayout.java +++ /dev/null @@ -1,38 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std140.Std140SizeCalculator; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std140.Std140Writer; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std430.Std430SizeCalculator; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std430.Std430Writer; - -import java.nio.ByteBuffer; - -public interface BufferLayout { - BufferLayout STD140 = new BufferLayout() { - @Override - public BufferSizeCalculator createSizeCalculator() { - return new Std140SizeCalculator(); - } - - @Override - public BufferWriter createWriter(ByteBuffer buffer) { - return new Std140Writer(buffer); - } - }; - - BufferLayout STD430 = new BufferLayout() { - @Override - public BufferSizeCalculator createSizeCalculator() { - return new Std430SizeCalculator(); - } - - @Override - public BufferWriter createWriter(ByteBuffer buffer) { - return new Std430Writer(buffer); - } - }; - - BufferSizeCalculator createSizeCalculator(); - - BufferWriter createWriter(ByteBuffer buffer); -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferSizeCalculator.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferSizeCalculator.java deleted file mode 100644 index 4cefe363..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferSizeCalculator.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; - -public interface BufferSizeCalculator { - void putFloat(); - - void putVec2(); - - void putVec3(); - - void putVec4(); - - void putInt(); - - void putIVec2(); - - void putIVec3(); - - void putIVec4(); - - void putMat4f(); - - void putStructArray(BufferObjectLayoutDefinition definition, int size); - - int get(); -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferWriter.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferWriter.java deleted file mode 100644 index ab77d5c8..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/BufferWriter.java +++ /dev/null @@ -1,42 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; -import org.joml.Matrix4f; -import org.joml.Vector2f; -import org.joml.Vector2i; -import org.joml.Vector3f; -import org.joml.Vector3i; -import org.joml.Vector4f; -import org.joml.Vector4i; - -import java.nio.ByteBuffer; - -public interface BufferWriter { - void putMat4f(Matrix4f object); - - void putIVec4(Vector4i object); - - void putIVec3(Vector3i object); - - void putIVec2(Vector2i object); - - void putInt(int object); - - void putVec4(Vector4f object); - - void putVec3(Vector3f object); - - void putVec2(Vector2f object); - - void putFloat(float object); - - ByteBuffer intoBuffer(); - - void putStructArray(int index, E object, BufferObjectLayoutDefinition definition); - - default void putStructArray(E[] objects, BufferObjectLayoutDefinition definition) { - for (int i = 0; i < objects.length; i++) { - putStructArray(i, objects[i], definition); - } - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140SizeCalculator.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140SizeCalculator.java deleted file mode 100644 index 2b9360de..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140SizeCalculator.java +++ /dev/null @@ -1,76 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std140; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferSizeCalculator; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; -import net.minecraft.util.Mth; - -public final class Std140SizeCalculator implements BufferSizeCalculator { - - private int size; - - public Std140SizeCalculator() { - } - - public void align(int alignment) { - this.size = Mth.roundToward(this.size, alignment); - } - - public void putFloat() { - this.align(4); - this.size += 4; - } - - public void putInt() { - this.align(4); - this.size += 4; - } - - public void putVec2() { - this.align(8); - this.size += 8; - } - - public void putIVec2() { - this.align(8); - this.size += 8; - } - - public void putVec3() { - this.align(16); - this.size += 12; - } - - public void putIVec3() { - this.align(16); - this.size += 12; - } - - public void putVec4() { - this.align(16); - this.size += 16; - } - - public void putIVec4() { - this.align(16); - this.size += 16; - } - - public void putMat4f() { - this.align(16); - this.size += 64; - } - - @Override - public void putStructArray(BufferObjectLayoutDefinition definition, int size) { - int alignment = Mth.roundToward(definition.alignment(BufferLayout.STD140), 16); - int stride = Mth.roundToward(definition.size(BufferLayout.STD140), alignment); - this.align(alignment); - this.size += stride * size; - } - - @Override - public int get() { - return size; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140Writer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140Writer.java deleted file mode 100644 index 568864bb..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std140/Std140Writer.java +++ /dev/null @@ -1,132 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std140; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferWriter; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; -import net.minecraft.util.Mth; -import org.joml.Matrix4f; -import org.joml.Vector2f; -import org.joml.Vector2i; -import org.joml.Vector3f; -import org.joml.Vector3i; -import org.joml.Vector4f; -import org.joml.Vector4i; - -import java.nio.ByteBuffer; - -public final class Std140Writer implements BufferWriter { - - private final ByteBuffer buffer; - private final int pointer; - private BufferObjectLayoutDefinition indexedArrayDefinition; - private int indexedArrayStart; - private int indexedArrayStride; - private int indexedArrayEnd; - - public Std140Writer(ByteBuffer buffer) { - this.buffer = buffer; - this.pointer = buffer.position(); - } - - public void align(int alignment) { - int position = this.buffer.position(); - this.buffer.position(this.pointer + Mth.roundToward(position - this.pointer, alignment)); - } - - public void putFloat(float value) { - this.align(4); - this.buffer.putFloat(value); - } - - public void putInt(int value) { - this.align(4); - this.buffer.putInt(value); - } - - public void putVec2(Vector2f vec) { - this.align(8); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 8); - } - - public void putIVec2(Vector2i vec) { - this.align(8); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 8); - } - - public void putVec3(Vector3f vec) { - this.align(16); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 12); - } - - public void putIVec3(Vector3i vec) { - this.align(16); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 12); - } - - public void putVec4(Vector4f vec) { - this.align(16); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 16); - } - - public void putIVec4(Vector4i vec) { - this.align(16); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 16); - } - - public void putMat4f(Matrix4f vec) { - this.align(16); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 64); - } - - public ByteBuffer intoBuffer() { - return this.buffer.flip(); - } - - @Override - public void putStructArray(int index, E object, BufferObjectLayoutDefinition definition) { - int alignment = Mth.roundToward(definition.alignment(BufferLayout.STD140), 16); - int stride = Mth.roundToward(definition.size(BufferLayout.STD140), alignment); - int arrayStart = this.indexedArrayStart(definition, alignment, stride); - int elementStart = arrayStart + stride * index; - this.buffer.position(elementStart); - definition.writeInto(this, object); - this.indexedArrayEnd = Math.max(this.indexedArrayEnd, elementStart + stride); - this.buffer.position(this.indexedArrayEnd); - } - - @Override - public void putStructArray(E[] objects, BufferObjectLayoutDefinition definition) { - int alignment = Mth.roundToward(definition.alignment(BufferLayout.STD140), 16); - int stride = Mth.roundToward(definition.size(BufferLayout.STD140), alignment); - int arrayStart = this.pointer + Mth.roundToward(this.buffer.position() - this.pointer, alignment); - for (int i = 0; i < objects.length; i++) { - this.buffer.position(arrayStart + stride * i); - definition.writeInto(this, objects[i]); - } - this.buffer.position(arrayStart + stride * objects.length); - this.indexedArrayDefinition = null; - } - - private int indexedArrayStart(BufferObjectLayoutDefinition definition, int alignment, int stride) { - int position = this.buffer.position(); - if (this.indexedArrayDefinition == definition - && this.indexedArrayStride == stride - && position >= this.indexedArrayStart - && position <= this.indexedArrayEnd) { - return this.indexedArrayStart; - } - - this.indexedArrayDefinition = definition; - this.indexedArrayStart = this.pointer + Mth.roundToward(position - this.pointer, alignment); - this.indexedArrayStride = stride; - this.indexedArrayEnd = this.indexedArrayStart; - return this.indexedArrayStart; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430SizeCalculator.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430SizeCalculator.java deleted file mode 100644 index 0a165ef3..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430SizeCalculator.java +++ /dev/null @@ -1,75 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std430; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferSizeCalculator; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; -import net.minecraft.util.Mth; - -public class Std430SizeCalculator implements BufferSizeCalculator { - private int size; - - public Std430SizeCalculator() { - } - - public void align(int alignment) { - this.size = Mth.roundToward(this.size, alignment); - } - - public void putFloat() { - this.align(4); - this.size += 4; - } - - public void putInt() { - this.align(4); - this.size += 4; - } - - public void putVec2() { - this.align(8); - this.size += 8; - } - - public void putIVec2() { - this.align(8); - this.size += 8; - } - - public void putVec3() { - this.align(16); - this.size += 12; - } - - public void putIVec3() { - this.align(16); - this.size += 12; - } - - public void putVec4() { - this.align(16); - this.size += 16; - } - - public void putIVec4() { - this.align(16); - this.size += 16; - } - - public void putMat4f() { - this.align(16); - this.size += 64; - } - - @Override - public void putStructArray(BufferObjectLayoutDefinition definition, int size) { - int alignment = definition.alignment(BufferLayout.STD430); - int stride = Mth.roundToward(definition.size(BufferLayout.STD430), alignment); - this.align(alignment); - this.size += stride * size; - } - - @Override - public int get() { - return size; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430Writer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430Writer.java deleted file mode 100644 index 63ef1369..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/layout/std430/Std430Writer.java +++ /dev/null @@ -1,134 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.std430; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferWriter; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; -import net.minecraft.util.Mth; -import org.joml.Matrix4f; -import org.joml.Vector2f; -import org.joml.Vector2i; -import org.joml.Vector3f; -import org.joml.Vector3i; -import org.joml.Vector4f; -import org.joml.Vector4i; - -import java.nio.ByteBuffer; - -public class Std430Writer implements BufferWriter { - - private final ByteBuffer buffer; - private final int pointer; - private BufferObjectLayoutDefinition indexedArrayDefinition; - private int indexedArrayStart; - private int indexedArrayStride; - private int indexedArrayEnd; - - public Std430Writer(ByteBuffer buffer) { - this.buffer = buffer; - this.pointer = buffer.position(); - } - - public void align(int alignment) { - int position = this.buffer.position(); - this.buffer.position(this.pointer + Mth.roundToward(position - this.pointer, alignment)); - } - - public void putFloat(float value) { - this.align(4); - this.buffer.putFloat(value); - } - - public void putInt(int value) { - this.align(4); - this.buffer.putInt(value); - } - - public void putVec2(Vector2f vec) { - this.align(8); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 8); - } - - public void putIVec2(Vector2i vec) { - this.align(8); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 8); - } - - public void putVec3(Vector3f vec) { - this.align(16); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 12); - } - - public void putIVec3(Vector3i vec) { - this.align(16); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 12); - } - - public void putVec4(Vector4f vec) { - this.align(16); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 16); - } - - public void putIVec4(Vector4i vec) { - this.align(16); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 16); - } - - public void putMat4f(Matrix4f vec) { - this.align(16); - vec.get(this.buffer); - this.buffer.position(this.buffer.position() + 64); - } - - public ByteBuffer intoBuffer() { - return this.buffer.flip(); - } - - @Override - public void putStructArray(int index, E object, BufferObjectLayoutDefinition definition) { - int alignment = definition.alignment(BufferLayout.STD430); - int stride = Mth.roundToward(definition.size(BufferLayout.STD430), alignment); - int arrayStart = this.indexedArrayStart(definition, alignment, stride); - int elementStart = arrayStart + stride * index; - this.buffer.position(elementStart); - definition.writeInto(this, object); - this.indexedArrayEnd = Math.max(this.indexedArrayEnd, elementStart + stride); - this.buffer.position(this.indexedArrayEnd); - } - - @Override - public void putStructArray(E[] objects, BufferObjectLayoutDefinition definition) { - int alignment = definition.alignment(BufferLayout.STD430); - int stride = Mth.roundToward(definition.size(BufferLayout.STD430), alignment); - int arrayStart = this.pointer + Mth.roundToward(this.buffer.position() - this.pointer, alignment); - for (int i = 0; i < objects.length; i++) { - this.buffer.position(arrayStart + stride * i); - definition.writeInto(this, objects[i]); - } - this.buffer.position(arrayStart + stride * objects.length); - this.indexedArrayDefinition = null; - } - - private int indexedArrayStart(BufferObjectLayoutDefinition definition, int alignment, int stride) { - int position = this.buffer.position(); - if ( - this.indexedArrayDefinition == definition - && this.indexedArrayStride == stride - && position >= this.indexedArrayStart - && position <= this.indexedArrayEnd - ) { - return this.indexedArrayStart; - } - - this.indexedArrayDefinition = definition; - this.indexedArrayStart = this.pointer + Mth.roundToward(position - this.pointer, alignment); - this.indexedArrayStride = stride; - this.indexedArrayEnd = this.indexedArrayStart; - return this.indexedArrayStart; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObject.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObject.java deleted file mode 100644 index b11b885f..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObject.java +++ /dev/null @@ -1,31 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers.object; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import javax.annotation.Nonnull; - -import java.nio.ByteBuffer; - -public abstract class BufferObject> { - - protected final BufferLayout layout; - protected final ShaderBufferObjectUsage usage; - - protected BufferObject(BufferLayout layout, ShaderBufferObjectUsage usage) { - this.layout = layout; - this.usage = usage; - } - - protected abstract BufferObjectLayoutDefinition getDefinition(); - - /** - * Writes this buffer object into the provided {@link ByteBuffer} using its layout. - *

    - * Ported from 26.1: the original {@code upload(CommandEncoder, GpuBufferSlice)} (26.1 GPU pipeline) - * and {@code DynamicUniformStorage} are not available on 1.21.1, so the object writes into a - * user-provided buffer instead. - */ - @SuppressWarnings("unchecked") - public void write(@Nonnull ByteBuffer buffer) { - getDefinition().write(buffer, (T) this, this.layout); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutDefinition.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutDefinition.java deleted file mode 100644 index 39235b4a..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutDefinition.java +++ /dev/null @@ -1,51 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers.object; - -import com.google.common.collect.ImmutableList; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferSizeCalculator; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferWriter; - -import java.nio.ByteBuffer; - -public class BufferObjectLayoutDefinition { - private final ImmutableList> entries; - - public BufferObjectLayoutDefinition(ImmutableList> entries) { - this.entries = entries; - } - - public ByteBuffer write(ByteBuffer buffer, T object, BufferLayout layout) { - BufferWriter builder = layout.createWriter(buffer); - this.writeInto(builder, object); - return builder.intoBuffer(); - } - - public void writeInto(BufferWriter writer, T object) { - for (BufferObjectLayoutEntry entry : entries) { - entry.acceptWriter(writer, object); - } - } - - public int size(BufferLayout layout) { - BufferSizeCalculator sizeCalculator = layout.createSizeCalculator(); - for (BufferObjectLayoutEntry entry : entries) { - entry.acceptSizeCalculator(sizeCalculator); - } - return sizeCalculator.get(); - } - - public int alignment(BufferLayout layout) { - int alignment = 1; - for (BufferObjectLayoutEntry entry : entries) { - alignment = Math.max(alignment, entry.alignment(layout)); - } - return layout == BufferLayout.STD140 ? Math.max(alignment, 16) : alignment; - } - - @SafeVarargs - public static BufferObjectLayoutDefinition create(BufferObjectLayoutEntry... entries) { - ImmutableList.Builder> builder = ImmutableList.builder(); - builder.add(entries); - return new BufferObjectLayoutDefinition<>(builder.build()); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntry.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntry.java deleted file mode 100644 index c44fad34..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntry.java +++ /dev/null @@ -1,87 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers.object; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferSizeCalculator; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferWriter; -import org.joml.Matrix4f; -import org.joml.Vector2f; -import org.joml.Vector2i; -import org.joml.Vector3f; -import org.joml.Vector3i; -import org.joml.Vector4f; -import org.joml.Vector4i; - -import java.util.function.Function; - -public record BufferObjectLayoutEntry(BufferObjectLayoutEntryType type, Function getter) { - - void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { - type.acceptSizeCalculator(sizeCalculator); - } - - void acceptWriter(BufferWriter writer, I object) { - type.acceptWriter(writer, getter.apply(object)); - } - - int alignment(BufferLayout layout) { - return type.alignment(layout); - } - - public static Builder builder(BufferObjectLayoutEntryType type) { - return new Builder<>(type); - } - - public static Builder ofInt() { - return builder(BufferObjectLayoutEntryType.INT); - } - - public static Builder ofFloat() { - return builder(BufferObjectLayoutEntryType.FLOAT); - } - - public static Builder ofVec2f() { - return builder(BufferObjectLayoutEntryType.VEC2); - } - - public static Builder ofVec3f() { - return builder(BufferObjectLayoutEntryType.VEC3); - } - - public static Builder ofVec4f() { - return builder(BufferObjectLayoutEntryType.VEC4); - } - - public static Builder ofVec2i() { - return builder(BufferObjectLayoutEntryType.IVEC2); - } - - public static Builder ofVec3i() { - return builder(BufferObjectLayoutEntryType.IVEC3); - } - - public static Builder ofVec4i() { - return builder(BufferObjectLayoutEntryType.IVEC4); - } - - public static Builder ofMat4f() { - return builder(BufferObjectLayoutEntryType.MAT4); - } - - public static class Builder { - private final BufferObjectLayoutEntryType type; - private Function getter; - - public Builder(BufferObjectLayoutEntryType type) { - this.type = type; - } - - public Builder forGetter(Function getter) { - this.getter = getter; - return this; - } - - public BufferObjectLayoutEntry build() { - return new BufferObjectLayoutEntry<>(type, getter); - } - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntryType.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntryType.java deleted file mode 100644 index 59619c46..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/BufferObjectLayoutEntryType.java +++ /dev/null @@ -1,173 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers.object; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferSizeCalculator; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferWriter; -import org.joml.Matrix4f; -import org.joml.Vector2f; -import org.joml.Vector2i; -import org.joml.Vector3f; -import org.joml.Vector3i; -import org.joml.Vector4f; -import org.joml.Vector4i; - -public interface BufferObjectLayoutEntryType { - BufferObjectLayoutEntryType FLOAT = new BufferObjectLayoutEntryType<>() { - @Override - public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { - sizeCalculator.putFloat(); - } - - @Override - public void acceptWriter(BufferWriter writer, Float object) { - writer.putFloat(object); - } - - @Override - public int alignment(BufferLayout layout) { - return 4; - } - }; - - BufferObjectLayoutEntryType VEC2 = new BufferObjectLayoutEntryType<>() { - @Override - public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { - sizeCalculator.putVec2(); - } - - @Override - public void acceptWriter(BufferWriter writer, Vector2f object) { - writer.putVec2(object); - } - - @Override - public int alignment(BufferLayout layout) { - return 8; - } - }; - - BufferObjectLayoutEntryType VEC3 = new BufferObjectLayoutEntryType<>() { - @Override - public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { - sizeCalculator.putVec3(); - } - - @Override - public void acceptWriter(BufferWriter writer, Vector3f object) { - writer.putVec3(object); - } - - @Override - public int alignment(BufferLayout layout) { - return 16; - } - }; - - BufferObjectLayoutEntryType VEC4 = new BufferObjectLayoutEntryType<>() { - @Override - public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { - sizeCalculator.putVec4(); - } - - @Override - public void acceptWriter(BufferWriter writer, Vector4f object) { - writer.putVec4(object); - } - - @Override - public int alignment(BufferLayout layout) { - return 16; - } - }; - - BufferObjectLayoutEntryType INT = new BufferObjectLayoutEntryType<>() { - @Override - public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { - sizeCalculator.putInt(); - } - - @Override - public void acceptWriter(BufferWriter writer, Integer object) { - writer.putInt(object); - } - - @Override - public int alignment(BufferLayout layout) { - return 4; - } - }; - - BufferObjectLayoutEntryType IVEC2 = new BufferObjectLayoutEntryType<>() { - @Override - public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { - sizeCalculator.putIVec2(); - } - - @Override - public void acceptWriter(BufferWriter writer, Vector2i object) { - writer.putIVec2(object); - } - - @Override - public int alignment(BufferLayout layout) { - return 8; - } - }; - - BufferObjectLayoutEntryType IVEC3 = new BufferObjectLayoutEntryType<>() { - @Override - public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { - sizeCalculator.putIVec3(); - } - - @Override - public void acceptWriter(BufferWriter writer, Vector3i object) { - writer.putIVec3(object); - } - - @Override - public int alignment(BufferLayout layout) { - return 16; - } - }; - - BufferObjectLayoutEntryType IVEC4 = new BufferObjectLayoutEntryType<>() { - @Override - public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { - sizeCalculator.putIVec4(); - } - - @Override - public void acceptWriter(BufferWriter writer, Vector4i object) { - writer.putIVec4(object); - } - - @Override - public int alignment(BufferLayout layout) { - return 16; - } - }; - - BufferObjectLayoutEntryType MAT4 = new BufferObjectLayoutEntryType<>() { - @Override - public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { - sizeCalculator.putMat4f(); - } - - @Override - public void acceptWriter(BufferWriter writer, Matrix4f object) { - writer.putMat4f(object); - } - - @Override - public int alignment(BufferLayout layout) { - return 16; - } - }; - - void acceptSizeCalculator(BufferSizeCalculator sizeCalculator); - - void acceptWriter(BufferWriter writer, T object); - - int alignment(BufferLayout layout); -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/ShaderBufferObjectUsage.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/ShaderBufferObjectUsage.java deleted file mode 100644 index 923648bb..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/buffers/object/ShaderBufferObjectUsage.java +++ /dev/null @@ -1,6 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.buffers.object; - -public enum ShaderBufferObjectUsage { - UBO, - SSBO -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/compound/DirtyTracked.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/compound/DirtyTracked.java deleted file mode 100644 index 00ee6fd0..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/foundation/compound/DirtyTracked.java +++ /dev/null @@ -1,5 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.foundation.compound; - -public interface DirtyTracked { - void markDirty(); -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchParametersUbo.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchParametersUbo.java deleted file mode 100644 index b991a51b..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchParametersUbo.java +++ /dev/null @@ -1,32 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.glitch; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutEntry; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.ShaderBufferObjectUsage; -import lombok.Getter; -import lombok.Setter; -import org.joml.Vector2f; - -@Getter -@Setter -public class GlitchParametersUbo extends BufferObject { - - public static final BufferObjectLayoutDefinition DEFINITION = BufferObjectLayoutDefinition.create( - BufferObjectLayoutEntry.ofVec2f().forGetter(GlitchParametersUbo::getInSize).build(), - BufferObjectLayoutEntry.ofFloat().forGetter(GlitchParametersUbo::getGameTime).build() - ); - - private Vector2f inSize = new Vector2f(); - private float gameTime = 0; - - protected GlitchParametersUbo() { - super(BufferLayout.STD140, ShaderBufferObjectUsage.UBO); - } - - @Override - protected BufferObjectLayoutDefinition getDefinition() { - return DEFINITION; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchPostEffect.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchPostEffect.java deleted file mode 100644 index 657d6f45..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/glitch/GlitchPostEffect.java +++ /dev/null @@ -1,83 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.glitch; - -import com.mojang.blaze3d.pipeline.RenderTarget; -import com.mojang.blaze3d.pipeline.TextureTarget; -import com.mojang.blaze3d.vertex.MeshData; -import dev.anvilcraft.lib.v2.rendering.bloom.TransformsUbo; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.postprocess.ALRPostEffectShaders; -import dev.anvilcraft.lib.v2.rendering.postprocess.GlPostProcess; -import dev.anvilcraft.lib.v2.rendering.postprocess.GlUniformBuffer; -import lombok.Getter; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.ShaderInstance; -import org.joml.Matrix4f; - -/** - * Time-driven glitch post-processing effect. - *

    - * Ported from 26.1 (see #P7b): {@code process(GpuTextureView)} becomes - * {@code process(RenderTarget, int, int)} returning a {@link RenderTarget}; the 26.1 - * {@code DeltaTracker} is replaced by {@code Minecraft.getTimer().getGameTimeDeltaPartialTick(false)}. - * The shader and its std140 layout are unchanged. - */ -public class GlitchPostEffect { - public static final int UNIFORM_GLITCH_SIZE = GlitchParametersUbo.DEFINITION.size(BufferLayout.STD140); - public static final int UNIFORM_TRANSFORM_SIZE = TransformsUbo.DEFINITION.size(BufferLayout.STD140); - - @Getter - private final RenderTarget glitchOutputTarget = new TextureTarget(854, 480, false, false); - - private final GlUniformBuffer transformUBO = new GlUniformBuffer(UNIFORM_TRANSFORM_SIZE); - private final GlUniformBuffer glitchParameterUBO = new GlUniformBuffer(UNIFORM_GLITCH_SIZE); - - private final TransformsUbo transform = new TransformsUbo(new Matrix4f()); - private final GlitchParametersUbo glitchParameters = new GlitchParametersUbo(); - private int lastWidth = 0; - private int lastHeight = 0; - private MeshData quadMesh; - - public RenderTarget process(RenderTarget input, int width, int height) { - if (glitchOutputTarget.width < width || glitchOutputTarget.height < height) { - glitchOutputTarget.resize(width, height, false); - } - - Minecraft minecraft = Minecraft.getInstance(); - long gameTime = minecraft.level.getGameTime(); - float partialTick = minecraft.getTimer().getGameTimeDeltaPartialTick(false); - - if (lastHeight != height || lastWidth != width) { - this.lastWidth = width; - this.lastHeight = height; - this.quadMesh = GlPostProcess.buildQuad(width, height, true); - } - - transform.getProjMat().setOrtho( - 0, - glitchOutputTarget.width, - 0, - glitchOutputTarget.height, - -10000, - 10000 - ); - glitchParameters.getInSize().set(width, height); - float uGameTime = (float) (gameTime % 24000L) + partialTick; - uGameTime /= 10f; - glitchParameters.setGameTime(uGameTime); - - transformUBO.upload(transform); - glitchParameterUBO.upload(glitchParameters); - - ShaderInstance shader = ALRPostEffectShaders.getGlitchShader(); - if (shader == null) { - return glitchOutputTarget; - } - GlPostProcess.beginTarget(glitchOutputTarget); - shader.setSampler("DiffuseSampler", input); - GlPostProcess.draw(shader, quadMesh, s -> { - transformUBO.bindTo(s.getId(), "Transforms", 0); - glitchParameterUBO.bindTo(s.getId(), "GlitchParameters", 1); - }); - return glitchOutputTarget; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/IrisSupport.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/IrisSupport.java deleted file mode 100644 index f2e8d561..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/IrisSupport.java +++ /dev/null @@ -1,156 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.integration; - -import net.neoforged.fml.ModList; -import javax.annotation.Nullable; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Stack; - -/** - * Optional integration with Iris (shader mod) for the cached BER pipeline. - *

    - * Ported from 26.1: the API shape ({@code net.irisshaders.iris.api.v0.IrisApi} + - * {@code net.irisshaders.iris.vertices.ImmediateState}) is identical in 1.21.1-era Iris (verified - * against 1.8.12+1.21.1-neoforge). The dev branch used a {@code compileOnly} dependency from the - * Modrinth maven, which has been shut down (the 1.21.1 Iris coordinate can no longer be resolved), so - * the Iris classes are accessed through reflection instead: the integration compiles without Iris and - * activates at runtime only when Iris is actually loaded. The "oculus" branch does not exist on 1.21.1 - * (kept for API parity, always false). - */ -public class IrisSupport { - private static final boolean IRIS_PRESENT; - private static final Stack irisStateStack = new Stack<>(); - - @Nullable private static Method isShaderPackInUseMethod; - @Nullable private static Object irisApiInstance; - @Nullable private static Field isRenderingLevelField; - @Nullable private static Field skipExtensionField; - - static { - boolean present = ModList.get().isLoaded("iris") || ModList.get().isLoaded("oculus"); - if (present) { - try { - Class irisApiClass = Class.forName("net.irisshaders.iris.api.v0.IrisApi"); - Method getInstance = irisApiClass.getMethod("getInstance"); - irisApiInstance = getInstance.invoke(null); - isShaderPackInUseMethod = irisApiClass.getMethod("isShaderPackInUse"); - - Class immediateStateClass = Class.forName("net.irisshaders.iris.vertices.ImmediateState"); - isRenderingLevelField = immediateStateClass.getField("isRenderingLevel"); - skipExtensionField = immediateStateClass.getField("skipExtension"); - } catch (ReflectiveOperationException e) { - // The loaded Iris version does not match the expected class shape: disable the - // integration instead of breaking the cached BER pipeline. - present = false; - irisApiInstance = null; - isShaderPackInUseMethod = null; - isRenderingLevelField = null; - skipExtensionField = null; - } - } - IRIS_PRESENT = present; - } - - public static boolean isIrisPresent() { - return IRIS_PRESENT; - } - - public static void pushIrisGlobalState() { - if (IRIS_PRESENT) { - _pushIrisGlobalState(); - } - } - - public static void popIrisGlobalState() { - if (IRIS_PRESENT) { - _popIrisGlobalState(); - } - } - - public static boolean isShaderEnabled() { - if (IRIS_PRESENT) { - return isShaderEnabledInternal(); - } - return false; - } - - private static void _pushIrisGlobalState() { - irisStateStack.push( - new IrisState( - getIsRenderingLevel(), - getSkipExtension() - ) - ); - setIsRenderingLevel(true); - setSkipExtension(false); - } - - private static void _popIrisGlobalState() { - IrisState peek = irisStateStack.peek(); - if (peek != null) { - irisStateStack.pop(); - setIsRenderingLevel(peek.isRenderingLevel); - setSkipExtension(peek.skipExtension); - } - } - - private static boolean isShaderEnabledInternal() { - try { - return (boolean) isShaderPackInUseMethod.invoke(irisApiInstance); - } catch (ReflectiveOperationException e) { - return false; - } - } - - private static boolean getIsRenderingLevel() { - try { - return isRenderingLevelField != null && (boolean) isRenderingLevelField.get(null); - } catch (IllegalAccessException e) { - return false; - } - } - - private static void setIsRenderingLevel(boolean value) { - try { - if (isRenderingLevelField != null) { - isRenderingLevelField.set(null, value); - } - } catch (IllegalAccessException e) { - // ignore - } - } - - private static boolean getSkipExtension() { - try { - if (skipExtensionField != null) { - @SuppressWarnings("unchecked") - ThreadLocal threadLocal = (ThreadLocal) skipExtensionField.get(null); - return threadLocal != null && threadLocal.get(); - } - } catch (IllegalAccessException e) { - // ignore - } - return false; - } - - private static void setSkipExtension(boolean value) { - try { - if (skipExtensionField != null) { - @SuppressWarnings("unchecked") - ThreadLocal threadLocal = (ThreadLocal) skipExtensionField.get(null); - if (threadLocal != null) { - threadLocal.set(value); - } - } - } catch (IllegalAccessException e) { - // ignore - } - } - - private record IrisState( - boolean isRenderingLevel, - boolean skipExtension - ) { - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/ALRIntegrationCompatMixinPlugin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/ALRIntegrationCompatMixinPlugin.java deleted file mode 100644 index 992593a5..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/ALRIntegrationCompatMixinPlugin.java +++ /dev/null @@ -1,63 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.integration.mixins; - -import com.google.common.collect.ImmutableMap; -import net.neoforged.fml.loading.LoadingModList; -import org.jetbrains.annotations.ApiStatus; -import org.objectweb.asm.tree.ClassNode; -import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; -import org.spongepowered.asm.mixin.extensibility.IMixinInfo; - -import java.util.List; -import java.util.Set; - -@ApiStatus.Internal -public class ALRIntegrationCompatMixinPlugin implements IMixinConfigPlugin { - - private ImmutableMap mixinConditions; - - @Override - public void onLoad(String mixinPackage) { - mixinConditions = ImmutableMap.builder() - .put( - "dev.anvilcraft.lib.v2.rendering.integration.mixins.CachedBlockEntityRenderingPipelineMixin", - isPresent("iris") - ) - .put( - "dev.anvilcraft.lib.v2.rendering.integration.mixins.RebuildTaskMixin", - isPresent("iris") - ) - .build(); - } - - private boolean isPresent(String modid) { - return LoadingModList.get().getModFileById(modid) != null; - } - - @Override - public String getRefMapperConfig() { - return null; - } - - @Override - public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { - Boolean bl = mixinConditions.get(mixinClassName); - return bl == null || bl; - } - - @Override - public void acceptTargets(Set myTargets, Set otherTargets) { - } - - @Override - public List getMixins() { - return null; - } - - @Override - public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { - } - - @Override - public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/CachedBlockEntityRenderingPipelineMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/CachedBlockEntityRenderingPipelineMixin.java deleted file mode 100644 index 5a62d251..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/CachedBlockEntityRenderingPipelineMixin.java +++ /dev/null @@ -1,42 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.integration.mixins; - -import dev.anvilcraft.lib.v2.rendering.cachedber.pipeline.CachedBlockEntityRenderingPipeline; -import dev.anvilcraft.lib.v2.rendering.integration.IrisSupport; -import org.jetbrains.annotations.ApiStatus; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -/** - * Invalidates the cached BER buffers when the Iris shader pack enablement changes (e.g. a shader pack - * is toggled in-game), so the compiled meshes are recompiled under the new pipeline. - *

    - * Gated by {@link ALRIntegrationCompatMixinPlugin} on the "iris" mod being loaded. - */ -@Mixin(CachedBlockEntityRenderingPipeline.class) -@ApiStatus.Internal -public abstract class CachedBlockEntityRenderingPipelineMixin { - - @Shadow - public abstract void forcedUpdate(); - - @Unique - private boolean previousShaderEnabled = false; - - @Inject( - method = "handleIntegration", - at = @At("HEAD") - ) - void handleIris(CallbackInfo ci) { - boolean shaderEnabled = IrisSupport.isShaderEnabled(); - - if (previousShaderEnabled != shaderEnabled) { - this.forcedUpdate(); - } - - this.previousShaderEnabled = shaderEnabled; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/RebuildTaskMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/RebuildTaskMixin.java deleted file mode 100644 index c89b02ef..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/integration/mixins/RebuildTaskMixin.java +++ /dev/null @@ -1,35 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.integration.mixins; - -import dev.anvilcraft.lib.v2.rendering.cachedber.pipeline.RebuildTask; -import dev.anvilcraft.lib.v2.rendering.integration.IrisSupport; -import org.jetbrains.annotations.ApiStatus; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -/** - * Marks the cached BER compile as a "level" Iris render (so Iris extensions such as extended vertex - * formats are skipped for the cached geometry) and restores the previous Iris state afterwards. - *

    - * Gated by {@link ALRIntegrationCompatMixinPlugin} on the "iris" mod being loaded. - */ -@Mixin(RebuildTask.class) -@ApiStatus.Internal -public class RebuildTaskMixin { - @Inject( - method = "run", - at = @At("HEAD") - ) - void handleBegin(CallbackInfo ci) { - IrisSupport.pushIrisGlobalState(); - } - - @Inject( - method = "run", - at = @At("RETURN") - ) - void handleEnd(CallbackInfo ci) { - IrisSupport.popIrisGlobalState(); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/GameRendererMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/GameRendererMixin.java deleted file mode 100644 index f255774d..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/GameRendererMixin.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.mixins; - -import dev.anvilcraft.lib.v2.rendering.event.MainTargetResizeEvent; -import net.minecraft.client.renderer.GameRenderer; -import net.neoforged.fml.ModLoader; -import org.jetbrains.annotations.ApiStatus; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(GameRenderer.class) -@ApiStatus.Internal -public class GameRendererMixin { - - @Inject( - method = "resize", - at = @At("HEAD") - ) - void onResize(int width, int height, CallbackInfo ci) { - ModLoader.postEvent(new MainTargetResizeEvent(width, height)); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/MinecraftMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/MinecraftMixin.java deleted file mode 100644 index a4a8ce63..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/MinecraftMixin.java +++ /dev/null @@ -1,37 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.mixins; - -import dev.anvilcraft.lib.v2.rendering.ALRPostEffects; -import dev.anvilcraft.lib.v2.rendering.cachedber.pipeline.CachedBlockEntityRenderingPipeline; -import net.minecraft.client.Minecraft; -import net.minecraft.client.main.GameConfig; -import net.minecraft.client.multiplayer.ClientLevel; -import org.jetbrains.annotations.ApiStatus; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(Minecraft.class) -@ApiStatus.Internal -public class MinecraftMixin { - - @Inject( - method = "", - at = @At("RETURN") - ) - private void onCreateInstance(GameConfig gameConfig, CallbackInfo ci) { - // #P7b: creates the post-processing effects (bloom/glitch) once the main render target exists. - ALRPostEffects.createPostEffects(); - // #P7d: initializes the cached BER pipeline (GL debug label support). - CachedBlockEntityRenderingPipeline.create(); - } - - @Inject( - method = "updateLevelInEngines(Lnet/minecraft/client/multiplayer/ClientLevel;)V", - at = @At("RETURN") - ) - private void onUpdateLevel(ClientLevel level, CallbackInfo ci) { - // #P7d: swap the cached BER pipeline instance when the client level changes. - CachedBlockEntityRenderingPipeline.updateLevel(level); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/RenderTypeMixin.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/RenderTypeMixin.java deleted file mode 100644 index 42894676..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/mixins/RenderTypeMixin.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.mixins; - -import dev.anvilcraft.lib.v2.rendering.extension.ALRRenderTypeExtension; -import net.minecraft.client.renderer.RenderType; -import org.jetbrains.annotations.ApiStatus; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; - -@Mixin(RenderType.class) -@ApiStatus.Internal -public class RenderTypeMixin implements ALRRenderTypeExtension { - - @Unique - private boolean anvillib_rendering$bloomRendering = false; - - public boolean anvillib_rendering$bloomRendering() { - return anvillib_rendering$bloomRendering; - } - - public void anvillib_rendering$setBloomRendering(boolean value) { - this.anvillib_rendering$bloomRendering = value; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/ALRPostEffectShaders.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/ALRPostEffectShaders.java deleted file mode 100644 index f4905b6e..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/ALRPostEffectShaders.java +++ /dev/null @@ -1,66 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.postprocess; - -import com.mojang.blaze3d.vertex.DefaultVertexFormat; -import dev.anvilcraft.lib.v2.rendering.AnvilLibRendering; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.minecraft.client.renderer.ShaderInstance; -import net.neoforged.neoforge.client.event.RegisterShadersEvent; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; - -/** - * Holder for the custom {@link ShaderInstance}s used by the post-processing pipeline - * (bloom downsample/upsample/apply, gaussian blur, glitch). - *

    - * Ported from 26.1: the original declared the shaders as {@code RenderPipeline}s through - * {@code RegisterRenderPipelinesEvent}; on 1.21.1 they are registered as core shaders through - * {@link RegisterShadersEvent} (same pattern as the wheel module's {@code LibShaders}). - * The std140 uniform blocks are bound manually through {@link GlUniformBuffer}. - */ -@ApiStatus.Internal -@Slf4j -public final class ALRPostEffectShaders { - @Getter - private static @Nullable ShaderInstance downSampleShader; - @Getter - private static @Nullable ShaderInstance upSampleShader; - @Getter - private static @Nullable ShaderInstance applyBloomShader; - @Getter - private static @Nullable ShaderInstance blurShader; - @Getter - private static @Nullable ShaderInstance glitchShader; - - private ALRPostEffectShaders() { - } - - public static void register(RegisterShadersEvent event) { - try { - event.registerShader( - new ShaderInstance(event.getResourceProvider(), AnvilLibRendering.location("down_sample"), DefaultVertexFormat.POSITION_TEX), - it -> downSampleShader = it - ); - event.registerShader( - new ShaderInstance(event.getResourceProvider(), AnvilLibRendering.location("up_sample"), DefaultVertexFormat.POSITION_TEX), - it -> upSampleShader = it - ); - event.registerShader( - new ShaderInstance(event.getResourceProvider(), AnvilLibRendering.location("apply_bloom"), DefaultVertexFormat.POSITION_TEX), - it -> applyBloomShader = it - ); - event.registerShader( - new ShaderInstance(event.getResourceProvider(), AnvilLibRendering.location("blur"), DefaultVertexFormat.POSITION_TEX), - it -> blurShader = it - ); - event.registerShader( - new ShaderInstance(event.getResourceProvider(), AnvilLibRendering.location("glitch"), DefaultVertexFormat.POSITION_TEX), - it -> glitchShader = it - ); - } catch (IOException e) { - log.error("Failed to register post-processing shaders", e); - } - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlPostProcess.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlPostProcess.java deleted file mode 100644 index 6df81094..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlPostProcess.java +++ /dev/null @@ -1,67 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.postprocess; - -import com.mojang.blaze3d.pipeline.RenderTarget; -import com.mojang.blaze3d.platform.GlStateManager; -import com.mojang.blaze3d.vertex.BufferUploader; -import com.mojang.blaze3d.vertex.DefaultVertexFormat; -import com.mojang.blaze3d.vertex.MeshData; -import com.mojang.blaze3d.vertex.Tesselator; -import com.mojang.blaze3d.vertex.VertexFormat; -import net.minecraft.client.renderer.ShaderInstance; -import org.jetbrains.annotations.ApiStatus; - -import java.util.function.Consumer; - -/** - * Shared helpers for the 1.21.1 port of the post-processing pipeline. - *

    - * Replaces the 26.1 {@code GpuDevice}/{@code CommandEncoder}/{@code RenderPass} execution layer with the - * classic {@code RenderTarget} + {@code ShaderInstance} + fullscreen-quad ({@code BufferUploader}/{@code Tesselator}) - * path. The std140 uniform blocks declared by the GLSL are fed through {@link GlUniformBuffer}. - */ -@ApiStatus.Internal -public final class GlPostProcess { - private GlPostProcess() { - } - - /** - * Binds {@code target} as the current framebuffer, sets the viewport and clears its color attachment. - */ - public static void beginTarget(RenderTarget target) { - target.bindWrite(true); - GlStateManager._clearColor(0.0F, 0.0F, 0.0F, 1.0F); - GlStateManager._clear(16384, false); - } - - /** - * Applies the given shader (binding its samplers), runs {@code setup} (e.g. UBO binding), - * draws the fullscreen quad and clears the shader state again. - */ - public static void draw(ShaderInstance shader, MeshData quad, Consumer setup) { - shader.apply(); - setup.accept(shader); - BufferUploader.draw(quad); - shader.clear(); - } - - /** - * Builds a fullscreen quad mesh covering {@code [0,width]x[0,height]} in {@code POSITION_TEX} format. - * The {@code flipV} parameter controls the vertical texture coordinate orientation. - */ - public static MeshData buildQuad(int width, int height, boolean flipV) { - Tesselator tesselator = Tesselator.getInstance(); - var builder = tesselator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); - if (flipV) { - builder.addVertex(0, 0, 100).setUv(0, 1); - builder.addVertex(0, height, 100).setUv(0, 0); - builder.addVertex(width, height, 100).setUv(1, 0); - builder.addVertex(width, 0, 100).setUv(1, 1); - } else { - builder.addVertex(0, 0, 100).setUv(0, 0); - builder.addVertex(0, height, 100).setUv(0, 1); - builder.addVertex(width, height, 100).setUv(1, 1); - builder.addVertex(width, 0, 100).setUv(1, 0); - } - return builder.buildOrThrow(); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlUniformBuffer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlUniformBuffer.java deleted file mode 100644 index abe7d206..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/postprocess/GlUniformBuffer.java +++ /dev/null @@ -1,57 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.postprocess; - -import com.mojang.blaze3d.platform.GlStateManager; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; -import org.lwjgl.opengl.GL31; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * Self-managed GL uniform buffer object (UBO) used to feed {@code layout(std140) uniform} blocks - * of the post-processing shaders. - *

    - * Ported from 26.1: the original relied on {@code GpuDevice.createBuffer} + - * {@code CommandEncoder.writeToTexture}/{@code DynamicUniformStorage}, which do not exist on 1.21.1. - * The data is still produced by the shared {@link BufferObject}/{@code Std140Writer} layout system - * (see {@code foundation.buffers}), only the upload path now uses raw LWJGL buffer management. - */ -public final class GlUniformBuffer implements AutoCloseable { - private final int bufferId; - private final int size; - - public GlUniformBuffer(int size) { - this.size = size; - this.bufferId = GlStateManager._glGenBuffers(); - GlStateManager._glBindBuffer(GL31.GL_UNIFORM_BUFFER, this.bufferId); - GlStateManager._glBufferData(GL31.GL_UNIFORM_BUFFER, size, GL31.GL_STREAM_DRAW); - } - - /** - * Writes the given buffer object into this UBO using its std140 layout and uploads it. - */ - public void upload(BufferObject object) { - ByteBuffer buffer = ByteBuffer.allocate(this.size).order(ByteOrder.nativeOrder()); - object.write(buffer); - GlStateManager._glBindBuffer(GL31.GL_UNIFORM_BUFFER, this.bufferId); - GlStateManager._glBufferData(GL31.GL_UNIFORM_BUFFER, buffer, GL31.GL_STREAM_DRAW); - } - - /** - * Binds this UBO to the uniform block {@code blockName} of the given program at {@code bindingPoint}. - * The block index is looked up per program; if the program does not declare the block, nothing happens. - */ - public void bindTo(int programId, String blockName, int bindingPoint) { - int blockIndex = GL31.glGetUniformBlockIndex(programId, blockName); - if (blockIndex == GL31.GL_INVALID_INDEX) { - return; - } - GL31.glUniformBlockBinding(programId, blockIndex, bindingPoint); - GL31.glBindBufferRange(GL31.GL_UNIFORM_BUFFER, bindingPoint, this.bufferId, 0L, this.size); - } - - @Override - public void close() { - GlStateManager._glDeleteBuffers(this.bufferId); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/Sdf2d.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/Sdf2d.java deleted file mode 100644 index 96c3ef8a..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/Sdf2d.java +++ /dev/null @@ -1,379 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.sdf; - -import lombok.experimental.UtilityClass; -import net.minecraft.util.Mth; -import org.jetbrains.annotations.NotNull; - -@UtilityClass -public class Sdf2d { - - /** - * @param params Sdf 参数 - * @param pX 点 x 坐标 - * @param pY 点 y 坐标 - * @param rX 矩形 x 起始点 - * @param rY 矩形 y 起始点 - * @param rotation 旋转角度 - * @param centred 居中 - * @return 点距离 sdf 的距离 - */ - public static float sd( - @NotNull SdfParameters params, - float pX, - float pY, - float rX, - float rY, - float rotation, - boolean centred - - ) { - var rect = params.getRect(); - var round = params.getRound(); - var smooth = params.getSmooth(); - var stroke = params.getStroke(); - - var ex = (round + smooth + stroke) * 2.0f; - - var width = rect.z + ex; - var height = rect.w + ex; - final var hw = width * 0.5f; - final var hh = height * 0.5f; - - var radian = rotation * Mth.DEG_TO_RAD; - var cos = Mth.cos(radian); - var sin = Mth.sin(radian); - - float cx; - float cy; - - if (centred) { - - cx = rX; - cy = rY; - - } else { - - cx = rX + hw - hw * cos + hh * sin; - cy = rY + hh - hw * sin - hh * cos; - } - - var px = pX - cx; - var py = pY - cy; - - if (rotation != 0f) { - - var tx = px * cos + py * sin; - var ty = py * cos - px * sin; - - px = tx; - py = ty; - } - - - var type = params.getRenderType(); - var shape = params.getShapeParams(); - - var d = switch (type) { - case BOX -> sdRect( - px, py, - shape.x - round, - shape.y - round - ) - round; - - case CIRCLE -> sdCircle( - px, py, - shape.x - ); - - case ARC -> sdArc( - px, py, - shape.x, shape.y, - shape.z, shape.w - ) - round; - - case SECTOR -> sdRing( - px, py, - shape.x, shape.y, - shape.z, shape.w - ) - round; - - case PIE -> sdPie( - px, py, - shape.x, shape.y, - shape.z - ); - - case SEGMENT -> sdSegment( - px, py, - shape.x, shape.y, - shape.z, shape.w - ) - round; - - case CAPSULE -> sdUnevenCapsule( - px, py, - shape.x, shape.y, - shape.z - ); - - case EGG -> sdEgg( - px, py, - shape.x, shape.y, - shape.z - ); - - case TRIANGLE_EQUILATERAL -> sdEquilateralTriangle( - px, py, - shape.x - round * 2.0f - ) - round; - - case TRIANGLE_ISOSCELES -> sdIsoscelesTriangle( - px, - py - (shape.y * 0.5f - round * 2.0f), - shape.x - round, - round * 2.0f - shape.y - ) - round; - - default -> Float.POSITIVE_INFINITY; - }; - - if (params.isOnion()) { - var half = stroke * 0.5f; - d = Mth.abs(d) - half; - } - - return d; - } - - public static float sdRect( - float px, float py, - float bx, float by - ) { - - float dx = Math.abs(px) - bx; - float dy = Math.abs(py) - by; - - float mx = Math.max(dx, 0.0f); - float my = Math.max(dy, 0.0f); - - return (float) Mth.length(mx, my) + - Math.min(Math.max(dx, dy), 0.0f); - } - - public static float sdCircle( - float px, float py, - float r - ) { - return (float) Mth.length(px, py) - r; - } - - public static float sdArc( - float px, float py, - float scx, float scy, - float ra, float rb - ) { - - px = Math.abs(px); - - float result; - - if (scy * px > scx * py) { - var dx = px - scx * ra; - var dy = py - scy * ra; - result = (float) Mth.length(dx, dy); - } else { - result = Math.abs((float) Mth.length(px, py) - ra); - } - - return result - rb; - } - - public static float sdRing( - float px, float py, - float nx, float ny, - float r, float th - ) { - - px = Math.abs(px); - - float rx = nx * px + (-ny) * py; - float ry = ny * px + nx * py; - - float a = Math.abs((float) Mth.length(rx, ry) - r) - th * 0.5f; - - float by = Math.max(0.0f, - Math.abs(r - ry) - th * 0.5f); - - float b = (float) Mth.length(rx, by) * - Mth.sign(rx); - - return Math.max(a, b); - } - - public static float sdPie( - float px, float py, - float cx, float cy, - float r - ) { - - px = Math.abs(px); - - float l = (float) Mth.length(px, py) - r; - - float dot = px * cx + py * cy; - float clamped = Mth.clamp(dot, 0.0f, r); - - float mx = px - cx * clamped; - float my = py - cy * clamped; - - float m = (float) Mth.length(mx, my); - - return Math.max( - l, - m * Mth.sign(cy * px - cx * py) - ); - } - - public static float sdUnevenCapsule( - float px, float py, - float r1, - float r2, - float h - ) { - - px = Math.abs(px); - - float b = (r1 - r2) / h; - float a = (float)Math.sqrt(1.0f - b * b); - float k = px * (-b) + py * a; - - if (k < 0.0f) { - return (float) Mth.length(px, py) - r1; - } - - if (k > a * h) { - - float dx = px; - float dy = py - h; - - return (float) Mth.length(dx, dy) - r2; - } - - return px * a + py * b - r1; - } - - public static float sdEgg( - float px, float py, - float he, - float ra, - float rb - ) { - - float ce = - 0.5f * ( - he * he - - (ra - rb) * (ra - rb) - ) / (ra - rb); - - px = Math.abs(px); - - if (py < 0.0f) { - - return (float) Mth.length(px, py) - ra; - } - - if (py * ce - px * he > he * ce) { - - return (float) Mth.length( - px, - py - he - ) - rb; - } - - return (float) Mth.length( - px + ce, - py - ) - (ce + ra); - } - - public static float sdSegment( - float px, float py, - float ax, float ay, - float bx, float by - ) { - float bax = bx - ax; - float bay = by - ay; - float pax = px - ax; - float pay = py - ay; - float dot = pax * bax + pay * bay; - float h = Mth.clamp( - dot / (bax * bax + bay * bay), - 0.0f, - 1.0f - ); - - return (float) Mth.length( - pax - h * bax, - pay - h * bay - ); - } - - public static float sdEquilateralTriangle( - float px, float py, - float r - ) { - final float k = (float) Math.sqrt(3.0); - - px = Math.abs(px) - r; - py = py + r / k; - - if (px + k * py > 0.0f) { - var tx = (px - k * py) * 0.5f; - var ty = (-k * px - py) * 0.5f; - px = tx; - py = ty; - } - - px -= Mth.clamp(px, -2.0f * r, 0.0f); - - return -(float) Mth.length(px, py) * Mth.sign(py); - } - - public static float sdIsoscelesTriangle( - float px, float py, - float qx, float qy - ) { - px = Math.abs(px); - - float dotQ = qx * qx + qy * qy; - float h1 = Mth.clamp( - (px * qx + py * qy) / dotQ, - 0.0f, 1.0f - ); - float ax = px - qx * h1; - float ay = py - qy * h1; - - float h2; - if (qx != 0.0f) { - h2 = Mth.clamp(px / qx, 0.0f, 1.0f); - } else { - h2 = px > 0.0f ? 1.0f : 0.0f; - } - float bx = px - qx * h2; - float by = py - qy; - - float s = -Mth.sign(qy); - - float da = ax * ax + ay * ay; - float sa = s * (px * qy - py * qx); - - float db = bx * bx + by * by; - float sb = s * (py - qy); - - float dDist = Math.min(da, db); - float dSign = Math.min(sa, sb); - - return -(float) Math.sqrt(dDist) - * Mth.sign(dSign); - } - -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfGraphics.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfGraphics.java deleted file mode 100644 index 08beeb6d..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfGraphics.java +++ /dev/null @@ -1,568 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.sdf; - -import com.mojang.blaze3d.vertex.DefaultVertexFormat; -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; -import com.mojang.blaze3d.vertex.VertexFormat; -import com.mojang.math.Axis; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.postprocess.GlUniformBuffer; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.renderer.RenderStateShard; -import net.minecraft.client.renderer.RenderType; -import net.minecraft.client.renderer.ShaderInstance; -import net.minecraft.util.FastColor; -import net.minecraft.util.Mth; -import org.jetbrains.annotations.NotNull; -import org.lwjgl.opengl.GL31; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * Draws 2D shapes (circle/rect/rounded rect/...) on top of a {@link GuiGraphics} using the nine SDF - * distance functions declared in {@code sdf_graphics.fsh}. - *

    - * Ported from 26.1 (see #P7c): the original submitted {@code LibGuiElementRenderState}s through the - * 26.1 GUI render pipeline (with {@code Matrix3x2f} poses and a {@code GpuDevice} UBO); on 1.21.1 the - * shapes are appended as 4-vertex quads into the {@link GuiGraphics} {@code BufferSource} using a custom - * {@link RenderType} bound to the {@code sdf_graphics} {@link ShaderInstance} (registered through - * {@code RegisterShadersEvent}, see {@link SdfShaders}). Per-shape parameters are uploaded into a - * self-managed {@code layout(std140) uniform SDFParameters} UBO (one slot per quad, referenced through - * {@code UV1.x}); the UBO is bound through {@code glBindBufferRange} right before the batch is drawn. - *

    - * The {@code mixins/GuiRendererMixin} SDF injection of the 26.1 branch is not ported: its target - * class ({@code GuiRenderer}) does not exist on 1.21.1. {@link #flush()} is instead invoked automatically - * at the start of every frame (see {@code AnvilLibRendering}). - */ -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public final class SdfGraphics { - private static final int MAX_SDF_AMOUNT = 256; - private static final int MAX_SHARED_SDF_AMOUNT = 64; - private static final int SDF_PARAMETER_SIZE = SdfParameters.DEFINITION.size(BufferLayout.STD140); - /** - * GL uniform buffer binding point used by the SDF parameters block. The post-processing pipeline - * ({@link GlUniformBuffer}) uses points 0 and 1, so 2 is free for the SDF UBO. - */ - private static final int UBO_BINDING_POINT = 2; - - @Getter - public static final SdfGraphics instance = new SdfGraphics(new SdfParameters()); - - private static boolean debug = false; - - private final SdfParameters[] shared = new SdfParameters[MAX_SHARED_SDF_AMOUNT]; - - private final SdfParameters parameters; - - /** Self-managed {@code SDFParameters} UBO (all 256 slots). Created lazily on the first draw. */ - private static int uboId = -1; - private static final ByteBuffer SLOT_BUFFER = ByteBuffer.allocateDirect(SDF_PARAMETER_SIZE).order(ByteOrder.nativeOrder()); - - private int shareCursor = 0; - - private float x, y, rotation; - private int color = -1; - private boolean centred = false; - - private static int index; - - private static final RenderType SDF_RENDER_TYPE = RenderType.create( - "anvillib_rendering:sdf_graphics", - DefaultVertexFormat.POSITION_COLOR_TEX_LIGHTMAP, - VertexFormat.Mode.QUADS, - 1536, - false, - false, - RenderType.CompositeState.builder() - .setShaderState(new RenderStateShard.ShaderStateShard(SdfGraphics::bindAndGetShader)) - .setTransparencyState(RenderStateShard.TRANSLUCENT_TRANSPARENCY) - .setDepthTestState(RenderStateShard.NO_DEPTH_TEST) - .setCullState(RenderStateShard.NO_CULL) - .setWriteMaskState(RenderStateShard.COLOR_WRITE) - .createCompositeState(false) - ); - - public SdfGraphics box(float x, float y, float width, float height) { - this.parameters.getRect().set(0, 0, width, height); - this.parameters.box(width, height); - this.x = x; - this.y = y; - - return this; - } - - public SdfGraphics circle(float x, float y, float radius) { - this.parameters.getRect().set(0, 0, radius * 2, radius * 2); - this.parameters.circle(radius); - this.x = x; - this.y = y; - - return this; - } - - public SdfGraphics arc(float x, float y, float sweep, float radius, float width) { - var scale = radius * 2 + width; - - this.parameters.getRect().set(0, 0, scale, scale); - this.parameters.arc(sweep, radius, width); - this.x = x; - this.y = y; - - return this; - } - - public SdfGraphics sector(float x, float y, float sweep, float radius, float width) { - this.parameters.getRect().set(0, 0, radius * 2, radius * 2); - this.parameters.sector(sweep, radius, width); - this.x = x; - this.y = y; - - return this; - } - - public SdfGraphics pie(float x, float y, float sweep, float radius) { - this.parameters.getRect().set(0, 0, radius * 2, radius * 2); - this.parameters.pie(sweep, radius); - this.x = x; - this.y = y; - - return this; - } - - public SdfGraphics capsule( - float x, float y, - float topRadius, float bottomRadius, - float height - ) { - var width = Math.max(topRadius, bottomRadius) * 2; - - this.parameters.getRect().set(0, 0, width, height + (topRadius + bottomRadius) * 3); - this.parameters.capsule(topRadius, bottomRadius, height); - this.x = x; - this.y = y; - - return this; - } - - public SdfGraphics egg( - float x, float y, - float topRadius, float bottomRadius, - float height - ) { - var width = Math.max(topRadius, bottomRadius) * 2; - - this.parameters.getRect().set(0, 0, width, height + (topRadius + bottomRadius) * 2); - this.parameters.egg(topRadius, bottomRadius, height); - this.x = x; - this.y = y; - - return this; - } - - public SdfGraphics segment( - float x0, float y0, - float x1, float y1 - ) { - var left = Math.min(x0, x1); - var top = Math.min(y0, y1); - var width = Math.abs(x1 - x0); - var height = Math.abs(y1 - y0); - - var halfWidth = (x1 - x0) * 0.5f; - var halfHeight = (y1 - y0) * 0.5f; - - this.parameters.getRect().set(left + width / 2, top + height / 2, width, height); - this.parameters.segment(-halfWidth, -halfHeight, +halfWidth, +halfHeight); - this.x = left + width / 2; - this.y = top + height / 2; - - return this; - } - - public SdfGraphics triangleEquilateral(float x, float y, float radius) { - var actual = radius * (1.0f / 1.2f); - - this.parameters.getRect().set(0, 0, radius * 2, radius * 2); - this.parameters.triangleEquilateral(actual); - this.x = x; - this.y = y; - - return this; - } - - public SdfGraphics triangleIsosceles(float x, float y, float width, float height) { - final var factor = 1.0f / 1.2f; - this.parameters.getRect().set(0, 0, width * 2.0f, height); - this.parameters.triangleIsosceles(width * factor, height * factor); - this.x = x; - this.y = y; - - return this; - } - - public SdfGraphics color(int color) { - this.color = color; - return this; - } - - public SdfGraphics color(float red, float green, float blue, float alpha) { - this.color = FastColor.ARGB32.color( - (int) (alpha * 255.0F), - (int) (red * 255.0F), - (int) (green * 255.0F), - (int) (blue * 255.0F) - ); - return this; - } - - public SdfGraphics color(int red, int green, int blue, int alpha) { - this.color = FastColor.ARGB32.color(alpha, red, green, blue); - return this; - } - - public SdfGraphics smooth(float radius) { - var v = Math.max(0.0f, radius); - this.parameters.smooth(v); - return this; - } - - public SdfGraphics round(float radius) { - var v = Math.max(0.0f, radius); - this.parameters.round(v); - return this; - } - - public SdfGraphics stroke(float width) { - var v = Math.max(0.0f, width * 0.5f); - this.parameters.stroke(v); - this.parameters.onion(width > 0.0f); - return this; - } - - public SdfGraphics rotate(float degrees) { - this.rotation = degrees; - return this; - } - - public SdfGraphics center(boolean center) { - this.centred = center; - return this; - } - - public SdfGraphics onion(boolean onion) { - this.parameters.onion(onion); - return this; - } - - public SdfGraphics fill() { - this.parameters.fill(); - return this; - } - - public SdfGraphics light(float radius) { - this.parameters.light(radius); - return this; - } - - public SdfGraphics draw( - @NotNull GuiGraphics graphics - ) { - _draw( - graphics, - this.parameters, - this.x, - this.y, - this.rotation, - this.color, - this.centred - ); - return this; - } - - public SdfGraphics draw( - @NotNull GuiGraphics graphics, - @NotNull SdfParameters parameters, - float x, - float y - ) { - - if (parameters.isShared()) { - _draw( - graphics, - parameters, - x, - y, - this.rotation, - this.color, - this.centred - ); - } - - return this; - } - - public SdfGraphics reset() { - this.parameters.reset(); - this.x = 0; - this.y = 0; - this.rotation = 0; - this.color = -1; - this.centred = false; - return this; - } - - public boolean collide(float x, float y, float threshold) { - return this.collide( - this.parameters, - x, - y, - this.x, - this.y, - threshold - ); - } - - public boolean collide( - @NotNull SdfParameters parameters, - float pointX, - float pointY, - float x, - float y, - float threshold - ) { - return Sdf2d.sd( - parameters, - pointX, - pointY, - x, - y, - this.rotation, - this.centred - ) < threshold; - } - - public @NotNull SdfParameters cache() { - return this.parameters.duplicate(); - } - - public @NotNull SdfParameters share() { - final var cache = this.cache(); - this.share(cache); - return cache; - } - - public void share(@NotNull SdfParameters cache) { - final var cursor = this.shareCursor; - if (cursor == MAX_SHARED_SDF_AMOUNT) { - return; - } - - this.shared[cursor] = cache; - cache.uboIndex = MAX_SDF_AMOUNT - cursor - 1; - cache.uploaded = false; - - do { - this.shareCursor++; - } while (this.shareCursor < MAX_SHARED_SDF_AMOUNT && this.shared[this.shareCursor] != null); - } - - public void unshare(@NotNull SdfParameters cache) { - var cursor = 0; - // find index - for (; cursor < MAX_SHARED_SDF_AMOUNT; cursor++) { - if (this.shared[cursor] == cache) { - cache.uploaded = false; - cache.uboIndex = -1; - break; - } - } - - if (cursor == MAX_SHARED_SDF_AMOUNT) { - return; - } - - this.shared[cursor] = null; - if (cursor < this.shareCursor) { - this.shareCursor = cursor; - } - } - - /** - * Resets the per-frame SDF slot cursor (and the singleton's parameters). Called automatically at the - * start of every frame; kept public for manual use inside screens. - */ - public static void flush() { - instance.parameters.reset(); - SdfGraphics.index = 0; - } - - public static void debug(boolean enable) { - SdfGraphics.debug = enable; - } - - /** - * Lazily creates the SDF parameters UBO and (re)binds it to the SDF uniform block of the current - * shader program. This runs as the {@code ShaderStateShard} supplier, i.e. right before the SDF - * batch is drawn, so the binding is guaranteed to be current at draw time even if another pipeline - * touched the binding point in between. - */ - private static ShaderInstance bindAndGetShader() { - ShaderInstance shader = SdfShaders.getSdfGraphicsShader(); - if (shader == null) { - return null; - } - if (uboId == -1) { - uboId = GL31.glGenBuffers(); - GL31.glBindBuffer(GL31.GL_UNIFORM_BUFFER, uboId); - GL31.glBufferData(GL31.GL_UNIFORM_BUFFER, (long) SDF_PARAMETER_SIZE * MAX_SDF_AMOUNT, GL31.GL_DYNAMIC_DRAW); - } - int programId = shader.getId(); - int blockIndex = GL31.glGetUniformBlockIndex(programId, "SDFParameters"); - if (blockIndex != GL31.GL_INVALID_INDEX) { - GL31.glUniformBlockBinding(programId, blockIndex, UBO_BINDING_POINT); - } - GL31.glBindBufferRange( - GL31.GL_UNIFORM_BUFFER, - UBO_BINDING_POINT, - uboId, - 0L, - (long) SDF_PARAMETER_SIZE * MAX_SDF_AMOUNT - ); - return shader; - } - - private static void _draw( - @NotNull GuiGraphics graphics, - @NotNull SdfParameters parameters, - float x, - float y, - float rotation, - int color, - boolean centred - ) { - var round = parameters.getRound(); - var smooth = parameters.getSmooth(); - var stroke = parameters.getStroke(); - - var rect = parameters.getRect(); - var z = rect.z; - var w = rect.w; - - var pose = graphics.pose(); - var ex = (round + smooth + stroke) * 2.0f; - var width = rect.z + ex; - var height = rect.w + ex; - final var hw = width * 0.5f; - final var hh = height * 0.5f; - - final var radian = rotation * Mth.DEG_TO_RAD; - final var cos = Mth.cos(radian); - final var sin = Mth.sin(radian); - - float cx; - float cy; - pose.pushPose(); - if (centred) { - pose.translate(x, y, 0.0f); - - cx = x; - cy = y; - } else { - pose.translate( - x + hw, - y + hh, - 0.0f - ); - - cx = x + hw - hw * cos + hh * sin; - cy = y + hh - hw * sin - hh * cos; - } - - if (rotation != 0.0f) { - pose.mulPose(Axis.ZP.rotationDegrees(rotation)); - } - - if (!centred) { - pose.translate(-hw, -hh, 0.0f); - } - - pose.scale(width, height, 1.0f); - - rect.z = width; - rect.w = height; - - var extX = Mth.abs(hw * cos) + Mth.abs(hh * sin) + 1.0f; - var extY = Mth.abs(hw * sin) + Mth.abs(hh * cos) + 1.0f; - - var x0 = cx - extX; - var x1 = cx + extX; - var y0 = cy - extY; - var y1 = cy + extY; - - var shared = parameters.isShared(); - var idx = shared ? parameters.uboIndex : index; - - // If the sdf_graphics shader failed to register, don't append quads: a RenderType whose - // shader supplier returns null would NPE inside BufferUploader at flush time. - if (SdfShaders.getSdfGraphicsShader() == null) { - pose.popPose(); - rect.z = z; - rect.w = w; - return; - } - - if (debug) { - graphics.renderOutline( - (int) x0, (int) y0, - (int) (x1 - x0), (int) (y1 - y0), - 0xFF0000FF - ); - } - - if (idx >= MAX_SDF_AMOUNT) { - pose.popPose(); - rect.z = z; - rect.w = w; - return; - } - - if (!shared || !parameters.uploaded) { - upload(parameters, idx); - parameters.uploaded = shared; - } - - var poseEntry = pose.last(); - VertexConsumer consumer = graphics.bufferSource().getBuffer(SDF_RENDER_TYPE); - consumer.addVertex(poseEntry, -0.5f, -0.5f, 0.0f).setUv(0, 0).setUv1(idx, 0).setColor(color); - consumer.addVertex(poseEntry, -0.5f, +0.5f, 0.0f).setUv(0, 1).setUv1(idx, 0).setColor(color); - consumer.addVertex(poseEntry, +0.5f, +0.5f, 0.0f).setUv(1, 1).setUv1(idx, 0).setColor(color); - consumer.addVertex(poseEntry, +0.5f, -0.5f, 0.0f).setUv(1, 0).setUv1(idx, 0).setColor(color); - - pose.popPose(); - rect.z = z; - rect.w = w; - - if (!shared) { - SdfGraphics.index++; - } - } - - private static void upload(@NotNull SdfParameters parameters, int slot) { - if (uboId == -1) { - return; - } - SLOT_BUFFER.clear(); - parameters.write(SLOT_BUFFER); - GL31.glBindBuffer(GL31.GL_UNIFORM_BUFFER, uboId); - GL31.glBufferSubData( - GL31.GL_UNIFORM_BUFFER, - (long) slot * SDF_PARAMETER_SIZE, - SLOT_BUFFER - ); - GL31.glBindBuffer(GL31.GL_UNIFORM_BUFFER, 0); - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfParameters.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfParameters.java deleted file mode 100644 index 2ee2ea8f..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfParameters.java +++ /dev/null @@ -1,202 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.sdf; - -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.layout.BufferLayout; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutDefinition; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObjectLayoutEntry; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.BufferObject; -import dev.anvilcraft.lib.v2.rendering.foundation.buffers.object.ShaderBufferObjectUsage; -import lombok.Getter; -import net.minecraft.util.Mth; -import org.joml.Vector4f; -import org.joml.Vector4i; - -@Getter -public class SdfParameters extends BufferObject { - - public static final BufferObjectLayoutDefinition DEFINITION = BufferObjectLayoutDefinition.create( - BufferObjectLayoutEntry.ofVec4f().forGetter(SdfParameters::getSharedParams).build(), - BufferObjectLayoutEntry.ofVec4f().forGetter(SdfParameters::getShapeParams).build(), - BufferObjectLayoutEntry.ofVec4f().forGetter(SdfParameters::getRect).build(), - BufferObjectLayoutEntry.ofVec4i().forGetter(SdfParameters::getTypeParams).build() - ); - - private final Vector4f sharedParams = new Vector4f(); - private final Vector4f shapeParams = new Vector4f(); - private final Vector4f rect = new Vector4f(); - private final Vector4i typeParams = new Vector4i(); - - int uboIndex = -1; - boolean uploaded = false; - - protected SdfParameters() { - super(BufferLayout.STD140, ShaderBufferObjectUsage.UBO); - } - - public void box(float width, float height) { - this ._renderType(SdfRenderType.BOX); - this.shapeParams .set(width * 0.5f, height * 0.5f, 0.0f, 0.0f); - } - - public void circle(float radius) { - this ._renderType(SdfRenderType.CIRCLE); - this.shapeParams .set(radius, 0.0f, 0.0f, 0.0f); - } - - public void arc(float sweep, float radius, float thickness) { - this ._renderType(SdfRenderType.ARC); - - var radian = sweep * Mth.DEG_TO_RAD; - var cos = Mth.cos(radian); - var sin = Mth.sin(radian); - this.shapeParams .set(sin, cos, radius - thickness * 0.5f, thickness); - } - - public void sector(float sweep, float radius, float thickness) { - this ._renderType(SdfRenderType.SECTOR); - - var radian = sweep * Mth.DEG_TO_RAD; - var cos = Mth.cos(radian); - var sin = Mth.sin(radian); - this.shapeParams .set(cos, sin, radius - thickness * 0.5f, thickness); - } - - public void pie(float sweep, float radius) { - this ._renderType(SdfRenderType.PIE); - - var radian = sweep * Mth.DEG_TO_RAD; - var cos = Mth.cos(radian); - var sin = Mth.sin(radian); - this.shapeParams .set(cos, sin, radius, 0.0f); - } - - public void capsule(float topRadius, float bottomRadius, float height) { - this ._renderType(SdfRenderType.CAPSULE); - this.shapeParams .set(topRadius, bottomRadius, height, 0.0f); - } - - public void egg(float topRadius, float bottomRadius, float height) { - this ._renderType(SdfRenderType.EGG); - this.shapeParams .set(height, bottomRadius, topRadius, 0.0f); - } - - public void segment(float x0, float y0, float x1, float y1) { - this ._renderType(SdfRenderType.SEGMENT); - this.shapeParams .set(x0, y0, x1, y1); - } - - public void triangleEquilateral(float radius) { - this ._renderType(SdfRenderType.TRIANGLE_EQUILATERAL); - this.shapeParams .set(radius, 0.0f, 0.0f, 0.0f); - } - - public void triangleIsosceles(float base, float height) { - this ._renderType(SdfRenderType.TRIANGLE_ISOSCELES); - this.shapeParams .set(base, height, 0.0f, 0.0f); - } - - public void smooth(float smooth) { - this ._smooth(smooth); - } - - public void stroke(float width) { - this ._width(width); - } - - public void round(float radius) { - this ._cornerRadius(radius); - } - - public void fill() { - this ._pass(SdfPassType.FILL); - } - - public void onion(boolean enable) { - this.typeParams.z = enable ? 1 : 0; - } - - public void light(float v) { - this ._pass(SdfPassType.LIGHT); - this ._smooth(v); - this ._light(v); - } - - public SdfParameters duplicate() { - var copy = new SdfParameters(); - - copy.sharedParams .set(this.sharedParams); - copy.shapeParams .set(this.shapeParams); - copy.rect .set(this.rect); - copy.typeParams .set(this.typeParams); - - return copy; - } - - public float getSmooth() { - return this.sharedParams.x; - } - - public float getStroke() { - return this.sharedParams.y; - } - - public float getRound() { - return this.sharedParams.z; - } - - public SdfRenderType getRenderType() { - return SdfRenderType - .fromOrdinal(this.typeParams.y); - } - - public boolean isOnion() { - return this.typeParams.z != 0; - } - - private void _smooth(float value) { - this.sharedParams.x = value; - this.uploaded = false; - } - - private void _light(float value) { - this.sharedParams.w = 4.605f / value; - this.uploaded = false; - } - - private void _width(float value) { - this.sharedParams.y = value; - this.uploaded = false; - } - - private void _cornerRadius(float value) { - this.sharedParams.z = value; - this.uploaded = false; - } - - private void _pass(SdfPassType value) { - this.typeParams.x = value.ordinal(); - this.uploaded = false; - } - - private void _renderType(SdfRenderType value) { - this.typeParams.y = value.ordinal(); - this.uploaded = false; - } - - @Override - protected BufferObjectLayoutDefinition getDefinition() { - return DEFINITION; - } - - public void reset() { - this.sharedParams .set(0.0f, 0.0f, 0.0f, 0.0f); - this.shapeParams .set(0.0f, 0.0f, 0.0f, 0.0f); - this.rect .set(0.0f, 0.0f, 0.0f, 0.0f); - this.typeParams .set(0, 0, 0, 4); - - this.uploaded = false; - } - - public boolean isShared() { - return this.uboIndex != -1; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfPassType.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfPassType.java deleted file mode 100644 index 24fc13e4..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfPassType.java +++ /dev/null @@ -1,6 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.sdf; - -public enum SdfPassType { - FILL, - LIGHT -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfRenderType.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfRenderType.java deleted file mode 100644 index f55b43d3..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfRenderType.java +++ /dev/null @@ -1,22 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.sdf; - -public enum SdfRenderType { - BOX, - CIRCLE, - ARC, - SECTOR, - PIE, - CAPSULE, - EGG, - SEGMENT, - - TRIANGLE_EQUILATERAL, - TRIANGLE_ISOSCELES, - ; - - private static final SdfRenderType[] VALUES = values(); - - public static SdfRenderType fromOrdinal(int ordinal) { - return VALUES[ordinal]; - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfShaders.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfShaders.java deleted file mode 100644 index ab76d134..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfShaders.java +++ /dev/null @@ -1,46 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.sdf; - -import com.mojang.blaze3d.vertex.DefaultVertexFormat; -import dev.anvilcraft.lib.v2.rendering.AnvilLibRendering; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.minecraft.client.renderer.ShaderInstance; -import net.neoforged.neoforge.client.event.RegisterShadersEvent; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; - -/** - * Holder for the custom {@link ShaderInstance} used by {@link SdfGraphics}. - *

    - * Ported from 26.1: the original declared the SDF pipeline through {@code RenderPipeline} - * ({@code RegisterRenderPipelinesEvent}); on 1.21.1 it is registered as a core shader through - * {@link RegisterShadersEvent} (same pattern as the wheel module's {@code LibShaders}). - * The {@code layout(std140) uniform SDFParameters} block is fed by a self-managed GL UBO - * (see {@link SdfGraphics}), bound through {@code glBindBufferRange} at draw time. - */ -@ApiStatus.Internal -@Slf4j -public final class SdfShaders { - @Getter - private static @Nullable ShaderInstance sdfGraphicsShader; - - private SdfShaders() { - } - - public static void register(RegisterShadersEvent event) { - try { - event.registerShader( - new ShaderInstance( - event.getResourceProvider(), - AnvilLibRendering.location("sdf_graphics"), - DefaultVertexFormat.POSITION_COLOR_TEX_LIGHTMAP - ), - it -> sdfGraphicsShader = it - ); - } catch (IOException e) { - log.error("Failed to register sdf_graphics shader", e); - } - } -} diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/util/Timer.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/util/Timer.java deleted file mode 100644 index ab22da58..00000000 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/util/Timer.java +++ /dev/null @@ -1,9 +0,0 @@ -package dev.anvilcraft.lib.v2.rendering.util; - -import net.minecraft.client.Minecraft; - -public class Timer { - public static float getPartialTick() { - return Minecraft.getInstance().getTimer().getGameTimeDeltaPartialTick(Minecraft.getInstance().isPaused()); - } -} diff --git a/module.rendering/src/main/resources/META-INF/accesstransformer.cfg b/module.rendering/src/main/resources/META-INF/accesstransformer.cfg deleted file mode 100644 index 703b0d0f..00000000 --- a/module.rendering/src/main/resources/META-INF/accesstransformer.cfg +++ /dev/null @@ -1,5 +0,0 @@ -# #P7a: exposes CompositeRenderType.state() for ALRRenderTypeExtension.copyWithBloom (RenderType.create(name, format, mode, bufferSize, state) on 1.21.1). -public net.minecraft.client.renderer.RenderType$CompositeRenderType -public net.minecraft.client.renderer.RenderType$CompositeRenderType state()Lnet/minecraft/client/renderer/RenderType$CompositeState; -# #P7c: makes ShaderStateShard constructible outside net.minecraft.client.renderer for the custom sdf_graphics RenderType used by SdfGraphics. -public net.minecraft.client.renderer.RenderStateShard$ShaderStateShard (Ljava/util/function/Supplier;)V diff --git a/module.rendering/src/main/resources/META-INF/neoforge.mods.toml b/module.rendering/src/main/resources/META-INF/neoforge.mods.toml deleted file mode 100644 index 508ab566..00000000 --- a/module.rendering/src/main/resources/META-INF/neoforge.mods.toml +++ /dev/null @@ -1,90 +0,0 @@ -# This is an example mods.toml file. It contains the data relating to the loading mods. -# There are several mandatory fields (#mandatory), and many more that are optional (#optional). -# The overall format is standard TOML format, v0.5.0. -# Note that there are a couple of TOML lists in this file. -# Find more information on toml format here: https://github.com/toml-lang/toml -# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml -modLoader = "javafml" #mandatory -# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. -loaderVersion = "${loader_version_range}" #mandatory -# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. -# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. -license = "${mod_license}" -# A URL to refer people to when problems occur with this mod -#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional -# A list of mods - how many allowed here is determined by the individual mod loader -[[mods]] #mandatory -# The modid of the mod -modId = "${mod_id}" #mandatory -# The version number of the mod -version = "${mod_version}" #mandatory -# A display name for the mod -displayName = "${mod_name}" #mandatory -# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ -#updateJSONURL="https://change.me.example.invalid/updates.json" #optional -# A URL for the "homepage" for this mod, displayed in the mod UI -displayURL="https://github.com/Anvil-Dev/AnvilLib" -# A file name (in the root of the mod JAR) containing a logo for display -logoFile="icon.png" #optional -# A text field displayed in the mod UI -#credits="" #optional -# A text field displayed in the mod UI -authors = "${mod_authors}" #optional -# Display Test controls the display for your mod in the server connection screen -# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. -# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. -# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. -# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. -# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. -#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) - -# The description text for the mod (multi line!) (#mandatory) -description = '''${mod_description}''' - -# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. -[[mixins]] -config = "${mod_id}.mixins.json" - -# Iris-optional integration mixins for the cached BER pipeline (gated by ALRIntegrationCompatMixinPlugin). -[[mixins]] -config = "${mod_id}.integration.mixins.json" - -# The [[accessTransformers]] block allows you to declare where your AT file is. -# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg -#[[accessTransformers]] -#file="META-INF/accesstransformer.cfg" - -# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json - -# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. -[[dependencies."${mod_id}"]] #optional -# the modid of the dependency -modId = "neoforge" #mandatory -# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). -# 'required' requires the mod to exist, 'optional' does not -# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning -type = "required" #mandatory -# Optional field describing why the dependency is required or why it is incompatible -# reason="..." -# The version range of the dependency -versionRange = "${neo_version_range}" #mandatory -# An ordering relationship for the dependency. -# BEFORE - This mod is loaded BEFORE the dependency -# AFTER - This mod is loaded AFTER the dependency -ordering = "NONE" -# Side this dependency is applied on - BOTH, CLIENT, or SERVER -side = "BOTH" -# Here's another dependency -[[dependencies."${mod_id}"]] -modId = "minecraft" -type = "required" -# This version range declares a minimum of the current minecraft version up to but not including the next major version -versionRange = "${minecraft_version_range}" -ordering = "NONE" -side = "BOTH" - -# Features are specific properties of the game environment, that you may want to declare you require. This example declares -# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't -# stop your mod loading on the server for example. -#[features."${mod_id}"] -#openGLVersion="[3.2,)" diff --git a/module.rendering/src/main/resources/anvillib_rendering.integration.mixins.json b/module.rendering/src/main/resources/anvillib_rendering.integration.mixins.json deleted file mode 100644 index 2622e009..00000000 --- a/module.rendering/src/main/resources/anvillib_rendering.integration.mixins.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "required": true, - "minVersion": "0.8", - "package": "dev.anvilcraft.lib.v2.rendering.integration.mixins", - "compatibilityLevel": "JAVA_8", - "refmap": "anvillib.refmap.json", - "plugin": "dev.anvilcraft.lib.v2.rendering.integration.mixins.ALRIntegrationCompatMixinPlugin", - "mixins": [ - ], - "client": [ - "CachedBlockEntityRenderingPipelineMixin", - "RebuildTaskMixin" - ], - "injectors": { - "defaultRequire": 1 - } -} diff --git a/module.rendering/src/main/resources/anvillib_rendering.mixins.json b/module.rendering/src/main/resources/anvillib_rendering.mixins.json deleted file mode 100644 index 950b510f..00000000 --- a/module.rendering/src/main/resources/anvillib_rendering.mixins.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "required": true, - "minVersion": "0.8", - "package": "dev.anvilcraft.lib.v2.rendering.mixins", - "compatibilityLevel": "JAVA_8", - "refmap": "anvillib.refmap.json", - "mixins": [ - ], - "client": [ - "GameRendererMixin", - "MinecraftMixin", - "RenderTypeMixin" - ], - "injectors": { - "defaultRequire": 1 - } -} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.fsh deleted file mode 100644 index 6389d901..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.fsh +++ /dev/null @@ -1,23 +0,0 @@ -#version 330 - -layout(std140) uniform BloomParameters { - float BloomIntensity; - float BloomBlendThreshold; - float LuminanceSensitivity; -}; - -uniform sampler2D GameSampler; -uniform sampler2D DiffuseSampler; - -in vec2 texCoord; - -out vec4 fragColor; - -void main() { - vec4 colorGame = texture(GameSampler, texCoord); - vec3 color = colorGame.rgb; - vec4 bloom4 = texture(DiffuseSampler, texCoord); - vec3 bloom = bloom4.rgb * BloomIntensity; - vec3 finalColor = color + (bloom * pow(BloomBlendThreshold, length(color) * LuminanceSensitivity)); - fragColor = vec4(finalColor, colorGame.a + bloom4.a); -} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.json b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.json deleted file mode 100644 index 038c2b8a..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/apply_bloom.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "vertex": "anvillib_rendering:blit", - "fragment": "anvillib_rendering:apply_bloom", - "samplers": [ - { "name": "GameSampler" }, - { "name": "DiffuseSampler" } - ], - "uniforms": [] -} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blit.vsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blit.vsh deleted file mode 100644 index b4c4ff95..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blit.vsh +++ /dev/null @@ -1,16 +0,0 @@ -#version 330 - -layout(std140) uniform Transforms { - mat4 ProjMat; -}; - -in vec3 Position; -in vec2 UV0; - -out vec2 texCoord; - -void main() { - gl_Position = ProjMat * vec4(Position, 1.0); - - texCoord = UV0; -} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.fsh deleted file mode 100644 index df2c9796..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.fsh +++ /dev/null @@ -1,36 +0,0 @@ -#version 330 - -#moj_import - -layout(std140) uniform BlurParameters { - vec2 BlurDir; - float SampleStepLength; - float ColorMultiplier; -}; - -uniform sampler2D DiffuseSampler; - -const float weight[7] = float[] ( - 0.1827621234, - 0.1566532716, - 0.1220589867, - 0.0998664144, - 0.0130542649, - 0.0089357635, - 0.0080502373 -); - -in vec2 texCoord; - -out vec4 fragColor; - -void main() { - vec2 texOffset = 1.0 / textureSize(DiffuseSampler, 0); - - vec3 result = texture(DiffuseSampler, texCoord).rgb * weight[0]; - for (int i = 1; i < 7; ++i) { - result += texture(DiffuseSampler, texCoord + vec2(BlurDir.x * texOffset.x * i * SampleStepLength, BlurDir.y * texOffset.y * i * SampleStepLength)).rgb * weight[i]; - result += texture(DiffuseSampler, texCoord - vec2(BlurDir.x * texOffset.x * i * SampleStepLength, BlurDir.y * texOffset.y * i * SampleStepLength)).rgb * weight[i]; - } - fragColor = vec4(saturate(result * ColorMultiplier), 1.0); -} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.json b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.json deleted file mode 100644 index 7a7e73d0..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/blur.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "vertex": "anvillib_rendering:blit", - "fragment": "anvillib_rendering:blur", - "samplers": [ - { "name": "DiffuseSampler" } - ], - "uniforms": [] -} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.fsh deleted file mode 100644 index 04ac5dee..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.fsh +++ /dev/null @@ -1,26 +0,0 @@ -#version 330 - -uniform sampler2D DiffuseSampler; - -layout(std140) uniform BloomParameters { - vec2 uResolution; - int uFrameIndex; -}; - -in vec2 texCoord; -out vec4 fragColor; - -void main() { - vec2 uv = texCoord; - - vec2 texel = uResolution; - vec4 bloom = - texture(DiffuseSampler, uv + vec2(-0.5, -0.5) * texel) + - texture(DiffuseSampler, uv + vec2(0.5, -0.5) * texel) + - texture(DiffuseSampler, uv + vec2(-0.5, 0.5) * texel) + - texture(DiffuseSampler, uv + vec2(0.5, 0.5) * texel); - - bloom *= 0.25; - - fragColor = bloom; -} \ No newline at end of file diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.json b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.json deleted file mode 100644 index 15bf6c51..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/down_sample.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "vertex": "anvillib_rendering:blit", - "fragment": "anvillib_rendering:down_sample", - "samplers": [ - { "name": "DiffuseSampler" } - ], - "uniforms": [] -} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.fsh deleted file mode 100644 index 3deee49a..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.fsh +++ /dev/null @@ -1,51 +0,0 @@ -#version 330 - -uniform sampler2D DiffuseSampler; - -layout(std140) uniform GlitchParameters { - vec2 InSize; - float GameTime; -}; - -in vec2 texCoord; -out vec4 fragColor; - -float hash(vec2 p) { - return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); -} - -void main() { - vec4 color = texture(DiffuseSampler, texCoord); - - // 亮度提取 — 用于蓝色染色混合 - float luminance = dot(color.rgb, vec3(0.299, 0.587, 0.114)); - - // 扫描线间距与行号 - float lineSpacing = 4.0; - float lineY = floor((texCoord.y * InSize.y) / lineSpacing); - - // 基于行号和时间变化的随机值 - float lineHash = hash(vec2(lineY, floor(GameTime * 3.0))); - - // 扫描线亮度变化 (0.7 ~ 1.0) - float scanBrightness = 0.7 + 0.3 * lineHash; - - // 约30%的扫描线有水平抖动 - float jitterStrength = 2.0; - float jitter = (lineHash - 0.5) * 2.0 * jitterStrength / InSize.x; - float shouldJitter = step(0.7, hash(vec2(lineY, 42.0))); - - // 带水平偏移的采样 - vec2 jitteredCoord = texCoord + vec2(jitter * shouldJitter, 0.0); - vec4 jitteredColor = texture(DiffuseSampler, jitteredCoord); - - // 蓝色染色 - vec3 blueTint = vec3(0.2, 0.45, 1.0); - float jitteredLum = dot(jitteredColor.rgb, vec3(0.299, 0.587, 0.114)); - vec3 finalColor = mix(jitteredColor.rgb, blueTint * jitteredLum, 0.75); - - // 应用扫描线亮度 - finalColor *= scanBrightness; - - fragColor = vec4(finalColor, 0.85 * color.a); -} \ No newline at end of file diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.json b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.json deleted file mode 100644 index fa67d802..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/glitch.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "vertex": "anvillib_rendering:blit", - "fragment": "anvillib_rendering:glitch", - "samplers": [ - { "name": "DiffuseSampler" } - ], - "uniforms": [] -} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh deleted file mode 100644 index 76f5c52d..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh +++ /dev/null @@ -1,194 +0,0 @@ -#version 150 - -#define MAX_SDFS 256 - -struct Sdf { - vec4 Shared; - vec4 Shape; - vec4 Rect; - ivec4 Types; -}; - -layout(std140) uniform SDFParameters { - Sdf[MAX_SDFS] SDFs; -}; - -#define RT_BOX 0 -#define RT_CIRCLE 1 -#define RT_ARC 2 -#define RT_SECTOR 3 -#define RT_PIE 4 -#define RT_UCAPSULE 5 -#define RT_EGG 6 -#define RT_SEGMENT 7 -#define RT_ETRIANGLE 8 -#define RT_ITRIANGLE 9 - -#define PASS_FILL 0 -#define PASS_LIGHT 1 - -in vec2 vPosition; -in vec4 vColor; -flat in int vIndex; - -out vec4 fragColor; - -#define uSmoothRadius (params.Shared.x) -#define uStrokeWidth (params.Shared.y) -#define uCornerRadius (params.Shared.z) -#define uLightDecay (params.Shared.w) - -#define uPassType (params.Types.x) -#define uRenderType (params.Types.y) -#define uOnion (params.Types.z) - -// from https://iquilezles.org/articles/distfunctions2d/ -float sdRect( in vec2 p, in vec2 b ) { - vec2 d = abs(p)-b; - return length(max(d,0.0)) + min(max(d.x,d.y),0.0); -} - -// from https://iquilezles.org/articles/distfunctions2d/ -float sdCircle(vec2 p, float r) { - return length(p) - r; -} - -// from https://iquilezles.org/articles/distfunctions2d/ -float sdArc(in vec2 p, in vec2 sc, in float ra, in float rb) { - // sc is the sin/cos of the arc's aperture - p.x = abs(p.x); - return ((sc.y*p.x>sc.x*p.y) ? length(p-sc*ra) : - abs(length(p)-ra)) - rb; -} - -// from https://iquilezles.org/articles/distfunctions2d/ -float sdRing(in vec2 p, in vec2 n, in float r, in float th) { - p.x = abs(p.x); - p = mat2x2(n.x,n.y,-n.y,n.x)*p; - return max( abs(length(p)-r)-th*0.5, - length(vec2(p.x,max(0.0,abs(r-p.y)-th*0.5)))*sign(p.x) ); -} - -// from https://iquilezles.org/articles/distfunctions2d/ -float sdPie(in vec2 p, in vec2 c, in float r) { - p.x = abs(p.x); - float l = length(p) - r; - float m = length(p-c*clamp(dot(p,c),0.0,r)); // c=sin/cos of aperture - return max(l,m*sign(c.y*p.x-c.x*p.y)); -} - -// from https://iquilezles.org/articles/distfunctions2d/ -float sdUnevenCapsule( vec2 p, float r1, float r2, float h ) -{ - p.x = abs(p.x); - float b = (r1-r2)/h; - float a = sqrt(1.0-b*b); - float k = dot(p,vec2(-b,a)); - if( k < 0.0 ) return length(p) - r1; - if( k > a*h ) return length(p-vec2(0.0,h)) - r2; - return dot(p, vec2(a,b) ) - r1; -} - -// from https://iquilezles.org/articles/distfunctions2d/ -float sdEgg( in vec2 p, in float he, in float ra, in float rb ) -{ - // all this can be precomputed for any given shape - float ce = 0.5*(he*he-(ra-rb)*(ra-rb))/(ra-rb); - - // only this needs to be run per pixel - p.x = abs(p.x); - if( p.y<0.0 ) return length(p)-ra; - if( p.y*ce-p.x*he>he*ce ) return length(vec2(p.x,p.y-he))-rb; - return length(vec2(p.x+ce,p.y))-(ce+ra); -} - -// from https://iquilezles.org/articles/distfunctions2d/ -float sdSegment( in vec2 p, in vec2 a, in vec2 b ) -{ - vec2 ba = b-a; - vec2 pa = p-a; - float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 ); - return length(pa-h*ba); -} - -// from https://iquilezles.org/articles/distfunctions2d/ -float sdEquilateralTriangle( in vec2 p, in float r ) -{ - const float k = sqrt(3.0); - p.x = abs(p.x) - r; - p.y = p.y + r/k; - if( p.x+k*p.y>0.0 ) p = vec2(p.x-k*p.y,-k*p.x-p.y)/2.0; - p.x -= clamp( p.x, -2.0*r, 0.0 ); - return -length(p)*sign(p.y); -} - -// from https://iquilezles.org/articles/distfunctions2d/ -float sdIsoscelesTriangle( in vec2 p, in vec2 q ) -{ - p.x = abs(p.x); - vec2 a = p - q*clamp( dot(p,q)/dot(q,q), 0.0, 1.0 ); - vec2 b = p - q*vec2( clamp( p.x/q.x, 0.0, 1.0 ), 1.0 ); - float s = -sign( q.y ); - vec2 d = min( vec2( dot(a,a), s*(p.x*q.y-p.y*q.x) ), - vec2( dot(b,b), s*(p.y-q.y) )); - return -sqrt(d.x)*sign(d.y); -} - -void main() { - Sdf params = SDFs[vIndex]; - vec4 shape = params.Shape; - vec2 p = vPosition; - float alpha = 0.0; - - float d = 1e5; - - float r = uCornerRadius; - float r2 = r + r; - switch (uRenderType) { - case RT_BOX: - d = sdRect(p, shape.xy - vec2(r)) - r; - break; - case RT_CIRCLE: - d = sdCircle(p, shape.x); - break; - case RT_ARC: - d = sdArc(p, shape.xy, shape.z, shape.w) - r; - break; - case RT_SECTOR: - d = sdRing(p, shape.xy, shape.z, shape.w) - r; - break; - case RT_PIE: - d = sdPie(p, shape.xy, shape.z); - break; - case RT_UCAPSULE: - d = sdUnevenCapsule(p, shape.x, shape.y, shape.z); - break; - case RT_EGG: - d = sdEgg(p, shape.x, shape.y, shape.z); - break; - case RT_SEGMENT: - d = sdSegment(p, shape.xy, shape.zw) - r; - break; - case RT_ETRIANGLE: - d = sdEquilateralTriangle(p, shape.x - r2) - r; - break; - case RT_ITRIANGLE: - d = sdIsoscelesTriangle(p - vec2(0.0, shape.y * 0.5 - r2), vec2(shape.x - r, r2 - shape.y)) - r; - break; - } - - float aa = max(fwidth(d) * 0.5, uSmoothRadius); - float halfWidth = uStrokeWidth * 0.5; - float useOnion = float(uOnion); // int to float - d = mix(d, abs(d) - halfWidth, useOnion); - - float useLight = float(uPassType); // now only have two passes, so just directly int to float - float fillAlpha = smoothstep(0.0 + aa, 0.0 - aa, d); - float lightAlpha = useLight * exp(-d * uLightDecay); - - alpha = mix(fillAlpha, lightAlpha, useLight); - - vec4 color = vColor; - color.a *= alpha; - fragColor = color; -} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.json b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.json deleted file mode 100644 index 92512715..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "vertex": "anvillib_rendering:sdf_graphics", - "fragment": "anvillib_rendering:sdf_graphics", - "samplers": [ - ], - "uniforms": [ - { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, - { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] } - ] -} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.vsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.vsh deleted file mode 100644 index 0651a71b..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.vsh +++ /dev/null @@ -1,35 +0,0 @@ -#version 150 - -#define MAX_SDFS 256 - -struct Sdf { - vec4 Shared; - vec4 Shape; - vec4 Rect; - ivec4 Types; -}; - -layout(std140) uniform SDFParameters { - Sdf[MAX_SDFS] SDFs; -}; - -in vec3 Position; -in vec4 Color; -in vec2 UV0; -in ivec2 UV1; - -uniform mat4 ModelViewMat; -uniform mat4 ProjMat; - -out vec2 vPosition; -out vec4 vColor; -flat out int vIndex; - -void main() { - gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); - - int index = UV1.x; - vPosition = (UV0 - 0.5) * SDFs[index].Rect.zw; - vColor = Color; - vIndex = index; -} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.fsh deleted file mode 100644 index df5500a5..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.fsh +++ /dev/null @@ -1,61 +0,0 @@ -#version 330 - -uniform sampler2D DiffuseSampler; -uniform sampler2D PreviousSampler; - -layout(std140) uniform BloomParameters { - vec2 uResolution; - int uFrameIndex; -}; - -in vec2 texCoord; -out vec4 fragColor; - -#define R 2 -#define KERNEL_SIZE 5 - -const float GAUSSIAN_KERNEL[25] = float[]( - 0.002968, 0.013304, 0.021945, 0.013304, 0.002968, - 0.013304, 0.059634, 0.098320, 0.059634, 0.013304, - 0.021945, 0.098320, 0.162102, 0.098320, 0.021945, - 0.013304, 0.059634, 0.098320, 0.059634, 0.013304, - 0.002968, 0.013304, 0.021945, 0.013304, 0.002968 -); - -vec4 samplerGaussian( - sampler2D tex, - vec2 texCoord, - vec2 stride -) { - vec4 color = vec4(0.0); - - for (int y = -R; - y <= R; - y ++ - ) { - for ( - int x = -R; - x <= R; - x ++ - ) { - int idx = (y + R) * KERNEL_SIZE + (x + R); - float w = GAUSSIAN_KERNEL[idx]; - - vec2 uv = texCoord + vec2(x, y) * stride; - color += texture(tex, uv) * w; - } - } - - return color; -} - -void main() { -// vec2 noiseUV = mod(texCoord.xy + vec2(uFrameIndex * 17), 128.0) * 0.0078125; - - vec2 curStride = uResolution; - vec2 lstStride = curStride * 0.5; - - vec4 curMipped = samplerGaussian(DiffuseSampler, texCoord, curStride); - vec4 prvMipped = samplerGaussian(PreviousSampler, texCoord, lstStride); - fragColor = curMipped * 0.5 + prvMipped * 0.9; -} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.json b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.json deleted file mode 100644 index f20e8687..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/up_sample.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "vertex": "anvillib_rendering:blit", - "fragment": "anvillib_rendering:up_sample", - "samplers": [ - { "name": "DiffuseSampler" }, - { "name": "PreviousSampler" } - ], - "uniforms": [] -} diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/include/util.glsl b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/include/util.glsl deleted file mode 100644 index 32ac3b04..00000000 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/include/util.glsl +++ /dev/null @@ -1,13 +0,0 @@ - -vec3 saturate(vec3 color) { - return vec3(clamp(color.r, 0, 1), clamp(color.g, 0, 1), clamp(color.b, 0, 1)); -} - -float toneMap(float f) { - return - (f - 1) * (f - 1) + 1; -} - -vec3 toneMap(vec3 f) { - return vec3(toneMap(f.r), toneMap(f.g), toneMap(f.b)); -} - diff --git a/module.rendering/src/main/resources/icon.png b/module.rendering/src/main/resources/icon.png deleted file mode 100644 index 15304bd54d5d97d8482049d5f16a7544748ce05a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1089 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabRA=0U}gyL32}YB&qp?9cb}fRhL)ab z>**&8P1h&;i5V@+5z~{9lu=Mp{cG+rNOuP4jFK;J~t_xRpI&Nf4%?X zZmY?-7zkw=;bC zsjT>Uk~ZUn{GF#RHtMc+sGWYc$A>9-;igj6hzoLO7wEVa*QxB)zM=ff)bhV&`;h}b zD&$S4O;$O;Sf&`=w_-tNu&sr4nCukimfSM|=84-o^=H0sFgVR)oOSu;mLIn4lUH@E zPWkvN(C^lx3(G6wS;EsJIMe%PuP~i06!s*lLHg)A!3fTUY`(JJ860vaMb2DUI{zB$ z25}G0l9!&RrHovbwaBi{I35yXA*NOEX}{&%4{Sd|jrPVzaJhpb&(qb TEST_VEC2 = new BufferObjectLayoutEntryType<>() { - @Override - public void acceptSizeCalculator(BufferSizeCalculator sizeCalculator) { - sizeCalculator.putVec2(); - } - - @Override - public void acceptWriter(BufferWriter writer, Object object) { - } - - @Override - public int alignment(BufferLayout layout) { - return 8; - } - }; - - private Std140LayoutRulesTest() { - } - - public static void main(String[] args) { - BufferObjectLayoutDefinition structDefinition = BufferObjectLayoutDefinition.create( - BufferObjectLayoutEntry.builder(TEST_VEC2).forGetter(ignored -> null).build(), - BufferObjectLayoutEntry.ofFloat().forGetter(TestStruct::value).build() - ); - - assertEquals(16, structDefinition.alignment(BufferLayout.STD140), "std140 struct alignment"); - - Std140SizeCalculator calculator = new Std140SizeCalculator(); - calculator.putStructArray(structDefinition, 2); - - assertEquals(32, calculator.get(), "std140 struct array size"); - - BufferObjectLayoutDefinition compactStructDefinition = BufferObjectLayoutDefinition.create( - BufferObjectLayoutEntry.ofFloat().forGetter(TestStruct::value).build(), - BufferObjectLayoutEntry.ofFloat().forGetter(TestStruct::value).build() - ); - - Std430SizeCalculator std430Calculator = new Std430SizeCalculator(); - std430Calculator.putStructArray(compactStructDefinition, 2); - - assertEquals(16, std430Calculator.get(), "std430 struct array size"); - - BufferObjectLayoutDefinition pairDefinition = BufferObjectLayoutDefinition.create( - BufferObjectLayoutEntry.ofFloat().forGetter(TestPair::first).build(), - BufferObjectLayoutEntry.ofFloat().forGetter(TestPair::second).build() - ); - - ByteBuffer std140Buffer = ByteBuffer.allocate(64); - Std140Writer std140Writer = new Std140Writer(std140Buffer); - std140Writer.putStructArray(new TestPair[]{ - new TestPair(1.0f, 2.0f), - new TestPair(3.0f, 4.0f) - }, pairDefinition); - - assertFloatEquals(1.0f, std140Buffer.getFloat(0), "std140 array[0].first"); - assertFloatEquals(2.0f, std140Buffer.getFloat(4), "std140 array[0].second"); - assertFloatEquals(3.0f, std140Buffer.getFloat(16), "std140 array[1].first"); - assertFloatEquals(4.0f, std140Buffer.getFloat(20), "std140 array[1].second"); - - ByteBuffer std430Buffer = ByteBuffer.allocate(64); - Std430Writer std430Writer = new Std430Writer(std430Buffer); - std430Writer.putStructArray(new TestPair[]{ - new TestPair(1.0f, 2.0f), - new TestPair(3.0f, 4.0f) - }, pairDefinition); - - assertFloatEquals(1.0f, std430Buffer.getFloat(0), "std430 array[0].first"); - assertFloatEquals(2.0f, std430Buffer.getFloat(4), "std430 array[0].second"); - assertFloatEquals(3.0f, std430Buffer.getFloat(8), "std430 array[1].first"); - assertFloatEquals(4.0f, std430Buffer.getFloat(12), "std430 array[1].second"); - - ByteBuffer std140IndexedBuffer = ByteBuffer.allocate(64); - Std140Writer std140IndexedWriter = new Std140Writer(std140IndexedBuffer); - std140IndexedWriter.putStructArray(1, new TestPair(5.0f, 6.0f), pairDefinition); - - assertFloatEquals(0.0f, std140IndexedBuffer.getFloat(0), "std140 indexed array[0].first"); - assertFloatEquals(5.0f, std140IndexedBuffer.getFloat(16), "std140 indexed array[1].first"); - assertFloatEquals(6.0f, std140IndexedBuffer.getFloat(20), "std140 indexed array[1].second"); - - ByteBuffer std430IndexedBuffer = ByteBuffer.allocate(64); - Std430Writer std430IndexedWriter = new Std430Writer(std430IndexedBuffer); - std430IndexedWriter.putStructArray(1, new TestPair(5.0f, 6.0f), pairDefinition); - - assertFloatEquals(0.0f, std430IndexedBuffer.getFloat(0), "std430 indexed array[0].first"); - assertFloatEquals(5.0f, std430IndexedBuffer.getFloat(8), "std430 indexed array[1].first"); - assertFloatEquals(6.0f, std430IndexedBuffer.getFloat(12), "std430 indexed array[1].second"); - - ByteBuffer std140SequentialIndexedBuffer = ByteBuffer.allocate(64); - Std140Writer std140SequentialIndexedWriter = new Std140Writer(std140SequentialIndexedBuffer); - std140SequentialIndexedWriter.putStructArray(0, new TestPair(1.0f, 2.0f), pairDefinition); - std140SequentialIndexedWriter.putStructArray(1, new TestPair(3.0f, 4.0f), pairDefinition); - - assertFloatEquals(1.0f, std140SequentialIndexedBuffer.getFloat(0), "std140 sequential indexed array[0].first"); - assertFloatEquals(2.0f, std140SequentialIndexedBuffer.getFloat(4), "std140 sequential indexed array[0].second"); - assertFloatEquals(3.0f, std140SequentialIndexedBuffer.getFloat(16), "std140 sequential indexed array[1].first"); - assertFloatEquals(4.0f, std140SequentialIndexedBuffer.getFloat(20), "std140 sequential indexed array[1].second"); - - ByteBuffer std430SequentialIndexedBuffer = ByteBuffer.allocate(64); - Std430Writer std430SequentialIndexedWriter = new Std430Writer(std430SequentialIndexedBuffer); - std430SequentialIndexedWriter.putStructArray(0, new TestPair(1.0f, 2.0f), pairDefinition); - std430SequentialIndexedWriter.putStructArray(1, new TestPair(3.0f, 4.0f), pairDefinition); - - assertFloatEquals(1.0f, std430SequentialIndexedBuffer.getFloat(0), "std430 sequential indexed array[0].first"); - assertFloatEquals(2.0f, std430SequentialIndexedBuffer.getFloat(4), "std430 sequential indexed array[0].second"); - assertFloatEquals(3.0f, std430SequentialIndexedBuffer.getFloat(8), "std430 sequential indexed array[1].first"); - assertFloatEquals(4.0f, std430SequentialIndexedBuffer.getFloat(12), "std430 sequential indexed array[1].second"); - } - - private static void assertEquals(int expected, int actual, String label) { - if (expected != actual) { - throw new AssertionError(label + ": expected " + expected + ", got " + actual); - } - } - - private static void assertFloatEquals(float expected, float actual, String label) { - if (Float.compare(expected, actual) != 0) { - throw new AssertionError(label + ": expected " + expected + ", got " + actual); - } - } - - private record TestStruct(float value) { - } - - private record TestPair(float first, float second) { - } -} diff --git a/renderdoc-loader/build.gradle b/renderdoc-loader/build.gradle deleted file mode 100644 index 546d1782..00000000 --- a/renderdoc-loader/build.gradle +++ /dev/null @@ -1,120 +0,0 @@ -plugins { - id 'java' - id 'maven-publish' - alias libs.plugins.lombok -} - -String buildType = 'snapshot' -String buildNumber -if (System.getenv("CI_BUILD") == 'false') buildNumber = null else { - if (System.getenv("PR_BUILD") != 'false') buildType = 'pr' - buildNumber = System.getenv("GITHUB_RUN_NUMBER") -} -version = "${mod_version}" + (buildNumber != null ? "+${buildType}.${buildNumber}" : "") -group = mod_group_id - -base { - archivesName = "${project.mod_id}-neoforge-${libs.versions.minecraft.get()}" -} - -repositories { - mavenCentral() -} - -dependencies { - compileOnly 'org.jetbrains:annotations:24.1.0' -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 -} - -jar { - manifest { - attributes( - 'Premain-Class': 'dev.anvilcraft.lib.renderdoc.loader.RenderDocAgent', - 'Agent-Class': 'dev.anvilcraft.lib.renderdoc.loader.RenderDocAgent', - 'Can-Redefine-Classes': 'false', - 'Can-Retransform-Classes': 'false', - 'Can-Set-Native-Method-Prefix': 'false' - ) - } -} - -tasks.register('sourcesJar', Jar) { - from sourceSets.main.allSource - archiveClassifier = 'sources' -} - -tasks.register('javadocJar', Jar) { - from javadoc.destinationDir - dependsOn javadoc - archiveClassifier = 'javadoc' -} - -javadoc { - // options.encoding = 'UTF-8' - options { - encoding = 'UTF-8' - addStringOption 'Xdoclint:none', '-quiet' - addStringOption 'tag', 'apiNote:a:API Note:' - addStringOption 'tag', 'implSpec:a:Implementation Requirement:' - addStringOption 'tag', 'implNote:a:Implementation Note:' - } -} - -artifacts { - archives tasks.jar - archives tasks.sourcesJar - archives tasks.javadocJar -} - -def this_name = base.archivesName.get() -def this_description = mod_description - -publishing { - publications { - register('mavenJava', MavenPublication) { - from components.java - - artifact sourcesJar - artifact javadocJar - - pom { - name = this_name - description = this_description - url = 'https://github.com/Anvil-Dev/AnvilLib' - - licenses { - license { - name = 'MIT License' - url = 'https://opensource.org/license/mit' - } - } - developers { - developer { - id = 'Anvil-Dev' - name = 'AnvilCraft Dev' - email = 'admin@anvilcraft.dev' - } - } - scm { - url = 'https://github.com/Anvil-Dev/AnvilLib' - connection = 'https://github.com/Anvil-Dev/AnvilLib.git' - developerConnection = 'https://github.com/Anvil-Dev/AnvilLib.git' - } - } - } - } - repositories { - mavenLocal() - } -} - -lombok { - version = "1.18.34" -} diff --git a/renderdoc-loader/gradle.properties b/renderdoc-loader/gradle.properties deleted file mode 100644 index 864ce045..00000000 --- a/renderdoc-loader/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -## Mod Properties -mod_id=anvillib_renderdoc_loader -mod_name=AnvilLib-RenderDoc-Loader -mod_description=Java Agent for dynamically loading RenderDoc DLL into JVM diff --git a/renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/RenderDocAgent.java b/renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/RenderDocAgent.java deleted file mode 100644 index 7f22ddb5..00000000 --- a/renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/RenderDocAgent.java +++ /dev/null @@ -1,99 +0,0 @@ -package dev.anvilcraft.lib.renderdoc.loader; - -import org.jetbrains.annotations.ApiStatus; - -import java.lang.instrument.Instrumentation; - -/** - * Java Agent for dynamically loading RenderDoc DLL into JVM. - *

    - * This agent allows RenderDoc to be attached to a running JVM process - * for graphics debugging purposes. - *

    - */ -@ApiStatus.Internal -public class RenderDocAgent { - - /** - * System property key for specifying the RenderDoc library path. - */ - public static final String RENDERDOC_LIBRARY_PATH = "renderdoc.library.path"; - - /** - * System property key for specifying the RenderDoc library path (alternative). - */ - public static final String RENDERDOC_LIBRARY_PATH_ALT = "neoforge.rendernurse.renderdoc.library"; - - /** - * JVM entry point for Java Agent when started with -javaagent flag. - * - * @param agentArgs agent arguments passed via -javaagent:jarpath=args - * @param inst the instrumentation instance - */ - public static void premain(String agentArgs, Instrumentation inst) { - loadRenderDoc(); - } - - /** - * JVM entry point for Java Agent when attached to a running JVM. - * - * @param agentArgs agent arguments - * @param inst the instrumentation instance - */ - public static void agentmain(String agentArgs, Instrumentation inst) { - loadRenderDoc(); - } - - /** - * Attempts to load the RenderDoc native library. - *

    - * The library path can be specified via: - *

      - *
    • System property {@link #RENDERDOC_LIBRARY_PATH}
    • - *
    • System property {@link #RENDERDOC_LIBRARY_PATH_ALT}
    • - *
    • Agent arguments (as a path)
    • - *
    - *

    - */ - private static void loadRenderDoc() { - String libraryPath = getLibraryPath(); - - if (libraryPath == null || libraryPath.isEmpty()) { - System.err.println("[RenderDoc-Loader] No RenderDoc library path specified. " + - "Set system property '" + RENDERDOC_LIBRARY_PATH + "' or '" + RENDERDOC_LIBRARY_PATH_ALT + "'"); - return; - } - - try { - System.load(libraryPath); - System.out.println("[RenderDoc-Loader] Successfully loaded RenderDoc from: " + libraryPath); - } catch (UnsatisfiedLinkError e) { - System.err.println("[RenderDoc-Loader] Failed to load RenderDoc from: " + libraryPath); - e.printStackTrace(); - } catch (SecurityException e) { - System.err.println("[RenderDoc-Loader] Security exception when loading RenderDoc from: " + libraryPath); - e.printStackTrace(); - } - } - - /** - * Gets the RenderDoc library path from system properties or agent arguments. - * - * @return the library path, or null if not specified - */ - private static String getLibraryPath() { - // Try primary system property first - String path = System.getProperty(RENDERDOC_LIBRARY_PATH); - if (path != null && !path.isEmpty()) { - return path; - } - - // Try alternative system property - path = System.getProperty(RENDERDOC_LIBRARY_PATH_ALT); - if (path != null && !path.isEmpty()) { - return path; - } - - return null; - } -} diff --git a/renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/package-info.java b/renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/package-info.java deleted file mode 100644 index 4012b38d..00000000 --- a/renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/package-info.java +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Java Agent for dynamically loading RenderDoc DLL into JVM. - *

    - * Usage: - *

    - * java -javaagent:renderdoc-loader.jar -Drenderdoc.library.path=/path/to/renderdoc.dll -jar app.jar
    - * 
    - *

    - *

    - * Or use the alternative property name: - *

    - * java -javaagent:renderdoc-loader.jar -Dneoforge.rendernurse.renderdoc.library=/path/to/renderdoc.dll -jar app.jar
    - * 
    - *

    - */ -package dev.anvilcraft.lib.renderdoc.loader; diff --git a/settings.gradle b/settings.gradle index a2ad1a29..b1aa275b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,48 +10,44 @@ plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' } -include 'module.config' include 'module.codec' +include 'module.collision' +include 'module.config' +include 'module.explosion' +include 'module.font' include 'module.integration' include 'module.moveable-entity-block' include 'module.multiblock' include 'module.network' include 'module.recipe' include 'module.registrum' -include 'module.util' -include 'module.wheel' -include 'module.yukkuri' -include 'module.main' include 'module.rpc' -include 'module.rendering' -include 'module.font' include 'module.space-select' -include 'module.explosion' -include 'module.collision' -include 'renderdoc-loader' include 'module.sync' include 'module.sync:processor' +include 'module.util' +include 'module.wheel' +include 'module.yukkuri' +include 'module.main' include 'module.test' -project(':module.config').name = 'anvillib-config-neoforge-1.21.1' project(':module.codec').name = 'anvillib-codec-neoforge-1.21.1' +project(':module.collision').name = 'anvillib-collision-neoforge-1.21.1' +project(':module.config').name = 'anvillib-config-neoforge-1.21.1' +project(':module.explosion').name = 'anvillib-explosion-neoforge-1.21.1' +project(':module.font').name = 'anvillib-font-neoforge-1.21.1' project(':module.integration').name = 'anvillib-integration-neoforge-1.21.1' project(':module.moveable-entity-block').name = 'anvillib-moveable-entity-block-neoforge-1.21.1' project(':module.multiblock').name = 'anvillib-multiblock-neoforge-1.21.1' project(':module.network').name = 'anvillib-network-neoforge-1.21.1' project(':module.recipe').name = 'anvillib-recipe-neoforge-1.21.1' project(':module.registrum').name = 'anvillib-registrum-neoforge-1.21.1' -project(':module.util').name = 'anvillib-util-neoforge-1.21.1' -project(':module.wheel').name = 'anvillib-wheel-neoforge-1.21.1' -project(':module.yukkuri').name = 'anvillib-yukkuri-neoforge-1.21.1' -project(':module.main').name = 'anvillib-neoforge-1.21.1' project(':module.rpc').name = 'anvillib-rpc-neoforge-1.21.1' -project(':module.rendering').name = 'anvillib-rendering-neoforge-1.21.1' -project(':module.font').name = 'anvillib-font-neoforge-1.21.1' project(':module.space-select').name = 'anvillib-space-select-neoforge-1.21.1' -project(':module.explosion').name = 'anvillib-explosion-neoforge-1.21.1' -project(':module.collision').name = 'anvillib-collision-neoforge-1.21.1' -project(':renderdoc-loader').name = 'renderdoc-loader-internal' project(':module.sync').name = 'anvillib-sync-neoforge-1.21.1' project(':module.sync:processor').name = 'anvillib-sync-processor-neoforge-1.21.1' +project(':module.util').name = 'anvillib-util-neoforge-1.21.1' +project(':module.wheel').name = 'anvillib-wheel-neoforge-1.21.1' +project(':module.yukkuri').name = 'anvillib-yukkuri-neoforge-1.21.1' +project(':module.main').name = 'anvillib-neoforge-1.21.1' project(':module.test').name = 'anvillib-test-neoforge-1.21.1' From 5392d42bf75a4072e648ec90067b55818c67a025 Mon Sep 17 00:00:00 2001 From: QiuShui1012 Date: Wed, 12 Aug 2026 08:36:32 +0800 Subject: [PATCH 35/38] fix(explosion): fix explosion crashing --- .../lib/v2/explosion/AnvilLibExplosion.java | 11 +++++------ .../explosion/mixin/SingleItemRecipeAccessor.java | 14 -------------- .../main/resources/anvillib_explosion.mixins.json | 3 +-- 3 files changed, 6 insertions(+), 22 deletions(-) delete mode 100644 module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/mixin/SingleItemRecipeAccessor.java diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/AnvilLibExplosion.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/AnvilLibExplosion.java index c77aa557..c79d1dbf 100644 --- a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/AnvilLibExplosion.java +++ b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/AnvilLibExplosion.java @@ -1,8 +1,8 @@ package dev.anvilcraft.lib.v2.explosion; import dev.anvilcraft.lib.v2.config.ConfigManager; -import dev.anvilcraft.lib.v2.explosion.mixin.SingleItemRecipeAccessor; import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; @@ -78,16 +78,15 @@ private static void registerMelting(MinecraftServer server) { for (RecipeHolder holder : server.getRecipeManager().getRecipes()) { Recipe value = holder.value(); if (value instanceof SmeltingRecipe || value instanceof BlastingRecipe) { - AnvilLibExplosion.registerAbstractCookingRecipe((AbstractCookingRecipe) value); + AnvilLibExplosion.registerAbstractCookingRecipe((AbstractCookingRecipe) value, server.registryAccess()); } } } - @SuppressWarnings("deprecation") - private static void registerAbstractCookingRecipe(AbstractCookingRecipe recipe) { - Ingredient input = recipe.getIngredients().get(0); + private static void registerAbstractCookingRecipe(AbstractCookingRecipe recipe, HolderLookup.Provider registries) { + Ingredient input = recipe.getIngredients().getFirst(); List items = Arrays.stream(input.getItems()).map(ItemStack::getItem).toList(); - ItemStack result = ((SingleItemRecipeAccessor) recipe).getResult(); + ItemStack result = recipe.getResultItem(registries); if (result.getCount() != 1) { return; } diff --git a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/mixin/SingleItemRecipeAccessor.java b/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/mixin/SingleItemRecipeAccessor.java deleted file mode 100644 index c73ecfe2..00000000 --- a/module.explosion/src/main/java/dev/anvilcraft/lib/v2/explosion/mixin/SingleItemRecipeAccessor.java +++ /dev/null @@ -1,14 +0,0 @@ -package dev.anvilcraft.lib.v2.explosion.mixin; - -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.SingleItemRecipe; -import org.jetbrains.annotations.ApiStatus; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; - -@Mixin(SingleItemRecipe.class) -@ApiStatus.Internal -public interface SingleItemRecipeAccessor { - @Accessor - ItemStack getResult(); -} diff --git a/module.explosion/src/main/resources/anvillib_explosion.mixins.json b/module.explosion/src/main/resources/anvillib_explosion.mixins.json index 6795cbd8..7256ac24 100644 --- a/module.explosion/src/main/resources/anvillib_explosion.mixins.json +++ b/module.explosion/src/main/resources/anvillib_explosion.mixins.json @@ -5,11 +5,10 @@ "compatibilityLevel": "JAVA_21", "refmap": "anvillib.refmap.json", "mixins": [ - "SingleItemRecipeAccessor" ], "client": [ ], "injectors": { "defaultRequire": 1 } -} \ No newline at end of file +} From 72b0be0f5373d58e580bf9e07874ca50d21278b6 Mon Sep 17 00:00:00 2001 From: QiuShui1012 Date: Wed, 12 Aug 2026 16:58:11 +0800 Subject: [PATCH 36/38] =?UTF-8?q?ci:=20=E5=9B=9E=E7=A7=BB=E6=A4=8D?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=8C=96=E6=9E=84=E5=BB=BA=E7=9F=A9=E9=98=B5?= =?UTF-8?q?=E4=B8=8E=20roseau=20=E6=A3=80=E6=9F=A5=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 回移植 aa59e372(#53)的模块化构建:新增 .github/modules.json 与 generate-matrix.js,ci/pull_request/release 改为 prepare → build/deploy-l0/l1/l2 → main 的分层矩阵 - 新增 roseau_check/roseau_comment 工作流并适配 1.21.1:任务名后缀 -neoforge-1.21.1,模块集剔除 rendering、纳入 explosion/rpc/yukkuri - 修复 roseauCheck 本机运行:roseau-cli 0.6.0 需 Java 25,降至 0.4.0;基线构件改由 rootProject 解析;任务改为 doFirst 内两次 javaexec 产出 CSV/HTML 报告 - 更新 #P19 跟踪文档:roseauCheck 本机验证通过(codec/rpc),L-1 遗留销账 --- .github/modules.json | 21 ++ .github/workflows/ci.yml | 493 ++++---------------------- .github/workflows/generate-matrix.js | 57 +++ .github/workflows/pull_request.yml | 253 +++++--------- .github/workflows/release.yml | 496 ++++----------------------- .github/workflows/roseau_check.yml | 77 +++++ .github/workflows/roseau_comment.yml | 133 +++++++ docs/migration-plan-port-1.21.1.md | 6 +- docs/migration-todo-port-1.21.1.md | 13 +- gradle/scripts/roseau.gradle | 71 ++-- 10 files changed, 568 insertions(+), 1052 deletions(-) create mode 100644 .github/modules.json create mode 100644 .github/workflows/generate-matrix.js create mode 100644 .github/workflows/roseau_check.yml create mode 100644 .github/workflows/roseau_comment.yml diff --git a/.github/modules.json b/.github/modules.json new file mode 100644 index 00000000..6fd5f94e --- /dev/null +++ b/.github/modules.json @@ -0,0 +1,21 @@ +[ + { "module": "codec", "needs": [] }, + { "module": "collision", "needs": [] }, + { "module": "config", "needs": [] }, + { "module": "integration", "needs": [] }, + { "module": "moveable-entity-block", "needs": [] }, + { "module": "network", "needs": [] }, + { "module": "font", "needs": [] }, + { "module": "yukkuri", "needs": [] }, + + { "module": "explosion", "needs": ["config"] }, + { "module": "rpc", "needs": ["network"] }, + { "module": "space-select", "needs": ["network"] }, + { "module": "util", "needs": ["codec"] }, + + { "module": "multiblock", "needs": ["codec", "config", "network", "util"] }, + { "module": "recipe", "needs": ["codec", "config", "util"] }, + { "module": "registrum", "needs": ["util"] }, + { "module": "sync", "needs": ["codec", "network", "util"] }, + { "module": "wheel", "needs": ["util"] } +] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f6b6213..6d4b3131 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,12 @@ on: - dev/** # - releases/** paths: + - .github/modules.json + - .github/workflows/build_and_test.yml + - .github/workflows/publish_maven_central.yml + - .github/workflows/ci.yml + - .github/workflows/generate-matrix.js + - gradle/** - module.*/src/** - module.*/build.gradle - module.*/gradle.properties @@ -14,178 +20,119 @@ on: - settings.gradle jobs: - codec: - uses: ./.github/workflows/build_and_test.yml - with: - module: codec - module_id: anvillib-codec - mod_id: anvillib_codec - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - codec-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - codec - with: - module: codec - module_id: anvillib-codec - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - collision: + prepare: + runs-on: ubuntu-latest + outputs: + level_0: ${{ steps.matrix.outputs.level_0 }} + level_1: ${{ steps.matrix.outputs.level_1 }} + level_2: ${{ steps.matrix.outputs.level_2 }} + steps: + - uses: actions/checkout@v6 + - id: matrix + run: node .github/workflows/generate-matrix.js + + # ── Build (topological levels) ────────────────────────────────── + + build-l0: + needs: [prepare] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_0) }} uses: ./.github/workflows/build_and_test.yml with: - module: collision - module_id: anvillib-collision - mod_id: anvillib_collision + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} + mod_id: ${{ matrix.mod_id }} ci_build: true pr_build: false secrets: maven_url: ${{ secrets.MAVEN_URL }} maven_user: ${{ secrets.MAVEN_USER }} maven_pass: ${{ secrets.MAVEN_PASS }} - collision-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - collision - with: - module: collision - module_id: anvillib-collision - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - config: + + build-l1: + needs: [prepare, build-l0] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_1) }} uses: ./.github/workflows/build_and_test.yml with: - module: config - module_id: anvillib-config - mod_id: anvillib_config + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} + mod_id: ${{ matrix.mod_id }} ci_build: true pr_build: false secrets: maven_url: ${{ secrets.MAVEN_URL }} maven_user: ${{ secrets.MAVEN_USER }} maven_pass: ${{ secrets.MAVEN_PASS }} - config-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - config - with: - module: config - module_id: anvillib-config - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - explosion: + + build-l2: + needs: [prepare, build-l1] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_2) }} uses: ./.github/workflows/build_and_test.yml - needs: - - config with: - module: explosion - module_id: anvillib-explosion - mod_id: anvillib_explosion + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} + mod_id: ${{ matrix.mod_id }} ci_build: true pr_build: false secrets: maven_url: ${{ secrets.MAVEN_URL }} maven_user: ${{ secrets.MAVEN_USER }} maven_pass: ${{ secrets.MAVEN_PASS }} - explosion-maven-central-deploy: + + # ── Maven Central deploy ──────────────────────────────────────── + + deploy-l0: + needs: [prepare, build-l0] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_0) }} uses: ./.github/workflows/publish_maven_central.yml - needs: - - explosion with: - module: explosion - module_id: anvillib-explosion + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} secrets: maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} - font: - uses: ./.github/workflows/build_and_test.yml - with: - module: font - module_id: anvillib-font - mod_id: anvillib_font - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - font-maven-central-deploy: + + deploy-l1: + needs: [prepare, build-l1] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_1) }} uses: ./.github/workflows/publish_maven_central.yml - needs: - - font with: - module: font - module_id: anvillib-font + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} secrets: maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} - integration: - uses: ./.github/workflows/build_and_test.yml - with: - module: integration - module_id: anvillib-integration - mod_id: anvillib_integration - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - integration-maven-central-deploy: + + deploy-l2: + needs: [prepare, build-l2] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_2) }} uses: ./.github/workflows/publish_maven_central.yml - needs: - - integration with: - module: integration - module_id: anvillib-integration + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} secrets: maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} + + # ── Main (aggregator) ─────────────────────────────────────────── + main: + needs: [build-l2] uses: ./.github/workflows/build_and_test.yml - needs: - - codec - - collision - - config - - explosion - - font - - integration - - moveable-entity-block - - multiblock - - network - - recipe - - registrum - - rpc - - space-select - - sync - - util - - wheel - - yukkuri with: module: main module_id: anvillib @@ -199,10 +146,10 @@ jobs: maven_pass: ${{ secrets.MAVEN_PASS }} curseforge_token: ${{ secrets.CURSEFORGE_TOKEN }} modrinth_token: ${{ secrets.MODRINTH_TOKEN }} + main-maven-central-deploy: + needs: [main] uses: ./.github/workflows/publish_maven_central.yml - needs: - - main with: module: main module_id: anvillib @@ -212,301 +159,3 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} - moveable-entity-block: - uses: ./.github/workflows/build_and_test.yml - with: - module: moveable-entity-block - module_id: anvillib-moveable-entity-block - mod_id: anvillib_moveable_entity_block - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - moveable-entity-block-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - moveable-entity-block - with: - module: moveable-entity-block - module_id: anvillib-moveable-entity-block - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - multiblock: - uses: ./.github/workflows/build_and_test.yml - needs: - - codec - - config - - network - - util - with: - module: multiblock - module_id: anvillib-multiblock - mod_id: anvillib_multiblock - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - multiblock-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - multiblock - with: - module: multiblock - module_id: anvillib-multiblock - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - network: - uses: ./.github/workflows/build_and_test.yml - with: - module: network - module_id: anvillib-network - mod_id: anvillib_network - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - network-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - network - with: - module: network - module_id: anvillib-network - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - recipe: - uses: ./.github/workflows/build_and_test.yml - needs: - - codec - - config - - util - with: - module: recipe - module_id: anvillib-recipe - mod_id: anvillib_recipe - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - recipe-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - recipe - with: - module: recipe - module_id: anvillib-recipe - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - registrum: - uses: ./.github/workflows/build_and_test.yml - needs: - - util - with: - module: registrum - module_id: anvillib-registrum - mod_id: anvillib_registrum - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - registrum-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - registrum - with: - module: registrum - module_id: anvillib-registrum - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - rpc: - uses: ./.github/workflows/build_and_test.yml - needs: - - network - with: - module: rpc - module_id: anvillib-rpc - mod_id: anvillib_rpc - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - rpc-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - rpc - with: - module: rpc - module_id: anvillib-rpc - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - space-select: - uses: ./.github/workflows/build_and_test.yml - needs: - - network - with: - module: space-select - module_id: anvillib-space-select - mod_id: anvillib_space_select - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - space-select-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - space-select - with: - module: space-select - module_id: anvillib-space-select - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - sync: - uses: ./.github/workflows/build_and_test.yml - needs: - - codec - - network - - util - with: - module: sync - module_id: anvillib-sync - mod_id: anvillib_sync - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - sync-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - sync - with: - module: sync - module_id: anvillib-sync - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - util: - uses: ./.github/workflows/build_and_test.yml - needs: - - codec - with: - module: util - module_id: anvillib-util - mod_id: anvillib_util - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - util-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - util - with: - module: util - module_id: anvillib-util - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - wheel: - uses: ./.github/workflows/build_and_test.yml - needs: - - util - with: - module: wheel - module_id: anvillib-wheel - mod_id: anvillib_wheel - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - wheel-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - wheel - with: - module: wheel - module_id: anvillib-wheel - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - yukkuri: - uses: ./.github/workflows/build_and_test.yml - with: - module: yukkuri - module_id: anvillib-yukkuri - mod_id: anvillib_yukkuri - ci_build: true - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - yukkuri-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - yukkuri - with: - module: yukkuri - module_id: anvillib-yukkuri - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} diff --git a/.github/workflows/generate-matrix.js b/.github/workflows/generate-matrix.js new file mode 100644 index 00000000..603ba88b --- /dev/null +++ b/.github/workflows/generate-matrix.js @@ -0,0 +1,57 @@ +const fs = require('fs'); +const path = require('path'); + +const modulesPath = path.join(process.env.GITHUB_WORKSPACE, '.github', 'modules.json'); +const modules = JSON.parse(fs.readFileSync(modulesPath, 'utf8')); + +// Derive IDs from module name +const all = modules.map(m => ({ + module: m.module, + module_id: `anvillib-${m.module}`, + mod_id: `anvillib_${m.module.replace(/-/g, '_')}`, + needs: m.needs || [] +})); + +// ── Topological sort (Kahn's algorithm) ────────────────────────── +const deps = new Map(all.map(m => [m.module, m.needs])); +const inDegree = new Map(all.map(m => [m.module, m.needs.length])); +const queue = all.filter(m => inDegree.get(m.module) === 0).map(m => m.module); +const levels = []; + +while (queue.length > 0) { + const size = queue.length; + const current = []; + for (let i = 0; i < size; i++) { + const name = queue.shift(); + current.push(name); + for (const m of all) { + if (m.needs.includes(name)) { + const deg = inDegree.get(m.module) - 1; + inDegree.set(m.module, deg); + if (deg === 0) queue.push(m.module); + } + } + } + levels.push(current); +} + +// ── Outputs ────────────────────────────────────────────────────── +const out = process.env.GITHUB_OUTPUT; + +levels.forEach((names, i) => { + const entries = all.filter(m => names.includes(m.module)); + const matrix = JSON.stringify({ + include: entries.map(m => ({ + module: m.module, + module_id: m.module_id, + mod_id: m.mod_id + })) + }); + fs.appendFileSync(out, `level_${i}=${matrix}\n`); +}); + +fs.appendFileSync(out, `level_count=${levels.length}\n`); + +// Flat list for roseau_comment.yml +const names = JSON.stringify(all.map(m => m.module)); +fs.appendFileSync(out, `module_names=${names}\n`); diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 77ddcac8..34389044 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -6,6 +6,12 @@ on: - dev/** # - releases/** paths: + - .github/modules.json + - .github/workflows/build_and_test.yml + - .github/workflows/pull_request.yml + - .github/workflows/roseau_check.yml + - .github/workflows/generate-matrix.js + - gradle/** - module.*/src/** - module.*/build.gradle - module.*/gradle.properties @@ -13,77 +19,96 @@ on: - gradle.properties - settings.gradle +permissions: + contents: read + actions: read + packages: read + pull-requests: write + jobs: - codec: - uses: ./.github/workflows/build_and_test.yml - with: - module: codec - module_id: anvillib-codec - mod_id: anvillib_codec - ci_build: true - pr_build: true - collision: - uses: ./.github/workflows/build_and_test.yml - with: - module: collision - module_id: anvillib-collision - mod_id: anvillib_collision - ci_build: true - pr_build: true - config: - uses: ./.github/workflows/build_and_test.yml - with: - module: config - module_id: anvillib-config - mod_id: anvillib_config - ci_build: true - pr_build: true - explosion: + prepare: + runs-on: ubuntu-latest + outputs: + level_0: ${{ steps.matrix.outputs.level_0 }} + level_1: ${{ steps.matrix.outputs.level_1 }} + level_2: ${{ steps.matrix.outputs.level_2 }} + steps: + - uses: actions/checkout@v6 + - id: matrix + run: node .github/workflows/generate-matrix.js + + # ── Build (topological levels) ────────────────────────────────── + + build-l0: + needs: [prepare] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_0) }} uses: ./.github/workflows/build_and_test.yml - needs: - - config with: - module: explosion - module_id: anvillib-explosion - mod_id: anvillib_explosion + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} + mod_id: ${{ matrix.mod_id }} ci_build: true pr_build: true - font: + + build-l1: + needs: [prepare, build-l0] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_1) }} uses: ./.github/workflows/build_and_test.yml with: - module: font - module_id: anvillib-font - mod_id: anvillib_font + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} + mod_id: ${{ matrix.mod_id }} ci_build: true pr_build: true - integration: + + build-l2: + needs: [prepare, build-l1] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_2) }} uses: ./.github/workflows/build_and_test.yml with: - module: integration - module_id: anvillib-integration - mod_id: anvillib_integration + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} + mod_id: ${{ matrix.mod_id }} ci_build: true pr_build: true + + # ── Roseau API checks ─────────────────────────────────────────── + + roseau-l0: + needs: [prepare, build-l0] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_0) }} + uses: ./.github/workflows/roseau_check.yml + with: + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} + + roseau-l1: + needs: [prepare, build-l1] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_1) }} + uses: ./.github/workflows/roseau_check.yml + with: + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} + + roseau-l2: + needs: [prepare, build-l2] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_2) }} + uses: ./.github/workflows/roseau_check.yml + with: + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} + + # ── Main (aggregator) ─────────────────────────────────────────── + main: + needs: [build-l2] uses: ./.github/workflows/build_and_test.yml - needs: - - codec - - collision - - config - - explosion - - font - - integration - - moveable-entity-block - - multiblock - - network - - recipe - - registrum - - rpc - - space-select - - sync - - util - - wheel - - yukkuri with: module: main module_id: anvillib @@ -91,114 +116,10 @@ jobs: ci_build: true pr_build: true gametest: true - moveable-entity-block: - uses: ./.github/workflows/build_and_test.yml - with: - module: moveable-entity-block - module_id: anvillib-moveable-entity-block - mod_id: anvillib_moveable_entity_block - ci_build: true - pr_build: true - multiblock: - uses: ./.github/workflows/build_and_test.yml - needs: - - codec - - config - - network - - util - with: - module: multiblock - module_id: anvillib-multiblock - mod_id: anvillib_multiblock - ci_build: true - pr_build: true - network: - uses: ./.github/workflows/build_and_test.yml - with: - module: network - module_id: anvillib-network - mod_id: anvillib_network - ci_build: true - pr_build: true - recipe: - uses: ./.github/workflows/build_and_test.yml - needs: - - codec - - config - - util - with: - module: recipe - module_id: anvillib-recipe - mod_id: anvillib_recipe - ci_build: true - pr_build: true - registrum: - uses: ./.github/workflows/build_and_test.yml - needs: - - util - with: - module: registrum - module_id: anvillib-registrum - mod_id: anvillib_registrum - ci_build: true - pr_build: true - rpc: - uses: ./.github/workflows/build_and_test.yml - needs: - - network - with: - module: rpc - module_id: anvillib-rpc - mod_id: anvillib_rpc - ci_build: true - pr_build: true - space-select: - uses: ./.github/workflows/build_and_test.yml - needs: - - network - with: - module: space-select - module_id: anvillib-space-select - mod_id: anvillib_space_select - ci_build: true - pr_build: true - sync: - uses: ./.github/workflows/build_and_test.yml - needs: - - codec - - network - - util - with: - module: sync - module_id: anvillib-sync - mod_id: anvillib_sync - ci_build: true - pr_build: true - util: - uses: ./.github/workflows/build_and_test.yml - needs: - - codec - with: - module: util - module_id: anvillib-util - mod_id: anvillib_util - ci_build: true - pr_build: true - wheel: - uses: ./.github/workflows/build_and_test.yml - needs: - - util - with: - module: wheel - module_id: anvillib-wheel - mod_id: anvillib_wheel - ci_build: true - pr_build: true - yukkuri: - uses: ./.github/workflows/build_and_test.yml + + main-roseau: + needs: [main] + uses: ./.github/workflows/roseau_check.yml with: - module: yukkuri - module_id: anvillib-yukkuri - mod_id: anvillib_yukkuri - ci_build: true - pr_build: true + module: main + module_id: anvillib diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55d4b258..be9ff32f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,190 +2,126 @@ name: Release for Mod on: workflow_dispatch: release: + types: + - published branches: - dev/** # - releases/** - paths: - - module.*/src/** - - module.*/build.gradle - - module.*/gradle.properties - - build.gradle - - gradle.properties - - settings.gradle jobs: - codec: - uses: ./.github/workflows/build_and_test.yml - with: - module: codec - module_id: anvillib-codec - mod_id: anvillib_codec - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - codec-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - codec - with: - module: codec - module_id: anvillib-codec - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - collision: + prepare: + runs-on: ubuntu-latest + outputs: + level_0: ${{ steps.matrix.outputs.level_0 }} + level_1: ${{ steps.matrix.outputs.level_1 }} + level_2: ${{ steps.matrix.outputs.level_2 }} + steps: + - uses: actions/checkout@v6 + - id: matrix + run: node .github/workflows/generate-matrix.js + + # ── Build (topological levels) ────────────────────────────────── + + build-l0: + needs: [prepare] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_0) }} uses: ./.github/workflows/build_and_test.yml with: - module: collision - module_id: anvillib-collision - mod_id: anvillib_collision + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} + mod_id: ${{ matrix.mod_id }} ci_build: false pr_build: false secrets: maven_url: ${{ secrets.MAVEN_URL }} maven_user: ${{ secrets.MAVEN_USER }} maven_pass: ${{ secrets.MAVEN_PASS }} - collision-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - collision - with: - module: collision - module_id: anvillib-collision - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - config: + + build-l1: + needs: [prepare, build-l0] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_1) }} uses: ./.github/workflows/build_and_test.yml with: - module: config - module_id: anvillib-config - mod_id: anvillib_config + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} + mod_id: ${{ matrix.mod_id }} ci_build: false pr_build: false secrets: maven_url: ${{ secrets.MAVEN_URL }} maven_user: ${{ secrets.MAVEN_USER }} maven_pass: ${{ secrets.MAVEN_PASS }} - config-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - config - with: - module: config - module_id: anvillib-config - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - explosion: + + build-l2: + needs: [prepare, build-l1] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_2) }} uses: ./.github/workflows/build_and_test.yml - needs: - - config with: - module: explosion - module_id: anvillib-explosion - mod_id: anvillib_explosion + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} + mod_id: ${{ matrix.mod_id }} ci_build: false pr_build: false secrets: maven_url: ${{ secrets.MAVEN_URL }} maven_user: ${{ secrets.MAVEN_USER }} maven_pass: ${{ secrets.MAVEN_PASS }} - explosion-maven-central-deploy: + + # ── Maven Central deploy ──────────────────────────────────────── + + deploy-l0: + needs: [prepare, build-l0] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_0) }} uses: ./.github/workflows/publish_maven_central.yml - needs: - - explosion with: - module: explosion - module_id: anvillib-explosion + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} secrets: maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} - font: - uses: ./.github/workflows/build_and_test.yml - with: - module: font - module_id: anvillib-font - mod_id: anvillib_font - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - font-maven-central-deploy: + + deploy-l1: + needs: [prepare, build-l1] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_1) }} uses: ./.github/workflows/publish_maven_central.yml - needs: - - font with: - module: font - module_id: anvillib-font + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} secrets: maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} - integration: - uses: ./.github/workflows/build_and_test.yml - with: - module: integration - module_id: anvillib-integration - mod_id: anvillib_integration - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - integration-maven-central-deploy: + + deploy-l2: + needs: [prepare, build-l2] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.level_2) }} uses: ./.github/workflows/publish_maven_central.yml - needs: - - integration with: - module: integration - module_id: anvillib-integration + module: ${{ matrix.module }} + module_id: ${{ matrix.module_id }} secrets: maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} + + # ── Main (aggregator) ─────────────────────────────────────────── + main: + needs: [build-l2] uses: ./.github/workflows/build_and_test.yml - needs: - - codec - - collision - - config - - explosion - - font - - integration - - moveable-entity-block - - multiblock - - network - - recipe - - registrum - - rpc - - space-select - - sync - - util - - wheel - - yukkuri with: module: main module_id: anvillib @@ -196,10 +132,10 @@ jobs: maven_url: ${{ secrets.MAVEN_URL }} maven_user: ${{ secrets.MAVEN_USER }} maven_pass: ${{ secrets.MAVEN_PASS }} + main-maven-central-deploy: + needs: [main] uses: ./.github/workflows/publish_maven_central.yml - needs: - - main with: module: main module_id: anvillib @@ -209,301 +145,3 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} - moveable-entity-block: - uses: ./.github/workflows/build_and_test.yml - with: - module: moveable-entity-block - module_id: anvillib-moveable-entity-block - mod_id: anvillib_moveable_entity_block - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - moveable-entity-block-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - moveable-entity-block - with: - module: moveable-entity-block - module_id: anvillib-moveable-entity-block - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - multiblock: - uses: ./.github/workflows/build_and_test.yml - needs: - - codec - - config - - network - - util - with: - module: multiblock - module_id: anvillib-multiblock - mod_id: anvillib_multiblock - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - multiblock-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - multiblock - with: - module: multiblock - module_id: anvillib-multiblock - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - network: - uses: ./.github/workflows/build_and_test.yml - with: - module: network - module_id: anvillib-network - mod_id: anvillib_network - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - network-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - network - with: - module: network - module_id: anvillib-network - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - recipe: - uses: ./.github/workflows/build_and_test.yml - needs: - - codec - - config - - util - with: - module: recipe - module_id: anvillib-recipe - mod_id: anvillib_recipe - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - recipe-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - recipe - with: - module: recipe - module_id: anvillib-recipe - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - registrum: - uses: ./.github/workflows/build_and_test.yml - needs: - - util - with: - module: registrum - module_id: anvillib-registrum - mod_id: anvillib_registrum - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - registrum-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - registrum - with: - module: registrum - module_id: anvillib-registrum - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - rpc: - uses: ./.github/workflows/build_and_test.yml - needs: - - network - with: - module: rpc - module_id: anvillib-rpc - mod_id: anvillib_rpc - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - rpc-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - rpc - with: - module: rpc - module_id: anvillib-rpc - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - space-select: - uses: ./.github/workflows/build_and_test.yml - needs: - - network - with: - module: space-select - module_id: anvillib-space-select - mod_id: anvillib_space_select - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - space-select-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - space-select - with: - module: space-select - module_id: anvillib-space-select - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - sync: - uses: ./.github/workflows/build_and_test.yml - needs: - - codec - - network - - util - with: - module: sync - module_id: anvillib-sync - mod_id: anvillib_sync - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - sync-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - sync - with: - module: sync - module_id: anvillib-sync - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - util: - uses: ./.github/workflows/build_and_test.yml - needs: - - codec - with: - module: util - module_id: anvillib-util - mod_id: anvillib_util - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - util-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - util - with: - module: util - module_id: anvillib-util - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - wheel: - uses: ./.github/workflows/build_and_test.yml - needs: - - util - with: - module: wheel - module_id: anvillib-wheel - mod_id: anvillib_wheel - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - wheel-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - wheel - with: - module: wheel - module_id: anvillib-wheel - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} - yukkuri: - uses: ./.github/workflows/build_and_test.yml - with: - module: yukkuri - module_id: anvillib-yukkuri - mod_id: anvillib_yukkuri - ci_build: false - pr_build: false - secrets: - maven_url: ${{ secrets.MAVEN_URL }} - maven_user: ${{ secrets.MAVEN_USER }} - maven_pass: ${{ secrets.MAVEN_PASS }} - yukkuri-maven-central-deploy: - uses: ./.github/workflows/publish_maven_central.yml - needs: - - yukkuri - with: - module: yukkuri - module_id: anvillib-yukkuri - secrets: - maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - signing_key: ${{ secrets.SIGNING_KEY }} - signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} - signing_password: ${{ secrets.SIGNING_PASSWORD }} diff --git a/.github/workflows/roseau_check.yml b/.github/workflows/roseau_check.yml new file mode 100644 index 00000000..a48a5e92 --- /dev/null +++ b/.github/workflows/roseau_check.yml @@ -0,0 +1,77 @@ +name: Roseau Check + +on: + workflow_call: + inputs: + module: + description: 'Module directory name (e.g. codec, config)' + required: true + type: string + module_id: + description: 'Module short ID (e.g. anvillib-codec)' + required: true + type: string + outputs: + has_bc: + description: 'Whether breaking changes were detected' + value: ${{ jobs.check.outputs.has_bc }} + bc_count: + description: 'Number of breaking changes' + value: ${{ jobs.check.outputs.bc_count }} + +jobs: + check: + runs-on: ubuntu-latest + outputs: + has_bc: ${{ steps.result.outputs.has_bc }} + bc_count: ${{ steps.result.outputs.bc_count }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Read Java version + id: properties + uses: christian-draeger/read-properties@1.1.1 + with: + path: gradle.properties + properties: java_version + + - name: Setup Java ${{ steps.properties.outputs.java_version }} + uses: actions/setup-java@v5.2.0 + with: + distribution: zulu + java-version: ${{ steps.properties.outputs.java_version }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Run roseauCheck + id: roseau + continue-on-error: true + run: | + chmod +x ./gradlew + ./gradlew :${{ inputs.module_id }}-neoforge-1.21.1:roseauCheck + + - name: Collect result + id: result + if: always() + run: | + CSV="module.${{ inputs.module }}/build/reports/roseau/report.csv" + if [ -f "$CSV" ] && [ "$(wc -l < "$CSV")" -gt 1 ]; then + COUNT=$(($(wc -l < "$CSV") - 1)) + echo "has_bc=true" >> $GITHUB_OUTPUT + echo "bc_count=${COUNT}" >> $GITHUB_OUTPUT + echo "Roseau: ${COUNT} breaking change(s) in ${{ inputs.module }}" + else + echo "has_bc=false" >> $GITHUB_OUTPUT + echo "bc_count=0" >> $GITHUB_OUTPUT + echo "Roseau: no breaking changes in ${{ inputs.module }}" + fi + + - name: Upload report + if: always() + uses: actions/upload-artifact@v7 + with: + name: roseau-${{ inputs.module }} + path: module.${{ inputs.module }}/build/reports/roseau/ + retention-days: 7 diff --git a/.github/workflows/roseau_comment.yml b/.github/workflows/roseau_comment.yml new file mode 100644 index 00000000..6cc1eb35 --- /dev/null +++ b/.github/workflows/roseau_comment.yml @@ -0,0 +1,133 @@ +name: Roseau PR Comment + +on: + workflow_run: + workflows: + - Pull Request Check + types: + - completed + +permissions: + contents: read + actions: read + pull-requests: write + +jobs: + comment: + if: ${{ github.event.workflow_run.conclusion != 'skipped' }} + runs-on: ubuntu-latest + steps: + - name: Download Roseau reports + uses: actions/download-artifact@v4 + with: + pattern: roseau-* + path: roseau-reports + run-id: ${{ github.event.workflow_run.id }} + + - name: Checkout repo (needed for gh CLI) + uses: actions/checkout@v4 + + - name: Find PR number + id: pr + run: | + HEAD_BRANCH="${{ github.event.workflow_run.head_branch }}" + HEAD_SHA="${{ github.event.workflow_run.head_sha }}" + PR_NUMBER=$(gh pr list \ + --head "$HEAD_BRANCH" \ + --state open \ + --json number \ + --jq '.[0].number') + if [ -z "$PR_NUMBER" ]; then + PR_NUMBER=$(gh pr list \ + --search "$HEAD_SHA" \ + --state open \ + --json number \ + --jq '.[0].number') + fi + if [ -z "$PR_NUMBER" ]; then + echo "::warning::Could not find PR number for branch=$HEAD_BRANCH sha=$HEAD_SHA" + fi + echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ secrets.PAT_TOKEN }} + + - name: Generate report body + run: | + MODULES=( + codec collision config explosion font integration + moveable-entity-block multiblock network recipe registrum + rpc space-select sync util wheel yukkuri main + ) + + row() { + local name=$1 dir=$2 + local csv="${dir}/report.csv" + if [ -f "$csv" ] && [ "$(wc -l < "$csv")" -gt 1 ]; then + local count=$(($(wc -l < "$csv") - 1)) + echo "| ${name} | 🔴 BC detected | ${count} |" + elif [ -f "$csv" ]; then + echo "| ${name} | ✅ Compatible | 0 |" + else + echo "| ${name} | ⚪ Skipped | — |" + fi + } + + append_details() { + local name=$1 dir=$2 + local csv="${dir}/report.csv" + if [ ! -f "$csv" ]; then return; fi + local count=$(($(wc -l < "$csv") - 1)) + if [ "$count" -le 0 ]; then return; fi + { + echo "### 🔴 ${name} — ${count} breaking change(s)" + echo "" + while IFS=';' read -r type symbol kind nature location newSymbol binaryBreaking sourceBreaking; do + echo "#### \`${location}\`" + echo '```' + echo "${type}" + echo "${symbol}" + echo "${kind}" + [ "$binaryBreaking" = "true" ] && echo "✗ binary-breaking" || echo "✓ binary-compatible" + [ "$sourceBreaking" = "true" ] && echo "✗ source-breaking" || echo "✓ source-compatible" + echo '```' + echo "" + done < <(tail -n +2 "$csv") + } >> /tmp/roseau-body.md + } + + { + echo "## 🌿 Roseau API Breaking Change Report" + echo "" + echo "| Module | Status | Breaking Changes |" + echo "|--------|--------|-----------------|" + for mod in "${MODULES[@]}"; do + row "$mod" "roseau-reports/roseau-${mod}" + done + echo "" + } > /tmp/roseau-body.md + + for mod in "${MODULES[@]}"; do + append_details "$mod" "roseau-reports/roseau-${mod}" + done + + echo "> Full CSVs: see the *Artifacts* section of [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }})." >> /tmp/roseau-body.md + + - name: Find existing Roseau comment + id: find-comment + if: steps.pr.outputs.number != '' + uses: peter-evans/find-comment@v3 + with: + token: ${{ secrets.PAT_TOKEN }} + issue-number: ${{ steps.pr.outputs.number }} + comment-author: anvil-craft + body-includes: Roseau API Breaking Change Report + + - name: Create or update PR comment + if: steps.pr.outputs.number != '' + uses: peter-evans/create-or-update-comment@v4 + with: + token: ${{ secrets.PAT_TOKEN }} + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ steps.pr.outputs.number }} + body-path: /tmp/roseau-body.md + edit-mode: replace diff --git a/docs/migration-plan-port-1.21.1.md b/docs/migration-plan-port-1.21.1.md index ff170035..86b7597e 100644 --- a/docs/migration-plan-port-1.21.1.md +++ b/docs/migration-plan-port-1.21.1.md @@ -1039,6 +1039,8 @@ done ### 4.19 回移植项 #P19:构建体系——roseauCheck(中) +> ✅ 2026-08-12:#P19 已落地并随模块化 CI 重构一并收尾——roseauCheck 本机验证通过(roseau-cli 0.6.0 需 Java 25,降为 0.4.0;基线构件经 rootProject 解析;CSV/HTML 双报告),并回移植 `aa59e372`(#53)的模块化构建矩阵(`.github/modules.json` + `generate-matrix.js`,l0/l1/l2 分层构建/部署 + roseau_check/roseau_comment 工作流);module.gradle 集中化维持「不做」结论(port 侧独立构建脚本自洽)。 + #### 4.19.1 目标 把 dev 侧构建体系中最具迁移价值的两项回移植:**roseauCheck API 兼容检查**(核心项,可直接搬);**module.gradle 集中化**(可选工程决策,需裁剪 26.1 特有内容)。 @@ -1068,8 +1070,8 @@ done #### 4.19.4 验收方式 -1. `gradlew roseauCheck`(或对应任务)在 port/1.21.1 基线上运行通过,输出 API 差分报告(可对 1.21.1 基线跑差分)。 -2. 若做集中化:全量 `gradlew build` 通过(全部模块聚合构建无回归)。 +1. `gradlew roseauCheck`(或对应任务)在 port/1.21.1 基线上运行通过,输出 API 差分报告(可对 1.21.1 基线跑差分)。——✅ 2026-08-12:`gradlew :anvillib-codec-neoforge-1.21.1:roseauCheck` 通过,`module.codec/build/reports/roseau/` 产出 report.csv(0 BC)与 report.html;rpc 模块同步验证通过。 +2. 若做集中化:全量 `gradlew build` 通过(全部模块聚合构建无回归)。——未做集中化(port 侧维持独立构建脚本)。 --- diff --git a/docs/migration-todo-port-1.21.1.md b/docs/migration-todo-port-1.21.1.md index 08384387..35749257 100644 --- a/docs/migration-todo-port-1.21.1.md +++ b/docs/migration-todo-port-1.21.1.md @@ -31,8 +31,8 @@ | #P16 | module.recipe:SpawnItem 守卫 + SetBlock nbt | 低 | ✅ 已完成 | `71bf9e54` | | #P17 | module.wheel:环形扇区选择效果 | 中-高 | ✅ 已完成 | `142e3296` | | #P18 | module.test:T2/T8/T9/T10 可选测试 | 低-中 | ✅ 已完成 | `a4ab3acd` | -| #P19 | 构建体系:roseauCheck | 中 | ✅ 已完成(运行待补验) | `544c9eb0` | -| #P20 | module.codec:CodecUtil 便携 create/mapCodec(计划外追加) | 低 | ✅ 已完成(待提交) | 来源 `173a454` | +| #P19 | 构建体系:roseauCheck + 模块化 CI 构建矩阵 | 中 | ✅ 已完成 | `544c9eb0` + 本轮 | +| #P20 | module.codec:CodecUtil 便携 create/mapCodec(计划外追加) | 低 | ✅ 已完成 | `05235aa`(来源 `173a454`) | > ⚠️ 2026-08-12 状态更新:#P7(module.rendering)与 #P8(renderdoc-loader)已从 `port/1.21.1` 分支整体移除;同时清理了 `module.font` / `module.main` 的渲染依赖、CI 工作流(ci / release / pull_request)中的渲染任务与相关文档引用。 @@ -187,9 +187,10 @@ ### #P19 构建体系(中) -- [ ] roseau.yaml + gradle/scripts/roseau.gradle + 根 build.gradle 接入 -- [ ] `gradlew roseauCheck` 运行通过 -- [ ] 提交 +- [x] roseau.yaml + gradle/scripts/roseau.gradle + 根 build.gradle 接入(`544c9eb0`) +- [x] `gradlew roseauCheck` 运行通过(2026-08-12 本机验证 codec/rpc;roseau-cli 0.4.0,CSV+HTML 双报告) +- [x] 模块化 CI 构建矩阵回移植(来源 `aa59e372`:#53):`.github/modules.json` + `generate-matrix.js` + l0/l1/l2 分层矩阵 + roseau_check/roseau_comment 工作流 +- [x] 提交 ### #P20 module.codec:CodecUtil 便携 create/mapCodec(计划外追加 · 2026-08-11) @@ -231,7 +232,7 @@ | 编号 | 事项 | 说明 | 复现/补验步骤 | |------|------|------|----------------| -| L-1 | roseauCheck 运行验证(#P19) | 本机运行 roseauCheck 时 Gradle daemon 崩溃(疑似 roseau-cli 0.6.0 与本地 Java 21 环境兼容问题);任务可注册、配置阶段通过 | CI 或人工:`./gradlew :anvillib-<模块>:roseauCheck` | +| L-1 | roseauCheck 运行验证(#P19) | ✅ 已解决(2026-08-12):roseau-cli 0.6.0 按 Java 25 编译,降至 0.4.0(Java 21 兼容);roseauCheck 改为普通任务并在 doFirst 内以 javaexec 分两次产出 CSV/HTML;基线构件改由 rootProject 解析;已声明与配置缓存不兼容;本机 codec/rpc 验证通过 | `./gradlew :anvillib-<模块>-neoforge-1.21.1:roseauCheck` | | L-2 | RPC 行为验证(#P1/#P18 T8) | 远程调用往返、@CallableParam 编解码、tick 超时 | runClient/runServer,`-Danvillib.test.rpc.auto` 系统属性 | | L-3 | sync 行为验证(#P2) | @Sync 字段同步、LazySync、processor 字节码注入生效 | runClient 启动日志 + 反编译核对注入点 | | L-4 | explosion 行为验证(#P3/#P18 T9) | 分层球壳破坏、熔化替换、实体 hurt | runClient/runServer 执行测试命令 | diff --git a/gradle/scripts/roseau.gradle b/gradle/scripts/roseau.gradle index 8b3d4d03..e64497bd 100644 --- a/gradle/scripts/roseau.gradle +++ b/gradle/scripts/roseau.gradle @@ -3,12 +3,21 @@ configurations { } dependencies { - roseau 'io.github.alien-tools:roseau-cli:0.6.0' + roseau 'io.github.alien-tools:roseau-cli:0.4.0' } -def jarFileProvider = tasks.named('jar', Jar) -def reportsDirProvider = layout.buildDirectory.dir('reports/roseau') -def compileClasspathProvider = provider { sourceSets.main.compileClasspath.asPath } +// 基线构件统一由根工程解析:子工程在设置 group/version 后解析自身坐标会被 +// Gradle 解析成本地工程组件('default' 配置缺失),故借根工程仓库规避自引用。 +def ensureRootRepositories = { + if (!rootProject.repositories.any { it instanceof org.gradle.api.artifacts.repositories.MavenArtifactRepository && it.url.toString().contains('server.cjsah.net') }) { + rootProject.repositories.maven { it.url = 'https://server.cjsah.net:1002/maven/' } + } + if (!rootProject.repositories.any { it instanceof org.gradle.api.artifacts.repositories.MavenArtifactRepository && it.url.toString().startsWith('file:') }) { + rootProject.repositories.mavenLocal() + } +} +ensureRootRepositories() + def roseauGroup = project.group.toString() def roseauArtifact = project.name def roseauBaselineVersion = project.findProperty('roseauBaselineVersion') ?: { @@ -39,41 +48,49 @@ def roseauBaselineVersion = project.findProperty('roseauBaselineVersion') ?: { return version.toString() }() def roseauMvnCoord = "${roseauGroup}:${roseauArtifact}:${roseauBaselineVersion}" -def roseauBaselineDep = dependencies.create(group: roseauGroup, name: roseauArtifact, version: roseauBaselineVersion) -def roseauBaselineConfig = configurations.detachedConfiguration(roseauBaselineDep) -roseauBaselineConfig.transitive = false -def roseauBaselineJarProvider = provider { roseauBaselineConfig.singleFile } def roseauConfig = rootProject.file("roseau.yaml").absolutePath +def roseauLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(21) +} -tasks.register('roseauCheck', JavaExec) { +tasks.register('roseauCheck') { group = 'verification' description = 'Detects API breaking changes against a baseline version using Roseau' dependsOn tasks.named('jar', Jar) - classpath = configurations.roseau - mainClass = 'io.github.alien.roseau.cli.RoseauCLI' - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(21) - } + notCompatibleWithConfigurationCache('roseauCheck 基线解析依赖 detached configuration,无法参与配置缓存') doFirst { - def currentJar = jarFileProvider.get().archiveFile.get().asFile - def baselineJar = roseauBaselineJarProvider.get() - def reportsDir = reportsDirProvider.get().asFile + def currentJar = tasks.named('jar', Jar).get().archiveFile.get().asFile + def baselineDep = dependencies.create(group: roseauGroup, name: roseauArtifact, version: roseauBaselineVersion) + def baselineConfig = rootProject.configurations.detachedConfiguration(baselineDep) + baselineConfig.transitive = false + def baselineJar = baselineConfig.singleFile + def reportsDir = layout.buildDirectory.dir('reports/roseau').get().asFile reportsDir.mkdirs() logger.lifecycle("Roseau: comparing baseline ${roseauMvnCoord} against ${currentJar.name}") - args( - '--diff', - '--v1', baselineJar.absolutePath, - '--v2', currentJar.absolutePath, - '--classpath', compileClasspathProvider.get(), - '--fail-on-bc', - '--report', "HTML=${new File(reportsDir, 'report.html').absolutePath}", - '--report', "CSV=${new File(reportsDir, 'report.csv').absolutePath}", - '--config', roseauConfig - ) + // roseau-cli 0.4.0(Java 21 兼容)每次 --report 仅支持单一格式,故分别产出 CSV 与 HTML + def runRoseau = { String format, String reportFile -> + javaexec { + executable = roseauLauncher.get().executablePath.asFile.absolutePath + classpath = configurations.roseau + mainClass = 'io.github.alien.roseau.cli.RoseauCLI' + args( + '--diff', + '--v1', baselineJar.absolutePath, + '--v2', currentJar.absolutePath, + '--classpath', sourceSets.main.compileClasspath.asPath, + '--fail-on-bc', + '--report', reportFile, + '--format', format, + '--config', roseauConfig + ) + } + } + runRoseau('CSV', new File(reportsDir, 'report.csv').absolutePath) + runRoseau('HTML', new File(reportsDir, 'report.html').absolutePath) } } From e7f6928c649a67f92c036db3cd1fb179e1f6bfee Mon Sep 17 00:00:00 2001 From: Gugle Date: Thu, 13 Aug 2026 13:36:52 +0800 Subject: [PATCH 37/38] =?UTF-8?q?feat(wheel):=20=E5=90=8C=E6=AD=A5=20dev/2?= =?UTF-8?q?6.1=20=E7=8E=AF=E6=89=87=E6=A8=A1=E5=BC=8F=E8=BD=AE=E7=9B=98?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E9=87=8D=E5=81=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 盘面新增半透明深色圆盘与毛玻璃模糊背景(WheelFrostedBackground + frosted_disc/blur shader) - 中心区域与扇区之间新增白色不透明细圆环分隔 - 中心留白垂直居中显示当前悬停项名称 - 新增贴分隔圆环内侧、指向鼠标方向的尖括号箭头与左右下角浮动翻页箭头(segment shader) - 高亮扇区在扇区间平滑滑动,停住后内外缘同步外扩 2px 且透明度由 20% 升至 100% - annular_sector shader 半径语义修正(直径→半径),WheelScreen 半径比例调整为 0.17/0.33 - 渲染适配 1.21.1 Tesselator + ShaderInstance 模式,不引入 module.rendering - module.test 演示菜单扩至 9/8 项并补齐图标渲染器,新增扇形效果按键 B/N --- docs/migration-todo-port-1.21.1.md | 20 + .../assets/anvillib_test/lang/en_ud.json | 2 + .../assets/anvillib_test/lang/en_us.json | 2 + .../lib/v2/test/wheel/WheelDemoMenus.java | 109 +- .../v2/test/wheel/WheelTestClientHandler.java | 43 +- .../lib/v2/test/wheel/WheelTestKeys.java | 22 +- .../assets/anvillib_test/lang/zh_cn.json | 4 +- .../gui/component/WheelFrostedBackground.java | 103 ++ .../client/gui/component/WheelWidget.java | 1124 +++++++++++------ .../wheel/client/gui/screen/WheelScreen.java | 50 +- .../lib/v2/wheel/client/init/LibShaders.java | 40 + .../anvillib/shaders/core/annular_sector.fsh | 9 +- .../anvillib/shaders/core/annular_sector.json | 4 +- .../assets/anvillib/shaders/core/blur.fsh | 26 + .../assets/anvillib/shaders/core/blur.json | 31 + .../assets/anvillib/shaders/core/disc.fsh | 24 + .../assets/anvillib/shaders/core/disc.json | 45 + .../anvillib/shaders/core/frosted_disc.fsh | 29 + .../anvillib/shaders/core/frosted_disc.json | 48 + .../assets/anvillib/shaders/core/segment.fsh | 29 + .../assets/anvillib/shaders/core/segment.json | 54 + 21 files changed, 1408 insertions(+), 410 deletions(-) create mode 100644 module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelFrostedBackground.java create mode 100644 module.wheel/src/main/resources/assets/anvillib/shaders/core/blur.fsh create mode 100644 module.wheel/src/main/resources/assets/anvillib/shaders/core/blur.json create mode 100644 module.wheel/src/main/resources/assets/anvillib/shaders/core/disc.fsh create mode 100644 module.wheel/src/main/resources/assets/anvillib/shaders/core/disc.json create mode 100644 module.wheel/src/main/resources/assets/anvillib/shaders/core/frosted_disc.fsh create mode 100644 module.wheel/src/main/resources/assets/anvillib/shaders/core/frosted_disc.json create mode 100644 module.wheel/src/main/resources/assets/anvillib/shaders/core/segment.fsh create mode 100644 module.wheel/src/main/resources/assets/anvillib/shaders/core/segment.json diff --git a/docs/migration-todo-port-1.21.1.md b/docs/migration-todo-port-1.21.1.md index 35749257..437b4562 100644 --- a/docs/migration-todo-port-1.21.1.md +++ b/docs/migration-todo-port-1.21.1.md @@ -33,6 +33,7 @@ | #P18 | module.test:T2/T8/T9/T10 可选测试 | 低-中 | ✅ 已完成 | `a4ab3acd` | | #P19 | 构建体系:roseauCheck + 模块化 CI 构建矩阵 | 中 | ✅ 已完成 | `544c9eb0` + 本轮 | | #P20 | module.codec:CodecUtil 便携 create/mapCodec(计划外追加) | 低 | ✅ 已完成 | `05235aa`(来源 `173a454`) | +| #P21 | module.wheel:环扇模式轮盘渲染重做同步(计划外追加) | 中-高 | ✅ 已完成 | `92661c6` | > ⚠️ 2026-08-12 状态更新:#P7(module.rendering)与 #P8(renderdoc-loader)已从 `port/1.21.1` 分支整体移除;同时清理了 `module.font` / `module.main` 的渲染依赖、CI 工作流(ci / release / pull_request)中的渲染任务与相关文档引用。 @@ -203,6 +204,25 @@ - [x] `gradlew :anvillib-codec-neoforge-1.21.1:compileJava --rerun-tasks` 通过 - [ ] 提交(conventional commit) +### #P21 module.wheel:环扇模式轮盘渲染重做同步(计划外追加 · 2026-08-13) + +> 来源:`dev/26.1` 提交 `aa123c9`(feat(wheel): 重做环扇模式轮盘渲染 #98) +> 要求:效果与用法(公开 API)与 dev 一致,实现适配 1.21.1(module.rendering 已在 port 删除,不引入) + +- [x] API 核对:api 包两边一致(`WheelEntry.mutableCopy` 位置、`WheelSelectionEffect` 空行差异无关紧要;`WheelEntryRenderer` 保持 1.21.1 `(GuiGraphics, PoseStack)` 签名) +- [x] shader:`annular_sector` 半径语义修正(InnerDiameter/OuterDiameter → InnerRadius/OuterRadius);新增 `frosted_disc`/`blur`/`segment`/`disc` 四组 fsh+json(1.21.1 坐标约定:位置任意形状按 `selection.fsh` 做 Y 翻转) +- [x] `LibShaders` 新增 4 个 ShaderInstance 注册(frostedDisc/blur/segment/disc) +- [x] `WheelFrostedBackground` 自建模糊管线(`TextureTarget`×2 ping-pong,4 pass 高斯模糊,替代 dev 的 rendering `GaussianBlur`) +- [x] `WheelWidget` 以 dev 版为蓝本重写:盘面/毛玻璃/分隔圆环/中心标题/hover 箭头/翻页箭头/环扇滑动 + settle 外扩动画(500ms 停驻 + 200ms 外扩 2px + 透明度 20%→100%);渲染全部落在 Tesselator + ShaderInstance + 多边形 fallback 模式 +- [x] `WheelScreen` 半径 0.17/0.33、毛玻璃生命周期(init/removed)、翻页箭头替代「1 / N」文字 +- [x] module.test 对齐:`WheelDemoMenus` 扩至 9/8 项 + APPLE 图标 renderer(1.21.1 签名),新增 B/N 键(TAP/HOLD 扇形效果),lang 补齐 zh_cn/en_us/en_ud +- [x] `gradlew :anvillib-wheel-neoforge-1.21.1:compileJava` + `:anvillib-test-neoforge-1.21.1:compileJava` 通过;`grep v2.rendering` 零残留 +- [x] runClient 运行验证(2026-08-13):6 个 shader 全部注册成功,无 FATAL、无 wheel 渲染错误,正常进入世界;修掉 1.21.1 shader json `samplers` 字段格式问题(需 JsonObject 数组而非字符串数组) +- [x] 提交(conventional commit) + +> ⚠️ 环境遗留:module.test `runData` 受 modDevGradle 2.0.78 注入 `--width/--height` 影响无法运行(1.21.1 datagen 不识别该参数),en_us/en_ud 已按 dev 文案手动补齐;该问题为既有构建环境问题,非本次改动引入。 +> ⚠️ 环境遗留:runClient 首启时 `anvillib_moveable_entity_block` mixin 崩溃(PistonBaseBlockMixin 找不到)为模块 classes 未编译的环境状态问题(moddev 对仅含 resources 的模块 mod file 无法加载 mixin 类),全量编译后自愈,与本次改动无关。 + --- ## 待验证事项销账(计划附录 B) diff --git a/module.test/src/generated/resources/assets/anvillib_test/lang/en_ud.json b/module.test/src/generated/resources/assets/anvillib_test/lang/en_ud.json index 38d7e5fb..42aed1a0 100644 --- a/module.test/src/generated/resources/assets/anvillib_test/lang/en_ud.json +++ b/module.test/src/generated/resources/assets/anvillib_test/lang/en_ud.json @@ -2,6 +2,8 @@ "block.anvillib_test.test_controller": "ɹǝꞁꞁoɹʇuoƆ ʇsǝ⟘", "itemGroup.anvillib_test.tab": "qɐ⟘ ʇsǝ⟘", "key.anvillib_test.wheel_hold": "pꞁoH ꞁǝǝɥM", + "key.anvillib_test.wheel_hold_annular": "(ɹɐꞁnuuⱯ) pꞁoH ꞁǝǝɥM", "key.anvillib_test.wheel_tap": "dɐ⟘ ꞁǝǝɥM", + "key.anvillib_test.wheel_tap_annular": "(ɹɐꞁnuuⱯ) dɐ⟘ ꞁǝǝɥM", "key.categories.anvillib_test.wheel": "ꞁǝǝɥM-ʇsǝ⟘ qᴉꞀꞁᴉʌuⱯ" } \ No newline at end of file diff --git a/module.test/src/generated/resources/assets/anvillib_test/lang/en_us.json b/module.test/src/generated/resources/assets/anvillib_test/lang/en_us.json index f7050783..e73b9002 100644 --- a/module.test/src/generated/resources/assets/anvillib_test/lang/en_us.json +++ b/module.test/src/generated/resources/assets/anvillib_test/lang/en_us.json @@ -2,6 +2,8 @@ "block.anvillib_test.test_controller": "Test Controller", "itemGroup.anvillib_test.tab": "Test Tab", "key.anvillib_test.wheel_hold": "Wheel Hold", + "key.anvillib_test.wheel_hold_annular": "Wheel Hold (Annular)", "key.anvillib_test.wheel_tap": "Wheel Tap", + "key.anvillib_test.wheel_tap_annular": "Wheel Tap (Annular)", "key.categories.anvillib_test.wheel": "AnvilLib Test-Wheel" } \ No newline at end of file diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelDemoMenus.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelDemoMenus.java index 8712a08c..cfd7386c 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelDemoMenus.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelDemoMenus.java @@ -1,35 +1,118 @@ package dev.anvilcraft.lib.v2.test.wheel; +import com.mojang.blaze3d.vertex.PoseStack; import dev.anvilcraft.lib.v2.wheel.api.WheelMenuBuilder; import dev.anvilcraft.lib.v2.wheel.api.WheelMenuModel; +import dev.anvilcraft.lib.v2.wheel.api.WheelSelectionEffect; +import net.minecraft.client.gui.GuiGraphics; import net.minecraft.network.chat.Component; +import net.minecraft.world.item.Items; public final class WheelDemoMenus { private WheelDemoMenus() { } - public static WheelMenuModel buildTapDemo(int slotsPerPage) { + public static WheelMenuModel buildTapDemo(int slotsPerPage, WheelSelectionEffect selectionEffect) { return WheelMenuBuilder.create() .slotsPerPage(slotsPerPage) - .action("action_1", Component.literal("Action 1"), ctx -> {}) - .action("action_2", Component.literal("Action 2"), ctx -> {}) - .submenu("tools", Component.literal("Tools"), submenu -> submenu - .action("tool_a", Component.literal("Tool A"), ctx -> {}) - .action("tool_b", Component.literal("Tool B"), ctx -> {}) + .selectionEffect(selectionEffect) + .action( + "action_1", Component.literal("Action 1"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "action_2", Component.literal("Action 2"), WheelDemoMenus::render, ctx -> { + } + ) + .submenu( + "tools", Component.literal("Tools"), WheelDemoMenus::render, submenu -> submenu + .action( + "tool_a", Component.literal("Tool A"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "tool_b", Component.literal("Tool B"), WheelDemoMenus::render, ctx -> { + } + ) + ) + .action( + "action_3", Component.literal("Action 3"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "action_4", Component.literal("Action 4"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "action_5", Component.literal("Action 5"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "action_6", Component.literal("Action 6"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "action_7", Component.literal("Action 7"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "action_8", Component.literal("Action 8"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "action_9", Component.literal("Action 9"), WheelDemoMenus::render, ctx -> { + } ) - .action("action_3", Component.literal("Action 3"), ctx -> {}) .build(); } - public static WheelMenuModel buildHoldDemo(int slotsPerPage) { + public static WheelMenuModel buildHoldDemo(int slotsPerPage, WheelSelectionEffect selectionEffect) { return WheelMenuBuilder.create() .slotsPerPage(slotsPerPage) - .action("hold_1", Component.literal("Hold 1"), (g, p, w, h) -> {}, ctx -> {}) - .submenu("ignored_submenu", Component.literal("Ignored Submenu"), (g, p, w, h) -> {}, submenu -> submenu - .action("never_opened", Component.literal("Never Opened"), (g, p, w, h) -> {}, ctx -> {}) + .selectionEffect(selectionEffect) + .action( + "hold_1", Component.literal("Hold 1"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "hold_2", Component.literal("Hold 2"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "hold_3", Component.literal("Hold 3"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "hold_4", Component.literal("Hold 4"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "hold_5", Component.literal("Hold 5"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "hold_6", Component.literal("Hold 6"), WheelDemoMenus::render, ctx -> { + } + ) + .action( + "hold_7", Component.literal("Hold 7"), WheelDemoMenus::render, ctx -> { + } + ) + .submenu( + "ignored_submenu", Component.literal("Ignored Submenu"), WheelDemoMenus::render, submenu -> submenu + .action( + "never_opened", Component.literal("Never Opened"), WheelDemoMenus::render, ctx -> { + } + ) + ) + .action( + "hold_8", Component.literal("Hold 8"), WheelDemoMenus::render, ctx -> { + } ) - .action("hold_2", Component.literal("Hold 2"), (g, p, w, h) -> {}, ctx -> {}) .build(); } -} + public static void render(GuiGraphics graphics, PoseStack pose, int width, int height) { + graphics.renderItem(Items.APPLE.getDefaultInstance(), -8, -8); + } +} diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelTestClientHandler.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelTestClientHandler.java index 147f71f4..c7b7bef9 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelTestClientHandler.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelTestClientHandler.java @@ -1,6 +1,7 @@ package dev.anvilcraft.lib.v2.test.wheel; import dev.anvilcraft.lib.v2.test.AnvilLibTest; +import dev.anvilcraft.lib.v2.wheel.api.WheelSelectionEffect; import dev.anvilcraft.lib.v2.wheel.client.input.WheelScreenController; import net.minecraft.client.Minecraft; import net.neoforged.api.distmarker.Dist; @@ -13,7 +14,8 @@ @EventBusSubscriber(modid = AnvilLibTest.MOD_ID, value = Dist.CLIENT) public final class WheelTestClientHandler { private static final WheelScreenController CONTROLLER = new WheelScreenController(); - private static boolean holdKeyWasDown = false; + private static boolean holdDotKeyWasDown = false; + private static boolean holdAnnularKeyWasDown = false; private WheelTestClientHandler() { } @@ -30,7 +32,10 @@ public static void onClientTick(ClientTickEvent.Pre event) { // --- TAP --- while (WheelTestKeys.TAP_KEY.consumeClick()) { - CONTROLLER.openTap(WheelDemoMenus.buildTapDemo(8)); + CONTROLLER.openTap(WheelDemoMenus.buildTapDemo(8, WheelSelectionEffect.DOT)); + } + while (WheelTestKeys.TAP_ANNULAR_KEY.consumeClick()) { + CONTROLLER.openTap(WheelDemoMenus.buildTapDemo(8, WheelSelectionEffect.ANNULAR_SECTOR)); } } @@ -40,20 +45,38 @@ public static void onClientTick(ClientTickEvent.Pre event) { @SubscribeEvent public static void onKeyInput(InputEvent.Key event) { Minecraft mc = Minecraft.getInstance(); - if (mc.player == null || !WheelTestKeys.HOLD_KEY.matches(event.getKey(), event.getScanCode())) { + if (mc.player == null) { + return; + } + boolean holdDotKey = WheelTestKeys.HOLD_KEY.matches(event.getKey(), event.getScanCode()); + boolean holdAnnularKey = WheelTestKeys.HOLD_ANNULAR_KEY.matches(event.getKey(), event.getScanCode()); + if (!holdDotKey && !holdAnnularKey) { + return; + } + // 长按模式下松开按键时轮盘 Screen 已打开,必须放行 RELEASE 事件才能触发关闭 + if (mc.screen != null && event.getAction() != GLFW.GLFW_RELEASE) { return; } if (event.getAction() == GLFW.GLFW_PRESS) { - if (!holdKeyWasDown) { - CONTROLLER.onHoldKeyPressed(WheelDemoMenus.buildHoldDemo(8)); - holdKeyWasDown = true; + if (holdDotKey && !holdDotKeyWasDown) { + CONTROLLER.onHoldKeyPressed(WheelDemoMenus.buildHoldDemo(8, WheelSelectionEffect.DOT)); + holdDotKeyWasDown = true; + } + if (holdAnnularKey && !holdAnnularKeyWasDown) { + CONTROLLER.onHoldKeyPressed(WheelDemoMenus.buildHoldDemo(8, WheelSelectionEffect.ANNULAR_SECTOR)); + holdAnnularKeyWasDown = true; } return; } - if (event.getAction() == GLFW.GLFW_RELEASE && holdKeyWasDown) { - CONTROLLER.onHoldKeyReleased(); - holdKeyWasDown = false; + if (event.getAction() == GLFW.GLFW_RELEASE) { + if (holdDotKey && holdDotKeyWasDown) { + CONTROLLER.onHoldKeyReleased(); + holdDotKeyWasDown = false; + } + if (holdAnnularKey && holdAnnularKeyWasDown) { + CONTROLLER.onHoldKeyReleased(); + holdAnnularKeyWasDown = false; + } } } } - diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelTestKeys.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelTestKeys.java index f1a9cd43..243f61bc 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelTestKeys.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelTestKeys.java @@ -13,7 +13,7 @@ public final class WheelTestKeys { public static final String CATEGORY = "key.categories.anvillib_test.wheel"; - /** 点按模式:按一下打开,鼠标选择后点击触发 */ + /** 点按模式(圆点选中效果):按一下打开,鼠标选择后点击触发 */ public static final KeyMapping TAP_KEY = new KeyMapping( "key.anvillib_test.wheel_tap", InputConstants.Type.KEYSYM, @@ -21,7 +21,7 @@ public final class WheelTestKeys { CATEGORY ); - /** 长按模式:按住显示,松开触发当前选中项 */ + /** 长按模式(圆点选中效果):按住显示,松开触发当前选中项 */ public static final KeyMapping HOLD_KEY = new KeyMapping( "key.anvillib_test.wheel_hold", InputConstants.Type.KEYSYM, @@ -29,6 +29,22 @@ public final class WheelTestKeys { CATEGORY ); + /** 点按模式(扇形选中效果) */ + public static final KeyMapping TAP_ANNULAR_KEY = new KeyMapping( + "key.anvillib_test.wheel_tap_annular", + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_B, + CATEGORY + ); + + /** 长按模式(扇形选中效果) */ + public static final KeyMapping HOLD_ANNULAR_KEY = new KeyMapping( + "key.anvillib_test.wheel_hold_annular", + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_N, + CATEGORY + ); + private WheelTestKeys() { } @@ -36,5 +52,7 @@ private WheelTestKeys() { public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) { event.register(TAP_KEY); event.register(HOLD_KEY); + event.register(TAP_ANNULAR_KEY); + event.register(HOLD_ANNULAR_KEY); } } diff --git a/module.test/src/main/resources/assets/anvillib_test/lang/zh_cn.json b/module.test/src/main/resources/assets/anvillib_test/lang/zh_cn.json index 01f416d1..69653ab7 100644 --- a/module.test/src/main/resources/assets/anvillib_test/lang/zh_cn.json +++ b/module.test/src/main/resources/assets/anvillib_test/lang/zh_cn.json @@ -1,5 +1,7 @@ { "key.categories.anvillib_test.wheel": "铁砧库 测试-轮盘", "key.anvillib_test.wheel_tap": "轮盘(触发)", - "key.anvillib_test.wheel_hold": "轮盘(长按)" + "key.anvillib_test.wheel_hold": "轮盘(长按)", + "key.anvillib_test.wheel_tap_annular": "轮盘(触发,扇形)", + "key.anvillib_test.wheel_hold_annular": "轮盘(长按,扇形)" } \ No newline at end of file diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelFrostedBackground.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelFrostedBackground.java new file mode 100644 index 00000000..044de8cc --- /dev/null +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelFrostedBackground.java @@ -0,0 +1,103 @@ +package dev.anvilcraft.lib.v2.wheel.client.gui.component; + +import com.mojang.blaze3d.pipeline.RenderTarget; +import com.mojang.blaze3d.pipeline.TextureTarget; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; +import dev.anvilcraft.lib.v2.wheel.client.init.LibShaders; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.ShaderInstance; +import org.jetbrains.annotations.ApiStatus; +import org.joml.Matrix4f; + +import java.util.Objects; + +/** + * 轮盘毛玻璃盘面背景:每帧对主渲染目标做一次可分离高斯模糊,供盘面着色器采样。 + * 两个纹理目标乒乓交替,水平/垂直各模糊两轮共四 pass,与 dev/26.1 的 GaussianBlur(2.0f, 1.0f) 行为一致。 + */ +@ApiStatus.Internal +@Slf4j +public final class WheelFrostedBackground implements AutoCloseable { + private static final float SAMPLE_STEP_LENGTH = 2.0f; + + private TextureTarget firstTarget = new TextureTarget(2, 2, true, true); + private TextureTarget secondTarget = new TextureTarget(2, 2, true, true); + private int width; + private int height; + + /** + * 对当前主渲染目标执行模糊,返回模糊结果纹理 id;blur shader 未就绪时返回 -1。 + */ + public int capture(GuiGraphics guiGraphics) { + RenderTarget mainTarget = Minecraft.getInstance().getMainRenderTarget(); + this.guardSize(mainTarget); + return this.blur(guiGraphics, mainTarget.getColorTextureId()); + } + + private int blur(GuiGraphics guiGraphics, int inputTextureId) { + this.blurOnce(guiGraphics, this.secondTarget, inputTextureId, 1.0f, 0.0f); + this.blurOnce(guiGraphics, this.firstTarget, this.secondTarget.getColorTextureId(), 0.0f, 1.0f); + this.blurOnce(guiGraphics, this.secondTarget, this.firstTarget.getColorTextureId(), 1.0f, 0.0f); + this.blurOnce(guiGraphics, this.firstTarget, this.secondTarget.getColorTextureId(), 0.0f, 1.0f); + return this.firstTarget.getColorTextureId(); + } + + private void blurOnce( + GuiGraphics guiGraphics, + TextureTarget target, + int inputTextureId, + float directionX, + float directionY + ) { + ShaderInstance blurShader = LibShaders.getBlurShader(); + if (blurShader == null) { + log.error("Wheel frosted background blur shader is not ready"); + return; + } + target.bindWrite(false); + RenderSystem.setShaderTexture(0, inputTextureId); + Matrix4f matrix4f = guiGraphics.pose().last().pose(); + float w = (float) Minecraft.getInstance().getWindow().getGuiScaledWidth(); + float h = (float) Minecraft.getInstance().getWindow().getGuiScaledHeight(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + // 绘制到 FBO 时 GUI 顶部 (y=0) 对应纹理 v=1(纹理数据顶部),此处做 V 轴翻转 + bufferBuilder.addVertex(matrix4f, 0, 0, 0).setUv(0, 1); + bufferBuilder.addVertex(matrix4f, 0, h, 0).setUv(0, 0); + bufferBuilder.addVertex(matrix4f, w, h, 0).setUv(1, 0); + bufferBuilder.addVertex(matrix4f, w, 0, 0).setUv(1, 1); + RenderSystem.disableDepthTest(); + RenderSystem.setShader(() -> blurShader); + blurShader.safeGetUniform("Direction").set(directionX, directionY); + blurShader.safeGetUniform("SampleStepLength").set(SAMPLE_STEP_LENGTH); + RenderSystem.setShaderColor(1, 1, 1, 1); + BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); + RenderSystem.enableDepthTest(); + Minecraft.getInstance().getMainRenderTarget().bindWrite(false); + } + + private void guardSize(RenderTarget mainTarget) { + if (mainTarget.width == this.width && mainTarget.height == this.height) { + return; + } + this.width = mainTarget.width; + this.height = mainTarget.height; + this.firstTarget.destroyBuffers(); + this.secondTarget.destroyBuffers(); + this.firstTarget = new TextureTarget(this.width, this.height, true, true); + this.secondTarget = new TextureTarget(this.width, this.height, true, true); + } + + @Override + public void close() { + this.firstTarget.destroyBuffers(); + this.secondTarget.destroyBuffers(); + } +} diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java index 60f2f0ff..73f5995a 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java @@ -13,6 +13,7 @@ import dev.anvilcraft.lib.v2.wheel.client.init.LibShaders; import lombok.Getter; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; @@ -20,6 +21,7 @@ import net.minecraft.client.renderer.GameRenderer; import net.minecraft.client.renderer.ShaderInstance; import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; import org.joml.Matrix4f; import org.joml.Vector2f; @@ -28,15 +30,47 @@ import java.util.Objects; import javax.annotation.Nullable; +@Slf4j +@SuppressWarnings( + { + "UnusedReturnValue", + "unused" + } +) public class WheelWidget extends AbstractWidget { public static final int IGNORE_CURSOR_MOVE_LENGTH = 15; public static final int DEFAULT_SELECTION_EFFECT_COLOR = 0xFFFABC02; private static final float SELECTION_DOT_DIAMETER_RATIO = 0.9f; private static final float TAU = (float) (Math.PI * 2.0); private static final float ANGLE_AA_RAD = 0.06f; + private static final Vector2f ROTATION_START = new Vector2f(0, 1); + // 中心区域与扇区之间的白色不透明分隔圆环:半径在中心区基础上外扩的距离与线宽 + // (dev/26.1 的 SdfGraphics.stroke 实际厚度为入参一半) + private static final float SEPARATOR_RING_GAP = 4f; + private static final float SEPARATOR_RING_THICKNESS = 2f; + // 高亮扇区内缘相对中心区的内缩量(贴合分隔圆环外侧) + private static final float SECTION_INNER_INSET = 8f; + // 未停住时高亮扇区外缘相对盘面外边缘的内缩量;停住后内外缘同步外扩 SETTLE_EXPAND 距离 + private static final float SECTION_OUTER_INSET = 2f; + // 鼠标停住后高亮扇区内外缘同步向外扩张的距离与动画参数 + private static final float SETTLE_EXPAND = 2f; + private static final long SETTLE_DELAY_MS = 500L; + private static final long SETTLE_ANIM_MS = 200L; + // 指向鼠标方向的尖括号箭头(贴在分隔圆环内侧)尺寸与线宽 + private static final float HOVER_CHEVRON_SIZE = 9f; + private static final float HOVER_CHEVRON_THICKNESS = 4f; + private static final float HOVER_CHEVRON_RING_GAP = 4f; + // 中心悬停名缩放倍率 + private static final float CENTER_TITLE_SCALE = 1.4f; + // 翻页箭头(尖括号)尺寸与线宽 + private static final float CHEVRON_SIZE = 8f; + private static final float CHEVRON_THICKNESS = 3f; + // 毛玻璃盘面叠加色:白 * 40% 透明度,混合在深色盘面上 + private static final int FROSTED_TINT = 0x66FFFFFF; + // 绘制深度分层:盘面/毛玻璃 < 分隔圆环 < 高亮 < 图标 + private static final float DISC_Z = 50f; private static final float RING_Z = 60f; private static final float SELECTION_Z = 80f; - private static final Vector2f ROTATION_START = new Vector2f(0, 1); private final Minecraft minecraft = Minecraft.getInstance(); private final Vector2f centerPos; @@ -56,6 +90,13 @@ public class WheelWidget extends AbstractWidget { private float currentAngle = 0; @Getter private int currentSectionIndex = -1; + // 环扇高亮当前渲染角度(弧度,轮盘坐标系),每帧向目标扇区平滑逼近 + private float selectionAngleRad = 0; + // 最近一次选中扇区变化的时间,用于“鼠标停住”检测 + private long selectionChangeTime = System.currentTimeMillis(); + // 鼠标相对轮盘中心的方向角(弧度,与 WheelSection.angle 同坐标系),在死区内时为 null + @Nullable + private Float mouseAngleRad; private Vector2f selectionEffectPos; private boolean animationStarted = false; @Getter @@ -63,107 +104,205 @@ public class WheelWidget extends AbstractWidget { private boolean closingAnimationStarted = false; private final int deadZone; private WheelSelectionEffect selectionEffect = WheelSelectionEffect.DOT; + @Nullable + private WheelFrostedBackground frostedBackground; + private boolean hasPreviousPage; + private boolean hasNextPage; public WheelWidget( - int x, int y, int width, int height, - float ringInnerRadius, float ringOuterRadius, float textScale, - List sections, int deadZone + int x, + int y, + int width, + int height, + float ringInnerRadius, + float ringOuterRadius, + float textScale, + List sections, + int deadZone ) { this(x, y, width, height, Component.empty(), ringInnerRadius, ringOuterRadius, textScale, sections, deadZone); } public WheelWidget( - int x, int y, int width, int height, - float ringInnerRadius, float ringOuterRadius, float textScale, float degreeOffsetAngle, - List sections, int deadZone + int x, + int y, + int width, + int height, + float ringInnerRadius, + float ringOuterRadius, + float textScale, + float degreeOffsetAngle, + List sections, + int deadZone ) { this(x, y, width, height, Component.empty(), ringInnerRadius, ringOuterRadius, textScale, degreeOffsetAngle, sections, deadZone); } - public WheelWidget( - int x, int y, int width, int height, - float ringInnerRadius, float ringOuterRadius, - List sections, int deadZone - ) { + public WheelWidget(int x, int y, int width, int height, float ringInnerRadius, float ringOuterRadius, List sections, int deadZone) { this(x, y, width, height, Component.empty(), ringInnerRadius, ringOuterRadius, sections, deadZone); } public WheelWidget( - int x, int y, int width, int height, Component message, - float ringInnerRadius, float ringOuterRadius, float textScale, float degreeOffsetAngle, - List sections, int deadZone + int x, + int y, + int width, + int height, + Component message, + float ringInnerRadius, + float ringOuterRadius, + float textScale, + float degreeOffsetAngle, + List sections, + int deadZone ) { this( - x, y, width, height, message, - ringInnerRadius, ringOuterRadius, - 150, 300, 150, + x, + y, + width, + height, + message, + ringInnerRadius, + ringOuterRadius, + 150, + 300, + 150, 0x88000000, - DEFAULT_SELECTION_EFFECT_COLOR, 5f, - 0xfdfdfd, textScale, degreeOffsetAngle, - sections, deadZone + DEFAULT_SELECTION_EFFECT_COLOR, + 5f, + 0xfdfdfd, + textScale, + degreeOffsetAngle, + sections, + deadZone ); } public WheelWidget( - int x, int y, int width, int height, Component message, - float ringInnerRadius, float ringOuterRadius, - List sections, int deadZone + int x, + int y, + int width, + int height, + Component message, + float ringInnerRadius, + float ringOuterRadius, + List sections, + int deadZone ) { this( - x, y, width, height, message, - ringInnerRadius, ringOuterRadius, - 150, 300, 150, + x, + y, + width, + height, + message, + ringInnerRadius, + ringOuterRadius, + 150, + 300, + 150, 0x88000000, - DEFAULT_SELECTION_EFFECT_COLOR, 5f, - 0xfdfdfd, 1f, 0f, - sections, deadZone + DEFAULT_SELECTION_EFFECT_COLOR, + 5f, + 0xfdfdfd, + 1f, + 0f, + sections, + deadZone ); } public WheelWidget( - int x, int y, int width, int height, Component message, - float ringInnerRadius, float ringOuterRadius, float degreeOffsetAngle, - List sections, int deadZone + int x, + int y, + int width, + int height, + Component message, + float ringInnerRadius, + float ringOuterRadius, + float degreeOffsetAngle, + List sections, + int deadZone ) { this( - x, y, width, height, message, - ringInnerRadius, ringOuterRadius, - 150, 300, 150, + x, + y, + width, + height, + message, + ringInnerRadius, + ringOuterRadius, + 150, + 300, + 150, 0x88000000, - DEFAULT_SELECTION_EFFECT_COLOR, 5f, - 0xfdfdfd, 1f, degreeOffsetAngle, - sections, deadZone + DEFAULT_SELECTION_EFFECT_COLOR, + 5f, + 0xfdfdfd, + 1f, + degreeOffsetAngle, + sections, + deadZone ); } public WheelWidget( - int x, int y, int width, int height, - float ringInnerRadius, float ringOuterRadius, - int delay, int animationMs, int closingAnimationMs, + int x, + int y, + int width, + int height, + float ringInnerRadius, + float ringOuterRadius, + int delay, + int animationMs, + int closingAnimationMs, int ringColor, - int selectionEffectColor, float selectionAnimationSpeedFactor, - int textColor, float textScale, - List sections, int deadZone + int selectionEffectColor, + float selectionAnimationSpeedFactor, + int textColor, + float textScale, + List sections, + int deadZone ) { this( - x, y, width, height, Component.empty(), - ringInnerRadius, ringOuterRadius, - delay, animationMs, closingAnimationMs, + x, + y, + width, + height, + Component.empty(), + ringInnerRadius, + ringOuterRadius, + delay, + animationMs, + closingAnimationMs, ringColor, - selectionEffectColor, selectionAnimationSpeedFactor, - textColor, textScale, 0f, - sections, deadZone + selectionEffectColor, + selectionAnimationSpeedFactor, + textColor, + textScale, + 0f, + sections, + deadZone ); } public WheelWidget( - int x, int y, int width, int height, Component message, - float ringInnerRadius, float ringOuterRadius, - int delay, int animationMs, int closingAnimationMs, + int x, + int y, + int width, + int height, + Component message, + float ringInnerRadius, + float ringOuterRadius, + int delay, + int animationMs, + int closingAnimationMs, int ringColor, - int selectionEffectColor, float selectionAnimationSpeedFactor, - int textColor, float textScale, float degreeOffsetAngle, - List sections, int deadZone + int selectionEffectColor, + float selectionAnimationSpeedFactor, + int textColor, + float textScale, + float degreeOffsetAngle, + List sections, + int deadZone ) { super(x, y, width, height, message); this.centerPos = new Vector2f(this.getX() + this.getWidth() / 2f, this.getY() + this.getHeight() / 2f); @@ -184,7 +323,7 @@ public WheelWidget( float rotation = MathUtil.clampWithProportion((degreeEachRotation * i + degreeOffsetAngle) % 360, 0, 360); Vector2f rotated = MathUtil.rotationDegrees(ROTATION_START, rotation) .mul(1, -1) - .mul(this.getSectionCircleDiameter()) + .mul(this.getSectionCircleRadius()) .add(this.centerPos); float detectionStart = (float) (Math.toRadians(rotation - degreeEachRotation / 2f) + Math.PI * 2); float detectionEnd = (float) (Math.toRadians(rotation + degreeEachRotation / 2f) + Math.PI * 2); @@ -198,7 +337,207 @@ public WheelWidget( section )); } - this.selectionEffectPos = MathUtil.rotate(MathUtil.copy(ROTATION_START).mul(this.getSectionCircleDiameter()), this.currentAngle); + this.selectionEffectPos = MathUtil.rotate(MathUtil.copy(ROTATION_START).mul(this.getSectionCircleRadius()), this.currentAngle); + } + + public void renderDisc( + GuiGraphics guiGraphics, + float progress + ) { + this.renderDisc(guiGraphics, this.centerPos.x, this.centerPos.y, this.getDiscRadius() * progress, this.ringColor); + } + + public void renderFrostedBackground( + GuiGraphics guiGraphics, + float progress + ) { + if (this.frostedBackground == null) { + return; + } + try { + Window window = this.minecraft.getWindow(); + float guiScale = (float) window.getGuiScale(); + float radius = this.getDiscRadius() * progress * guiScale; + if (radius <= 0) { + return; + } + int blurredTextureId = this.frostedBackground.capture(guiGraphics); + if (blurredTextureId < 0) { + return; + } + Matrix4f matrix4f = guiGraphics.pose().last().pose(); + float w = (float) window.getGuiScaledWidth(); + float h = (float) window.getGuiScaledHeight(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR); + // 全屏 quad:GUI 顶部 (y=0) 对应纹理 v=1(纹理数据顶部),做 V 轴翻转 + bufferBuilder.addVertex(matrix4f, 0, 0, DISC_Z).setUv(0, 1).setColor(FROSTED_TINT); + bufferBuilder.addVertex(matrix4f, 0, h, DISC_Z).setUv(0, 0).setColor(FROSTED_TINT); + bufferBuilder.addVertex(matrix4f, w, h, DISC_Z).setUv(1, 0).setColor(FROSTED_TINT); + bufferBuilder.addVertex(matrix4f, w, 0, DISC_Z).setUv(1, 1).setColor(FROSTED_TINT); + RenderSystem.disableDepthTest(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + ShaderInstance frostedDiscShader = LibShaders.getFrostedDiscShader(); + if (frostedDiscShader == null) { + RenderSystem.disableBlend(); + RenderSystem.enableDepthTest(); + log.error("Wheel frosted disc shader is not ready"); + return; + } + RenderSystem.setShaderTexture(0, blurredTextureId); + RenderSystem.setShader(() -> frostedDiscShader); + frostedDiscShader.safeGetUniform("Center").set(this.centerPos.x * guiScale, this.centerPos.y * guiScale); + frostedDiscShader.safeGetUniform("FramebufferSize").set((float) window.getWidth(), (float) window.getHeight()); + frostedDiscShader.safeGetUniform("Radius").set(radius); + frostedDiscShader.safeGetUniform("AntiAliasingRadius").set(1.25f); + RenderSystem.setShaderColor(1, 1, 1, 1); + BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); + RenderSystem.disableBlend(); + RenderSystem.enableDepthTest(); + } catch (Exception e) { + RenderSystem.disableBlend(); + RenderSystem.enableDepthTest(); + log.error("Wheel frosted background failed to render", e); + } + } + + public void renderSeparatorRing( + GuiGraphics guiGraphics, + float progress + ) { + // SdfGraphics.stroke 实际厚度为入参一半,此处直接以厚度/2 作为环带半宽 + float radius = (this.ringInnerRadius + SEPARATOR_RING_GAP) * progress; + float halfThickness = SEPARATOR_RING_THICKNESS * 0.5f; + WheelWidget.renderRing( + guiGraphics, + this.centerPos.x, + this.centerPos.y, + 0xFFFFFFFF, + radius - halfThickness, + radius + halfThickness + ); + } + + public void renderSelectionEffect(GuiGraphics guiGraphics, float centerX, float centerY, int color, float ringWidth) { + float dotDiameter = this.getSelectionDotDiameter(ringWidth) * 2.0f; + float dotRadius = dotDiameter * 0.5f; + Window window = this.minecraft.getWindow(); + float guiScale = (float) window.getGuiScale(); + Matrix4f matrix4f = guiGraphics.pose().last().pose(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.begin( + VertexFormat.Mode.QUADS, + DefaultVertexFormat.POSITION_COLOR + ); + float x1 = centerX - dotRadius - 5; + float y1 = centerY - dotRadius - 5; + float x2 = centerX + dotRadius + 5; + float y2 = centerY + dotRadius + 5; + bufferBuilder.addVertex(matrix4f, x1, y1, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x1, y2, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x2, y2, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x2, y1, SELECTION_Z).setColor(color); + RenderSystem.disableDepthTest(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + ShaderInstance selectionShader = LibShaders.getSelectionShader(); + if (selectionShader == null) { + this.renderSelectionFallback(guiGraphics, centerX, centerY, color, dotRadius); + RenderSystem.disableBlend(); + RenderSystem.enableDepthTest(); + return; + } + RenderSystem.setShader(() -> selectionShader); + selectionShader + .safeGetUniform("Center") + .set(centerX * guiScale, centerY * guiScale); + selectionShader + .safeGetUniform("FramebufferSize") + .set((float) window.getWidth(), (float) window.getHeight()); + selectionShader + .safeGetUniform("Radius") + .set(dotRadius * guiScale); + RenderSystem.setShaderColor(1, 1, 1, 1); + BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); + RenderSystem.disableBlend(); + RenderSystem.enableDepthTest(); + } + + public void renderAnnularSectorSelection( + GuiGraphics guiGraphics, + float centerX, + float centerY, + int color, + float innerRadius, + float outerRadius, + float centerAngleRad, + float rangeAngleRad + ) { + float boundsRadius = outerRadius + 5; + float x1 = centerX - boundsRadius; + float y1 = centerY - boundsRadius; + float x2 = centerX + boundsRadius; + float y2 = centerY + boundsRadius; + Window window = this.minecraft.getWindow(); + float guiScale = (float) window.getGuiScale(); + // Wheel section angle uses "up" as zero; shader atan uses +X as zero. + float shaderCenterAngle = centerAngleRad + TAU / 4.0f; + Matrix4f matrix4f = guiGraphics.pose().last().pose(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.begin( + VertexFormat.Mode.QUADS, + DefaultVertexFormat.POSITION_COLOR + ); + bufferBuilder.addVertex(matrix4f, x1, y1, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x1, y2, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x2, y2, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x2, y1, SELECTION_Z).setColor(color); + RenderSystem.disableDepthTest(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + ShaderInstance annularSectorShader = LibShaders.getAnnularSectorShader(); + if (annularSectorShader == null) { + this.renderAnnularSectorFallback( + guiGraphics, + centerX, + centerY, + color, + innerRadius, + outerRadius, + centerAngleRad, + rangeAngleRad + ); + RenderSystem.disableBlend(); + RenderSystem.enableDepthTest(); + return; + } + RenderSystem.setShader(() -> annularSectorShader); + annularSectorShader + .safeGetUniform("Center") + .set(centerX * guiScale, centerY * guiScale); + annularSectorShader + .safeGetUniform("InnerRadius") + .set(innerRadius * guiScale); + annularSectorShader + .safeGetUniform("OuterRadius") + .set(outerRadius * guiScale); + annularSectorShader + .safeGetUniform("AntiAliasingRadius") + .set(1.25f); + annularSectorShader + .safeGetUniform("AngleAntiAliasingRad") + .set(ANGLE_AA_RAD); + annularSectorShader + .safeGetUniform("CenterAngleRad") + .set(shaderCenterAngle); + annularSectorShader + .safeGetUniform("RangeAngleRad") + .set(rangeAngleRad); + RenderSystem.setShaderColor(1, 1, 1, 1); + BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); + RenderSystem.disableBlend(); + RenderSystem.enableDepthTest(); } public WheelWidget setSelectionEffect(WheelSelectionEffect selectionEffect) { @@ -211,22 +550,38 @@ public WheelWidget setSelectionEffectColor(int selectionEffectColor) { return this; } + public WheelWidget setFrostedBackground(@Nullable WheelFrostedBackground frostedBackground) { + this.frostedBackground = frostedBackground; + return this; + } + + public WheelWidget setPageState(boolean hasPreviousPage, boolean hasNextPage) { + this.hasPreviousPage = hasPreviousPage; + this.hasNextPage = hasNextPage; + return this; + } + public WheelWidget setCurrentIndex(int index) { if (index < 0 || index >= this.sections.size()) return this; if (!this.sections.get(index).selectable()) return this; - this.currentSectionIndex = index; + this.setCurrentSectionIndex(index); this.currentAngle = this.sections.get(index).angle; - this.selectionEffectPos = MathUtil.rotate(MathUtil.copy(ROTATION_START).mul(this.getSectionCircleDiameter()), this.currentAngle); + this.selectionEffectPos = MathUtil.rotate(MathUtil.copy(ROTATION_START).mul(this.getSectionCircleRadius()), this.currentAngle); return this; } public WheelWidget clearSelection() { - this.currentSectionIndex = -1; + this.setCurrentSectionIndex(-1); return this; } - public float getSectionCircleDiameter() { - return this.ringOuterRadius - this.ringInnerRadius + this.ringInnerRadius * 2; + public float getSectionCircleRadius() { + // 图标圆心所在圆周的半径:分隔圆环与盘面外缘之间扇区的中间位置 + return (this.ringOuterRadius + this.ringInnerRadius) * 0.5f; + } + + private float getDiscRadius() { + return this.ringOuterRadius; } public int getSectionSize() { @@ -245,9 +600,9 @@ public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, doubl } if (scrollY > 0) { - this.currentSectionIndex = this.findNextSelectableIndex(index, 1); + this.setCurrentSectionIndex(this.findNextSelectableIndex(index, 1)); } else if (scrollY < 0) { - this.currentSectionIndex = this.findNextSelectableIndex(index, -1); + this.setCurrentSectionIndex(this.findNextSelectableIndex(index, -1)); } for (WheelSection section : this.sections) { @@ -265,27 +620,34 @@ public void checkMousePos(double mouseX, double mouseY) { float centerY = this.centerPos.y; Vector2f cursorPos = new Vector2f((float) mouseX - centerX, (float) mouseY - centerY); if (cursorPos.length() < this.deadZone) { - this.currentSectionIndex = -1; + this.mouseAngleRad = null; + this.setCurrentSectionIndex(-1); return; } Vector2f rotationStart = new Vector2f(0, 1); cursorPos.normalize(); double rot = Math.acos(rotationStart.dot(cursorPos) / (rotationStart.length() * cursorPos.length())); double rotation = cursorPos.x < 0 ? Math.PI - rot : Math.PI + rot; + this.mouseAngleRad = (float) (rotation % (Math.PI * 2)); for (WheelSection section : this.sections) { if (( - section.angleStart > section.angleEnd && rotation >= section.angleStart - || rotation >= section.angleStart && rotation <= section.angleEnd - ) - && section.selectable - ) { + section.angleStart > section.angleEnd && rotation >= section.angleStart || rotation >= section.angleStart && rotation <= section.angleEnd + ) && section.selectable) { this.currentAngle = section.angle; - this.currentSectionIndex = this.sections.indexOf(section); + this.setCurrentSectionIndex(this.sections.indexOf(section)); break; } } } + private void setCurrentSectionIndex(int index) { + if (this.currentSectionIndex == index) { + return; + } + this.currentSectionIndex = index; + this.selectionChangeTime = System.currentTimeMillis(); + } + private int findNextSelectableIndex(int start, int direction) { if (this.sections.isEmpty()) { return -1; @@ -313,8 +675,6 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia @Override protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { - RenderSystem.enableDepthTest(); - RenderSystem.enableBlend(); this.renderClosingAnimation(guiGraphics); if (!this.shouldRender()) { return; @@ -333,14 +693,11 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo this.renderProgressAnimation(guiGraphics, progress); return; } - WheelWidget.renderRing( - guiGraphics, - this.centerPos.x, - this.centerPos.y, - this.ringColor, - this.ringInnerRadius * 2, - this.ringOuterRadius * 2 - ); + + this.renderFrostedBackground(guiGraphics, 1f); + this.renderDisc(guiGraphics, 1f); + this.renderSeparatorRing(guiGraphics, 1f); + this.renderHoverChevron(guiGraphics); if (this.currentSectionIndex != -1) { this.renderSelection(guiGraphics); } @@ -350,35 +707,18 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo var renderer = value.renderer(); if (renderer != null) { poseStack.pushPose(); - poseStack.translate(x, y, 100); + poseStack.translate(x, y, 0); int renderSize = this.getRendererSize(this.ringOuterRadius - this.ringInnerRadius); renderer.render(guiGraphics, poseStack, renderSize, renderSize); poseStack.popPose(); } - poseStack.pushPose(); - float coordinateScale = 0.7f; - float offsetX = 0.1f * this.width; - float offsetY = 0.1f * this.height; - float adjustedX = (x - offsetX) / coordinateScale; - float adjustedY = renderer == null - ? (y - offsetY - (minecraft.font.lineHeight * this.textScale) / 2.0f) / coordinateScale - : (y - offsetY - 20 * this.textScale) / coordinateScale; - - poseStack.translate(offsetX, offsetY, 0); - poseStack.scale(coordinateScale, coordinateScale, coordinateScale); - poseStack.translate(adjustedX, adjustedY, 0); - poseStack.scale(this.textScale / coordinateScale, this.textScale / coordinateScale, this.textScale / coordinateScale); - guiGraphics.drawCenteredString( - minecraft.font, - value.subTitle, - 0, - 0, - (0xff << 24) | this.textColor - ); - poseStack.popPose(); } - RenderSystem.disableDepthTest(); - RenderSystem.disableBlend(); + this.renderCenterTitle(guiGraphics); + this.renderPageArrows(guiGraphics); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { } public void renderClosingAnimation(GuiGraphics guiGraphics) { @@ -396,81 +736,72 @@ private void renderProgressAnimation(GuiGraphics guiGraphics, float progress) { if (progress == 0) return; PoseStack poseStack = guiGraphics.pose(); poseStack.pushPose(); - WheelWidget.renderRing( - guiGraphics, - this.centerPos.x, - this.centerPos.y, - this.ringColor, - this.ringInnerRadius * 2 * progress, - this.ringOuterRadius * 2 * progress - ); + this.renderFrostedBackground(guiGraphics, progress); + this.renderDisc(guiGraphics, progress); + this.renderSeparatorRing(guiGraphics, progress); poseStack.popPose(); if (this.currentSectionIndex != -1) { if (this.selectionEffect == WheelSelectionEffect.ANNULAR_SECTOR) { WheelSection section = this.sections.get(this.currentSectionIndex); float rangeAngle = this.normalizePositiveAngle(section.angleEnd - section.angleStart) / 2.0f; + float settle = this.settleProgress(); + float expand = settle * SETTLE_EXPAND; this.renderAnnularSectorSelection( guiGraphics, this.centerPos.x, this.centerPos.y, - this.selectionEffectColor, - this.ringInnerRadius * 2 * progress, - this.ringOuterRadius * 2 * progress, + this.getSelectionSectorColor(settle), + (this.ringInnerRadius + SECTION_INNER_INSET + expand) * progress, + (this.ringOuterRadius - SECTION_OUTER_INSET + expand) * progress, section.angle, rangeAngle ); } else { WheelSection section = this.sections.get(this.currentSectionIndex); Vector2f center = new Vector2f( - (section.center.x - this.centerPos.x) / this.getSectionCircleDiameter(), - (section.center.y - this.centerPos.y) / this.getSectionCircleDiameter() - ).mul(this.getSectionCircleDiameter() * progress).add(this.centerPos.x, this.centerPos.y); - WheelWidget.renderSelectionEffect( + (section.center.x - this.centerPos.x) / this.getSectionCircleRadius(), + (section.center.y - this.centerPos.y) / this.getSectionCircleRadius() + ).mul(this.getSectionCircleRadius() * progress).add(this.centerPos.x, this.centerPos.y); + this.renderSelectionEffect( guiGraphics, center.x, center.y, this.selectionEffectColor, - this.getSelectionDotDiameter((this.ringOuterRadius - this.ringInnerRadius) * progress) + (this.ringOuterRadius - this.ringInnerRadius) * progress ); } } for (WheelSection value : this.sections) { Vector2f center = new Vector2f( - (value.center.x - this.centerPos.x) / this.getSectionCircleDiameter(), - (value.center.y - this.centerPos.y) / this.getSectionCircleDiameter() - ).mul(this.getSectionCircleDiameter() * progress).add(this.centerPos.x, this.centerPos.y); + (value.center.x - this.centerPos.x) / this.getSectionCircleRadius(), + (value.center.y - this.centerPos.y) / this.getSectionCircleRadius() + ).mul(this.getSectionCircleRadius() * progress).add(this.centerPos.x, this.centerPos.y); float x = center.x; float y = center.y; var renderer = value.renderer(); if (renderer != null) { poseStack.pushPose(); - poseStack.translate(x, y, 100); + poseStack.translate(x, y, 0); int renderSize = this.getRendererSize((this.ringOuterRadius - this.ringInnerRadius) * progress); renderer.render(guiGraphics, poseStack, renderSize, renderSize); poseStack.popPose(); } - final int textAlpha = (int) (progress * 0xff) << 24; - poseStack.pushPose(); - float coordinateScale = 0.7f; - float offsetX = 0.1f * this.width; - float offsetY = 0.1f * this.height; - float adjustedX = (x - offsetX) / coordinateScale; - float adjustedY = renderer == null - ? (y - offsetY - (minecraft.font.lineHeight * this.textScale) / 2.0f) / coordinateScale - : (y - offsetY - 20 * this.textScale) / coordinateScale; - - poseStack.translate(offsetX, offsetY, 0); - poseStack.scale(coordinateScale, coordinateScale, coordinateScale); - poseStack.translate(adjustedX, adjustedY, 0); - poseStack.scale(this.textScale / coordinateScale, this.textScale / coordinateScale, this.textScale / coordinateScale); - guiGraphics.drawCenteredString( - this.minecraft.font, - value.subTitle, - 0, - 0, - textAlpha | 0xfdfdfd - ); - poseStack.popPose(); + } + if (this.currentSectionIndex != -1) { + WheelSection section = this.sections.get(this.currentSectionIndex); + Component title = section.subTitle(); + if (title != null && !title.getString().isEmpty()) { + final int textAlpha = (int) (progress * 0xff) << 24; + final int shadowAlpha = (int) (progress * 0x99) << 24; + poseStack.pushPose(); + poseStack.translate(this.centerPos.x, this.centerPos.y, 0); + float scale = this.textScale * CENTER_TITLE_SCALE; + poseStack.scale(scale, scale, 1f); + int textY = -this.minecraft.font.lineHeight / 2; + guiGraphics.drawCenteredString(this.minecraft.font, title, 1, textY + 1, shadowAlpha); + guiGraphics.drawCenteredString(this.minecraft.font, title, 0, textY, textAlpha | 0xfdfdfd); + poseStack.popPose(); + } } } @@ -493,35 +824,186 @@ private void renderSelection(GuiGraphics guiGraphics) { Vector2f pos = MathUtil.copy(this.selectionEffectPos).mul(1, -1).add(this.centerPos); - WheelWidget.renderSelectionEffect( + this.renderSelectionEffect( guiGraphics, pos.x, pos.y, this.selectionEffectColor, - this.getSelectionDotDiameter(this.ringOuterRadius - this.ringInnerRadius) + this.ringOuterRadius - this.ringInnerRadius ); } private void renderSelectionAnnularSector(GuiGraphics guiGraphics) { WheelSection currentSection = this.sections.get(this.currentSectionIndex); + // 渲染角度沿最短路径向目标扇区角度平滑逼近,高亮在两个扇区间滑动 + float diffAngle = this.normalizeSignedAngle(currentSection.angle - this.selectionAngleRad); + this.selectionAngleRad += diffAngle / this.selectionAnimationSpeedFactor; + float rangeAngle = this.normalizePositiveAngle(currentSection.angleEnd - currentSection.angleStart) / 2.0f; + // 鼠标停住后,扇区内外缘同步向外扩张一小段距离(外缘最终贴合盘面外边缘), + // 同时透明度从 20% 平滑提升到 100% + float settle = this.settleProgress(); + float expand = settle * SETTLE_EXPAND; this.renderAnnularSectorSelection( guiGraphics, this.centerPos.x, this.centerPos.y, - this.selectionEffectColor, - this.ringInnerRadius * 2, - this.ringOuterRadius * 2, - currentSection.angle, + this.getSelectionSectorColor(settle), + this.ringInnerRadius + SECTION_INNER_INSET + expand, + this.ringOuterRadius - SECTION_OUTER_INSET + expand, + this.selectionAngleRad, rangeAngle ); } + /** + * 根据停住动画进度计算环扇高亮色:基础透明度为 20%,停住后平滑升至配置颜色的透明度。 + */ + private int getSelectionSectorColor(float settle) { + int baseAlpha = (this.selectionEffectColor >>> 24) & 0xFF; + int alpha = Math.round(baseAlpha * (0.2f + 0.8f * settle)); + return (alpha << 24) | (this.selectionEffectColor & 0xFFFFFF); + } + + /** + * 鼠标停住(选中扇区在 SETTLE_DELAY_MS 内未变化)后,0→1 的平滑外推动画进度。 + */ + private float settleProgress() { + long elapsed = System.currentTimeMillis() - this.selectionChangeTime; + float p = Mth.clamp((elapsed - SETTLE_DELAY_MS) / (float) SETTLE_ANIM_MS, 0.0f, 1.0f); + return p * p * (3.0f - 2.0f * p); + } + + private float normalizeSignedAngle(float angle) { + float normalized = angle % TAU; + if (normalized > Math.PI) { + normalized -= TAU; + } else if (normalized < -Math.PI) { + normalized += TAU; + } + return normalized; + } + + private void renderCenterTitle(GuiGraphics guiGraphics) { + if (this.currentSectionIndex < 0) { + return; + } + WheelSection section = this.sections.get(this.currentSectionIndex); + Component title = section.subTitle(); + if (title == null || title.getString().isEmpty()) { + return; + } + PoseStack poseStack = guiGraphics.pose(); + poseStack.pushPose(); + poseStack.translate(this.centerPos.x, this.centerPos.y, 0); + float scale = this.textScale * CENTER_TITLE_SCALE; + poseStack.scale(scale, scale, 1f); + // drawCenteredString 的 y 是文本顶部,向上偏移半个行高使文本垂直居中于轮盘中心 + int textY = -this.minecraft.font.lineHeight / 2; + guiGraphics.drawCenteredString(this.minecraft.font, title, 1, textY + 1, 0x99000000); + guiGraphics.drawCenteredString(this.minecraft.font, title, 0, textY, (0xff << 24) | this.textColor); + poseStack.popPose(); + } + + private void renderPageArrows(GuiGraphics guiGraphics) { + if (!this.hasPreviousPage && !this.hasNextPage) { + return; + } + // 左右下角的箭头做轻微的水平浮动动画,提示可以翻页 + float phase = System.currentTimeMillis() / 1000f; + float bob = (float) Math.sin(phase * 2.2f) * 4f; + float cornerDist = this.getDiscRadius() * 0.7071f + 26f; + float arrowY = this.centerPos.y + cornerDist; + float leftX = this.centerPos.x - cornerDist + bob; + float rightX = this.centerPos.x + cornerDist - bob; + if (this.hasPreviousPage) { + this.renderChevron(guiGraphics, leftX, arrowY, CHEVRON_SIZE, 0xFFFFFFFF, true); + } + if (this.hasNextPage) { + this.renderChevron(guiGraphics, rightX, arrowY, CHEVRON_SIZE, 0xFFFFFFFF, false); + } + } + + private void renderChevron(GuiGraphics guiGraphics, float x, float y, float size, int color, boolean pointingLeft) { + float halfThickness = CHEVRON_THICKNESS * 0.5f; + if (pointingLeft) { + this.renderSegment(guiGraphics, x + size, y - size, x - size, y, color, halfThickness); + this.renderSegment(guiGraphics, x - size, y, x + size, y + size, color, halfThickness); + } else { + this.renderSegment(guiGraphics, x - size, y - size, x + size, y, color, halfThickness); + this.renderSegment(guiGraphics, x + size, y, x - size, y + size, color, halfThickness); + } + } + private float normalizePositiveAngle(float angle) { float normalized = angle % TAU; return normalized < 0 ? normalized + TAU : normalized; } + /** + * 在分隔圆环内侧渲染一个指向鼠标方向的尖括号箭头。 + */ + private void renderHoverChevron(GuiGraphics guiGraphics) { + if (this.mouseAngleRad == null) { + return; + } + // 与扇区排布同一坐标系换算屏幕方向:正上方为 0 + Vector2f dir = MathUtil.rotate(MathUtil.copy(ROTATION_START), this.mouseAngleRad).mul(1, -1); + Vector2f perp = new Vector2f(-dir.y, dir.x); + float tipDist = this.ringInnerRadius - HOVER_CHEVRON_RING_GAP; + Vector2f tip = new Vector2f(dir).mul(tipDist).add(this.centerPos); + Vector2f base = new Vector2f(dir).mul(tipDist - HOVER_CHEVRON_SIZE).add(this.centerPos); + Vector2f wingA = new Vector2f(base).add(new Vector2f(perp).mul(HOVER_CHEVRON_SIZE * 0.8f)); + Vector2f wingB = new Vector2f(base).add(new Vector2f(perp).mul(-HOVER_CHEVRON_SIZE * 0.8f)); + float halfThickness = HOVER_CHEVRON_THICKNESS * 0.5f; + this.renderSegment(guiGraphics, wingA.x, wingA.y, tip.x, tip.y, 0xFFFFFFFF, halfThickness); + this.renderSegment(guiGraphics, wingB.x, wingB.y, tip.x, tip.y, 0xFFFFFFFF, halfThickness); + } + + private void renderSegment( + GuiGraphics guiGraphics, + float x0, + float y0, + float x1, + float y1, + int color, + float halfThickness + ) { + Window window = this.minecraft.getWindow(); + float guiScale = (float) window.getGuiScale(); + float margin = halfThickness + 5; + float minX = Math.min(x0, x1) - margin; + float minY = Math.min(y0, y1) - margin; + float maxX = Math.max(x0, x1) + margin; + float maxY = Math.max(y0, y1) + margin; + Matrix4f matrix4f = guiGraphics.pose().last().pose(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + bufferBuilder.addVertex(matrix4f, minX, minY, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, minX, maxY, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, maxX, maxY, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, maxX, minY, SELECTION_Z).setColor(color); + RenderSystem.disableDepthTest(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + ShaderInstance segmentShader = LibShaders.getSegmentShader(); + if (segmentShader == null) { + this.renderSegmentFallback(guiGraphics, x0, y0, x1, y1, color, halfThickness); + RenderSystem.disableBlend(); + RenderSystem.enableDepthTest(); + return; + } + RenderSystem.setShader(() -> segmentShader); + segmentShader.safeGetUniform("Point0").set(x0 * guiScale, y0 * guiScale); + segmentShader.safeGetUniform("Point1").set(x1 * guiScale, y1 * guiScale); + segmentShader.safeGetUniform("LineWidth").set(halfThickness * 2f * guiScale); + segmentShader.safeGetUniform("FramebufferSize").set((float) window.getWidth(), (float) window.getHeight()); + RenderSystem.setShaderColor(1, 1, 1, 1); + BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); + RenderSystem.disableBlend(); + RenderSystem.enableDepthTest(); + } + private float getSelectionDotDiameter(float ringWidth) { return ringWidth * SELECTION_DOT_DIAMETER_RATIO; } @@ -539,155 +1021,139 @@ public void onClosing() { } } - @Override - protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { - } - - public record WheelSection( - Vector2f center, - float angle, - float angleStart, - float angleEnd, - Component subTitle, - @Nullable SectionRenderer renderer, - boolean selectable - ) { - public WheelSection( - Vector2f center, - float angle, - float angleStart, - float angleEnd, - RawSection section - ) { - this(center, angle, angleStart, angleEnd, section.name(), section.renderer(), section.selectable()); + private void renderDisc(GuiGraphics guiGraphics, float centerX, float centerY, float radius, int color) { + Window window = this.minecraft.getWindow(); + float guiScale = (float) window.getGuiScale(); + Matrix4f matrix4f = guiGraphics.pose().last().pose(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.begin( + VertexFormat.Mode.QUADS, + DefaultVertexFormat.POSITION_COLOR + ); + float x1 = centerX - radius - 5; + float y1 = centerY - radius - 5; + float x2 = centerX + radius + 5; + float y2 = centerY + radius + 5; + bufferBuilder.addVertex(matrix4f, x1, y1, DISC_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x1, y2, DISC_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x2, y2, DISC_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x2, y1, DISC_Z).setColor(color); + RenderSystem.disableDepthTest(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + ShaderInstance discShader = LibShaders.getDiscShader(); + if (discShader == null) { + this.renderDiscFallback(guiGraphics, centerX, centerY, radius, color); + RenderSystem.disableBlend(); + RenderSystem.enableDepthTest(); + return; } + RenderSystem.setShader(() -> discShader); + discShader.safeGetUniform("Center").set(centerX * guiScale, centerY * guiScale); + discShader.safeGetUniform("FramebufferSize").set((float) window.getWidth(), (float) window.getHeight()); + discShader.safeGetUniform("Radius").set(radius * guiScale); + RenderSystem.setShaderColor(1, 1, 1, 1); + BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); + RenderSystem.disableBlend(); + RenderSystem.enableDepthTest(); } - public record RawSection(Component name, @Nullable SectionRenderer renderer, boolean selectable) { - public RawSection(Component name, @Nullable SectionRenderer renderer) { - this(name, renderer, true); + private void renderDiscFallback(GuiGraphics guiGraphics, float centerX, float centerY, float radius, int centerColor) { + Matrix4f matrix4f = guiGraphics.pose().last().pose(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.begin( + VertexFormat.Mode.TRIANGLE_FAN, + DefaultVertexFormat.POSITION_COLOR + ); + int edgeColor = centerColor & 0x00FFFFFF; + int segments = 48; + bufferBuilder.addVertex(matrix4f, centerX, centerY, DISC_Z).setColor(centerColor); + for (int i = 0; i <= segments; i++) { + float angle = (float) (Math.PI * 2 * i / segments); + float sin = (float) Math.sin(angle); + float cos = (float) Math.cos(angle); + bufferBuilder.addVertex(matrix4f, centerX + cos * radius, centerY + sin * radius, DISC_Z).setColor(edgeColor); } + RenderSystem.setShader(GameRenderer::getPositionColorShader); + RenderSystem.setShaderColor(1, 1, 1, 1); + BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); } - @FunctionalInterface - public interface SectionRenderer { - void render(GuiGraphics graphics, PoseStack pose, int width, int height); + private void renderSelectionFallback(GuiGraphics guiGraphics, float centerX, float centerY, int color, float radius) { + this.renderDiscFallback(guiGraphics, centerX, centerY, radius, color); } - public static void renderRing( + private void renderAnnularSectorFallback( GuiGraphics guiGraphics, float centerX, float centerY, int color, - float innerDiameter, - float outerDiameter + float innerRadius, + float outerRadius, + float centerAngleRad, + float rangeAngleRad ) { - PoseStack poseStack = guiGraphics.pose(); - Matrix4f matrix4f = poseStack.last().pose(); + Matrix4f matrix4f = guiGraphics.pose().last().pose(); Tesselator tesselator = Tesselator.getInstance(); BufferBuilder bufferBuilder = tesselator.begin( - VertexFormat.Mode.QUADS, + VertexFormat.Mode.TRIANGLE_STRIP, DefaultVertexFormat.POSITION_COLOR ); - float x1 = centerX - outerDiameter - 5; - float y1 = centerY - outerDiameter - 5; - float x2 = centerX + outerDiameter + 5; - float y2 = centerY + outerDiameter + 5; - bufferBuilder.addVertex(matrix4f, x1, y1, RING_Z).setColor(color); - bufferBuilder.addVertex(matrix4f, x1, y2, RING_Z).setColor(color); - bufferBuilder.addVertex(matrix4f, x2, y2, RING_Z).setColor(color); - bufferBuilder.addVertex(matrix4f, x2, y1, RING_Z).setColor(color); - - Window window = Minecraft.getInstance().getWindow(); - float guiScale = (float) window.getGuiScale(); - RenderSystem.disableDepthTest(); - ShaderInstance ringShader = LibShaders.getRingShader(); - if (ringShader == null) { - renderRingFallback(guiGraphics, centerX, centerY, color, innerDiameter, outerDiameter); - RenderSystem.enableDepthTest(); - return; + int segments = 48; + float startAngle = centerAngleRad - rangeAngleRad; + float sweepAngle = rangeAngleRad * 2; + for (int i = 0; i <= segments; i++) { + float angle = startAngle + sweepAngle * i / segments; + float sin = (float) Math.sin(angle); + float cos = (float) Math.cos(angle); + bufferBuilder.addVertex(matrix4f, centerX + cos * outerRadius, centerY + sin * outerRadius, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, centerX + cos * innerRadius, centerY + sin * innerRadius, SELECTION_Z).setColor(color); } - RenderSystem.setShader(() -> ringShader); - - ringShader - .safeGetUniform("Center") - .set(centerX * guiScale, centerY * guiScale); - ringShader - .safeGetUniform("InnerDiameter") - .set(innerDiameter * guiScale); - ringShader - .safeGetUniform("OuterDiameter") - .set(outerDiameter * guiScale); - + RenderSystem.setShader(GameRenderer::getPositionColorShader); RenderSystem.setShaderColor(1, 1, 1, 1); BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); - RenderSystem.enableDepthTest(); } - public static void renderSelectionEffect( + private void renderSegmentFallback( GuiGraphics guiGraphics, - float centerX, - float centerY, + float x0, + float y0, + float x1, + float y1, int color, - float radius + float halfThickness ) { - PoseStack poseStack = guiGraphics.pose(); - Matrix4f matrix4f = poseStack.last().pose(); + Vector2f dir = new Vector2f(x1 - x0, y1 - y0); + float length = dir.length(); + if (length <= 0) { + log.warn("Wheel segment fallback skipped: zero-length segment"); + return; + } + dir.div(length); + Vector2f perp = new Vector2f(-dir.y, dir.x).mul(halfThickness); + Matrix4f matrix4f = guiGraphics.pose().last().pose(); Tesselator tesselator = Tesselator.getInstance(); BufferBuilder bufferBuilder = tesselator.begin( - VertexFormat.Mode.QUADS, + VertexFormat.Mode.TRIANGLE_STRIP, DefaultVertexFormat.POSITION_COLOR ); - float x1 = centerX - radius - 5; - float y1 = centerY - radius - 5; - float x2 = centerX + radius + 5; - float y2 = centerY + radius + 5; - bufferBuilder.addVertex(matrix4f, x1, y1, SELECTION_Z).setColor(color); - bufferBuilder.addVertex(matrix4f, x1, y2, SELECTION_Z).setColor(color); - bufferBuilder.addVertex(matrix4f, x2, y2, SELECTION_Z).setColor(color); - bufferBuilder.addVertex(matrix4f, x2, y1, SELECTION_Z).setColor(color); - - Window window = Minecraft.getInstance().getWindow(); - float guiScale = (float) window.getGuiScale(); - RenderSystem.disableDepthTest(); - ShaderInstance selectionShader = LibShaders.getSelectionShader(); - if (selectionShader == null) { - renderSelectionFallback(guiGraphics, centerX, centerY, color, radius); - RenderSystem.enableDepthTest(); - return; - } - RenderSystem.setShader(() -> selectionShader); - - selectionShader - .safeGetUniform("Center") - .set(centerX * guiScale, centerY * guiScale); - selectionShader - .safeGetUniform("FramebufferSize") - .set((float) window.getWidth(), (float) window.getHeight()); - selectionShader - .safeGetUniform("Radius") - .set(radius * guiScale); - + bufferBuilder.addVertex(matrix4f, x0 + perp.x, y0 + perp.y, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x0 - perp.x, y0 - perp.y, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x1 + perp.x, y1 + perp.y, SELECTION_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x1 - perp.x, y1 - perp.y, SELECTION_Z).setColor(color); + RenderSystem.setShader(GameRenderer::getPositionColorShader); RenderSystem.setShaderColor(1, 1, 1, 1); BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); - RenderSystem.enableDepthTest(); } - public void renderAnnularSectorSelection( + public static void renderRing( GuiGraphics guiGraphics, float centerX, float centerY, int color, float innerDiameter, - float outerDiameter, - float centerAngleRad, - float rangeAngleRad + float outerDiameter ) { - float outerRadius = outerDiameter + 5; - float x1 = centerX - outerRadius; - float y1 = centerY - outerRadius; - float x2 = centerX + outerRadius; - float y2 = centerY + outerRadius; PoseStack poseStack = guiGraphics.pose(); Matrix4f matrix4f = poseStack.last().pose(); Tesselator tesselator = Tesselator.getInstance(); @@ -695,57 +1161,42 @@ public void renderAnnularSectorSelection( VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR ); - bufferBuilder.addVertex(matrix4f, x1, y1, SELECTION_Z).setColor(color); - bufferBuilder.addVertex(matrix4f, x1, y2, SELECTION_Z).setColor(color); - bufferBuilder.addVertex(matrix4f, x2, y2, SELECTION_Z).setColor(color); - bufferBuilder.addVertex(matrix4f, x2, y1, SELECTION_Z).setColor(color); + float x1 = centerX - outerDiameter - 5; + float y1 = centerY - outerDiameter - 5; + float x2 = centerX + outerDiameter + 5; + float y2 = centerY + outerDiameter + 5; + bufferBuilder.addVertex(matrix4f, x1, y1, RING_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x1, y2, RING_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x2, y2, RING_Z).setColor(color); + bufferBuilder.addVertex(matrix4f, x2, y1, RING_Z).setColor(color); Window window = Minecraft.getInstance().getWindow(); float guiScale = (float) window.getGuiScale(); RenderSystem.disableDepthTest(); - ShaderInstance annularSectorShader = LibShaders.getAnnularSectorShader(); - if (annularSectorShader == null) { - renderAnnularSectorFallback( - guiGraphics, - centerX, - centerY, - color, - innerDiameter, - outerDiameter, - centerAngleRad, - rangeAngleRad - ); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + ShaderInstance ringShader = LibShaders.getRingShader(); + if (ringShader == null) { + renderRingFallback(guiGraphics, centerX, centerY, color, innerDiameter, outerDiameter); + RenderSystem.disableBlend(); RenderSystem.enableDepthTest(); return; } - RenderSystem.setShader(() -> annularSectorShader); + RenderSystem.setShader(() -> ringShader); - // Wheel section angle uses "up" as zero; shader atan uses +X as zero. - float shaderCenterAngle = centerAngleRad + TAU / 4.0f; - annularSectorShader + ringShader .safeGetUniform("Center") .set(centerX * guiScale, centerY * guiScale); - annularSectorShader + ringShader .safeGetUniform("InnerDiameter") .set(innerDiameter * guiScale); - annularSectorShader + ringShader .safeGetUniform("OuterDiameter") .set(outerDiameter * guiScale); - annularSectorShader - .safeGetUniform("AntiAliasingRadius") - .set(1.25f); - annularSectorShader - .safeGetUniform("AngleAntiAliasingRad") - .set(ANGLE_AA_RAD); - annularSectorShader - .safeGetUniform("CenterAngleRad") - .set(shaderCenterAngle); - annularSectorShader - .safeGetUniform("RangeAngleRad") - .set(rangeAngleRad); RenderSystem.setShaderColor(1, 1, 1, 1); BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); + RenderSystem.disableBlend(); RenderSystem.enableDepthTest(); } @@ -779,75 +1230,24 @@ private static void renderRingFallback( BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); } - private static void renderSelectionFallback( - GuiGraphics guiGraphics, - float centerX, - float centerY, - int color, - float radius - ) { - renderDisc(guiGraphics, centerX, centerY, radius, color); + @FunctionalInterface + public interface SectionRenderer { + void render(GuiGraphics graphics, PoseStack pose, int width, int height); } - private static void renderAnnularSectorFallback( - GuiGraphics guiGraphics, - float centerX, - float centerY, - int color, - float innerDiameter, - float outerDiameter, - float centerAngleRad, - float rangeAngleRad + public record WheelSection( + Vector2f center, float angle, float angleStart, float angleEnd, Component subTitle, + @Nullable SectionRenderer renderer, boolean selectable ) { - PoseStack poseStack = guiGraphics.pose(); - Matrix4f matrix4f = poseStack.last().pose(); - Tesselator tesselator = Tesselator.getInstance(); - BufferBuilder bufferBuilder = tesselator.begin( - VertexFormat.Mode.TRIANGLE_STRIP, - DefaultVertexFormat.POSITION_COLOR - ); - float innerRadius = innerDiameter / 2f; - float outerRadius = outerDiameter / 2f; - int segments = 48; - float startAngle = centerAngleRad - rangeAngleRad; - float sweepAngle = rangeAngleRad * 2; - for (int i = 0; i <= segments; i++) { - float angle = startAngle + sweepAngle * i / segments; - float sin = (float) Math.sin(angle); - float cos = (float) Math.cos(angle); - bufferBuilder.addVertex(matrix4f, centerX + cos * outerRadius, centerY + sin * outerRadius, SELECTION_Z).setColor(color); - bufferBuilder.addVertex(matrix4f, centerX + cos * innerRadius, centerY + sin * innerRadius, SELECTION_Z).setColor(color); + public WheelSection(Vector2f center, float angle, float angleStart, float angleEnd, RawSection section) { + this(center, angle, angleStart, angleEnd, section.name(), section.renderer(), section.selectable()); } - RenderSystem.setShader(GameRenderer::getPositionColorShader); - RenderSystem.setShaderColor(1, 1, 1, 1); - BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); } - private static void renderDisc( - GuiGraphics guiGraphics, - float centerX, - float centerY, - float radius, - int centerColor - ) { - PoseStack poseStack = guiGraphics.pose(); - Matrix4f matrix4f = poseStack.last().pose(); - Tesselator tesselator = Tesselator.getInstance(); - BufferBuilder bufferBuilder = tesselator.begin( - VertexFormat.Mode.TRIANGLE_FAN, - DefaultVertexFormat.POSITION_COLOR - ); - int edgeColor = centerColor & 0x00FFFFFF; - int segments = 48; - bufferBuilder.addVertex(matrix4f, centerX, centerY, SELECTION_Z).setColor(centerColor); - for (int i = 0; i <= segments; i++) { - float angle = (float) (Math.PI * 2 * i / segments); - float sin = (float) Math.sin(angle); - float cos = (float) Math.cos(angle); - bufferBuilder.addVertex(matrix4f, centerX + cos * radius, centerY + sin * radius, SELECTION_Z).setColor(edgeColor); + public record RawSection(Component name, @Nullable SectionRenderer renderer, boolean selectable) { + public RawSection(Component name, @Nullable SectionRenderer renderer) { + this(name, renderer, true); } - RenderSystem.setShader(GameRenderer::getPositionColorShader); - RenderSystem.setShaderColor(1, 1, 1, 1); - BufferUploader.drawWithShader(Objects.requireNonNull(bufferBuilder.build())); } + } diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java index e8bee91b..ab85f1e4 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java @@ -5,6 +5,7 @@ import dev.anvilcraft.lib.v2.wheel.api.WheelMenuModel; import dev.anvilcraft.lib.v2.wheel.api.WheelOpenMode; import dev.anvilcraft.lib.v2.wheel.api.WheelPageModel; +import dev.anvilcraft.lib.v2.wheel.client.gui.component.WheelFrostedBackground; import dev.anvilcraft.lib.v2.wheel.client.gui.component.WheelWidget; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.Screen; @@ -13,10 +14,11 @@ import java.util.ArrayDeque; import java.util.Deque; import java.util.List; +import javax.annotation.Nullable; public class WheelScreen extends Screen { - private static final float WHEEL_INNER_RADIUS_SCALE = 0.12f; - private static final float WHEEL_OUTER_RADIUS_SCALE = 0.22f; + private static final float WHEEL_INNER_RADIUS_SCALE = 0.17f; + private static final float WHEEL_OUTER_RADIUS_SCALE = 0.33f; private final WheelMenuModel model; private final WheelOpenMode openMode; @@ -24,6 +26,8 @@ public class WheelScreen extends Screen { private WheelWidget wheelWidget; private int currentPageIndex; + @Nullable + private WheelFrostedBackground frostedBackground; public WheelScreen(WheelMenuModel model, WheelOpenMode openMode) { super(Component.empty()); @@ -32,16 +36,6 @@ public WheelScreen(WheelMenuModel model, WheelOpenMode openMode) { this.menuStack.addLast(this.model.rootEntries()); } - @Override - public boolean isPauseScreen() { - return false; - } - - @Override - protected void init() { - this.rebuildWheelWidget(); - } - public static WheelScreen tap(WheelMenuModel model) { return new WheelScreen(model, WheelOpenMode.TAP); } @@ -57,14 +51,34 @@ public void triggerFromHoldRelease() { this.triggerSelectedOrClose(); } + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + protected void init() { + if (this.frostedBackground != null) { + this.frostedBackground.close(); + } + this.frostedBackground = new WheelFrostedBackground(); + this.rebuildWheelWidget(); + } + + @Override + public void removed() { + if (this.frostedBackground != null) { + this.frostedBackground.close(); + this.frostedBackground = null; + } + super.removed(); + } + @Override public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { if (this.wheelWidget != null) { this.wheelWidget.render(guiGraphics, mouseX, mouseY, partialTick); } - - Component pageInfo = Component.literal((this.currentPageIndex + 1) + " / " + this.pageCountForCurrentMenu()); - guiGraphics.drawCenteredString(this.font, pageInfo, this.width / 2, this.height - 22, 0xFFFFFFFF); } @Override @@ -178,6 +192,11 @@ private void rebuildWheelWidget() { sections, this.model.deadZone() ); + this.wheelWidget.setFrostedBackground(this.frostedBackground); + this.wheelWidget.setPageState( + this.currentPageIndex > 0, + this.currentPageIndex < this.pageCountForCurrentMenu() - 1 + ); this.wheelWidget.setSelectionEffectColor(this.model.selectionEffectColor()); this.wheelWidget.setSelectionEffect(this.model.selectionEffect()); this.wheelWidget.clearSelection(); @@ -200,4 +219,3 @@ private List currentMenuEntries() { return this.menuStack.getLast(); } } - diff --git a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/LibShaders.java b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/LibShaders.java index 3eb7e314..53dcfb16 100644 --- a/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/LibShaders.java +++ b/module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/LibShaders.java @@ -22,6 +22,14 @@ public class LibShaders { static @Nullable ShaderInstance selectionShader; @Getter static @Nullable ShaderInstance annularSectorShader; + @Getter + static @Nullable ShaderInstance discShader; + @Getter + static @Nullable ShaderInstance frostedDiscShader; + @Getter + static @Nullable ShaderInstance blurShader; + @Getter + static @Nullable ShaderInstance segmentShader; @SubscribeEvent public static void register(RegisterShadersEvent event) { @@ -50,6 +58,38 @@ public static void register(RegisterShadersEvent event) { ), it -> annularSectorShader = it ); + event.registerShader( + new ShaderInstance( + event.getResourceProvider(), + AnvilLibWheel.of("disc"), + DefaultVertexFormat.POSITION_COLOR + ), + it -> discShader = it + ); + event.registerShader( + new ShaderInstance( + event.getResourceProvider(), + AnvilLibWheel.of("frosted_disc"), + DefaultVertexFormat.POSITION_TEX_COLOR + ), + it -> frostedDiscShader = it + ); + event.registerShader( + new ShaderInstance( + event.getResourceProvider(), + AnvilLibWheel.of("blur"), + DefaultVertexFormat.POSITION_TEX + ), + it -> blurShader = it + ); + event.registerShader( + new ShaderInstance( + event.getResourceProvider(), + AnvilLibWheel.of("segment"), + DefaultVertexFormat.POSITION_COLOR + ), + it -> segmentShader = it + ); } catch (IOException e) { log.error(e.getLocalizedMessage(), e); } diff --git a/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.fsh b/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.fsh index dd7d0bf3..53954bb0 100644 --- a/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.fsh +++ b/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.fsh @@ -4,8 +4,9 @@ in vec4 vertexColor; uniform vec4 ColorModulator; uniform vec2 Center; -uniform float InnerDiameter; -uniform float OuterDiameter; +// 注意:以下为半径而非直径(与 dev/26.1 语义对齐) +uniform float InnerRadius; +uniform float OuterRadius; uniform float AntiAliasingRadius; uniform float AngleAntiAliasingRad; uniform float CenterAngleRad; @@ -47,8 +48,8 @@ void main() { float angle = atan(fragPos.y - Center.y, fragPos.x - Center.x); float radialAa = max(min(AntiAliasingRadius, fwidth(distance) * 1.5), 0.0001); - color.a *= smoothstep(InnerDiameter - radialAa, InnerDiameter + radialAa, distance) - * (1.0 - smoothstep(OuterDiameter - radialAa, OuterDiameter + radialAa, distance)) + color.a *= smoothstep(InnerRadius - radialAa, InnerRadius + radialAa, distance) + * (1.0 - smoothstep(OuterRadius - radialAa, OuterRadius + radialAa, distance)) * calcAngleAlpha(angle, CenterAngleRad, RangeAngleRad, AngleAntiAliasingRad); fragColor = color * ColorModulator; diff --git a/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.json b/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.json index 724ab05a..10b92176 100644 --- a/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.json +++ b/module.wheel/src/main/resources/assets/anvillib/shaders/core/annular_sector.json @@ -17,7 +17,7 @@ ] }, { - "name": "InnerDiameter", + "name": "InnerRadius", "type": "float", "count": 1, "values": [ @@ -25,7 +25,7 @@ ] }, { - "name": "OuterDiameter", + "name": "OuterRadius", "type": "float", "count": 1, "values": [ diff --git a/module.wheel/src/main/resources/assets/anvillib/shaders/core/blur.fsh b/module.wheel/src/main/resources/assets/anvillib/shaders/core/blur.fsh new file mode 100644 index 00000000..74b9be71 --- /dev/null +++ b/module.wheel/src/main/resources/assets/anvillib/shaders/core/blur.fsh @@ -0,0 +1,26 @@ +#version 150 + +in vec2 texCoord0; + +uniform vec4 ColorModulator; +uniform sampler2D DiffuseSampler; +uniform vec2 Direction; +uniform float SampleStepLength; + +out vec4 fragColor; + +void main() { + // 按 Direction 做可分离高斯模糊,SampleStepLength 为采样步长(纹素) + vec2 texelSize = 1.0 / textureSize(DiffuseSampler, 0); + vec2 step = Direction * SampleStepLength * texelSize; + vec4 sum = texture(DiffuseSampler, texCoord0) * 0.227027; + sum += texture(DiffuseSampler, texCoord0 + step) * 0.1945946; + sum += texture(DiffuseSampler, texCoord0 - step) * 0.1945946; + sum += texture(DiffuseSampler, texCoord0 + step * 2.0) * 0.1216216; + sum += texture(DiffuseSampler, texCoord0 - step * 2.0) * 0.1216216; + sum += texture(DiffuseSampler, texCoord0 + step * 3.0) * 0.054054; + sum += texture(DiffuseSampler, texCoord0 - step * 3.0) * 0.054054; + sum += texture(DiffuseSampler, texCoord0 + step * 4.0) * 0.016216; + sum += texture(DiffuseSampler, texCoord0 - step * 4.0) * 0.016216; + fragColor = sum * ColorModulator; +} diff --git a/module.wheel/src/main/resources/assets/anvillib/shaders/core/blur.json b/module.wheel/src/main/resources/assets/anvillib/shaders/core/blur.json new file mode 100644 index 00000000..86b16141 --- /dev/null +++ b/module.wheel/src/main/resources/assets/anvillib/shaders/core/blur.json @@ -0,0 +1,31 @@ +{ + "vertex": "position_tex", + "fragment": "anvillib:blur", + "samplers": [ + { + "name": "DiffuseSampler" + } + ], + "uniforms": [ + { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] }, + { + "name": "Direction", + "type": "float", + "count": 2, + "values": [ + 1.0, + 0.0 + ] + }, + { + "name": "SampleStepLength", + "type": "float", + "count": 1, + "values": [ + 2.0 + ] + } + ] +} diff --git a/module.wheel/src/main/resources/assets/anvillib/shaders/core/disc.fsh b/module.wheel/src/main/resources/assets/anvillib/shaders/core/disc.fsh new file mode 100644 index 00000000..e31cae47 --- /dev/null +++ b/module.wheel/src/main/resources/assets/anvillib/shaders/core/disc.fsh @@ -0,0 +1,24 @@ +#version 150 + +in vec4 vertexColor; + +uniform vec4 ColorModulator; +uniform vec2 FramebufferSize; +uniform vec2 Center; +uniform float Radius; +uniform float AntiAliasingRadius; + +out vec4 fragColor; + +void main() { + // GUI 通道的 gl_FragCoord 以窗口左下角为原点,翻转至左上角原点的 GUI 坐标 + vec2 fragPos = vec2(gl_FragCoord.x, FramebufferSize.y - gl_FragCoord.y); + float dist = distance(fragPos, Center); + float aa = max(min(AntiAliasingRadius, fwidth(dist) * 1.5), 0.0001); + // 均匀圆形遮罩:半径内不透明,边缘抗锯齿过渡 + float mask = 1.0 - smoothstep(Radius - aa, Radius + aa, dist); + + vec4 color = vertexColor; + color.a *= mask; + fragColor = color * ColorModulator; +} diff --git a/module.wheel/src/main/resources/assets/anvillib/shaders/core/disc.json b/module.wheel/src/main/resources/assets/anvillib/shaders/core/disc.json new file mode 100644 index 00000000..554d78a5 --- /dev/null +++ b/module.wheel/src/main/resources/assets/anvillib/shaders/core/disc.json @@ -0,0 +1,45 @@ +{ + "vertex": "position_color", + "fragment": "anvillib:disc", + "samplers": [ + ], + "uniforms": [ + { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] }, + { + "name": "FramebufferSize", + "type": "float", + "count": 2, + "values": [ + 0.0, + 0.0 + ] + }, + { + "name": "Center", + "type": "float", + "count": 2, + "values": [ + 0.0, + 0.0 + ] + }, + { + "name": "Radius", + "type": "float", + "count": 1, + "values": [ + 0.0 + ] + }, + { + "name": "AntiAliasingRadius", + "type": "float", + "count": 1, + "values": [ + 1.5 + ] + } + ] +} diff --git a/module.wheel/src/main/resources/assets/anvillib/shaders/core/frosted_disc.fsh b/module.wheel/src/main/resources/assets/anvillib/shaders/core/frosted_disc.fsh new file mode 100644 index 00000000..42bb2020 --- /dev/null +++ b/module.wheel/src/main/resources/assets/anvillib/shaders/core/frosted_disc.fsh @@ -0,0 +1,29 @@ +#version 150 + +in vec2 texCoord0; +in vec4 vertexColor; + +uniform vec4 ColorModulator; +uniform vec2 FramebufferSize; +uniform vec2 Center; +uniform float Radius; +uniform float AntiAliasingRadius; + +uniform sampler2D Sampler0; + +out vec4 fragColor; + +void main() { + // GUI 通道的 gl_FragCoord 以窗口左下角为原点,翻转至左上角原点的 GUI 坐标 + vec2 fragPos = vec2(gl_FragCoord.x, FramebufferSize.y - gl_FragCoord.y); + float dist = distance(fragPos, Center); + float aa = max(min(AntiAliasingRadius, fwidth(dist) * 1.5), 0.0001); + // 圆形盘面遮罩:半径内可见,边缘抗锯齿过渡 + float mask = 1.0 - smoothstep(Radius - aa, Radius + aa, dist); + + // Sampler0 为每帧高斯模糊后的主渲染目标 + vec4 scene = texture(Sampler0, texCoord0); + + // vertexColor 作为毛玻璃着色与混合强度:rgb 调制模糊内容,a 控制叠加透明度 + fragColor = vec4(scene.rgb * vertexColor.rgb, mask * vertexColor.a) * ColorModulator; +} diff --git a/module.wheel/src/main/resources/assets/anvillib/shaders/core/frosted_disc.json b/module.wheel/src/main/resources/assets/anvillib/shaders/core/frosted_disc.json new file mode 100644 index 00000000..b72c2745 --- /dev/null +++ b/module.wheel/src/main/resources/assets/anvillib/shaders/core/frosted_disc.json @@ -0,0 +1,48 @@ +{ + "vertex": "position_tex_color", + "fragment": "anvillib:frosted_disc", + "samplers": [ + { + "name": "Sampler0" + } + ], + "uniforms": [ + { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] }, + { + "name": "FramebufferSize", + "type": "float", + "count": 2, + "values": [ + 0.0, + 0.0 + ] + }, + { + "name": "Center", + "type": "float", + "count": 2, + "values": [ + 0.0, + 0.0 + ] + }, + { + "name": "Radius", + "type": "float", + "count": 1, + "values": [ + 0.0 + ] + }, + { + "name": "AntiAliasingRadius", + "type": "float", + "count": 1, + "values": [ + 1.25 + ] + } + ] +} diff --git a/module.wheel/src/main/resources/assets/anvillib/shaders/core/segment.fsh b/module.wheel/src/main/resources/assets/anvillib/shaders/core/segment.fsh new file mode 100644 index 00000000..4a0ea5c7 --- /dev/null +++ b/module.wheel/src/main/resources/assets/anvillib/shaders/core/segment.fsh @@ -0,0 +1,29 @@ +#version 150 + +in vec4 vertexColor; + +uniform vec4 ColorModulator; +uniform vec2 FramebufferSize; +uniform vec2 Point0; +uniform vec2 Point1; +uniform float LineWidth; +uniform float AntiAliasingRadius; + +out vec4 fragColor; + +void main() { + // GUI 通道的 gl_FragCoord 以窗口左下角为原点,翻转至左上角原点的 GUI 坐标 + vec2 fragPos = vec2(gl_FragCoord.x, FramebufferSize.y - gl_FragCoord.y); + // 点到线段的最短距离 + vec2 segment = Point1 - Point0; + float length2 = dot(segment, segment); + float t = length2 > 0.0 ? clamp(dot(fragPos - Point0, segment) / length2, 0.0, 1.0) : 0.0; + float dist = distance(fragPos, Point0 + segment * t); + float aa = max(min(AntiAliasingRadius, fwidth(dist) * 1.5), 0.0001); + // LineWidth 为线段总宽度,边缘抗锯齿过渡 + float alpha = 1.0 - smoothstep(LineWidth * 0.5 - aa, LineWidth * 0.5 + aa, dist); + + vec4 color = vertexColor; + color.a *= alpha; + fragColor = color * ColorModulator; +} diff --git a/module.wheel/src/main/resources/assets/anvillib/shaders/core/segment.json b/module.wheel/src/main/resources/assets/anvillib/shaders/core/segment.json new file mode 100644 index 00000000..91293e24 --- /dev/null +++ b/module.wheel/src/main/resources/assets/anvillib/shaders/core/segment.json @@ -0,0 +1,54 @@ +{ + "vertex": "position_color", + "fragment": "anvillib:segment", + "samplers": [ + ], + "uniforms": [ + { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] }, + { + "name": "FramebufferSize", + "type": "float", + "count": 2, + "values": [ + 0.0, + 0.0 + ] + }, + { + "name": "Point0", + "type": "float", + "count": 2, + "values": [ + 0.0, + 0.0 + ] + }, + { + "name": "Point1", + "type": "float", + "count": 2, + "values": [ + 0.0, + 0.0 + ] + }, + { + "name": "LineWidth", + "type": "float", + "count": 1, + "values": [ + 0.0 + ] + }, + { + "name": "AntiAliasingRadius", + "type": "float", + "count": 1, + "values": [ + 1.5 + ] + } + ] +} From f107a889c59c7c7d89c0869e782635f4b2fbb66f Mon Sep 17 00:00:00 2001 From: Gugle Date: Thu, 13 Aug 2026 15:20:31 +0800 Subject: [PATCH 38/38] =?UTF-8?q?docs:=20=E5=88=A0=E9=99=A4=20AnvilLib=20?= =?UTF-8?q?=E5=88=86=E6=94=AF=E5=AF=B9=E6=AF=94=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 branch-comparison.md 文档文件 - 清理关于 port/1.21.1 与 dev/26.1 分支对比的详细分析内容 - 删除包含 595 行的分支拓扑、提交主题对比、模块清单等完整报告 - 移除分支迁移相关的结论性内容与技术细节说明 --- docs/branch-comparison.md | 595 ------- docs/migration-plan-dev-26.1.md | 909 ----------- docs/migration-plan-port-1.21.1.md | 1405 ----------------- docs/migration-todo-port-1.21.1.md | 266 ---- .../minecraft-neoforge-version-differences.md | 1140 ------------- 5 files changed, 4315 deletions(-) delete mode 100644 docs/branch-comparison.md delete mode 100644 docs/migration-plan-dev-26.1.md delete mode 100644 docs/migration-plan-port-1.21.1.md delete mode 100644 docs/migration-todo-port-1.21.1.md delete mode 100644 docs/minecraft-neoforge-version-differences.md diff --git a/docs/branch-comparison.md b/docs/branch-comparison.md deleted file mode 100644 index 0b18e9ba..00000000 --- a/docs/branch-comparison.md +++ /dev/null @@ -1,595 +0,0 @@ -# AnvilLib 分支对比报告:`port/1.21.1` ↔ `dev/26.1` - -> ⚠️ 状态更新(2026-08-12):`module.rendering` 与 `renderdoc-loader` 已从 `port/1.21.1` 分支移除(构建依赖、CI 任务与文档引用已同步清理)。本文为 2026-08-10 的快照分析,正文中 dev/26.1 侧的模块描述仍为历史事实。 - -> 文档信息 -> -> - 生成日期:2026-08-10 -> - 分析对象:`port/1.21.1` @ `fab551b`(Minecraft 1.21.1 / NeoForge 21.1.226)、`dev/26.1` @ `0b7e45f`(Minecraft 26.1.2 / - NeoForge 26.1.2.76) -> - 共同祖先(merge-base):`81c5a9c` -> - 数据来源:git 实测(`git log` / `git diff` / `git show` 只读命令)+ 8 个并行子代理模块级调研(见附录 D) -> - 关联文档:本文是 **分支对比报告**,聚焦「两侧关系与差异分类」;回迁执行细节见迁移计划文档(其中回迁项使用与本报告一致的 - canonical 编号 #1–#8) -> - 结论性质:除标注「待验证」处外,均基于实测提交与文件内容得出 - ---- - -## 1. 概述与结论 - -AnvilLib 的两个长期分支在共同祖先 `81c5a9c` 处分化后走向了不同的生命周期:`dev/26.1` 是 **演进主线**(在 merge-base 之后新增 -170 个提交),它沿 1.21.2 → 1.21.11 → 26.1 的版本链一路升级、引入 `module.gradle` 聚合构建体系、新建 rendering / rpc / -sync / explosion / font / collision / space-select 等一批新模块,并通过 `7019b38` 一次性移植了当时 1.21.1 侧的 -multiblock / util / network 后持续演进;`port/1.21.1` 则是 **维护与特性分支**(在 merge-base 之后新增 46 个提交),它在冻结的 -1.21.1 平台上继续落地了网络库、wheel 模块、multiblock 动态系统、油库(yukkuri)模块等特性并修复了一批 bug。迁移方向为 -**单向(port → dev)**:port 侧 46 个独有提交中,除约 23 个与 dev 早期提交同主题的镜像提交外,真正需要回迁的独有内容收敛为 -**8 项(#1–#8)**,其中 `module.yukkuri` 整模块(#1)是唯一的大项,其余为小型 bugfix 与可选 API 补充;codec / network / -config / integration / registrum / wheel / moveable-entity-block 七个共享模块判定为 **dev 超集或同源重写,无需回迁** -。结论是:以 dev/26.1 为最终目标,仅按 #1–#8 执行受控回迁,不做整分支合并。 - -### 1.1 结论要点 - -| # | 结论 | 依据 | -|----|----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------| -| C1 | 迁移方向单向:`port/1.21.1` → `dev/26.1`,不做反向合并 | dev/26.1 为演进主线(170 独有提交),port 侧为 1.21.1 平台维护分支(46 独有提交) | -| C2 | 回迁收敛为 8 项(#1–#8),唯一大项是 `module.yukkuri` 整模块(#1) | 逐模块双向 diff 分类结果(见第 5 章) | -| C3 | 七个共享模块无需回迁:codec / network / config / integration / registrum / wheel / moveable-entity-block | dev 侧为超集或同源重写;port 侧 bugfix 在新架构下天然规避 | -| C4 | 共享模块中 util / recipe / multiblock / test / main 存在回迁点,共 8 项中的 7 项(#2–#8) | 逐文件 diff 定位(见 5.2) | -| C5 | `module.yukkuri` 迁移需在「继续使用已弃用流体 API」与「适配新 `ResourceHandler`」之间二选一,推荐后者 | NeoForge 26.1.2.76 sources jar 实测(见 5.3) | -| C6 | port 侧独立构建脚本、lang datagen 产物、`ASYNC_MULTIBLOCK_CHECK_PLAN.md` 不回迁 | 构建体系已被 `module.gradle`(`ea4301f`)取代;文档已标注「已完成实现」 | - ---- - -## 2. 分支拓扑 - -### 2.1 拓扑数据 - -以下数据均为 2026-08-10 在本地仓库实测(只读命令): - -| 项 | 值 | -|----------------------------------------------|---------------------------------------------------------| -| merge-base(共同祖先) | `81c5a9c`(`81c5a9c5246a8d3642ea5cfddaf5298f885f7872`) | -| `port/1.21.1` HEAD | `fab551b`(`fab551bbda48837bf43b4423006b391e4f049221`) | -| `dev/26.1` HEAD | `0b7e45f`(`0b7e45f1bd52561f4ad42e468a72bfdcbee5d23c`) | -| dev 侧独有提交数(`port/1.21.1..dev/26.1`) | **170** | -| port 侧独有提交数(`dev/26.1..port/1.21.1`) | **46** | - -即:`port/1.21.1` 领先 `dev/26.1` **46 个提交**(port 独有),落后 **170 个提交**(dev 独有)。两侧从 merge-base -之后不再有共同提交,处于完全分叉状态。 - -```text - merge-base - 81c5a9c - │ - ┌──────────────────┼──────────────────┐ - │ (46 commits) │ (170 commits) │ - ▼ │ ▼ - port/1.21.1 │ dev/26.1 - fab551b │ 0b7e45f - MC 1.21.1 │ MC 26.1.2 - NeoForge 21.1.226 │ NeoForge 26.1.2.76 - │ - port 独有 46 提交 / dev 独有 170 提交 -``` - -### 2.2 版本与构建环境对比 - -| 项 | `port/1.21.1` | `dev/26.1` | -|-------------------|--------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------| -| Minecraft | 1.21.1(`gradle.properties`:`minecraft_version_range=[1.21.1,1.21.2)`) | 26.1.2(`gradle/libs.versions.toml`:`minecraft = "26.1.2"`) | -| NeoForge | 21.1.226 | 26.1.2.76(`gradle/libs.versions.toml`:`neoForge = "26.1.2.76"`) | -| Java | 21(`gradle.properties`:`java_version=21`) | 25(`gradle.properties`:`java_version=25`) | -| 映射(Parchment) | 1.21.1 / 2024.11.17 | 无 Parchment(26.1 时代不再使用) | -| 模组版本 | 2.0.0 | 2.0.0 | -| 许可证 | MIT | MIT | -| 构建体系 | 各模块独立 `build.gradle` + 根级 `gradle.properties` 传参 | 根级 `module.gradle` 聚合(`ea4301f`)+ `createModule` 任务(`16b7bb5`)+ 依赖坐标后缀 `-neoforge-26.1` | -| 依赖版本管理 | 无统一版本目录 | `gradle/libs.versions.toml`(modDevGradle 2.0.141、lombok 9.2.0、machete 2.0.1、jreleaser 1.23.0、mod-publish 1.1.0) | -| 模块结构 | 13 个 `module.*`(含 `module.yukkuri`) | 19 个 `module.*` + `module.gradle` + `renderdoc-loader`(不含 yukkuri) | - -dev/26.1 侧 `gradle.properties` 另有关键开关 `interface_injection=false`,port 侧无此项。 - -### 2.3 历史脉络与里程碑提交 - -两侧在 merge-base `81c5a9c` 之后的关键里程碑(按时间顺序): - -| 日期 | 提交 | 分支 | 事件 | -|------------|------------------|------|-------------------------------------------------------------------------------------------------------------------------------| -| 2026-03-26 | `6f2685a` | dev | `chore: update project to use Minecraft and NeoForge version 26.1`——正式切入 26.1 | -| 2026-04-14 | `5fc6f65` | port | `feat(multiblock): add dynamic multiblock system`——port 侧原创动态多方块系统 | -| 2026-05-02 | `7019b38` | dev | `chore: Port multiblock and util from 1.21.1`——一次性移植当时 1.21.1 侧的 multiblock / util / network,随后完成 26.1 API 适配 | -| 2026-05-11 | `ea4301f` | dev | `feat(build): update build configuration and add module.gradle for modular setup`——构建体系重构 | -| — | `569e01d`(#88) | port | `添加油库(里)`——新增 `module.yukkuri` 整模块 | -| — | `fab551b`(#91) | port | `修油库`——port 侧当前 HEAD | - -两侧在 merge-base 之后的完整升级链(dev 侧)与特性链(port 侧)见第 3 章提交主题对比。 - -### 2.4 拓扑解读 - -1. **dev/26.1 的 170 个独有提交**中,前段(`81d1f59` → `6f2685a`)是 1.21.2 → 26.1 的连续版本升级链(共 11 次版本更新提交),中段是 - 26.1 API 适配与模块演进,后段(`7019b38` 之后)是移植与新建模块的持续开发。170 的数字里包含大量重构/清理提交,实际「功能面」比表面小。 -2. **port/1.21.1 的 46 个独有提交**中,约 23 个与 dev 早期提交 **同主题镜像**(如网络库、license 改 MIT、bump 2.0.0、wheel - 等,对照表见附录 C),是两侧在分叉后分别重做/挑选的相同内容;真正 port 独有、dev 无对应物的内容集中在 multiblock - 动态系统、yukkuri、piston bugfix 与 util/recipe 的少量修复上——这正是回迁清单 #1–#8 的来源。 -3. 由于两侧无共同提交, **直接 merge 会引入大规模冲突且方向错误**(会把 dev 侧 26.1 演进全部回退到 1.21.1 - 语义),故采用「受控回迁」而非分支合并。 - ---- - -## 3. 提交主题对比 - -本章基于两条只读命令的完整输出归纳: - -```bash -git log --oneline port/1.21.1..dev/26.1 # dev 侧独有,170 条 -git log --oneline dev/26.1..port/1.21.1 # port 侧独有,46 条 -``` - -### 3.1 dev/26.1 侧关键演进主题(170 提交,7 个主题) - -#### 主题 D1:26.1 移植与版本升级链 - -dev 分支在 merge-base 之后的第一要务是版本升级:从 1.21.2 起步,经 1.21.3 → 1.21.11 共 11 次版本提交,最终在 `6f2685a` -(2026-03-26)切入 26.1;随后 `7019b38`(2026-05-02)移植 port 侧 multiblock / util / network,并做了一系列 26.1 API -适配(ItemStackTemplate、ResourceHandler 缓存、ResourceKey 配方标识、FMLLoader.getCurrent () 环境检查等)。 - -| 提交 | 主题 | -|-----------|----------------------------------------------------------------------------------------------------------------| -| `81d1f59` | `chore(build): 更新所有模块及相关配置至Minecraft 1.21.2版本`——升级链起点 | -| `d6fb290` | 1.21.5 升级 + `ItemSubPredicate → DataComponentPredicate` 重构 | -| `b3728b6` | 1.21.11 / 21.11.40-beta 升级 | -| `6f2685a` | `chore: update project to use Minecraft and NeoForge version 26.1`——正式切入 26.1 | -| `7019b38` | `chore: Port multiblock and util from 1.21.1`——一次性移植三模块 | -| `6e00420` | `refactor: replace ItemStack with ItemStackTemplate in various classes`——26.1 API 适配 | -| `2033fd9` | `refactor(recipe-cache): 替换物品处理器缓存为资源处理器缓存`——26.1 资源 API 适配 | -| `f14cde3` | `refactor: update RecipeMapMixin to use ResourceKey for recipe identification` | -| `f03fbd7` | `refactor: update BlockEntityBuilder and related classes to use FMLLoader.getCurrent() for environment checks` | - -#### 主题 D2:构建体系重构(`module.gradle` 聚合) - -dev 侧将各模块独立构建脚本统一为根级 `module.gradle` 聚合,配套 CI、发布配置同步重构。该体系 **不回迁**(port 侧不回迁 dev -的构建体系,见 5.4)。 - -| 提交 | 主题 | -|-----------|-----------------------------------------------------------------------------------------------------| -| `ea4301f` | `feat(build): update build configuration and add module.gradle for modular setup`——聚合构建核心提交 | -| `16b7bb5` | `feat(build): add createModule task for modular project setup` | -| `f97ec94` | `feat(build): add exclusive content configuration for Anvil Lib repository` | -| `1063ee9` | `Add sync processor module and refactor build configuration` | -| `aa59e37` | `refactor(ci): 重构CI工作流以支持模块化构建` | -| `7f27908` | `feat(ci): add synchronization workflows for build and Maven Central deployment` | -| `a26fde5` | `feat(build): 添加Maven发布仓库配置支持` | -| `ae2acd0` | `feat(build): add publishing configuration for CurseForge and Modrinth` | - -#### 主题 D3:新模块建设(rendering / rpc / sync / explosion / font / collision / space-select) - -dev 侧新建了一批 1.21.1 侧没有的模块:字体渲染(font)、SDF 渲染管线(rendering,含 RenderDoc 集成)、注解式 RPC、同步框架(sync,含独立 -processor 子模块)、爆炸模拟(explosion)、AABB/三角形相交检测(collision)、空间选择(space-select)。 - -| 提交 | 主题 | -|-----------|---------------------------------------------------------------------------------------| -| `1d56a4e` | `添加自定义文字渲染模块 module.font`(#22) | -| `7121bf6` | `feat(rendering): Add bloom post-processing and add RenderDoc integration`(#17) | -| `3ce2611` | `feat(sync): implement synchronization framework with SyncManager and annotations` | -| `bf49789` | `feat(sync): implement SyncConfigManager and payload handling for synchronization` | -| `d153e58` | `Implement RPC module with annotation-based remote method calls`(#60) | -| `4910620` | `feat(rpc): 添加对更多数据类型的网络传输支持`(#87) | -| `435e23f` | `Implement explosion module with features and optimizations`(#58) | -| `82b4f6a` | `feat(collision): add AnvilLibCollision for AABB and triangle intersection detection` | -| `1335fc5` | `Add space selection and district management features`(#24) | - -#### 主题 D4:rendering 模块持续演进(SDF / GUI / Bloom / 兼容性) - -rendering 是 dev 侧迭代最密集的模块:缓存型 Block Entity 渲染管线(视锥剔除、半透明排序、Bloom)、SDF 共享参数化、GUI 坐标 -int→float 破坏性重构、AMD/Mac 与 Iris 兼容修复等。 - -| 提交 | 主题 | -|-----------------------------------------------|----------------------------------------------------------------------------------------------------| -| `2751be4` | `feat(rendering): 引入缓存型 Block Entity Rendering 管线,支持视锥剔除、半透明排序与 Bloom`(#26) | -| `5b9cdcd` | `refactor(rendering)!: migrate GUI coordinate parameters from int to float`(#34)——破坏性重构 | -| `b5f5420` | `Implement bloom post-processing effect`(#20) | -| `8a818f0` | `feat(rendering): add compound submit node infrastructure with dirty tracking for bloom`(#23) | -| `6b5a544` / `6d5a97b` | SDF 渲染共享参数管理重构与索引修复(#44 / #45) | -| `d7a8f5d` | `fix(sdf): fix sdf not working on AMD/Mac` | -| `d121047` / `48bfe55` / `fba1027` / `0e8290d` | Iris 兼容(#68)、日志刷屏修复(#74)、Iris 顶点格式(#76)、bloom/compute 稳定性(#92) | - -#### 主题 D5:registrum 模块演进 - -registrum 在 dev 侧经历了注册表项修复、数据组件谓词构建器、CreativeTabBuilder、音效/配方注册、类重命名与文档链接修复等一系列演进。 - -| 提交 | 主题 | -|-----------------------------------|-------------------------------------------------------------------------------| -| `9b2d0e3` | `更多注册表注册`(#31) | -| `9627358` / `da801d2` / `8026804` | 修复误删的注册表项 + 补全 MIT 协议声明(#30) | -| `252922e` | `Registrum 添加数据组件谓词构建器支持`(#47) | -| `79965dc` | `refactor(registrum): 更新数据组件谓词方法的泛型类型定义`(#77) | -| `9c89bfa` | `Add CreativeTabBuilder for custom tabs and update item registration`(#27) | -| `c307a4c` | `添加了音效注册表,添加了配方注册`(#28) | -| `655ad78` | `refactor: rename classes and update original file links in Registrum module` | - -#### 主题 D6:multiblock 动态系统在 dev 侧的后续演进 - -`7019b38` 移植后,dev 侧继续演进动态多方块:定义获取改用 RegistryAccess、状态管理从 Long 换为 -BlockPos、新增成形/解体事件(#69)、修复未加载区块快照问题(#73)。 - -| 提交 | 主题 | -|-----------|------------------------------------------------------------------------------------------------------| -| `1b0a652` | `fix(multiblock): update definition retrieval to use registry access in form and unform packets` | -| `22091d4` | `fix(multiblock): update multiblock state management to use BlockPos instead of Long` | -| `1e5bfb3` | `feat(multiblock): implement dynamic multiblock event handling for formation and unformation`(#69) | -| `d888714` | `Fix bug where DynamicMultiblockManager attempts to load unloaded blocks`(#73) | -| `f40b016` | `feat(network): add Network annotation to multiblock package-info` | - -#### 主题 D7:代码质量与生态统一 - -| 提交 | 主题 | -|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `f94126f` | `refactor(nullness): replace javax.annotation with org.jspecify.annotations for nullability annotations`——@NullMarked 统一(注:另有 `3bf9b6c` 为「注册表更多种类添加 #25」,勿混淆) | -| `6fb6335` | `chore(license): update mod license from GNU LGPL 3.0 to MIT and change copyright holder` | -| `c72aa30` | `chore(annotations): 添加 ApiStatus.Internal 注解到内部接口和类`(#93) | -| `0b7e45f` | `refactor(common): 优化代码导入`(#96)——dev 当前 HEAD | -| `fd18fe9` / `b1484bc` | UnlimitedItemStack 弃用与构造优化(#55 / #86) | -| `113423a` | `feat(mod-description): update mod_description to provide a detailed overview of the space selection library` | - -### 3.2 port/1.21.1 侧关键演进主题(46 提交,6 个主题) - -#### 主题 P1:网络模块落地(1.21.1 平台) - -port 侧在分叉后完成了网络库的完整落地:模块创建、CI 补充、打包/mixin/自动注册三个修复。该系列与 dev 早期提交(`8d8ac8c` -等)同主题镜像,dev 侧为等价实现, **不回迁**。 - -| 提交 | 主题 | -|-----------------------|--------------------------------------------------| -| `a222300` | `feat(network): 添加网络库` | -| `19d98e3` / `10069a0` | 网络库描述与 javadoc 优化 | -| `0d6c1b4` | `feat(ci): 补充网络库缺失的ci` | -| `c7a790b` | `fix(packaging): 修复网络库不会打包进主库的问题` | -| `00d65d0` | `fix(mixin): 修复因缺失mixin文件导致的崩溃问题` | -| `6d2741f` | `fix(network): 修复网络包自动注册的问题` | -| `23719fb` / `d88b8a6` | `BoolAndInt` 紧凑编解码 record 与 long 编码改进 | - -#### 主题 P2:multiblock 动态系统(port 侧原创) - -动态多方块系统最初在 port 侧实现(`5fc6f65`,2026-04-14),后被 `7019b38` 移植到 dev。port 侧的独特贡献集中在异步检查、错误处理、位置修正与 -datagen 本地化。 - -| 提交 | 主题 | -|-----------|------------------------------------------------------------------------------------------------| -| `5fc6f65` | `feat(multiblock): add dynamic multiblock system`——动态系统核心 | -| `ba395e7` | `feat(multiblock): add data generation and localization support for multiblock configurations` | -| `2634e49` | `feat(multiblock): implement asynchronous multiblock checking with configurable parameters` | -| `3832895` | `feat(multiblock): enhance error handling in controller retrieval and logging` | -| `4aa9d13` | `feat(multiblock): improve position correction logic in multiblock controller detection` | - -#### 主题 P3:wheel 模块 - -wheel 在两侧均有提交(port `18834b5`/`3d0a364` ↔ dev `0d73ab0`/`1ae3411` 同主题),dev 侧为 26.1 等价实现, **不回迁**。 - -| 提交 | 主题 | -|-----------|-----------------------------------------------------------------------------------------------| -| `18834b5` | `feat(wheel): Implement Wheel module with API, documentation, and build updates`(#16) | -| `3d0a364` | `feat(wheel): Refactor WheelMenu and WheelEntry for improved action handling and null safety` | - -#### 主题 P4:util 模块扩展与修复 - -| 提交 | 主题 | -|-----------|--------------------------------------------------------------------------------------------------| -| `9d61997` | `feat(util): add ClientTickRecorder and utility methods for dimension and player name retrieval` | -| `9874db7` | `fix(tick): ensure tick recording only occurs when the level is not null`——**回迁项 #4** | -| `c8ee900` | `feat(util): update ISerializer package structure and add WeightedChanceBlockStates class` | -| `0efcd3d` | `feat(shape): add cut methods for VoxelShape and AABB to remove specified shapes` | -| `95bd5ca` | `feat(util): refactor utility classes to use UtilityClass annotation and improve organization` | - -#### 主题 P5:piston / moveable-entity-block 功能与 bugfix - -| 提交 | 主题 | -|-----------|---------------------------------------------------------------------------------------------| -| `53eaa11` | `feat(piston): implement moveable entity block functionality and rendering enhancements` | -| `1acdcf8` | `fix(piston): Fixed the issue of PoseStack not being emptied due to premature returns` | -| `9c437b1` | `feat(piston): enhance PistonBaseBlockMixin with shared block entity handling` | -| `f2b60b6` | `fix(cache): make setting the simulated item stack would properly handle empty type stacks` | - -结论:该模块 dev 侧以 26.1 渲染架构 **同源重写**(`83477ab` 等),port 侧 `1acdcf8` 修复的「PoseStack 提前 return -未清空」问题在新架构下天然规避, **无需回迁**(详见 5.4)。 - -#### 主题 P6:油库(yukkuri)模块与发布收尾 - -yukkuri 是 port 侧唯一整模块独有内容,也是回迁清单的最大项(#1);`1311782` 的 CurseForge 环境声明是 main 模块的回迁项(#8)。 - -| 提交 | 主题 | -|-----------|------------------------------------------------------------------------------------------------| -| `569e01d` | `添加油库(里)`(#88)——新增 `module.yukkuri`(14 个 Java 文件 + build + mods.toml + README) | -| `18b29ca` | `ci: publish yukkuri before aggregate module`(#89) | -| `1311782` | `fix: declare CurseForge environments`(#90)——**回迁项 #8** | -| `fab551b` | `修油库`(#91)——port 当前 HEAD,yukkuri 收尾修复 | - -#### 主题 P7:recipe / codec 杂项修复 - -| 提交 | 主题 | -|-----------------------|---------------------------------------------------------------------------------------------------------------| -| `edd71ec` | `修复鱼缸加工bug`(#83)——**回迁项 #2** | -| `e78d020` | `修复鱼缸加工bug`(#84)——**回迁项 #3** | -| `0b56109` | `Add DamageSourcePredicate StreamCodec and its relative StreamCodecs`(#67)——dev 侧 `3b5dfea` 同主题,不回迁 | -| `50aeac7` | `feat(codec): add zomListMap method ...`(#82)——dev 侧 `2e1b537` 同主题,不回迁 | -| `6a0e8a9` | `修复三个bug`(#79)——dev 侧 `922d523` 同主题,不回迁 | -| `6078b7b` / `88d5800` | en_ud 颠倒英语修复(port/`6078b7b` ↔ dev/`88d5800` 镜像) | - -### 3.3 两侧主题汇总对比 - -| 维度 | `port/1.21.1` | `dev/26.1` | -|--------------|------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| -| 平台 | 冻结在 MC 1.21.1 / NeoForge 21.1.226 | 升级链至 MC 26.1.2 / NeoForge 26.1.2.76 | -| 模块建设 | yukkuri(整模块)、wheel、网络库落地 | rendering / rpc / sync / explosion / font / collision / space-select / renderdoc-loader | -| 构建 | 各模块独立构建脚本(不回迁) | `module.gradle` 聚合 + `-neoforge-26.1` 坐标 + toml 版本目录(dev 特有) | -| multiblock | 动态系统原创、异步检查、datagen 本地化 | 移植后演进:成形/解体事件(#69)、未加载区块快照修复(#73)、BlockPos 状态 | -| bugfix 密度 | 高(网络打包、mixin 崩溃、鱼缸加工 ×2、PoseStack、ClientTickRecorder) | 高(误删注册表项、SDF AMD/Mac、Iris 兼容、log spam、gnome/xwayland 崩溃) | -| 代码质量动作 | UtilityClass 注解化、license MIT | @NullMarked(jspecify)统一、@ApiStatus.Internal、MIT | - ---- - -## 4. 模块清单对比 - -基于 `git ls-tree --name-only <分支>` 实测(见附录 A)。模块目录前缀均为 `module.*`;`module.gradle` 为 dev 侧构建聚合文件,不属于模块。 - -### 4.1 dev/26.1 独有模块(8 项) - -| 模块 | 一句话职责 | -|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------| -| `module.collision` | AABB 与三角形相交检测(`82b4f6a` 引入) | -| `module.explosion` | 爆炸模拟与优化(`435e23f` 引入) | -| `module.font` | 自定义文字渲染(`1d56a4e` 引入) | -| `module.rendering` | SDF 渲染管线、GUI 渲染、Bloom 后处理、RenderDoc 集成(`7121bf6` 等) | -| `module.rpc` | 注解式远程方法调用(`d153e58` 引入) | -| `module.space-select` | 空间选择与 DistrictKey 管理(`1335fc5` / `e75b10f`) | -| `module.sync` | 同步框架:SyncManager、SyncConfigManager、payload 处理,含独立 `processor/` 子模块(`3ce2611` / `1063ee9`) | -| `renderdoc-loader` | RenderDoc 原生加载器(`renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/RenderDocAgent.java`,与 `module.rendering` 配套) | - -### 4.2 port/1.21.1 独有模块(1 项) - -| 模块 | 一句话职责 | 状态 | -|------------------|-----------------------------------------------------------------------------------------------------------------------|---------------| -| `module.yukkuri` | 油库/汽化 API(`569e01d` #88 引入,14 个 Java 文件 + build.gradle + gradle.properties + META-INF/mods.toml + README) | **回迁项 #1** | - -### 4.3 共享模块(12 项) - -| 模块 | 结论(详见第 5 章) | 回迁项 | -|--------------------------------|----------------------------------|------------| -| `module.codec` | dev 超集 | 无 | -| `module.config` | dev 演进 | 无 | -| `module.integration` | dev 演进 | 无 | -| `module.main` | dev 演进 + port 侧 2 行回迁 | #8 | -| `module.moveable-entity-block` | 同源重写,无需回迁 | 无 | -| `module.multiblock` | dev 演进 + 可选回迁 | #6(可选) | -| `module.network` | dev 超集 | 无 | -| `module.recipe` | dev 演进 + 2 处 bugfix 回迁 | #2、#3 | -| `module.registrum` | dev 演进 | 无 | -| `module.test` | dev 演进 + multiblock 测试组回迁 | #7 | -| `module.util` | dev 演进 + 2 处回迁 | #4、#5 | -| `module.wheel` | dev 超集 | 无 | - ---- - -## 5. 逐模块差异分类总表 - -### 5.1 共享模块差异分类总表 - -对每个共享模块执行 `git diff port/1.21.1 dev/26.1 -- module.xxx`(见附录 B)并逐文件核对后,分类结论如下。标注【需回迁】的模块存在 -port 侧独有、dev 侧缺失的内容;标注【dev 超集/同源】的模块无需回迁。 - -| 模块 | 结论 | 关键依据 | 回迁项 | -|--------------------------------|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------| -| `module.codec` | **dev 超集** | port 侧 `0b56109`/`50aeac7`/`6a0e8a9` 均有 dev 同主题提交(`3b5dfea`/`2e1b537`/`922d523`);dev 另有 `52c3f8b`/`cf75837`(PositionPredicate AT 补充)、`3b5dfea` 等独有内容 | `173a454`(portable Codec/MapCodec 方法,2026-08-11 已回移植,落点待提交) | -| `module.network` | **dev 超集** | port 侧网络提交与 dev 早期提交同主题镜像(附录 C);dev 侧继续演进:`20721b1`(泛型检查优化)、`5733a9d`(发包工具)、`6220427`(PacketProtocol 公开)、`395495d`(双向注册 handler 参数) | 无 | -| `module.config` | **dev 演进** | dev 侧独有:`3d50d76`(Config 注解 group 属性)、`c0a619b`(配置管理器模组容器获取修复)、`946938f`/`1ddfd24`(配置管理增强与文档);port 侧无独有内容 | 无 | -| `module.integration` | **dev 演进** | dev 侧独有:`dc4d583`(data loader 重命名与类型处理)、`e8efbd4`(实例计数器自增);port 侧无独有内容 | 无 | -| `module.registrum` | **dev 演进** | dev 侧大幅演进(见主题 D5);port 侧无独有内容 | 无 | -| `module.wheel` | **dev 超集** | 两侧同主题提交(port `18834b5`/`3d0a364` ↔ dev `0d73ab0`/`1ae3411`),dev 侧在 26.1 API 上等价实现;port 侧无独有内容 | 无 | -| `module.moveable-entity-block` | **同源重写,无需回迁** | dev 侧 `83477ab`/`b5b27aa`/`2d4cd84` 以 26.1 渲染架构重写;port 侧 `1acdcf8`(PoseStack 未清空)在新架构下天然规避;`53eaa11`/`9c437b1` 与 dev `2d4cd84`/`b5b27aa` 对应 | 无 | -| `module.util` | **dev 演进 + 2 处回迁** | port 独有:`9874db7`(ClientTickRecorder 修复)、`5fc6f65` 中的 `BlockStatePredicate.Builder.with(BlockState)`;其余 port 提交(`9d61997`/`c8ee900`/`0efcd3d`/`95bd5ca`)dev 侧已有等价或超集实现 | **#4、#5** | -| `module.recipe` | **dev 演进 + 2 处 bugfix 回迁** | port 独有:`edd71ec`(InWorldRecipe 谓词栈回滚)、`e78d020`(ItemResourceHandlerCacheElement.sync() 短路/差量写入);dev 侧其余为演进(`2033fd9` 资源处理器缓存、`0d0649f` 配方代码优化等) | **#2、#3** | -| `module.multiblock` | **dev 演进 + 可选回迁** | dev 侧为移植后的超集(事件、快照修复、RegistryAccess);port 独有:`5fc6f65` 中 `MultiblockDefinition` 的 16 个 `BlockState` 重载——dev 侧是否保留待与维护者确认(见 #6) | **#6(可选)** | -| `module.test` | **dev 演进 + 测试组回迁** | dev 侧 module.test 为 26.1 新结构;port 侧独有的 multiblock 动态系统测试组(TestControllerBlock + LibBlocks/LibItemGroups/LibMultiblockControllers/LibMultiblocks + datagen 注册)在 dev 侧缺失 | **#7** | -| `module.main` | **dev 演进 + 2 行回迁** | port 独有:`1311782` 在 publishMods 块声明的 `clientRequired=true` / `serverRequired=true` 两行 CurseForge 环境配置;dev 侧 publishMods 块无此两行 | **#8** | - -### 5.2 回迁项详解(canonical 编号 #1–#8) - -> 编号、来源提交、落点与难度为既定事实(两侧调研一致);「迁移步骤 / 验收」为执行建议,供迁移计划文档引用。 - -#### #1 `module.yukkuri` 整模块(高 · 最大项) - -- **内容**:`module.yukkuri` 全部 14 个 Java 文件 + `build.gradle` + `gradle.properties` + `META-INF/mods.toml` + README。 -- **来源提交**:`569e01d`(#88,添加油库)、`18b29ca`(#89,CI 发布顺序——CI 配置部分不回迁,见 5.4)。 -- **落点**:dev/26.1 新模块 `module.yukkuri`(按 dev 侧 `module.gradle` 聚合结构落位,注册到根级构建)。 -- **API 适配点**(已核实,见 5.3): - - `ResourceLocation` → `Identifier`(26.1 重命名); - - 流体能力 API 二选一: (A) 继续使用已弃用的 `IFluidHandler`/`FluidStack`(可编译、有告警); (B) 适配新 - `ResourceHandler`(推荐,与 dev/26.1 物品能力迁移方向一致); - - `Level.getCapability(...)` 调用签名核对(26.1 下 capability 查询方式有调整,需对照 - `BlockCapability.createSided(Identifier, Class)` 现状)。 -- **迁移步骤**:① 从 `port/1.21.1:module.yukkuri/` 导出全部源码与资源 → ② 按 dev 侧包结构(`dev.anvilcraft.lib.v2` - 体系)落位 → ③ 全局替换 `ResourceLocation`/`Identifier` → ④ 按选项 B 重写流体交互层 → ⑤ 注册进 `module.gradle` - 聚合与主模块依赖 → ⑥ 编译 + 运行时冒烟。 -- **验收**:`gradlew :module.yukkuri:build` 通过;游戏内油库/汽化功能在 26.1.2 客户端与服务器端可正常交互;无弃用 API 告警(若选 - B)。 -- **难度**:高(涉及 26.1 流体新 API,需对照 sources jar 逐方法适配)。 - -#### #2 `module.recipe`:`InWorldRecipe.matches()` 谓词栈回滚(低) - -- **来源提交**:`edd71ec`(#83)。 -- **落点**:`module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java`(dev 侧同名文件)。 -- **内容**:`matches()` 中谓词计算栈的回滚修复(纯逻辑搬运,无 API 变更)。 -- **适配点**:无;两侧文件同名同包,按 diff 直接搬逻辑。 -- **验收**:port 侧 #83 对应的鱼缸加工回归场景在 dev 侧复现测试通过(dev 侧 module.test 补一条回归用例)。 - -#### #3 `module.recipe`:`ItemResourceHandlerCacheElement.sync()` 短路/差量写入(中) - -- **来源提交**:`e78d020`(#84)。 -- **落点**:`module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/ItemResourceHandlerCacheElement.java` - (port 侧原名 `ItemHandlerCacheElement.java`,dev 侧已随 `2033fd9` 改名并换用资源处理器缓存体系)。 -- **内容**:`sync()` 中避免无谓写入的短路与差量写入逻辑。 -- **适配点**:dev 侧 `ResourceHandler` 无 `setResource` 方法,需改为在 Transaction 内 extract + insert 实现差量写入(与 dev - 侧现有缓存写入模式对齐)。 -- **验收**:缓存同步逻辑在 dev 侧单元/集成测试通过;无空转写入路径(可日志/断言验证调用次数)。 - -#### #4 `module.util`:`ClientTickRecorder` 修复(低) - -- **来源提交**:`9874db7`(主菜单计 tick / 退出归零修复)。 -- **落点**:`module.util/src/main/java/dev/anvilcraft/lib/v2/util/ClientTickRecorder.java`(dev 侧同路径)。 -- **内容**:仅当 level 非空时记录 tick;退出时归零(`9874db7` 为 +9/-2 行的小修复)。 -- **适配点**:保留 dev 侧 `@EventBusSubscriber(modid = AnvilLibUtil.MOD_ID, ...)` 写法, **不要**搬 port 侧无 modid 的写法。 -- **验收**:主菜单不累计 tick、进世界正常计数、退出世界归零;dev 侧该文件编译通过。 - -#### #5 `module.util`:`BlockStatePredicate.Builder.with(BlockState)`(低–中) - -- **来源提交**:`5fc6f65`(与 #6 同提交)。 -- **落点**:`module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java`。 -- **内容**:恢复 `Builder.with(BlockState)` 便捷重载(16 个 `BlockState` 重载的底层支撑)。 -- **适配点**:port 实现使用 `Property.getName(T)`(26.1 已移除)→ 改用 dev 侧现有泛型重载 `with(Property, T)` 实现。 -- **联动决策**:与 #6 联动——若 #6 判定不迁(dev 有意精简 API),#5 应一并放弃;反之 #5 是 #6 的前置。 -- **验收**:`BlockStatePredicate.Builder` 以 `BlockState` 入参构造谓词编译通过并可正确匹配。 - -#### #6 `module.multiblock`:`MultiblockDefinition` 的 16 个 `BlockState` 重载(中 · 可选) - -- **来源提交**:`5fc6f65`。 -- **落点**: - `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/definition/MultiblockDefinition.java`(dev - 侧同路径)。 -- **内容**:16 个以 `BlockState` 直接入参的重载构造/方法。 -- **前置**:依赖 #5 恢复 `with(BlockState)`。 -- **决策点**:若判定 dev 侧删除是有意的 API 精简(`@ApiStatus.Internal` 化趋势,见 `c72aa30`),则 **放弃整项**——保留 dev - 现状,不引入 port 侧便捷层。 -- **验收**(若迁):dev 侧 `MultiblockDefinition` 以 `BlockState` 入参编译通过;动态多方块测试组(#7)覆盖该路径。 - -#### #7 `module.test`:multiblock 动态系统测试组(中–高) - -- **内容**:port 侧独有的动态多方块测试组:`TestControllerBlock` + `LibBlocks` / `LibItemGroups` / - `LibMultiblockControllers` / `LibMultiblocks` + datagen 注册。 -- **落点**:按 dev 侧 `module.test` 新结构落位(dev 侧 module.test 已按 26.1 重构)。 -- **适配点**:`@Mod` 构造器去 ModContainer 参数(26.1 签名变化);`REGISTRUM.addDataGenerator(...)` 替代 GatherDataEvent(dev - 侧现有注册模式);`ResourceLocation` → `Identifier`。 -- **验收**:dev 侧 `:module.test` 构建与测试运行通过,动态多方块成形/解体用例覆盖 #2/#6 涉及的逻辑。 - -#### #8 `module.main`:curseforge 环境两行(低) - -- **来源提交**:`1311782`(#90)。 -- **落点**:`module.main/build.gradle` 的 `publishMods` 块。 -- **内容**:`clientRequired = true` / `serverRequired = true` 两行 CurseForge 环境声明。 -- **适配点**:按 dev 侧 `publishMods` 现有结构落位(`ae2acd0` 引入的发布配置); **不要**把 port 侧 `jarJar(api ...)` 写法搬回。 -- **验收**:`module.main` 发布任务 dry-run 通过,CurseForge 元数据含环境声明。 - -### 5.3 油库(yukkuri)流体能力 API 迁移选项(已核实事实) - -以下事实来自本地 NeoForge **26.1.2.76 sources jar**(`neoforge-26.1.2.76-sources.jar`,路径见附录 D),可直接作为 #1 的适配依据: - -1. **旧 API 仍存在但已弃用**:`net.neoforged.neoforge.fluids.capability.IFluidHandler` 与 - `net.neoforged.neoforge.fluids.FluidStack` 仍可编译,但 `IFluidHandler` 标注 - `@Deprecated(since = "1.21.9", forRemoval = true)`,并提供了迁移包装器 `IFluidHandler.of(ResourceHandler)`。 -2. **新 API**: - - `net.neoforged.neoforge.transfer.ResourceHandler`:方法为 `getResource` / `extract` / `insert` / `isValid` / - `getCapacityAsInt` / `size`, **无 `setResource`**; - - `net.neoforged.neoforge.transfer.fluid.FluidResource`:不可变,实现 `DataComponentHolderResource`,静态字段 - `CODEC` / `OPTIONAL_CODEC` / `STREAM_CODEC` / `EMPTY`。 -3. **Capability 查询**:`BlockCapability.createSided(Identifier, Class)` 在 26.1 仍存在,仅标识符类型改为 `Identifier`。 - -| 选项 | 做法 | 代价 | 建议 | -|----------------------|----------------------------------------------------------------------|------------------------------------------------------------------------|--------------------| -| A 最小移植 | 继续使用已弃用的 `IFluidHandler` / `FluidStack` | 可编译但有弃用告警;未来 `forRemoval` 后必须再迁 | 不推荐(仅作兜底) | -| B 适配新 API(推荐) | 改用 `ResourceHandler`,Transaction 内 insert/extract | 与 dev/26.1 侧物品能力迁移方向(`2033fd9` 资源处理器化)一致,一步到位 | **推荐** | - -### 5.4 不回迁清单与理由 - -| 内容 | 理由 | -|------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `module.codec` / `module.network` / `module.config` / `module.integration` / `module.registrum` / `module.wheel` | dev 侧为超集或已独立演进(见 5.1 各模块行) | -| `module.moveable-entity-block`(含 `1acdcf8` PoseStack 修复) | dev 侧以 26.1 渲染架构同源重写(`83477ab` 等),port 侧 bugfix 在新架构下天然规避 | -| port 侧各模块独立构建脚本、`gradle.properties` | 已被 dev 侧 `module.gradle` 聚合 + toml 版本目录取代(`ea4301f`),回迁即倒退 | -| lang datagen 产物(`zh_cn.json` / `en_us.json`) | dev 侧统一由 datagen 生成(`e3ba2cd` 起块物品自行提供 lang),直接提交产物与 dev 工作流冲突 | -| `module.multiblock/ASYNC_MULTIBLOCK_CHECK_PLAN.md` | 文档已标注「已完成实现」;dev 侧异步检查实现等价且更完善(`d888714` 等),建议不迁移 | -| `18b29ca`(CI 发布顺序) | CI 体系 dev 侧已整体重构(`aa59e37`/`7f27908`),port 侧 CI 片段不适用 | -| dev 侧特有演进(不回迁方向) | 动态多方块事件 `1e5bfb3`(#69)、未加载区块快照修复 `d888714`(#73)、@NullMarked 统一 `f94126f`、`module.gradle` 构建体系 `ea4301f`、@ApiStatus.Internal `c72aa30`(#93)——均为 dev 独有,port 侧不反向吸收 | - ---- - -## 6. 附录:研究方法 - -### A. merge-base 分析 - -```bash -# 共同祖先与两侧 HEAD -git merge-base port/1.21.1 dev/26.1 # → 81c5a9c5246a8d3642ea5cfddaf5298f885f7872 -git rev-parse port/1.21.1 dev/26.1 # → fab551bbda... / 0b7e45f1bd... - -# 双向独有提交计数 -git rev-list --count port/1.21.1..dev/26.1 # → 170(dev 独有) -git rev-list --count dev/26.1..port/1.21.1 # → 46(port 独有) - -# 模块清单(顶层 module.* 目录) -git ls-tree --name-only dev/26.1 | grep '^module\.' -git ls-tree --name-only port/1.21.1 | grep '^module\.' -``` - -解读:merge-base 之后两侧完全分叉;dev 独有 170 提交中前段为版本升级链,port 独有 46 提交中约一半为 dev 早期提交的镜像(见附录 -C)。 - -### B. 双向 diff(逐模块差异分类) - -对每个共享模块执行双向 diff,人工核对「port 独有 vs dev 独有」的实质内容: - -```bash -# 两侧差异(粗粒度定位) -git diff port/1.21.1 dev/26.1 -- module.util -# port 侧独有修改(回迁候选) -git diff port/1.21.1..dev/26.1 -- module.util # 即 dev 相对 port 的变更 -# 按文件核对提交归属 -git log --oneline dev/26.1..port/1.21.1 -- module.recipe -``` - -差异分类规则: - -| 分类 | 判据 | 动作 | -|---------------|-------------------------------------------------------------------|-----------------------| -| port 独有内容 | port 侧有修改/文件,dev 侧无对应 | 进入回迁候选(#1–#8) | -| dev 超集 | dev 侧包含 port 侧全部实质内容且有更多演进 | 不回迁 | -| 同源重写 | 两侧同一功能在不同 API 架构上分别实现(如 moveable-entity-block) | 不回迁 | -| 同主题镜像 | port 提交与 dev 早期提交主题一致(如网络库、wheel) | 不回迁,以 dev 为准 | - -### C. 提交对照(port 独有 46 提交的镜像/独有判定) - -对 `dev/26.1..port/1.21.1` 的 46 条提交逐条与 dev 侧 170 条比对主题后,同主题镜像关系如下(节选): - -| port 提交 | 镜像主题 | dev 侧对应提交 | -|-----------------------------------------------------------------------------------|----------------------------------------|-----------------------------------------------------------------------------------| -| `a222300` / `19d98e3` / `10069a0` / `0d6c1b4` / `c7a790b` / `00d65d0` / `6d2741f` | 网络库落地/优化/CI/打包/mixin/自动注册 | `8d8ac8c` / `548cecd` / `0fc80e5` / `baac9a0` / `d85e3d5` / `0e86a9d` / `09e435c` | -| `18834b5` / `3d0a364` | wheel 模块 | `0d73ab0` / `1ae3411` | -| `bcefcb9` | license LGPL→MIT | `6fb6335` | -| `a74671a` | README 格式 | `371d5db` | -| `1dca02f` | bump 2.0.0 | `65a605d` | -| `6078b7b` / `109a935` | en_ud 修复 / 风格优化 | `88d5800` / `42f816d` | -| `23719fb` / `d88b8a6` | BoolAndInt 编解码 | `3fc6054` / `8e01120` | -| `d7985f6` / `f2b60b6` | ItemCache 范围/空栈处理 | `ec16ccb` / `1c523ee` | -| `53eaa11` / `9c437b1` | moveable entity block | `83477ab` / `2d4cd84` | -| `0b56109` | DamageSourcePredicate StreamCodec | `3b5dfea` | -| `50aeac7` | zomListMap | `2e1b537` | -| `6a0e8a9` | 三个 bug 修复 | `922d523` | - -判定为 port 真正独有(无 dev 对应)的提交:`5fc6f65`(动态多方块系统,dev 经 `7019b38` 移植后已改写)、`ba395e7` / `2634e49` / -`3832895` / `4aa9d13`(异步检查/错误处理/位置修正/datagen 本地化——其中部分已被 dev 侧等价实现吸收)、`9874db7` -(ClientTickRecorder 修复)、`edd71ec` / `e78d020`(鱼缸加工 bugfix ×2)、`569e01d` / `18b29ca` / `1311782` / `fab551b` -(yukkuri 系列)、`9d61997` / `c8ee900` / `0efcd3d` / `95bd5ca`(util 扩展——dev 侧已有等价或超集,不回迁)。 - -### D. 参考资料 - -| 资料 | 位置 | -|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 8 个并行子代理模块级调研报告(codec/util/network/recipe/registrum/multiblock/moveable-entity-block/main/config/integration/wheel/test 差异分类与 API 变化点明细) | `C:/Users/Administrator/.kimi-code/sessions/wd_anvillib_031cb785b7e3/session_6033f19b-f4ef-481a-b18c-d626cc22c314/agents/main/tool-results/AgentSwarm-call_00_nh47UIFAaqo9d96levr46332-594fb004-9f1f-4c9e-b230-43bc1440211d.txt` | -| NeoForge 26.1.2.76 sources jar(流体能力 API 核实依据) | `C:/Users/Administrator/.gradle/caches/modules-2/files-2.1/net.neoforged/neoforge/26.1.2.76/1d1dabe31afb953e46fb278e1cd8f04ab8e0d9a0/neoforge-26.1.2.76-sources.jar` | -| 只读 git 命令(随时可用) | `git show <分支>:<路径>`、`git diff port/1.21.1 dev/26.1 -- <路径>`、`git log --oneline port/1.21.1..dev/26.1 -- <路径>`、`git grep -n <关键字> <分支> -- <路径>` | - -### E. 待验证事项 - -| # | 事项 | 验证方法 | -|----|--------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| -| V1 | #5/#6 的 `with(BlockState)` 在 26.1 的可用性(`Property.getName(T)` 已移除,改用泛型重载后行为等价性) | dev 分支上编译运行 `BlockStatePredicate` 相关测试 | -| V2 | #1 选项 B 的 `ResourceHandler` 迁移完整性(14 个 Java 文件逐一核对 API 用法) | `gradlew :module.yukkuri:compileJava` 编译输出对照 26.1.2.76 sources jar | -| V3 | #7 测试组在 dev 侧 `module.test` 新结构的落位方式(`REGISTRUM.addDataGenerator` 注册模式) | 对照 dev 侧 module.test 现有注册代码 + 构建运行 | -| V4 | `module.yukkuri` 的 mods.toml 依赖声明与 `module.main` 聚合依赖是否需要调整 | 检查 dev 侧其他模块的 mods.toml 依赖写法 | -| V5 | #8 两行配置在 dev 侧 `publishMods` 块的结构兼容性 | `module.main` 发布任务 dry-run | diff --git a/docs/migration-plan-dev-26.1.md b/docs/migration-plan-dev-26.1.md deleted file mode 100644 index 81da3c3b..00000000 --- a/docs/migration-plan-dev-26.1.md +++ /dev/null @@ -1,909 +0,0 @@ -# AnvilLib 迁移计划:`port/1.21.1` → `dev/26.1`(回迁执行方案) - -> 文档信息 -> -> - 生成日期:2026-08-10 -> - 分析对象:`port/1.21.1` @ `fab551b`(Minecraft 1.21.1 / NeoForge 21.1.226)、`dev/26.1` @ `0b7e45f`(Minecraft 26.1.2 / - NeoForge 26.1.2.76) -> - 共同祖先(merge-base):`81c5a9c` -> - 数据来源:git 实测(`git show` / `git diff` / `git log` 只读命令,2026-08-10 本地仓库)+ NeoForge 26.1.2.76 sources jar - 实测 + 8 个并行子代理模块级调研 -> - 关联文档:本文是 **迁移计划文档**,与 `docs/branch-comparison.md`(分支对比报告)配套;回迁项使用与对比报告一致的 - canonical 编号 #1–#8 -> - 结论性质:除标注「待验证」处外,均基于实测提交、文件内容与 26.1.2.76 sources jar 得出 -> - 执行约束:本计划只描述 **内容回迁**,不含 dev 侧既有演进的搬运;本文件仅为计划,执行时需在 dev/26.1 分支上以受控提交落地 - ---- - -## 1. 概述与范围 - -### 1.1 迁移背景 - -AnvilLib 的两个长期分支在 merge-base `81c5a9c` 之后完全分叉:`dev/26.1` 是演进主线(独有 170 提交),沿 1.21.2 → 1.21.11 → -26.1 的版本链升级,并完成了 `module.gradle` 聚合构建体系重构;`port/1.21.1` 是维护与特性分支(独有 46 提交),在冻结的 1.21.1 -平台上落地了网络库、wheel、multiblock 动态系统、油库(yukkuri)模块并修复了一批 bug。 - -`docs/branch-comparison.md` 已对两侧关系做了完整分类:port 侧 46 个独有提交中,约 23 个与 dev 早期提交同主题镜像,真正需要回迁的独有内容收敛为 -**8 项(canonical 编号 #1–#8)**。其中 `module.yukkuri` 整模块(#1)是唯一大项,其余为小型 bugfix、API 补充与发布配置。 - -本文档即这 8 项的执行计划: **每项给出目标、步骤(含具体 git 命令)、API 适配点与验收方式**,并汇总构建/CI -接入点、不做清单、建议执行顺序与总体验收标准。 - -### 1.2 范围界定 - -本计划 **包含**: - -- 8 个回迁项(#1–#8)的完整执行方案与验收判据; -- 回迁涉及的构建 / CI / README 联动修改(仅 #1 与 #8 涉及); -- 每个回迁项与 port 侧来源提交、dev/26.1 落点文件的一一对应关系。 - -本计划 **不包含**(即「不做清单」的完整理由见第 5 章): - -- dev 侧既有演进的任何反向搬运(动态多方块事件 `1e5bfb3` #69、未加载区块快照修复 `d888714` #73、`@NullMarked` 统一 - `3bf9b6c`/`f94126f`、`module.gradle` 构建体系 `ea4301f`、`@ApiStatus.Internal` `c72aa30` #93 等); -- codec / network / config / integration / registrum / wheel / moveable-entity-block 七个共享模块的任何内容(dev - 侧为超集或同源重写,见第 5 章); -- port 侧各模块独立构建脚本与 lang datagen 产物(构建体系已被 dev 侧聚合取代,lang 由 datagen 统一生成)。 - -**迁移方式**:两侧无共同提交,直接 merge 方向错误且冲突不可控,故采用 **受控回迁**——在 dev/26.1 分支上按 #1–#8 -逐个以独立提交落地,每个提交只改本项内容,便于评审与回滚。 - -### 1.3 前置条件与约定 - -| 项 | 约定 | -|----------|---------------------------------------------------------------------------------------------------------------------------------| -| 编号 | 回迁项一律使用 canonical 编号 **#1–#8**,与 `docs/branch-comparison.md` 第 5.2 节一致 | -| 基线 | dev/26.1 @ `0b7e45f`(文档描述的全部落点均以该提交为准;执行时如 HEAD 已前进,需先以 `git log` 复核落点文件未变) | -| 提取方式 | 一律用 `git show port/1.21.1:<路径>` 提取源码,**不**进行分支切换、不产生 git 写入操作 | -| 工作区 | 回迁在 dev/26.1 分支工作树执行;只读对比可用 `git diff port/1.21.1 dev/26.1 -- <路径>` 或临时工作树(`git -C <临时目录> diff`) | -| 验证基线 | dev 侧构建命令:`gradlew :module.xxx:build`、`gradlew :module.xxx:compileJava`、`gradlew runData`(模块需要 datagen 时) | -| 待验证 | 所有标注「待验证」的条目均已给出验证方法,执行时须完成验证后才能标记该项完成 | -| API 事实 | NeoForge 26.1.2.76 的流体能力 API 事实见 3.1.4 小节(已实测),其余 26.1 API 差异以编译期核对为准 | - -### 1.4 回迁项一览(结论先行) - -| # | 内容 | 难度 | 一句话结论 | -|----|--------------------------------------------------------|-------|------------------------------------------------------------------------------------------------------------| -| #1 | `module.yukkuri` 整模块 | 高 | 唯一大项;14 个 Java 文件 + 构建/元数据/README,需按 dev 聚合结构落位并做 26.1 流体 API 适配(推荐选项 B) | -| #2 | `InWorldRecipe.matches()` 谓词栈回滚 | 低 | 纯逻辑搬运,无 API 变更 | -| #3 | `ItemResourceHandlerCacheElement.sync()` 短路/差量写入 | 中 | dev 侧已换 `ResourceHandler` 体系,需 Transaction 内 extract+insert 重写 | -| #4 | `ClientTickRecorder` 修复 | 低 | 判空 + 退出归零两处小改,保留 dev 侧 modid 写法 | -| #5 | `BlockStatePredicate.Builder.with(BlockState)` | 低-中 | 改走 dev 侧泛型重载实现;与 #6 联动决策 | -| #6 | `MultiblockDefinition` 16 个便捷重载 | 中 | 依赖 #5;若判定 dev 删除属有意精简则整体放弃 | -| #7 | `module.test` multiblock 测试组 | 中-高 | 按 dev 侧 module.test 新结构落位,适配 26.1 注册 API | -| #8 | `module.main` CurseForge 环境两行 | 低 | `publishMods` 块加两行,无其他影响 | - ---- - -## 2. 回迁项总表 - -### 2.1 总表(含前置依赖) - -> 内容、来源提交、落点与难度与 `docs/branch-comparison.md` 第 5.2 节完全一致;「前置依赖」列为本文档新增,用于编排执行顺序。 - -| # | 内容 | 来源提交 | 落点(dev/26.1) | 难度 | API 适配点 | 前置依赖 | -|---|------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|------------------|------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------| -| 1 | module.yukkuri 整模块(14 Java 文件 + build + mods.toml + README) | `569e01d` #88、`18b29ca` #89 | 新模块 `module.yukkuri/` | 高(最大项) | `ResourceLocation`→`Identifier`;流体 API(见 3.1.4);`Level.getCapability` 签名核对 | 无代码前置;需 `settings.gradle` / `module.main` 依赖 / CI(`modules.json`)/ README 四处联动(见 3.1.3) | -| 2 | module.recipe:`InWorldRecipe.matches()` 谓词栈回滚 | `edd71ec` #83 | `InWorldRecipe.java`(同名同包) | 低(纯逻辑搬运) | 无 | 无 | -| 3 | module.recipe:`ItemResourceHandlerCacheElement.sync()` 短路/差量写入 | `e78d020` #84 | `cache/item/ItemResourceHandlerCacheElement.java`(port 侧原名 `ItemHandlerCacheElement`,dev 侧已随 `2033fd9` 改名) | 中 | dev 侧 `ResourceHandler` 无 `setResource`,需在 Transaction 内 extract+insert 实现原子替换 | 无 | -| 4 | module.util:`ClientTickRecorder` 修复(主菜单计 tick / 退出归零) | `9874db7` | `util/ClientTickRecorder.java` | 低 | 无;保留 dev 侧 `@EventBusSubscriber(modid=AnvilLibUtil.MOD_ID,...)` 写法,勿搬 port 侧无 modid 写法 | 无 | -| 5 | module.util:`BlockStatePredicate.Builder.with(BlockState)` | `5fc6f65` | `predicate/BlockStatePredicate.java` | 低-中 | port 实现用 `Property.getName(T)`(26.1 已移除)→ 改用 dev 侧泛型重载 `with(Property, T)`;与 #6 联动决策 | 无 | -| 6 | module.multiblock:`MultiblockDefinition` 的 16 个 `BlockState` 便捷重载 | `5fc6f65` | `dynamic/definition/MultiblockDefinition.java` | 中 | 依赖 #5 恢复 `with(BlockState)`;若判定 dev 侧删除是有意 API 精简则放弃整项 | **#5** | -| 7 | module.test:multiblock 动态系统测试组(TestControllerBlock + LibBlocks/LibItemGroups/LibMultiblockControllers/LibMultiblocks + datagen 注册) | port 特有(`5fc6f65` 动态系统配套,`ba395e7` datagen) | 按 dev 侧 module.test 新结构落位 | 中-高 | `@Mod` 构造器去 ModContainer;`REGISTRUM.addDataGenerator` 替代 GatherDataEvent;`ResourceLocation`→`Identifier` | **#6**(测试组用到 `MultiblockDefinition.builder()` 便捷重载与 `BlockStatePredicate`) | -| 8 | module.main/build.gradle:curseforge `clientRequired=true`/`serverRequired=true` | `1311782` #90 | `module.main/build.gradle` publishMods 块 | 低(两行) | 按 dev 侧结构落位;勿把 port 侧 `jarJar(api ...)` 写法搬回 | 无 | - -### 2.2 编号一致性说明 - -- 本表 #1–#8 与 `docs/branch-comparison.md` 5.2 节一一对应,本次回迁以本表为唯一执行依据。 -- #5 与 #6 同源(均出自 port 提交 `5fc6f65`),#5 是 #6 的 **代码级前置**(`MultiblockDefinition` 的 `BlockState` 重载内部调用 - `BlockStatePredicate.builder().with(state)`);二者共享同一个「放弃则一并放弃」的联动决策(见 3.6.3)。 -- #1 无代码前置,但牵涉 4 处工程联动(`settings.gradle`、`module.main/build.gradle`、`.github/modules.json` - 、README),建议最先执行以尽早暴露 26.1 流体 API 适配风险。 -- #7 依赖 #6 的便捷重载(测试定义 `LibMultiblocks` 使用 `addController(...)` / `add(...)` / - `seriaBuilder().mapController(...)` 等),故排在 #6 之后。 -- #8 独立,可随时执行,安排在最后作为收尾。 - ---- - -## 3. 逐项详细计划 - -> 每节统一包含四部分: **目标**(一句话)、 **步骤**(具体到 git 命令与落点文件)、 **API 适配点**(旧 → 新)、 **验收方式**。步骤中的 -> `git show port/1.21.1:<路径>` 均为只读操作。 - -### 3.1 迁移项 #1:`module.yukkuri` 整模块(高 · 最大项) - -#### 3.1.1 目标 - -把 port 侧独有的油库/汽化运行时 `module.yukkuri`(14 个 Java 文件 + 构建文件 + mods.toml + README)整体迁移到 dev/26.1,按 -dev 侧 `module.gradle` 聚合结构落位为独立模块 `module.yukkuri/`(坐标 `anvillib-yukkuri-neoforge-26.1`),完成 26.1 API -适配并接入构建、CI 发布顺序与 README。 - -#### 3.1.2 来源与现状(已核实) - -| 项 | 值 | -|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 来源提交 | `569e01d`(#88「添加油库(里)」,21 文件 / 739 行新增)、`18b29ca`(#89「ci: publish yukkuri before aggregate module」,CI 发布顺序)、`fab551b`(#91「修油库」,port 当前 HEAD,收尾修复) | -| port 侧文件清单 | 18 个:`module.yukkuri/README.md`、`build.gradle`(270 行独立脚本)、`gradle.properties`(3 属性)、`src/main/resources/META-INF/neoforge.mods.toml`(25 行),以及 14 个 Java 文件 | -| Java 文件(14) | `Yukkuri.java`;`api/event/LargeCauldronProcessEvent.java`;`api/vapor/` 下 `IVaporConsumer` / `VaporAction` / `VaporStack` / `VaporizationCauldron` / `VaporizationContext` / `VaporizationManager` / `VaporizationOffer` / `VaporizationSource` / `VaporizationSources` / `YukkuriCapabilities` / `YukkuriVaporTypes` | -| 包名 | `dev.anvilcraft.lib.v2.yukkuri`(与 dev 侧 `v2` 包体系一致,无需改包) | -| 模块间依赖 | **无**(已逐文件核对 import:仅引用自身包 + MC/NeoForge 类,不依赖 util/codec 等 anvillib 模块)→ CI 拓扑 `needs: []`,Level 0 | -| port 侧独立构建脚本 | 不回迁(见第 5 章);dev 侧由根 `build.gradle` 的 `subprojects` 块统一 `apply from: rootProject.file("module.gradle")` 接管 | -| 行为契约 | `VaporizationManager.tick(ServerLevel, VaporizationCauldron)` 是运行时入口,由外部大锅实现(AnvilCraft)在服务器 tick 调用;`YukkuriCapabilities.VAPOR_CONSUMER` 通过 `BlockCapability.createSided` 注册于大锅正上方方块,查询方向 `Direction.DOWN` | -| port 收尾修复(fab551b) | `VaporizationManager.java`(+24/-2)、`VaporizationSource.java`(+6/-1)——迁移时直接以 `port/1.21.1` 侧文件为准,天然包含该修复 | - -#### 3.1.3 迁移步骤 - -**阶段 A:源码导出与落位** - -```bash -# 1) 在 dev/26.1 分支工作树创建目录(执行时在 dev/26.1 分支,以下为只读提取) -mkdir -p module.yukkuri/src/main/java/dev/anvilcraft/lib/v2/yukkuri/api/{event,vapor} -mkdir -p module.yukkuri/src/main/resources/META-INF - -# 2) 逐文件从 port 侧导出(示例;共 14 个 Java 文件,路径见 3.1.2 表) -git show port/1.21.1:module.yukkuri/src/main/java/dev/anvilcraft/lib/v2/yukkuri/Yukkuri.java \ - > module.yukkuri/src/main/java/dev/anvilcraft/lib/v2/yukkuri/Yukkuri.java -# 其余 13 个 Java 文件同理,逐一落位;可用循环批量提取: -for f in $(git ls-tree -r --name-only port/1.21.1 -- module.yukkuri | grep '\.java$'); do - git show "port/1.21.1:$f" > "${f/port\/1.21.1\//}" # 按 dev 工作树相对路径落位 -done - -# 3) 导出 README 与 mods.toml 作为内容底稿(build.gradle / gradle.properties 不回迁,见阶段 B) -git show port/1.21.1:module.yukkuri/README.md > module.yukkuri/README.md -git show port/1.21.1:module.yukkuri/src/main/resources/META-INF/neoforge.mods.toml \ - > module.yukkuri/src/main/resources/META-INF/neoforge.mods.toml -``` - -**阶段 B:构建文件按 dev 聚合结构重写(不搬 port 侧 270 行独立脚本)** - -- `module.yukkuri/build.gradle`:dev 侧普通模块的 build.gradle 只保留 `dependencies {}` 块(如 `module.util/build.gradle` - 的形态),其余(版本、neoForge 配置、runs、processResources 替换、sourcesJar/javadocJar、publishing、jreleaser)全部由根级 - `module.gradle` 聚合提供: - -```groovy -// module.yukkuri/build.gradle(按 dev 侧 module.util/build.gradle 精简) -dependencies { - // yukkuri 无 anvillib 模块间依赖,此块可为空;未来引入依赖时按 NOT_DEV 双分支写法 -} -``` - -- `module.yukkuri/gradle.properties`:dev 侧模块统一只含 3 个属性(参照 `module.util/gradle.properties`): - -```properties -## Mod Properties -mod_id=anvillib_yukkuri -mod_name=AnvilLib-Yukkuri -mod_description=Large-cauldron vaporization API and runtime for AnvilCraft addons -``` - -> 注:dev 侧 `module.gradle` 会读取 `anvillib.needRunConfig*` 系列属性(如 `module.explosion/gradle.properties` 设 -> `anvillib.needRunConfig=true`、`anvillib.needRunConfig.data=true`),未设置的模块等价于不生成 run 配置,yukkuri 默认 -> **不设置**(无 datagen / gametest 需求);如迁移后需要本地冒烟运行,可临时加 `anvillib.needRunConfig=true`,验证完删除。 - -- `module.yukkuri/src/main/resources/META-INF/neoforge.mods.toml`:以 dev 侧模块模板为准(参照 `module.util` 的 - mods.toml),保留 `[[mods]]` 与 `[[dependencies]]`(neoforge / minecraft 两条);port 侧版本无 mixin, **删除模板中的 - `[[mixins]]` 块**(模板默认含 `[[mixins]] config = "${mod_id}.mixins.json"`,yukkuri 没有 mixin 配置文件,保留会触发加载失败); - `displayURL`、`authors` 等占位符由 `processResources` 统一替换(`module.gradle` 已配置)。 - -**阶段 C:`settings.gradle` 注册(对照 `569e01d` 对 port 侧 settings.gradle 的改动,改到 dev 侧对应位置)** - -```groovy -// settings.gradle:include 列表加一行(与 dev 侧模块并列) -include 'module.yukkuri' -// settings.gradle:project 改名表加一行(dev 侧坐标为 -neoforge-26.1 后缀) -project(':module.yukkuri').name = 'anvillib-yukkuri-neoforge-26.1' -``` - -**阶段 D:`module.main/build.gradle` 聚合依赖(使用 dev 侧既有写法,NOT_DEV 双分支各加一行)** - -```groovy -// module.main/build.gradle:dependencies 块,NOT_DEV == 'true' 分支 -jarJar(api("dev.anvilcraft.lib:anvillib-yukkuri-neoforge-26.1:latest.release")) -// module.main/build.gradle:dependencies 块,else(本地开发)分支 -jarJar(implementation project(":anvillib-yukkuri-neoforge-26.1")) -``` - -> 严格沿用 dev 侧现有 19 个模块的两行写法(见 `module.main/build.gradle` 的 `dependencies` 块); **不要**把 port 侧 -> `569e01d` 中顺带做的 `jarJar(implementation ...)` → `jarJar(api ...)` 批量改写搬回(那是 port 侧的历史改动,dev 侧保持现状)。 - -**阶段 E:CI 发布顺序(对照 `18b29ca`,按 dev 侧机制落位)** - -- port 侧 `18b29ca` 的做法:在 `ci.yml` 手工新增 `yukkuri` 与 `yukkuri-maven-central-deploy` 两个 job,并把 `yukkuri` 加入 - `main` job 的 `needs` 列表,确保 yukkuri 在聚合模块 `main` 之前构建并发布。 -- dev 侧机制不同(`aa59e37` 重构 + `7f27908` 同步工作流):`ci.yml` 的 `prepare` job 执行 - `.github/workflows/generate-matrix.js`,读取 `.github/modules.json` 做拓扑分层,自动生成 `build-l0/l1/l2` 与 - `deploy-l0/l1/l2` 矩阵;`main` job `needs: [build-l2]` 传递依赖所有层级。因此 **只需在 `.github/modules.json`增加一行**: - -```json -{ - "module": "yukkuri", - "needs": [] -} -``` - -- `generate-matrix.js` 会从模块名自动推导 `module_id=anvillib-yukkuri`、`mod_id=anvillib_yukkuri`(与 port 侧手工声明一致); - `build_and_test.yml` 默认 `GRADLE_PROJECT=anvillib-yukkuri-neoforge-26.1`、`MODULE_DIR=module.yukkuri`,无需额外参数。 -- 效果等价于 `18b29ca` 的意图:yukkuri 位于 Level 0(无依赖),进入 `build-l0` / `deploy-l0`,先于 `main`(`needs: [build-l2]` - )构建发布;`main` 侧无需改动 `needs`。 -- 验收:本地 `node .github/workflows/generate-matrix.js` 逻辑核对(设置 `GITHUB_WORKSPACE` 后运行,确认 `level_0` 包含 - `yukkuri`);或在 PR 分支观察 CI 的 `build-l0` 出现 yukkuri 任务。 **待验证**:`modules.json` 是否需要在 `needs` - 中补充(yukkuri 无模块间依赖,理论为 `[]`;如编译期发现引用了其他模块则补入对应 needs)。 - -**阶段 F:README 更新(对照 `569e01d` 对 README.md / README.en.md 的 4 处改动,改到 dev 侧对应位置)** - -| 落点 | 改动 | -|----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `README.md` | 模块功能表加一行 `\| **Yukkuri** \| 大锅汽化 API 与运行时 \|`(对照 port 侧 README 的中文表样式);聚合模块清单(`- yukkuri`)与依赖坐标示例(`implementation "dev.anvilcraft.lib:anvillib-yukkuri-neoforge-26.1:2.0.0"`)各加一处,坐标后缀用 **`-neoforge-26.1`**(dev 侧坐标,勿写 `-1.21.1`) | -| `README.en.md` | 英文版同位置同步 4 处(模块表 / 模块清单 / 两处依赖坐标示例),坐标后缀 `-neoforge-26.1` | -| `module.yukkuri/README.md` | 直接采用 port 侧版本;其中「Dependency」小节引用的聚合坐标 `anvillib-neoforge-1.21.1` 需改为 dev 侧 `anvillib-neoforge-26.1`(或按 dev README 的当前聚合命名核对后改写) | - -**阶段 G:26.1 API 适配(核心工作,详见 3.1.4)** - -- 全局替换 `ResourceLocation` → `Identifier`(涉及 5 个文件:`Yukkuri.java`、`VaporStack.java`、`VaporizationSource.java`、 - `VaporizationSources.java`、`YukkuriVaporTypes.java`)。 -- 流体交互层二选一(选项 A / 选项 B,推荐 B),见 3.1.4。 -- `Level.getCapability` 调用点核对(`VaporizationManager.java` 第 40 行附近),见 3.1.4。 -- 全部文件过一遍 `gradlew :module.yukkuri:compileJava` 编译,逐个消灭报错。 - -#### 3.1.4 API 适配点 - -以下事实来自本地 NeoForge **26.1.2.76 sources jar** 实测(`net/neoforged/neoforge/...` 源码,2026-08-10 核对): - -**(a) `ResourceLocation` → `Identifier`(1.21.5+ 官方改名,必改)** - -| 文件 | port 侧用法 | 26.1 写法 | -|----------------------------|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------| -| `Yukkuri.java` | `ResourceLocation.fromNamespaceAndPath(...)`、`public static ResourceLocation of(String path)` | `Identifier.fromNamespaceAndPath(...)`;方法返回类型改 `Identifier` | -| `VaporStack.java` | `record VaporStack(ResourceLocation type, int amount)` | `record VaporStack(Identifier type, int amount)` | -| `VaporizationSource.java` | `ResourceLocation id();` | `Identifier id();` | -| `VaporizationSources.java` | `Map` | `Map` | -| `YukkuriVaporTypes.java` | `public static final ResourceLocation GASEOUS_OIL = Yukkuri.of("gaseous_oil")` 等 | `Identifier`;`isStandard(@Nullable ResourceLocation id)` 同步改 | -| `YukkuriCapabilities.java` | `BlockCapability.createSided(Yukkuri.of("vapor_consumer"), IVaporConsumer.class)` | `createSided` 首参类型即 `Identifier`(见下),**签名不变** | - -**(b) 流体能力 API 二选一(已实测)** - -旧 API 仍存在但已弃用: - -- `net.neoforged.neoforge.fluids.capability.IFluidHandler` 标注 `@Deprecated(since = "1.21.9", forRemoval = true)` - (sources jar 第 25 行),其各方法(fill/drain 等)同步弃用; -- `net.neoforged.neoforge.fluids.FluidStack` 仍可编译使用; -- 提供迁移包装器 `static IFluidHandler of(ResourceHandler handler)`(第 35 行)。 - -新 API: - -- `net.neoforged.neoforge.transfer.ResourceHandler`:方法为 `size()` / `getResource(int)` / - `getCapacityAsInt(int, Resource)` / `isValid(int, Resource)` / `insert(...)` / `extract(...)`, **无 `setResource`**; -- `net.neoforged.neoforge.transfer.fluid.FluidResource`:不可变,实现 `DataComponentHolderResource`,静态字段 - `CODEC` / `OPTIONAL_CODEC` / `STREAM_CODEC` / `EMPTY`。 - -| 选项 | 做法 | 影响面 | 建议 | -|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------| -| **A 最小移植** | 继续使用已弃用的 `IFluidHandler` / `FluidStack`(port 侧代码几乎不改) | 可编译但有弃用告警;`forRemoval` 后必须再迁一次;与 dev 侧物品能力已资源处理器化的现状(`2033fd9`)不一致 | 不推荐,仅作兜底 | -| **B 适配新 API(推荐)** | 用 `ResourceHandler` 重写 yukkuri 的流体读写层:槽位为 `index`(对应 port 侧 `IFluidHandler` 的 tank 序号,注意 26.1 无 `getTanks()`,用 `size()`);读写经 `getResource(index)` / `extract(index, resource, amount, transaction)` / `insert(index, resource, amount, transaction)`,`Transaction.openRoot()` + `commit()` 包裹;`FluidStack` 相关转换经 `FluidResource.toStack(...)` / `FluidResource.of(...)` 完成 | 一步到位,与 dev/26.1 物品能力迁移方向一致;`IVaporConsumer` 接口若暴露流体类型给外部(AnvilCraft 大锅实现),需同步约定新类型 | **推荐**(理由:与 dev 侧既有迁移方向一致、无弃用债、类型不可变更安全) | - -> yukkuri 中实际触碰流体的点:`VaporizationManager` 的 `context.topFluid()`(`FluidStack`)与 -> `VaporizationSource.createOffer(...)` 的 `FluidStack available` 出入参;选择 B 后这些签名改为 `FluidResource` -> (或保留内部转换、接口面最小化—— **待验证**:与 AnvilCraft 大锅实现的对接契约,需在 dev 侧仓库外确认调用方期望)。迁移完整性核对方法:14 -> 个 Java 文件逐一 grep `FluidStack` / `IFluidHandler`,确认零残留后编译。 - -**(c) Capability 查询与注册** - -- `BlockCapability.createSided(Identifier, Class)` 在 26.1.2.76 **仍存在**(sources jar `BlockCapability.java` 第 119 - 行:`public static BlockCapability createSided(Identifier name, Class typeClass)` - ),仅标识符类型改为 `Identifier`——`YukkuriCapabilities.VAPOR_CONSUMER` 声明只需改 `ResourceLocation`→`Identifier`。 -- `VaporizationManager.findConsumer(...)` 中的 - `context.level().getCapability(YukkuriCapabilities.VAPOR_CONSUMER, context.outletPos(), Direction.DOWN)`: - `Level.getCapability` 在 26.1 的签名与上下文参数(BlockCapability / BlockPos / Direction) **待验证**——NeoForge sources - jar 不含 MC patched 源码(`world/level/Level.java` 不在 jar 内),验证方法:① 迁移后直接编译,以编译错误定位签名;② - `gradlew :module.yukkuri:compileJava` 通过后,在 `runData` / 运行环境用 IDE 反编译 `Level.getCapability` 确认语义(返回 - `@Nullable`、查询 side 传参顺序)。 - -#### 3.1.5 验收方式 - -1. **编译**:dev/26.1 分支执行 `gradlew :module.yukkuri:build` 通过(含 `compileJava`、`jar`、`sourcesJar`、`javadocJar`);若选 - B,构建日志 **无弃用 API 告警**(`IFluidHandler` / `FluidStack` 相关 deprecation 警告为零)。 -2. **产物**:`module.yukkuri/build/libs/` 生成 `anvillib-yukkuri-neoforge-26.1-.jar`;`unzip -l` 核对包含 14 - 个类文件、`META-INF/neoforge.mods.toml`(替换后变量正确)、README; **不含** mixin 配置引用。 -3. **聚合**:`gradlew :module.main:build` 通过,主库 jar 的 `META-INF/jarjar/` 内含`anvillib-yukkuri-neoforge-26.1-*.jar`。 -4. **运行时行为**(#1 特有验收):在 dev/26.1 运行 `gradlew :module.main:runServer`(或 gameTest),构造大锅 + 上方消费者环境,验证: - - `VaporizationManager.tick(ServerLevel, VaporizationCauldron)` 每服务器 tick 被调用且无异常; - - 汽化流程:源(`VaporizationSources`)→ 流体抽取 → `IVaporConsumer.receiveVapor`(SIMULATE 不改变状态)→ 执行态差额入账; - - 出口密封(`isOutletBlocked` / `sealsOutlet`)回压路径行为与 port 侧一致; - - `YukkuriCapabilities.VAPOR_CONSUMER` 在 `Direction.DOWN` 查询正确返回消费者。 - - 若 module.test 迁入后(#7),可在 dev 侧为 yukkuri 补一条 gameTest 冒烟( **可选,不在 #1 范围**)。 -5. **CI**:PR 流水线 `build-l0` 矩阵出现 `yukkuri` 且通过;`deploy-l0` 发布 - `dev.anvilcraft.lib:anvillib-yukkuri-neoforge-26.1` 成功(若在发布分支触发);`main` 构建不受影响。 -6. **git 比对**:`git diff port/1.21.1 dev/26.1 -- module.yukkuri` 在迁移完成后应只剩「预期差异」(API - 适配、构建脚本形态、坐标后缀),无意外缺漏。 - -#### 3.1.6 文件级适配核对表(14 个 Java 文件逐一核对) - -> 下表基于对 `port/1.21.1` 侧 14 个文件的逐文件 import / 用法扫描(2026-08-10)。执行时以此表为清单,按「已适配 / -> 无需适配」逐项打勾,勾满后进入编译验收。 - -| 文件 | API 触点(port 侧实测) | 26.1 适配内容 | 选项 B 附加影响 | -|--------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| -| `Yukkuri.java` | `ResourceLocation.fromNamespaceAndPath`(`of(String)` 工厂,1 处) | `ResourceLocation`→`Identifier`,`of` 返回类型同步改 | 无 | -| `api/event/LargeCauldronProcessEvent.java` | 无(纯事件 record,携带 context 与阶段) | 无 | 无 | -| `api/vapor/IVaporConsumer.java` | 无(vapor 层面接口:`receiveVapor(VaporStack, VaporAction, VaporizationContext)`,不触碰流体类型) | 无 | 无 | -| `api/vapor/VaporAction.java` | 无(`EXECUTE` / `SIMULATE` 枚举) | 无;语义与 26.1 Transaction 的 execute/simulate 对应(见下) | 语义映射:EXECUTE→commit,SIMULATE→不 commit / 嵌套回滚 | -| `api/vapor/VaporStack.java` | `record VaporStack(ResourceLocation type, int amount)` | `ResourceLocation`→`Identifier` | 无 | -| `api/vapor/VaporizationCauldron.java` | **大锅契约接口**:`FluidStack getTopVaporizationFluid()`、`FluidStack drainVaporizationFluid(FluidStack, IFluidHandler.FluidAction)`(import `IFluidHandler`) | 弃用 API 面最大的一处:接口暴露 `IFluidHandler.FluidAction`(随 `IFluidHandler` 弃用) | 接口签名重设计:`FluidResource` + `boolean simulate`(或 Transaction 语义),**对外契约变化,需 AnvilCraft 大锅实现同步(见 V-Y3)** | -| `api/vapor/VaporizationContext.java` | `FluidStack topFluid()`(第 25 行,封装大锅流体读取) | 保持 `FluidStack`(选 A)或改 `FluidResource`(选 B) | 签名改 `FluidResource`,调用方(Manager / Source)同步 | -| `api/vapor/VaporizationManager.java` | `FluidStack`(`processFirstSource` 的 `available` 等)+ `level.getCapability(...)`(`findConsumer`,第 40 行) | `Level.getCapability` 签名核对(见 3.1.4-c,V-Y1);流体类型随选项 | 核心改造文件:`topFluid()` / `createOffer` 出入参全链改 `FluidResource`;`fab551b` 收尾修复已包含在 port 侧最终版中,勿丢 | -| `api/vapor/VaporizationOffer.java` | `record VaporizationOffer(FluidStack input, VaporStack output)` | 随选项 | 组件类型改 `FluidResource` | -| `api/vapor/VaporizationSource.java` | `ResourceLocation id()` + `createOffer(VaporizationContext, FluidStack available, int maxAmount)` | `ResourceLocation`→`Identifier`;流体随选项 | `createOffer` 入参改 `FluidResource`(源实现方 API 变化) | -| `api/vapor/VaporizationSources.java` | `Map SOURCES`(注册表,`register` / `getSources`) | `ResourceLocation`→`Identifier` | 无 | -| `api/vapor/YukkuriCapabilities.java` | `BlockCapability.createSided(Yukkuri.of("vapor_consumer"), IVaporConsumer.class)` | 仅 `Identifier` 类型变化,**签名不变**(26.1 实测 `createSided(Identifier, Class)` 存在) | 无 | -| `api/vapor/YukkuriVaporTypes.java` | `GASEOUS_OIL` / `GASEOUS_WATER` 常量 + `isStandard(ResourceLocation)` | `ResourceLocation`→`Identifier`;标准 ID 字符串 `yukkuri:gaseous_oil` / `yukkuri:gaseous_water` **保持不变**(历史存档与配方兼容,见 port README 约定) | 无 | - -> 核对方法:迁移后执行 `grep -rn "ResourceLocation\|IFluidHandler\|FluidStack" module.yukkuri/src`——`ResourceLocation` 应为 -> 0 匹配;`IFluidHandler` / `FluidStack` 选 B 时应为 0 匹配(选 A 时允许存在但必须逐处加 `@SuppressWarnings("deprecation")` -> 并登记到待验证 V-Y2)。 - ---- - -### 3.2 迁移项 #2:`module.recipe`:`InWorldRecipe.matches()` 谓词栈回滚(低) - -#### 3.2.1 目标 - -把 `edd71ec`(#83)对 `InWorldRecipe.matches()` 的谓词栈回滚修复搬到 dev 侧同名同包文件,修复「匹配失败后未回滚谓词栈导致后续匹配串栈」的鱼缸加工 -bug。 - -#### 3.2.2 步骤 - -```bash -# 1) 查看 port 侧修复的完整 diff(已核实:+17/-5,单文件) -git show edd71ec -# 2) 查看 dev 侧现状(已核实:仍是修复前的旧逻辑——失败路径 context.getStack().clear(), -# 成功路径 getStack().forEach(predicate -> predicate.clearStack(context))) -git show dev/26.1:module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java -# 3) 在 dev/26.1 工作树按 diff 语义改 module.recipe/.../InWorldRecipe.java 的 matches(): -# - 进入 matches() 先记录 initialStackSize = context.getStack().size(); -# - 两处失败路径改调新增私有方法 rollbackPredicates(context, initialStackSize): -# while (stack.size() > initialStackSize) context.pop(stack.getLast()); -# - 成功路径改为仅清理由本次匹配新增的谓词: -# for (int i = initialStackSize; i < stack.size(); i++) stack.get(i).clearStack(context); -# - 文件末尾补换行(port 侧修复顺带修掉了 "\\ No newline at end of file") -``` - -**API 适配点**:无。两侧 `InWorldRecipe.java` 同名同包(`dev.anvilcraft.lib.v2.recipe`),`InWorldRecipeContext` / -`IRecipePredicate` / `ShapelessMatcher` 接口在 dev 侧均存在且语义一致;纯逻辑搬运,仅需确认 dev 侧 `context.pop(...)` 与 -`getStack().getLast()` 可用(`List.getLast()` 为 JDK 21+,dev 侧 Java 25 工具链无问题;`pop` 方法是否存在 **待验证** -,验证方法:改完直接 `gradlew :module.recipe:compileJava`,如无 `pop` 则对照 dev 侧 `InWorldRecipeContext` 的栈操作 API -等价实现)。 - -#### 3.2.3 验收方式 - -1. `gradlew :module.recipe:compileJava` 通过。 -2. 行为核对:对照 port 侧 `edd71ec` 的 diff 逐行比对 dev 侧改动一致(`git diff` 语义等价)。 -3. 回归:dev 侧 `module.test` 补一条鱼缸加工场景回归用例( **待验证**:dev 侧 module.test 是否已有 InWorldRecipe - 相关测试设施;若有则扩展,若无则按 #7 方式新建最小用例),验证:匹配失败后 `context.getStack()` - 尺寸回到初始值、后续配方可正常匹配;成功后仅新增谓词被清理。 -4. `gradlew :module.recipe:test`(若有测试)通过。 - ---- - -### 3.3 迁移项 #3:`module.recipe`:`ItemResourceHandlerCacheElement.sync()` 短路/差量写入(中) - -#### 3.3.1 目标 - -把 `e78d020`(#84)对缓存元素 `sync()` 的「无谓写入短路 + 差量写入」逻辑迁移到 dev 侧已资源处理器化的 -`ItemResourceHandlerCacheElement.sync()`,避免每次同步都执行清空后重写的全量操作。 - -#### 3.3.2 步骤 - -```bash -# 1) 查看 port 侧修复的完整 diff(已核实:+20,单文件 ItemHandlerCacheElement.java) -git show e78d020 -# 2) 查看 dev 侧现状(已核实:ItemResourceHandlerCacheElement.java,sync() 为无条件 -# extract-all + insert simulate,Transaction 包裹;无短路/差量) -git show dev/26.1:module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/ItemResourceHandlerCacheElement.java -# 3) 在 dev/26.1 工作树改写 sync(),语义对照 port 侧: -# 短路:getResource(slot) 与 simulate 转换后的 ItemStack 完全一致(数量 + 组件)→ 直接 return; -# 差量:isSameItemSameComponents 但数量不同 → 仅 extract/insert 差额; -# 全量替换:类型不同 → extract 全部后 insert simulate(即 dev 现有逻辑兜底)。 -``` - -**API 适配点**(旧 → 新): - -| port 侧(1.21.1) | dev 侧(26.1) | -|------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| -| `IItemHandler.getStackInSlot(slot)` | `ResourceHandler.getResource(slot)`(空时返回 `ItemResource.EMPTY`,需 `toStack()` 转 `ItemStack` 做比较) | -| `IItemHandlerModifiable.setStackInSlot(...)`(快捷整槽替换) | `ResourceHandler` **无 setResource**,此快捷路径不存在;统一走「extract 全量 + insert 目标」的 Transaction 原子替换(dev 现状即此模式) | -| `IItemHandler.insertItem(slot, stack, false)` / `extractItem(slot, amount, false)` | `insert(slot, resource, amount, transaction)` / `extract(slot, resource, amount, transaction)`,`Transaction.openRoot()` + `transaction.commit()` | -| `ItemStack.isSameItemSameComponents(a, b)` | 26.1 仍存在(`ItemStack` 静态方法,**待验证**:以编译为准;若移除则用 `ItemResource` 等价比较或 `ItemStack` 组件比对) | -| `stack.copyWithCount(n)` | 26.1 `ItemStack.copyWithCount` 仍存在(**待验证**,同上) | - -> 注意 dev 侧类内已有私有辅助 `extract(ResourceHandler, int)`(`ItemResource` → `ItemStack` -> 转换),差量实现可复用该模式;短路比较建议在 `Transaction` 开启 **之前**完成(只读 `getResource`,无需事务),确认需写入时再开事务。 - -#### 3.3.3 验收方式 - -1. `gradlew :module.recipe:compileJava` 通过。 -2. 单元/集成测试(dev 侧 module.recipe 或 module.test):构造 `ItemResourceHandlerCacheElement`,覆盖四类输入—— (a) 槽位与 - simulate 完全一致(期望:零写入调用); (b) 同类型差量(期望:仅 extract/insert 差额,不触碰多余数量); (c) 不同类型(期望:全量替换); - (d) 槽位空 / simulate 空(期望:与 port 侧行为一致)。 -3. 写入次数可观测:在测试中包裹 `ResourceHandler` 实现并计数 `extract` / `insert` 调用,断言短路场景为 0 次、差量场景各 1 - 次(避免空转写入路径回归)。 -4. 行为回归:鱼缸加工(#83/#84 同源场景)在 dev 侧缓存流程中物品数量与组件与 simulate 一致。 - ---- - -### 3.4 迁移项 #4:`module.util`:`ClientTickRecorder` 修复(低) - -#### 3.4.1 目标 - -把 `9874db7`(+9/-2)的两处修复搬到 dev 侧 `ClientTickRecorder`:主菜单(level 为 null)不累计 tick;退出世界时 tick 归零。 - -#### 3.4.2 步骤 - -```bash -# 1) 查看 port 侧修复 diff(已核实:onTick 加 level != null 判断 + 新增 LoggingOut 事件归零) -git show 9874db7 -# 2) 查看 dev 侧现状(已核实:@EventBusSubscriber(modid = AnvilLibUtil.MOD_ID, value = Dist.CLIENT), -# onTick 仅 isPaused() 判断,无判空、无归零) -git show dev/26.1:module.util/src/main/java/dev/anvilcraft/lib/v2/util/ClientTickRecorder.java -# 3) 在 dev/26.1 工作树修改: -# - onTick 判断改为:if (Minecraft.getInstance().level != null && !Minecraft.getInstance().isPaused()) -# - 新增 @SubscribeEvent onClientExit(ClientPlayerNetworkEvent.LoggingOut e) { ticks = 0; } -# - 新增 import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent -``` - -**API 适配点**: - -| 项 | port 侧(9874db7 后) | dev 侧应保留 | -|--------|--------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 类注解 | `@EventBusSubscriber(value = Dist.CLIENT)`(无 modid) | **保留 dev 现状** `@EventBusSubscriber(modid = AnvilLibUtil.MOD_ID, value = Dist.CLIENT)`——dev 侧 `AnvilLibUtil.MOD_ID` 存在,带 modid 更精确(只接收本 mod 事件总线),**不搬** port 侧无 modid 写法 | -| 事件类 | `ClientPlayerNetworkEvent.LoggingOut` | 26.1 同包同类仍存在(**待验证**:以编译为准;若改名则对照 dev 侧 `ClientPlayerNetworkEvent` 现状) | - -> 搬逻辑(判空 + 归零)而不搬注解写法;`ClientPlayerNetworkEvent.LoggingOut` 在 26.1 客户端事件体系的可用性以 -> `gradlew :module.util:compileJava` 验证。 - -#### 3.4.3 验收方式 - -1. `gradlew :module.util:compileJava` 通过。 -2. 运行验收(`gradlew :module.main:runClient`):主菜单停留 N 秒 → `/time` 或调试输出确认 `ClientTickRecorder.ticks` - 不增长;进入世界 → 正常累计(每 24 小时取模重置不变);退出世界回主菜单 → 归零。 -3. 源码核对:`git diff` 语义与 `9874db7` 等价(除保留 dev 侧 modid 写法外无其他差异)。 - ---- - -### 3.5 迁移项 #5:`module.util`:`BlockStatePredicate.Builder.with(BlockState)`(低-中) - -#### 3.5.1 目标 - -在 dev 侧 `BlockStatePredicate.Builder` 恢复 `with(BlockState)` 便捷重载(#6 的代码级前置),为动态多方块提供「直接以方块状态描述谓词」的入口。 - -#### 3.5.2 步骤 - -```bash -# 1) 查看 port 侧实现(已核实:port 侧 Builder 有 5 个 with 重载,含 with(BlockState); -# dev 侧有 4 个(String/int/boolean/泛型 T),缺 with(BlockState)) -git show port/1.21.1:module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java -git show dev/26.1:module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java -# 2) 在 dev/26.1 工作树 BlockStatePredicate.Builder 内、现有 with(Property, String) 之前 -# (或其他合适位置)新增: -# public Builder with(BlockState state) { -# for (Property> property : state.getProperties()) { -# this.with(property, state.getValue(property)); // 走 dev 侧泛型重载,见适配点 -# } -# return this; -# } -``` - -**API 适配点**(旧 → 新): - -- port 侧实现为 `this.with(property, property.getName(Util.cast(state.getValue(property))))`——`Property.getName(T)` 在 - 26.1 已移除,且依赖 `Util.cast` 辅助。 -- dev 侧已有泛型重载 `public > Builder with(Property property, T value)`,其内部处理 - `StringRepresentable → getSerializedName()`、其余 `String.valueOf(value)`——与 1.21.1 的 `Property.getName` 行为 - **完全等价**(`getName` 的 1.21.1 实现即为 - `value instanceof StringRepresentable ? ((StringRepresentable) value).getSerializedName() : String.valueOf(value)`)。 -- 因此新实现直接调用 `this.with(property, state.getValue(property))`(泛型推断 `T` 为属性值类型), **不**使用 - `property.getName`,也不引入 `Util.cast`(dev 侧 `Util` 类是否保留该辅助 **待验证**,但新实现无需它)。 -- 泛型细节:`state.getProperties()` 返回 `Collection>>`,`state.getValue(property)`可推断为对应 - `T`;若编译器因通配符无法推断,可照 dev 侧泛型重载签名做强转(`@SuppressWarnings` 或 `Util.cast`,以编译为准)。 **待验证** - :通配符泛型调用的编译通过性(方法签名 `with(Property, T)` 的 `T` 需绑定到 `Comparable` 的实际类型,Dev 侧如遇编译错误,参照 - port 侧同文件其余泛型写法的强转模式解决)。 - -#### 3.5.3 验收方式 - -1. `gradlew :module.util:compileJava` 通过。 -2. 行为测试:`BlockStatePredicate.builder().with(Blocks.IRON_DOOR.defaultBlockState())` - 构造谓词,对同状态与改属性后的状态分别断言匹配/不匹配(属性级精确匹配);含 `StringRepresentable` 属性(如 - `FurnaceBlock.FACING`)与枚举/整型属性各一例。 -3. 与 #6 联动:若 #6 判定放弃(见 3.6.3),本项 **一并放弃**(回滚本项改动);决策前本项保持 pending 状态。 - ---- - -### 3.6 迁移项 #6:`module.multiblock`:`MultiblockDefinition` 的 16 个 `BlockState` 便捷重载(中 · 可选) - -#### 3.6.1 目标 - -在 dev 侧 `MultiblockDefinition` 恢复 port 侧 `5fc6f65` 引入的 16 个便捷重载(`add` / `addController` 各 8 个,其中 8 个直接以 -`BlockState` 入参),使动态多方块定义可用「具体方块状态」而非仅「方块类型」描述; **可选**——若判定 dev 侧删除为有意 API -精简则整体放弃(见 3.6.3)。 - -#### 3.6.2 步骤 - -```bash -# 1) 对比两侧 Builder 方法集(已核实): -# port 侧:add ×8(predicate/block/BlockState/block+BlockState/tag/block+tag/BlockState+tag/block+BlockState+tag) -# + addController ×8(同构) -# dev 侧:add ×4(predicate/block/tag/block+tag)+ addController ×4(同构)——缺全部 BlockState 入参变体 -git show port/1.21.1:module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/definition/MultiblockDefinition.java -git show dev/26.1:module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/definition/MultiblockDefinition.java -# 2) 在 dev/26.1 工作树 MultiblockDefinition.java 的 Builder 内补 8 个重载: -# add(Vec3i, BlockState) / add(Vec3i, Block, BlockState) / add(Vec3i, BlockState, CompoundTag) / -# add(Vec3i, Block, BlockState, CompoundTag) 与 addController 对应 4 个; -# 实现均为委托 BlockStatePredicate.builder().with(state)(依赖 #5),如: -# public Builder add(Vec3i localPos, BlockState state) { -# return this.add(localPos, BlockStatePredicate.builder().with(state)); -# } -# 3) 同步核对 dev 侧 SeriaBuilder(map/mapController)是否需要 BlockState 变体: -# port 侧 SeriaBuilder 无 BlockState 变体(已核实 map ×4 / mapController ×4 均无), -# 故 dev 侧 SeriaBuilder 保持现状,不动。 -``` - -**API 适配点**: - -- 依赖 #5 的 `with(BlockState)`;若 #5 按新实现落地,本项重载编译即通。 -- dev 侧 `MultiblockDefinition` 其余结构(record 定义、`toGlobal`、`isController`、SeriaBuilder)与 port 侧一致(已核实),无其他 - 26.1 API 适配点。 -- `add(Vec3i, Block, BlockState)` 语义与 port 一致:`BlockStatePredicate.builder().of(block).with(state)`(先限定方块再限定属性)。 - -#### 3.6.3 联动决策(执行前置) - -| 判据 | 决策 | -|----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------| -| dev 侧删除 `BlockState` 重载是**无意的迁移遗漏**(`7019b38` 移植时改写掉了) | 迁:本项 + #5 一起落地,测试组(#7)覆盖该路径 | -| dev 侧删除是**有意的 API 精简**(对齐 `@ApiStatus.Internal` 化趋势,见 `c72aa30` #93;或维护者认为只暴露 `block` 级 API 更干净) | **放弃整项**:保留 dev 现状,不引入 port 侧便捷层,#5 一并放弃 | - -> 该决策需在迁移执行前与 dev 侧维护者确认(本计划不替维护者做决定);确认前 #5/#6 保持 pending。 - -#### 3.6.4 验收方式(若迁) - -1. `gradlew :module.multiblock:compileJava` 通过。 -2. 行为测试:以 `Blocks.IRON_DOOR.defaultBlockState()` 等含属性状态构造 - `MultiblockDefinition.builder().addController(state)`,与 `BlockStatePredicate` 匹配断言一致(#7 测试组覆盖成形/解体路径)。 -3. 无行为回归:dev 侧现有 `MultiblockDefinition` 用例(如有)不受影响。 - ---- - -### 3.7 迁移项 #7:`module.test`:multiblock 动态系统测试组(中-高) - -#### 3.7.1 目标 - -把 port 侧独有的动态多方块测试组(`TestControllerBlock` + `LibBlocks` / `LibItemGroups` / `LibMultiblockControllers` / -`LibMultiblocks` + datagen 注册)迁入 dev 侧 `module.test`,按 dev 侧 26.1 -新结构落位,为动态多方块系统提供游戏内可运行验证(成形/解体行为 + 定义数据生成)。 - -#### 3.7.2 步骤 - -```bash -# 1) 查看 port 侧测试组文件(已核实清单): -# module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/block/TestControllerBlock.java -# module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/init/LibBlocks.java -# module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/init/LibItemGroups.java -# module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/init/LibMultiblockControllers.java -# module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/init/LibMultiblocks.java -# module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTestDatagen.java(datagen 注册入口) -# datagen 产物:src/generated/resources/data/anvillib_test/anvillib/definitions/{simple,complicated,waht}.json -# 2) 查看 dev 侧 module.test 新结构(已核实):all/{TestBlocks,TestItems,TestItemGroups,TestTiles}、 -# data/TestLangGenerator、AnvilLibTest(@Mod(AnvilLibTest.MOD_ID) 无 ModContainer 参数; -# REGISTRUM.addDataGenerator(ProviderType.LANG, ...) 替代 GatherDataEvent) -git ls-tree -r --name-only dev/26.1 -- module.test -# 3) 落位决策:新建包 module.test/src/main/java/dev/anvilcraft/lib/v2/test/multiblock/(与 port 同构), -# 或并入 dev 侧 all 包(待维护者偏好);以下按「新建 multiblock 包」给出,代码语义与 dev all 包一致 -# 4) 适配并落位(各文件适配点见 3.7.3),在 AnvilLibTest 构造器中注册: -# LibItemGroups.setupRegistration(); LibBlocks.setupRegistration(); LibMultiblockControllers.setupRegistration(); -# 并在 setupDataGeneration() 中注册定义数据生成(见适配点 d) -# 5) datagen 产物:dev 侧产物由 runData 生成(module.test 的 gradle.properties 需开 -# anvillib.needRunConfig.data=true,参照 dev 侧已有模块),不手工提交 port 侧 JSON; -# 生成后核对 simple/complicated/waht 三个定义文件内容 -``` - -**API 适配点**(旧 → 新): - -| 点 | port 侧 | dev 侧(26.1) | -|-------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| (a) `@Mod` 构造器 | `AnvilLibTest(ModContainer container, IEventBus bus)`(port 侧现状) | `AnvilLibTest(IEventBus modBus)`——26.1 已去掉 ModContainer 参数(dev `AnvilLibTest` 现状即此签名) | -| (b) datagen 注册 | `@EventBusSubscriber` + `GatherDataEvent`(`AnvilLibTestDatagen.gatherData` 空壳)+ `REGISTRUM.getDataGenInitializer().add(LibRegistries.DEFINITIONS_KEY, LibMultiblocks::bootstrap)` | `REGISTRUM.addDataGenerator(...)` 体系(dev 现状);`DataProviderInitializer` 在 dev 侧仍存在(`AbstractRegistrum.getDataGenInitializer()` 第 656 行)且 `RegistrumDatapackProvider` 支持 datapack registry(`getDatapackRegistryProviders()`)——`DEFINITIONS_KEY` 的 datapack 数据生成落位**待验证**(add 方法签名与 port 是否一致,以 dev 侧 registrum 源码 + 编译为准) | -| (c) `ResourceLocation` | `TestControllerBlock.getDefinitionId()` 返回 `ResourceLocation`;`LibMultiblocks` 的 `ResourceKey` 构造 | 全部 `Identifier`;`LibMultiblocks.key(...)` 的 `Identifier.fromNamespaceAndPath` 同步 | -| (d) 注册链式 API | `LibBlocks`:`REGISTRUM.block(...).blockstate((ctx, provider) -> {}).item().model((ctx, provider) -> {}).build().register()`;`LibItemGroups`:`DeferredRegister` + `REGISTRUM.addLang` | dev 侧 `TestBlocks` 用 `.properties(p -> ...).blockstate(() -> ...).simpleItem().register()`、`TestItemGroups` 用 `REGISTRUM.creativeTab("test_tab", () -> ...)`——**待验证**:dev 侧 `BlockEntry` builder 是否保留 `.blockstate(...)` / `.item()` / `.model()` / `.build()` 形态(对照 `module.registrum` 源码;若无则按 dev 现有链式 API 改写语义) | -| (e) `IController` / `ControllerRecord` / `SimpleController` | `onFormed` / `onUnformed` 回调 | dev 侧 multiblock 已演进:`1e5bfb3`(#69)新增动态多方块事件、`22091d4` 状态管理改 `BlockPos`——`MultiblockState.getControllerPos()` 等接口**待验证**(以 dev 侧 `MultiblockState` / `IController` 现状为准微调测试回调体) | -| (f) 定义构造 | `MultiblockDefinition.builder().addController(Blocks.FURNACE).add(new Vec3i(0,1,0), Blocks.IRON_BLOCK)` | dev 侧 Builder 基础形态一致;若使用 `BlockState` 重载则依赖 #6 | - -> 包内 `package-info.java`(`@NullMarked` 风格)按 dev 侧模块惯例补充(dev 侧 26.1 已统一 jspecify `@NullMarked`,见 -> `3bf9b6c`/`f94126f` 演进)。 - -#### 3.7.3 验收方式 - -1. `gradlew :module.test:compileJava` 通过。 -2. `gradlew :module.test:runData`(需开 `anvillib.needRunConfig.data=true`)生成产物,核对: - - `data/anvillib_test/anvillib/definitions/simple.json`(熔炉控制器 + 铁块)、`complicated.json`(TestControllerBlock - 控制器 + 铁块)、`waht.json`(白床 + 分层 A/0)与 port 侧产物语义一致(序列化格式以 dev 侧 `DefinitionSerialization` - 为准); - - lang 产物(en_us / en_ud)含新增条目(若有)。 -3. 运行验收(`gradlew :module.test:runServer` 或 gameTest):进世界摆放 SIMPLE / COMPLICATED / WAHT 结构: - - 成形:控制器方块被识别,`onFormed` 触发(如铁锭弹出 64 个); - - 解体:破坏结构,`onUnformed` 触发(铁栏杆弹出); - - 控制器方块(TestControllerBlock)本身可放置、可查询 `getDefinitionId`。 -4. 覆盖联动:测试组运行路径覆盖 #2/#6 涉及的 `MultiblockDefinition` 便捷重载与谓词匹配逻辑。 - ---- - -### 3.8 迁移项 #8:`module.main`:CurseForge 环境两行(低) - -#### 3.8.1 目标 - -在 dev 侧 `module.main/build.gradle` 的 `publishMods.curseforge` 块补充 `clientRequired = true` /`serverRequired = true` -两行环境声明(port 侧 `1311782` #90 已修复的发布元数据问题)。 - -#### 3.8.2 步骤 - -```bash -# 1) 查看 port 侧改动(已核实:publishMods.curseforge 块内 +2 行) -git show 1311782 -# 2) 查看 dev 侧 publishMods 块(已核实:dev 侧结构与 port 同构—— -# file/changelog/type/modLoaders/additionalFiles/curseforge{accessToken,projectId,minecraftVersions}/modrinth) -git show dev/26.1:module.main/build.gradle -# 3) 在 dev/26.1 工作树 module.main/build.gradle 的 curseforge 块内 -# minecraftVersions.add(libs.versions.minecraft.get()) 之后追加两行: -# clientRequired = true -# serverRequired = true -``` - -**API 适配点**: - -- 无 API 适配;`publishMods` 扩展属性(mod-publish-plugin 1.1.0,与 port 侧同版本,见 `gradle/libs.versions.toml`)在 dev - 侧可用。 -- **不要**把 port 侧 `jarJar(api ...)` 批量写法或任何其他 port 侧 `module.main/build.gradle` 内容搬回(dev 侧保持现状)。 -- **待验证**:mod-publish 的 `curseforge` 块中 `clientRequired` / `serverRequired` 属性在 1.1.0 版本的确切类型(boolean), - `gradlew :module.main:compileGroovy` / 配置阶段即报错可发现;另注意 dev 侧是否已在其他发布块声明过环境(已核对 `0b7e45f` - 无)。 - -#### 3.8.3 验收方式 - -1. `gradlew :module.main:build` 配置阶段通过(publishMods 扩展解析无误)。 -2. 发布 dry-run:`gradlew :module.main:publishMods`(或对应 dry-run 任务)在无 token 环境下不报扩展错误;CI - 发布分支实际发布后,CurseForge 元数据含「客户端必需 / 服务端必需」声明( **待验证**:需一次真实发布核对页面元数据,或核对 - mod-publish 输出日志)。 -3. `git diff` 与 `1311782` 语义一致(两行,无多余改动)。 - ---- - -## 4. 构建 / CI 接入点汇总表 - -> 仅 #1 与 #8 涉及工程接入;#2–#7 均为模块内源码改动,不触碰下列文件。所有改动均按 dev/26.1 现状结构落位,不回迁 port 侧对应文件。 - -| 文件 | 回迁项 | 改动内容 | 对照来源 | -|-------------------------------------------------------------------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------| -| `settings.gradle` | #1 | `include 'module.yukkuri'` + `project(':module.yukkuri').name = 'anvillib-yukkuri-neoforge-26.1'`(dev 侧坐标后缀) | port `569e01d`(坐标后缀改 `-neoforge-26.1`) | -| `module.yukkuri/build.gradle`(新建) | #1 | 按 dev 侧精简形态:`dependencies {}` 块即可,其余由根级 `module.gradle` 聚合(`subprojects { apply from: rootProject.file("module.gradle") }`)提供 | dev 侧 `module.util/build.gradle` 形态;**非** port 侧 270 行独立脚本 | -| `module.yukkuri/gradle.properties`(新建) | #1 | `mod_id=anvillib_yukkuri` / `mod_name=AnvilLib-Yukkuri` / `mod_description=...`;不设 `anvillib.needRunConfig*`(无 datagen/gametest 需求) | dev 侧 `module.util/gradle.properties` | -| `module.yukkuri/src/main/resources/META-INF/neoforge.mods.toml`(新建) | #1 | dev 模板落位;删 `[[mixins]]` 块(无 mixin);`[[dependencies]]` 保留 neoforge / minecraft 两条 | dev 侧 `module.util` 模板;port `569e01d` 内容底稿 | -| `module.main/build.gradle` | #1 | `dependencies` 块 NOT_DEV 双分支各加一行:`jarJar(api("dev.anvilcraft.lib:anvillib-yukkuri-neoforge-26.1:latest.release"))` / `jarJar(implementation project(":anvillib-yukkuri-neoforge-26.1"))` | dev 侧同块既有 19 模块写法;port `569e01d` 对应改动 | -| `module.main/build.gradle` | #8 | `publishMods.curseforge` 块 `minecraftVersions.add(...)` 后加 `clientRequired = true` / `serverRequired = true` 两行 | port `1311782` | -| `.github/modules.json` | #1 | 增加 `{ "module": "yukkuri", "needs": [] }`(yukkuri 无模块间依赖 → Level 0) | port `18b29ca` 的 CI 意图(dev 侧机制为矩阵拓扑,等价实现) | -| `.github/workflows/ci.yml` | #1 | **无需改动**:`generate-matrix.js` 自动派生 `module_id=anvillib-yukkuri`、`mod_id=anvillib_yukkuri`、`GRADLE_PROJECT=anvillib-yukkuri-neoforge-26.1`、`MODULE_DIR=module.yukkuri`;`main` job 的 `needs: [build-l2]` 传递覆盖 Level 0 | 对比 port `18b29ca`(手工加 job + 改 main needs) | -| `.github/workflows/build_and_test.yml` / `publish_maven_central.yml` | #1 | **无需改动**(参数默认值即匹配 yukkuri 命名) | — | -| `README.md` / `README.en.md` | #1 | 各 4 处:模块功能表 + 聚合模块清单 + 两处依赖坐标示例(坐标后缀 `-neoforge-26.1`) | port `569e01d` | -| `module.yukkuri/README.md`(新建) | #1 | port 侧版本 + 聚合坐标改为 dev 侧 `-neoforge-26.1` | port `569e01d` | -| `module.test/gradle.properties` | #7 | 若采用 runData 验证,加 `anvillib.needRunConfig.data=true`(参照 dev 侧 `module.explosion` 等);验证后可保留(module.test 属开发模块) | dev 侧现有模块写法 | - ---- - -## 5. 不做清单与原因 - -> 以下内容 **明确不回迁**。理由均基于两侧 diff 分类结论(详见 `docs/branch-comparison.md` 第 5 章),执行时不得以「顺手」「保险」为由扩大范围。 - -| # | 不回迁内容 | 原因(依据) | -|-----|------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| N1 | `module.codec` / `module.network` / `module.config` / `module.integration` / `module.registrum` / `module.wheel` | dev 侧为超集或已独立演进:port 侧相关提交(`0b56109`/`50aeac7`/`6a0e8a9`、网络库系列、wheel 系列)均有 dev 同主题对应(`3b5dfea`/`2e1b537`/`922d523`、`8d8ac8c` 系列、`0d73ab0`/`1ae3411`),逐文件 diff 无 port 独有内容 | -| N2 | `module.moveable-entity-block`(含 port `1acdcf8` PoseStack 修复) | dev 侧以 26.1 渲染架构**同源重写**(`83477ab`/`b5b27aa`/`2d4cd84`);port 侧修复的「PoseStack 提前 return 未清空」问题在新渲染架构下天然规避,搬运旧修复反而可能破坏新架构 | -| N3 | port 侧各模块独立构建脚本与 `gradle.properties`(含 yukkuri 的 270 行 build.gradle) | 已被 dev 侧 `module.gradle` 聚合 + `gradle/libs.versions.toml` 版本目录取代(`ea4301f`/`16b7bb5`);回迁即倒退,且两套构建体系并存会产生维护分歧 | -| N4 | lang datagen 产物(`zh_cn.json` / `en_us.json` 等已生成 JSON) | dev 侧统一由 datagen 生成(`runData` 产出,块物品自行提供 lang 的工作流),直接提交产物与 dev 工作流冲突;需要语言内容时应以 datagen 代码回迁而非产物回迁(#7 即按此执行) | -| N5 | `module.multiblock/ASYNC_MULTIBLOCK_CHECK_PLAN.md` | 文档已标注「已完成实现」;dev 侧异步检查实现等价且更完善(含未加载区块快照修复 `d888714` #73),无迁移价值 | -| N6 | `18b29ca` 的 CI 片段本身 | CI 体系 dev 侧已整体重构(`aa59e37` 模块化矩阵 + `7f27908` 同步工作流),port 侧手工 job 片段不适用;#1 已按 dev 侧 `modules.json` 机制等价落位(见 3.1.3 阶段 E) | -| N7 | dev 侧特有演进(不回迁方向) | 动态多方块事件 `1e5bfb3`(#69)、未加载区块快照修复 `d888714`(#73)、`@NullMarked` 注解统一 `3bf9b6c`/`f94126f`、`module.gradle` 构建体系 `ea4301f`、`@ApiStatus.Internal` `c72aa30`(#93)——均为 dev 独有演进,port 侧不反向吸收 | -| N8 | port 侧 `module.test` 的 mixin 与 wheel 测试(`ChestBlockMixin`、`wheel/` 包) | dev 侧 module.test 已有 26.1 对应结构(`all/` 包、独立 wheel 相关演进),port 侧这些文件是旧平台形态;#7 只迁 multiblock 测试组 | -| N9 | `module.util` 其余 port 提交(`9d61997`/`c8ee900`/`0efcd3d`/`95bd5ca`) | dev 侧已有等价或超集实现(分维度/玩家名工具、WeightedChanceBlockStates、VoxelShape 裁剪、UtilityClass 注解化等,`7019b38` 移植后继续演进) | -| N10 | port 侧 `module.recipe` 其余内容 | dev 侧为演进主线(`2033fd9` 资源处理器缓存、`0d0649f` 配方代码优化等);仅 #2/#3 两个 bugfix 为 port 独有 | - -> 执行纪律:出现「N 项之外是否遗漏」的疑问时,回到 `git log --oneline dev/26.1..port/1.21.1` 与 -> `git diff port/1.21.1 dev/26.1 -- <模块>` 复核,确认有 port 独有内容再立项,不得凭印象扩大范围。 - ---- - -## 6. 建议执行顺序 - -> 总原则: **先大后小、先依赖后使用、独立项先验证**。每步完成即验收(第 3 章对应小节),全部通过再进入下一步;每步在 dev/26.1 -> 分支以独立提交落地(提交信息建议按 dev 侧现有风格:`feat(yukkuri): ...` / `fix(recipe): ...` 等,具体按仓库约定)。 - -### 第 1 步:yukkuri(#1)——最大项与最大风险项 - -先做 #1 的原因:唯一整模块回迁,涉及流体 API 适配(26.1 最大不确定点)与四处工程联动;尽早暴露风险,后续步骤都在已稳定的聚合构建上叠加。 - -```bash -# 验证命令(在 dev/26.1 分支) -gradlew :module.yukkuri:build # 模块编译 + 打包(验收见 3.1.5-1/2) -gradlew :module.main:build # 聚合验证(验收见 3.1.5-3) -node .github/workflows/generate-matrix.js # 本地核对 CI 矩阵含 yukkuri(需 GITHUB_WORKSPACE/GITHUB_OUTPUT 环境,见 3.1.3-E) -git diff port/1.21.1 dev/26.1 -- module.yukkuri # 只读复核:只剩预期差异 -``` - -### 第 2 步:recipe(#2 → #3)——纯模块内 bugfix - -两个 bugfix 同模块同主题(鱼缸加工),一起做;#2 无 API 风险先落,再落 #3(涉及资源处理器 API)。 - -```bash -git diff port/1.21.1 dev/26.1 -- module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java # #2 落位后核对 -gradlew :module.recipe:compileJava # #2 验收(3.2.3-1) -gradlew :module.recipe:compileJava # #3 验收(3.3.3-1);行为用例见 3.3.3-2/3 -``` - -### 第 3 步:util(#4 → #5)——小修复 + API 补充 - -#4 独立小改先落;#5 与 #6 联动,本步只做代码与编译验证, **不**合并 #6 决策(见 3.6.3)。 - -```bash -gradlew :module.util:compileJava # #4 / #5 验收(3.4.3-1、3.5.3-1) -gradlew :module.main:runClient # #4 运行验收(3.4.3-2) -# #5 行为断言(3.5.3-2)可在 dev 侧 module.util 补一个测试方法或临时 main 验证后移除 -``` - -### 第 4 步:multiblock(#6)——决策先行 - -先与 dev 侧维护者确认 3.6.3 的决策点:确认「迁」→ 落地 16 个重载(#5 生效);确认「不迁」→ 回滚 #5 并跳过本步与 #7 的 BlockState -依赖部分。 - -```bash -gradlew :module.multiblock:compileJava # #6 验收(3.6.4-1) -``` - -### 第 5 步:test(#7)——测试组落位 - -依赖 #6 的便捷重载(若 #6 放弃,本步定义改用 `block` 级重载构造,测试面收窄,需在验收中注明)。 - -```bash -gradlew :module.test:compileJava # #7 验收(3.7.3-1) -gradlew :module.test:runData # datagen 产物核对(3.7.3-2,需开 anvillib.needRunConfig.data=true) -gradlew :module.test:runServer # 成形/解体行为验证(3.7.3-3) -``` - -### 第 6 步:main(#8)——收尾 - -独立两行,作为回迁批次最后一个提交。 - -```bash -gradlew :module.main:build # 配置阶段验收(3.8.3-1) -``` - -### 顺序之外的两条纪律 - -1. 每步提交前先跑一遍该模块 `build`(含 `jar`),避免只验 `compileJava` 漏掉资源处理(mods.toml 替换、jarjar 内嵌)。 -2. #1 与 #7 涉及 `runData` / 运行类验证,如本地环境不便跑客户端,可先以编译 + CI 为准,把运行验收登记为「待 CI/人工补验」并给出复现步骤, - **不得**把未验证项标记为已完成(见第 7 章验收总表)。 - ---- - -## 7. 总体验收标准 - -### 7.1 构建验收(硬性门槛) - -| # | 验收项 | 命令 / 判据 | 覆盖回迁项 | -|----|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|------------| -| B1 | dev/26.1 全量构建 | 在 dev/26.1 分支 `gradlew build`(根级,聚合全部模块含新 yukkuri)**全部成功** | 全部 | -| B2 | 模块独立构建 | 依次 `gradlew :module.yukkuri:build` `:module.recipe:build` `:module.util:build` `:module.multiblock:build` `:module.test:build` `:module.main:build` 均通过 | #1–#8 | -| B3 | 弃用告警检查(仅 #1 选 B 时) | `gradlew :module.yukkuri:compileJava` 日志中 `IFluidHandler` / `FluidStack` 相关 deprecation 警告为 0 | #1 | -| B4 | 聚合内嵌 | `module.main` 产物 jarjar 目录含 `anvillib-yukkuri-neoforge-26.1-*.jar` | #1 | -| B5 | CI 通过 | 回迁 PR 的 GitHub Actions 全绿:`build-l0`(含 yukkuri)、`build-l1/l2`、`main`(含 gametest) | #1、全部 | - -### 7.2 功能验收清单(行为核对) - -| # | 验收项 | 判据 | 执行方式 | -|----|----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------| -| F1 | 油库 API 行为(#1 特有验收) | `VaporizationManager.tick(ServerLevel, VaporizationCauldron)` 在服务器 tick 正常驱动汽化流程:源发现(按 priority 降序)→ 顶层流体抽取 → `IVaporConsumer.receiveVapor`(SIMULATE 不改变状态;执行态按模拟接受量入账)→ 出口密封回压(`isOutletBlocked` / `sealsOutlet`)路径正确;`VAPOR_CONSUMER` 以 `Direction.DOWN` 在出口方块可查到;全程无异常日志 | dev/26.1 运行环境 + 模拟消费者;细节见 3.1.5-4 | -| F2 | 鱼缸加工(#2) | 失败匹配后谓词栈回滚至初始尺寸,后续配方可正常匹配;成功路径仅清理本次新增谓词;`context.getStack()` 无残留 | module.test 回归用例(3.2.3-3) | -| F3 | 缓存同步(#3) | 完全一致场景零写入;同类型差量仅 extract/insert 差额;不同类型全量替换;空栈场景与 port 行为一致 | 计数包装的 ResourceHandler 测试(3.3.3-2/3) | -| F4 | ClientTickRecorder(#4) | 主菜单 tick 不增长;进世界累计;退出世界归零 | runClient 手工验证(3.4.3-2) | -| F5 | BlockStatePredicate(#5) | `with(BlockState)` 属性级精确匹配(含 StringRepresentable / 整型 / 布尔属性) | 行为断言(3.5.3-2) | -| F6 | MultiblockDefinition(#6,若迁) | 16 个重载编译可用;`BlockState` 入参定义与 `BlockStatePredicate` 匹配一致 | 编译 + #7 测试覆盖(3.6.4) | -| F7 | 动态多方块测试组(#7) | SIMPLE(熔炉)/ COMPLICATED(TestControllerBlock)/ WAHT(白床分层)三个定义 datagen 产物正确;进世界成形触发 `onFormed`、解体触发 `onUnformed`(铁锭/铁栏杆弹出);定义 JSON 与 port 语义一致 | runData + runServer(3.7.3-2/3) | -| F8 | CurseForge 环境(#8) | 发布后 CurseForge 元数据含客户端/服务端必需声明 | 发布 dry-run + 一次真实发布核对(3.8.3-2) | - -### 7.3 收尾核对 - -1. **提交完整性**:dev/26.1 分支上 #1–#8 各为独立提交(#5/#6 可合提交,取决于维护者偏好),提交信息含对应 issue - 号(#88/#89/#83/#84/#90 等)。 -2. **范围守卫**:`git log --oneline dev/26.1..port/1.21.1` 中,除 N 表(第 5 章)与 #1–#8 外,不应有「实际回迁但未立项」的内容;如执行中发现新 - port 独有内容,先补充本计划再执行。 -3. **文档同步**:`docs/branch-comparison.md` 的「待验证事项」(V1–V5)在回迁执行中逐项销账;本计划中的「待验证」标注同样逐一关闭,关闭记录可追加到附录 - B。 -4. **遗留登记**:任何因环境限制未完成的运行类验收(F1/F4/F7 的运行部分)登记为遗留项,附复现步骤,不得默认通过。 - ---- - -## 附录 - -### A. 常用只读 git 命令(本计划全部事实均来自以下命令) - -```bash -# 提取 port 侧文件内容 -git show port/1.21.1:module.yukkuri/src/main/java/dev/anvilcraft/lib/v2/yukkuri/Yukkuri.java - -# 查看来源提交的完整 diff -git show 569e01d # #1(yukkuri 引入) -git show 18b29ca # #1(CI 发布顺序,port 侧形态) -git show fab551b # #1(yukkuri 收尾修复) -git show edd71ec # #2 -git show e78d020 # #3 -git show 9874db7 # #4 -git show 5fc6f65 # #5 / #6(动态多方块系统 + BlockStatePredicate 引入) -git show 1311782 # #8 - -# 两侧同路径对比 -git diff port/1.21.1 dev/26.1 -- module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java - -# 某模块 port 独有提交清单(回迁候选扫描) -git log --oneline dev/26.1..port/1.21.1 -- module.recipe - -# dev 侧落点现状 -git show dev/26.1:module.util/src/main/java/dev/anvilcraft/lib/v2/util/ClientTickRecorder.java - -# 临时工作树对比(不切换当前分支) -# git worktree add <临时目录> dev/26.1 # 注意:此为 git 写入操作,仅当执行者确认可创建 worktree 时使用; -# git -C <临时目录> diff port/1.21.1 dev/26.1 -- <路径> -``` - -> 说明:`git worktree add` 属于工作区变更(非 commit/push),是否允许由执行者按仓库纪律判断;纯只读场景直接用 `git show` / -> `git diff` 两分支参数即可。 - -### B. 待验证事项汇总(执行时逐项销账) - -| 编号 | 事项 | 涉及项 | 验证方法 | 状态 | -|------|-------------------------------------------------------------------------------------------------------------|--------|--------------------------------------------------------------------------------------------------------|--------| -| V-Y1 | `Level.getCapability(BlockCapability, BlockPos, Direction)` 在 26.1 的签名与 side 传参顺序 | #1 | `gradlew :module.yukkuri:compileJava` 编译定位 + IDE 反编译 `net.minecraft.world.level.Level` 确认语义 | 未验证 | -| V-Y2 | 选项 B 迁移完整性:14 个 Java 文件 `FluidStack` / `IFluidHandler` 零残留 | #1 | `grep -rn "FluidStack\|IFluidHandler" module.yukkuri/src` 无匹配 + 编译零告警 | 未验证 | -| V-Y3 | yukkuri 与 AnvilCraft 大锅实现的流体对接契约(选 B 后 `FluidResource` 类型约定) | #1 | dev 侧仓库外与 AnvilCraft 维护者确认调用面 | 未验证 | -| V-Y4 | `modules.json` 的 `needs` 是否需补充(当前结论 `[]`) | #1 | yukkuri 编译通过后按实际 import 复核 | 未验证 | -| V-Y5 | mods.toml 删 `[[mixins]]` 块后模组加载正常 | #1 | `gradlew :module.main:runServer` 启动日志无 mixin 报错 | 未验证 | -| V-R1 | `InWorldRecipeContext.pop(...)` 在 dev 侧存在(或等价栈操作 API) | #2 | dev 侧 `InWorldRecipeContext` 源码 + 编译 | 未验证 | -| V-R2 | `ItemStack.isSameItemSameComponents` / `copyWithCount` 在 26.1 可用性 | #3 | `gradlew :module.recipe:compileJava` | 未验证 | -| V-U1 | `ClientPlayerNetworkEvent.LoggingOut` 在 26.1 客户端事件体系可用 | #4 | `gradlew :module.util:compileJava` + runClient 验证 | 未验证 | -| V-U2 | `with(Property, T)` 泛型重载下 `with(BlockState)` 通配符调用的编译通过性 | #5 | `gradlew :module.util:compileJava`;遇错参照 port 侧泛型强转模式 | 未验证 | -| V-U3 | dev 侧 `Util` 是否保留 `cast`(新实现不依赖,仅记录) | #5 | `git grep "class Util" dev/26.1 -- module.util` | 未验证 | -| V-T1 | dev 侧 registrum `DataProviderInitializer` 对 datapack registry(`DEFINITIONS_KEY`)的 add 方法签名 | #7 | dev 侧 `AbstractRegistrum.getDataGenInitializer` / `RegistrumDatapackProvider` 源码 + 编译 | 未验证 | -| V-T2 | dev 侧 `BlockEntry` builder 链式 API 形态(`.blockstate()` / `.item()` / `.model()` / `.build()` 是否保留) | #7 | dev 侧 `module.registrum` `BlockEntry` 源码 + 编译 | 未验证 | -| V-T3 | dev 侧 `MultiblockState` / `IController` 事件化后(`1e5bfb3`)回调体适配 | #7 | dev 侧 multiblock 源码 + `runServer` 行为验证 | 未验证 | -| V-M1 | mod-publish 1.1.0 的 `curseforge.clientRequired/serverRequired` 属性可用性 | #8 | `gradlew :module.main:build` 配置阶段 | 未验证 | -| V-M2 | 真实发布后 CurseForge 元数据含环境声明 | #8 | 一次真实发布核对 | 未验证 | - -### C. 参考资料 - -| 资料 | 位置 | -|---------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 分支对比报告(编号体系、分类依据、待验证 V1–V5) | `docs/branch-comparison.md` | -| 8 个并行子代理模块级调研报告 | `C:/Users/Administrator/.kimi-code/sessions/wd_anvillib_031cb785b7e3/session_6033f19b-f4ef-481a-b18c-d626cc22c314/agents/main/tool-results/AgentSwarm-call_00_nh47UIFAaqo9d96levr46332-594fb004-9f1f-4c9e-b230-43bc1440211d.txt` | -| NeoForge 26.1.2.76 sources jar(流体能力 API 核实依据) | `C:/Users/Administrator/.gradle/caches/modules-2/files-2.1/net.neoforged/neoforge/26.1.2.76/1d1dabe31afb953e46fb278e1cd8f04ab8e0d9a0/neoforge-26.1.2.76-sources.jar` | -| dev 侧关键文件实测基准 | `settings.gradle`、`module.gradle`、根 `build.gradle`、`.github/modules.json`、`.github/workflows/{ci,build_and_test,generate-matrix}.js/yml`(均以 `0b7e45f` 为准) | -| port 侧来源提交基准 | `569e01d` / `18b29ca` / `fab551b` / `edd71ec` / `e78d020` / `9874db7` / `5fc6f65` / `1311782` | - -### D. 回迁提交清单(PR 提交模板) - -> 每个回迁项在 dev/26.1 分支上对应一个独立提交(#5/#6 可合并为一个提交)。下表可直接作为 PR 描述模板,勾选「状态」列后随 PR -> 提交。 - -| 提交 | 回迁项 | 涉及文件(变更面) | 关键验证命令 | 状态 | -|----------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|------| -| commit 1 | #1 yukkuri | `module.yukkuri/**`(18 文件)+ `settings.gradle` + `module.main/build.gradle` + `.github/modules.json` + `README.md` / `README.en.md` | `gradlew :module.yukkuri:build`;`gradlew :module.main:build`;`grep -rn "ResourceLocation" module.yukkuri/src`(0 匹配) | ☐ | -| commit 2 | #2 谓词栈回滚 | `module.recipe/.../InWorldRecipe.java` | `gradlew :module.recipe:compileJava` | ☐ | -| commit 3 | #3 sync 短路/差量 | `module.recipe/.../cache/item/ItemResourceHandlerCacheElement.java` | `gradlew :module.recipe:compileJava` + 写入计数测试 | ☐ | -| commit 4 | #4 ClientTickRecorder | `module.util/.../util/ClientTickRecorder.java` | `gradlew :module.util:compileJava` + runClient 手工验证 | ☐ | -| commit 5 | #5 with(BlockState) | `module.util/.../predicate/BlockStatePredicate.java` | `gradlew :module.util:compileJava` + 行为断言 | ☐ | -| commit 6 | #6 16 重载(若迁) | `module.multiblock/.../dynamic/definition/MultiblockDefinition.java` | `gradlew :module.multiblock:compileJava` | ☐ | -| commit 7 | #7 测试组 | `module.test/.../test/multiblock/**`(6 文件)+ `AnvilLibTest.java` + `module.test/gradle.properties` | `gradlew :module.test:compileJava`;`runData`;`runServer` | ☐ | -| commit 8 | #8 CF 环境两行 | `module.main/build.gradle`(publishMods 块) | `gradlew :module.main:build` | ☐ | -| 收尾 | 全量回归 | 全仓 | `gradlew build`(根级)全绿;CI 全绿;附录 B 待验证清单销账 | ☐ | diff --git a/docs/migration-plan-port-1.21.1.md b/docs/migration-plan-port-1.21.1.md deleted file mode 100644 index 86b7597e..00000000 --- a/docs/migration-plan-port-1.21.1.md +++ /dev/null @@ -1,1405 +0,0 @@ -# AnvilLib 回移植计划:`dev/26.1` → `port/1.21.1`(功能回移植执行方案) - -> ⚠️ 状态更新(2026-08-12):#P7(`module.rendering`)与 #P8(`renderdoc-loader`)已从 `port/1.21.1` 分支整体移除;#P6(`module.font`)与 `module.main` 的渲染 jarJar 依赖、CI 工作流(ci / release / pull_request)中的渲染任务亦已同步清理。本文为历史计划文档,相关章节仅作记录保留。 - -> 文档信息 -> -> - 生成日期:2026-08-10 -> - 分析对象:`dev/26.1` @ `0b7e45f`(Minecraft 26.1.2 / NeoForge 26.1.2.76)、`port/1.21.1` @ `bd25229`(Minecraft 1.21.1 / NeoForge 21.1.226 / Java 21) -> - 共同祖先(merge-base):`81c5a9c` -> - 数据来源:8 个并行子代理模块级调研报告(2026-08-10,1395 行,路径见附录 C)+ git 只读命令(`git show` / `git diff` / `git ls-tree`,2026-08-10 本地仓库核对)+ NeoForge 21.1.226 / fancymodloader loader 4.0.42/4.0.43 sources jar 解压核实 + 1.21.1 混淆 jar javap 实测 -> - 关联文档与编号体系:本文与 `docs/migration-plan-dev-26.1.md`(port→dev 方向)**互为反向、互相补充**,两分支整体关系见 `docs/branch-comparison.md`;本文使用独立的 canonical 编号 **#P1–#P19**(「回移植项」),与 `docs/migration-plan-dev-26.1.md` 的 #1–#8 无任何对应关系,正文提及其它文档一律用文件名,避免编号混淆 -> - 结论性质:除标注「待验证」处外,均基于调研报告与本地 git / sources jar / javap 实测得出;所有「待验证」条目均已给出验证方法,执行时必须完成验证后方可标记该项完成 -> - 执行约束:本计划只描述 dev 侧「功能 / 修复 / 演进」的回移植,**不迁移**「26.1 平台适配」本身(`Identifier`、`ItemStackTemplate`、`RecipeMap`、`SavedDataType`、`ValueInput`/`ValueOutput`、`RenderPipeline`、`ResourceHandler`、`DataComponentPredicate`、`neoforgespi.transformation`、`@NullMarked` 等为 26.1 平台特性,port 侧已有 1.21.1 等价物,反向迁移无意义);本文仅为计划,执行时需在 port/1.21.1 分支工作树上以受控提交落地,本文档本身不产生任何 git 写入操作 - ---- - -## 1. 概述与范围 - -### 1.1 回移植背景 - -AnvilLib 的两个长期分支在 merge-base `81c5a9c` 之后完全分叉:`dev/26.1` 是演进主线(沿 1.21.2 → 1.21.11 → 26.1 版本链升级,独有 170 提交),沿路新增了 rpc / sync / explosion / collision / space-select / font / rendering / renderdoc-loader 八个模块,并对既有共享模块(config / integration / network / util / registrum / multiblock / recipe / wheel)做了大量功能演进;`port/1.21.1` 是维护与特性分支(独有 46 提交),冻结在 Minecraft 1.21.1 / NeoForge 21.1.226 / Java 21 平台。 - -`docs/migration-plan-dev-26.1.md` 已把 port 侧独有的少量内容(yukkuri 模块、三个 bugfix、测试组、发布配置,共 #1–#8)回迁到 dev/26.1。与之相反,**dev 侧有大量「功能」是 port 侧完全没有的**——尤其是八个新模块,以及共享模块中的功能性演进(注册表 builder 扩展、配置分组、网络工具方法等)。这些功能大多建立在与平台无关的逻辑之上,或可经「26.1 → 1.21.1」反向 API 替换后落地。 - -本文档即这 19 项(canonical 编号 #P1–#P19)的执行计划:每项给出**目标、步骤(具体到文件与要点)、API 适配点(26.1 → 1.21.1 反向替换清单)、验收方式**,并汇总不可移植清单(第 5 章)、建议执行顺序(第 6 章)与总体验收标准(第 7 章)。 - -### 1.2 范围界定 - -本计划 **包含**: - -- 19 个回移植项(#P1–#P19)的完整执行方案与验收判据; -- 每项与 dev 侧来源提交、port/1.21.1 落点文件的一一对应关系; -- 每项涉及的 26.1 → 1.21.1 反向 API 替换清单(全部以调研报告的 1.21.1 API 核实结论为准)。 - -本计划 **不包含**(即「不做清单」的完整理由见第 5 章): - -- 任何「26.1 平台适配」的反向搬运:`Identifier`、`ItemStackTemplate`、`RecipeMap`、`SavedDataType`、`ValueInput`/`ValueOutput`、`RenderPipeline`/`RenderState`/`SubmitNodeStorage`、`ResourceHandler`、`DataComponentPredicate`、`neoforgespi.transformation` SPI、jspecify `@NullMarked` 等,port 侧均有 1.21.1 等价物,反向迁移无意义; -- `module.moveable-entity-block` 的全部内容(dev 侧零功能增量,见 2.4 节); -- dev 侧构建体系重构中的 26.1 特有内容(`clientRenderDoc`/`clientIrisWorkaround` run 配置、`createModule` 模板中的 `Identifier`、`java_version=25` 等,见 #P19); -- module.test 中依赖 dev 新渲染模块的集成测试(Bloom / CachedBER / Compute / SDF 图层 / GUI 测试,见第 5 章)。 - -**迁移方式**:两侧无共同提交,直接 merge 方向错误且冲突不可控,故采用 **受控回移植**——在 port/1.21.1 分支工作树上按 #P1–#P19 逐个以独立提交落地,每个提交只改本项内容,便于评审与回滚。 - -### 1.3 前置条件与约定 - -| 项 | 约定 | -|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 编号 | 回移植项一律使用 canonical 编号 **#P1–#P19**;引用其它文档时使用文件名(如 `docs/migration-plan-dev-26.1.md`),不得沿用其 #1–#8 编号 | -| 基线 | port/1.21.1 @ `bd25229`(文档描述的全部落点均以该提交为准;执行时如 HEAD 已前进,需先以 `git log` 复核落点文件未变) | -| 提取方式 | 一律用 `git show dev/26.1:<路径>` 提取源码,**不**进行分支切换、不产生 git 写入操作 | -| 工作区 | 回移植在 port/1.21.1 分支工作树执行;只读对比可用 `git diff port/1.21.1 dev/26.1 -- <路径>` | -| 验证基线 | port 侧构建命令:`gradlew :module.xxx:build`、`gradlew :module.xxx:compileJava`、`gradlew :module.xxx:runData`(模块需要 datagen 时);port 侧工具链为 **Java 21**(`module.network/build.gradle:33` 确认),dev 侧代码中 Java 22+ 语法(如 lambda 参数 `_`、`field.accessFlags()`)必须改写 | -| API 事实 | 本计划全部 1.21.1 API 存在性结论来自调研报告(NeoForge 21.1.226 sources jar 解压、fancymodloader loader 4.0.42/4.0.43 sources、1.21.1 混淆 jar javap、port 分支现有代码引用),执行时以编译期核对为最终判据 | -| 待验证 | 所有标注「待验证」的条目均已给出验证方法(附录 B),执行时须完成验证后才能标记该项完成 | - -### 1.4 回移植项一览(结论先行) - -| #P | 内容 | 难度 | 一句话结论 | -|------|-----------------------------------------------------------------------------------------------|------------|-------------------------------------------------------------------------------------------------| -| #P1 | `module.rpc` 整模块(注解式远程调用,25 文件) | 低 | 整模块拷贝 + 3 处机械 API 反替换(Identifier / FMLLoader / accessFlags) | -| #P2 | `module.sync` + processor(同步框架,31 文件) | 中-高 | 约 50/56 文件直接搬;processor 的 26.1 SPI 需 CoreMod(ICoreMod + ITransformer)重写 | -| #P3 | `module.explosion` 整模块(10 文件) | 低 | 6 处 26.1 API 反替换,其余逐字可搬;依赖 port module.config | -| #P4 | `module.collision` 整模块(纯数学,3 文件) | 极低 | 零 MC/NeoForge API 依赖,逐字搬移 | -| #P5 | `module.space-select` 整模块(18 文件) | 中 | 唯一实现重写点是 DistrictRenderer 线框路径(ShapeRenderer → LevelRenderer.renderVoxelShape) | -| #P6 | `module.font` 整模块(25 文件) | 中 | 约 60% 纯 CPU 逻辑直接搬;GPU 上传/绘制层与 GUI 按 1.21.1 改写;依赖 #P7 | -| #P7 | `module.rendering`(113 文件,按功能块分 5 批) | 高(分批) | 纯逻辑 19 项直接搬;后处理 / SDF GUI / CachedBER / Compute 四块执行层按 1.21.1 重写 | -| #P8 | renderdoc-loader(Java Agent,2 文件) | 低 | 源码逐字搬;构建脚本按 port 侧(Java 21、无 jreleaser 别名)改写 | -| #P9 | `module.config`:group 分组 + TranslatableEnum + TOML key 点分隔 | 低 | 三处纯逻辑/配置演进,1.21.1 加载链路原生支持子目录文件名 | -| #P10 | `module.integration`:数据加载客户端/服务端拆分 + meter 递增 | 低 | 纯反射逻辑,可移植;破坏性 API(applyData 改名)需提示下游 | -| #P11 | `module.network`:PacketData 泛型检查修复 + NetworkUtil Included 方法 + PacketProtocol public | 低 | 三处纯逻辑/修饰符改动,无平台依赖 | -| #P12 | `module.util`:OutlineUtil + withCount + @EqualsAndHashCode | 中 | 四处功能演进; | -| #P13 | `module.registrum`:14 种新注册表 builder/entry | 低 | 绝大多数仅 Identifier 改名或个别签名微调;VillagerProfession 需按 1.21.1 6 元 record 适配 | -| #P14 | `module.registrum` datagen 增强(RecipeProvider public 化 + dataMap(provider)) | 中 | 方法 public 化改「公开包装内调静态方法」;Runner / 模型生成器不迁 | -| #P15 | `module.multiblock`:DynamicMultiblockEvent + 快照复用 + BlockPos 键化 + 懒解析 | 中 | 四项修复/演进均可移植;Config.group 依赖 #P9 | -| #P16 | `module.recipe`:SpawnItem 零数量守卫 + SetBlock nbt 默认值 | 低 | 两处纯逻辑修复(含一个潜在 NPE),与 dev 侧其它改动无关 | -| #P17 | `module.wheel`:环形扇区选择效果(WheelSelectionEffect + fsh + 渲染适配) | 中-高 | 纯 UI 功能可移植;渲染必须落在 port 旧 Tesselator/ShaderInstance 体系,切断 dev 渲染依赖 | -| #P18 | `module.test`:可选集成测试(T2 / T8 / T9 / T10) | 低-中 | 仅带回不依赖 dev 新渲染模块的测试;随 #P1/#P3/#P9/#P17 落地 | -| #P19 | 构建体系:roseauCheck API 兼容检查(+ module.gradle 集中化可选) | 中 | roseau-cli 无 MC 绑定直接搬;集中化需裁剪 26.1 特有内容 | - ---- - -## 2. 判定框架与分类规则 - -### 2.1 三类分类定义 - -调研报告(附录 C)对 dev 侧每个功能点给出了三分类判定。本计划沿用该分类,规则如下: - -**【可直接移植】**:功能逻辑完全不依赖 26.1 专有 API,或仅依赖两侧签名一致的稳定 API(如 `Mth`、JOML、`EventBus` 体系、`PacketDistributor`、codec/stream codec 基础件),可原样拷贝或仅做机械改名(`Identifier`→`ResourceLocation` 等)。判定标准:全部引用的类/方法在 1.21.1 sources / javap 中逐项核实存在,且无行为差异。 - -**【需反向适配】**:功能本质与平台无关(算法、数据结构、业务流程),但实现载体使用 26.1 平台 API;1.21.1 有直接等价物(`RegisterShadersEvent + ShaderInstance` 对应 `RegisterRenderPipelinesEvent + RenderPipeline`、`LevelRenderer.renderVoxelShape` 对应 `ShapeRenderer`、`GatherDataEvent` 对应 `GatherDataEvent.Client` 等)。移植 = 保留功能逻辑 + 按等价物重写执行层。判定标准:1.21.1 等价物存在且功能语义可完整承接;若等价物需整体重写(如 GPU 抽象层),难度上调。 - -**【不可移植】**(仅 26.1 平台):功能本身就是 26.1 平台适配(`Identifier` 改名、`@NullMarked` 风格、`neoforgespi.transformation` SPI、`RenderPipeline` 注册、`ItemStackTemplate` 编解码分支、`SavedDataType` 持久化、`ValueInput`/`ValueOutput` 序列化等),或依赖的 26.1 API 在 1.21.1 无对应物(如 `GpuDevice`/`CommandEncoder` 抽象、`PictureInPictureRenderer` 框架)。**反向迁移无意义**:port 侧已有 1.21.1 等价实现,且迁移会破坏 port 侧现状。第 5 章按模块给出完整清单。 - -> 三类关系的简化表述:**直接移植**搬「功能 + 平台无关逻辑」;**反向适配**搬「功能」但重写「平台载体」;**不可移植**的「功能」本身就是平台,不搬。 - -### 2.2 判定时的参考判据(源自报告) - -- **功能面判定**:一个 dev 侧差异是否构成「功能」,看其是否改变模块对外行为/能力(新增 API、修复 bug、新增注册表入口、性能演进);纯风格、注解风格(`@NullMarked` vs `@MethodsReturnNonnullByDefault`)、反混淆名调整、构建重构均不构成功能。 -- **平台面判定**:引用 26.1 专属类型/机制(`Identifier`、`ItemStackTemplate`、`RecipeMap`、`SavedDataType`、`ValueInput`/`ValueOutput`、`RenderPipeline`/`SubmitNodeStorage`/`GpuDevice`、`GuiGraphicsExtractor`、`neoforgespi.transformation.*`、`DataComponentPredicate`、`ResourceHandler`、jspecify `@NullMarked`、`GatherDataEvent.Client/Server`、`AddClientReloadListenersEvent`、`RegisterRenderPipelinesEvent` 等,完整清单见 1.2 与第 5 章)的任何代码,其「平台部分」一律不可迁移。 -- **困难等级**:低 = 机械改名/拷贝,1 个提交可完成;中 = 涉及执行层重写或跨模块协调;高 = 需按 1.21.1 生态整体重写(后处理、CachedBER、Compute、SPI 适配层)。 -- **已由 port 侧平行 PR 接收的内容不重复移植**:如 multiblock 双半方块修复、`AbstractCacheElement.grow()` 空堆修复、`ICacheInputOutputImpl.grow()` 只记录实际消耗、`ItemCache` 元素范围 `0.5→1.0`(port `6a0e8a9` #79 / `f2b60b6`,对应 dev `922d523` #78 / `1c523ee`),以及 wheel 与活塞模块的基础移植(`18834b5`/`53eaa11`/`9c437b1` 等)——均已核实存在,**不立项**。 - -### 2.3 为什么 moveable-entity-block 被整体排除 - -`module.moveable-entity-block` 在 dev 侧的差异经逐文件核实**全部为 26.1 平台适配,零功能增量**: - -| dev 侧差异 | 性质 | -|---|---| -| `PistonMovingBlockEntityMixin`:`ValueInput`/`ValueOutput` 序列化重构(`b5b27aa`) | 26.1 NBT 序列化体系;port 侧 `loadAdditional(CompoundTag, HolderLookup)`/`saveAdditional` 等价 | -| `PistonHeadRendererMixin` + 新增 `PistonHeadRenderStateMixin` + `IPistonHeadRenderStateExtension`(`83477ab` 后续 26.1 适配) | 注入点改 `extractRenderState`/`submit`,依赖 `BlockEntityRenderState`/`SubmitNodeCollector`(26.1 渲染管线);port 侧注入 `render(...MultiBufferSource)` 功能等价 | -| `PistonBaseBlockMixin` 局部变量名/反混淆名调整 | 26.1 反混淆名(`blockpos3→pos`、`blockstate→newState`),逻辑零变化 | -| 活塞 bug 修复(`922d523`) | port 侧 `6a0e8a9`(#79)已含同一修复,无需迁移 | -| `@ApiStatus.Internal` / `@NullMarked` 注解 | 纯注解风格,非功能 | - -**结论**:该模块在 port/1.21.1 侧的现有实现(`PistonHeadRendererMixin` 注入 `render` + 原版 NBT 序列化)即 dev 侧功能的 1.21.1 等价物,反向迁移只会破坏 port 侧现状。整体排除,不立项。 - -### 2.4 平台适配「不迁移」的通用原则 - -除第 5 章清单外,以下 **机械性平台适配** 在任何一个回移植项中出现时均按「保留 port 侧写法」处理,不作为独立改动: - -- `Identifier` → 保持 port 侧 `ResourceLocation`(`ResourceLocation.fromNamespaceAndPath` 与 26.1 `Identifier.fromNamespaceAndPath` 同名同参); -- `FMLLoader.getCurrent().getLoadingModList()` / `getCurrent().getDist()` → 保持 port 侧 `LoadingModList.get()` / `FMLLoader.getDist()`(1.21.1 的 `FMLLoader` **无 `getCurrent()`**,loader 4.0.42/4.0.43 源码核实); -- `field.accessFlags()` + `AccessFlag`(Java 22 API)→ `Modifier.isPublic/isStatic/isFinal`(port 工具链 Java 21); -- `isClientSide()` 方法调用 → port 侧 `isClientSide` 字段;`ResourceKey::identifier` → port 侧 `ResourceKey::location`; -- `getValue/getKey`(26.1 注册表改名)→ port 侧 `get/getKey`; -- `GatherDataEvent.Client/Server` → port 侧单一 `GatherDataEvent`(21.1.226 核实无子类,构造器 `(ModContainer, DataGenerator, DataGeneratorConfig, ExistingFileHelper)`);`AddClientReloadListenersEvent` → port 侧 `RegisterClientReloadListenersEvent`; -- `@NullMarked`(jspecify)→ 保持 port 侧 `@MethodsReturnNonnullByDefault` + `javax.annotation.ParametersAreNonnullByDefault`; -- lambda 参数 `_`(Java 22+ 语法)→ 改名(如 `ignored`)。 - ---- - -## 3. 回移植项总表 - -> 内容、来源提交、难度、前置依赖与 `docs/migration-plan-dev-26.1.md` 的方向相反(本文为 dev→port);「1.21.1 落点」列为本文新增。来源提交为**代表提交**(同项可能含多个提交,正文给出完整清单)。 - -| #P | 内容 | 来源提交(代表) | 难度 | 前置依赖 | 1.21.1 落点 | -|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|--------|------------------------------|-----------------------------------------------------------------------------------------| -| #P1 | module.rpc 整模块(注解式远程调用,25 文件) | `d153e58` #60、`8d57cba` #63、`4910620` #87 | 低 | module.network/util 在 port 侧已就绪 | 新模块 `module.rpc/`(坐标 `anvillib-rpc-neoforge-1.21.1`) | -| #P2 | module.sync + processor(同步框架,31 文件;processor 需 CoreMod 重写) | `3ce2611`、`bf49789`、`8d57cba`、`1063ee9` #65 | 中-高 | 同 #P1 | 新模块 `module.sync/` + `module.sync/processor/`(坐标 `anvillib-sync-neoforge-1.21.1`)| -| #P3 | module.explosion 整模块(10 文件) | `435e23f`、`fba1027` | 低 | module.config | 新模块 `module.explosion/`(坐标 `anvillib-explosion-neoforge-1.21.1`) | -| #P4 | module.collision 整模块(纯数学,3 文件) | `82b4f6a` | 极低 | 无 | 新模块 `module.collision/`(坐标 `anvillib-collision-neoforge-1.21.1`) | -| #P5 | module.space-select 整模块(18 文件) | `1335fc5`、`e75b10f` | 中 | module.network | 新模块 `module.space-select/`(坐标 `anvillib-space-select-neoforge-1.21.1`) | -| #P6 | module.font 整模块(25 文件) | `1d56a4e` #22、`946938f` #56 | 中 | 依赖 module.rendering 的 1.21.1 版(#P7) | 新模块 `module.font/`(坐标 `anvillib-font-neoforge-1.21.1`) | -| #P7 | module.rendering(113 文件,不可整模块;按功能块分批:#P7a 纯逻辑 19 项 / #P7b 后处理三件套 / #P7c SDF GUI / #P7d CachedBER / #P7e Compute) | `7121bf6`、`494274e` #21、`7d07f20` #41、`64bcf21` #33 | 高(分批) | 无 | 新模块 `module.rendering/`(坐标 `anvillib-rendering-neoforge-1.21.1`) | -| #P8 | renderdoc-loader(Java Agent,2 文件) | `2751be4`、`1063ee9` | 低 | 无 | 新模块 `renderdoc-loader/`(坐标 `renderdoc-loader-internal`) | -| #P9 | module.config:group 分组 + TranslatableEnum 枚举翻译 + TOML key 点分隔 | `3d50d76`、`946938f` #56 | 低 | 无 | `module.config` 现有文件原地增强(`Config`/`ConfigManager`/`ConfigRecord`/`ConfigData`/`FormattingUtil`/`util/TranslatableEnum.java` 新增) | -| #P10 | module.integration:数据加载客户端/服务端拆分 + meter 递增 | `dc4d583`、`e8efbd4` | 低 | 无(破坏性 API) | `module.integration` 三个文件(`IntegrationInstance`/`IntegrationManager`/`IntegrationType`) | -| #P11 | module.network:PacketData 泛型检查修复 + NetworkUtil Included 方法 + PacketProtocol public | `20721b1` #52、`5733a9d`、`6220427` | 低 | 无 | `module.network` 三个文件(`PacketData`/`NetworkUtil`/`PacketProtocol`) | -| #P12 | module.util:OutlineUtil + predicate HolderGetter 重构 + withCount + @EqualsAndHashCode | `e4c09fb` #75、`bb09805`、`d84912e`、`c480e9f` #49 | 中 | module.recipe 调用方连带 | `module.util`(`OutlineUtil`/`client/Line`/测试 + 三个 predicate 文件)+ `module.recipe` 四个调用方 | -| #P13 | module.registrum:14 种新注册表 builder/entry(GameEvent/MobEffect/Potion/VillagerType/PoiType/Self/三类 Modifier/Condition/CreativeTab/Attachment/DataComponent/SoundEvent/RecipeSerializer/RecipeType/VillagerProfession) | `9b2d0e3` #31、`9c89bfa` #27、`9627358` #30、`c307a4c` #28、`252922e` #47 | 低 | 无 | `module.registrum` 的 `builders/**`、`util/entry/**` 新增文件 + `AbstractRegistrum` 入口方法 | -| #P14 | module.registrum datagen 增强(RecipeProvider 方法 public 化、dataMap(provider) 重载;GeneratorType 拆分可选) | `58f01d4`、`b4989c0` | 中 | #P13(可选) | `module.registrum` 的 `providers/RegistrumRecipeProvider.java`、`providers/RegistrumDataMapProvider.java`、`builders/Builder.java` | -| #P15 | module.multiblock:DynamicMultiblockEvent + 未加载区块快照复用 + Long→BlockPos 键化 + 定义懒解析 | `1e5bfb3` #69、`d888714` #73、`22091d4`、`1b0a652` | 中 | #P9(Config.group,M7 项) | `module.multiblock` 现有文件(`dynamic/event/` 新建 + `DynamicMultiblockManager`/`MultiblockState`/`MultiblockCheckSnapshot`/两个 packet) | -| #P16 | module.recipe:SpawnItem 零数量守卫 + SetBlock nbt 默认值 | `40b8f84`、`91327c1`、`b8825bd` | 低 | 无 | `module.recipe` 的 `outcome/SpawnItem.java`、`outcome/SetBlock.java` | -| #P17 | module.wheel:环形扇区选择效果(WheelSelectionEffect + fsh + 渲染适配) | `5d0b285`、`46e1b73`、`8a818f0` | 中-高 | 保留 port 旧渲染体系 | `module.wheel` 现有文件(`api/WheelSelectionEffect.java` 新建 + `WheelMenuBuilder`/`WheelMenuModel`/`WheelWidget`/`WheelScreen` + `annular_sector.fsh`) | -| #P18 | module.test:可选集成测试(T2 配置测试 / T8 RPC / T9 爆炸 / T10 wheel 按键) | `a6a24f1`、`d153e58`、`435e23f` | 低-中 | 随 #P1/#P3/#P9/#P17 | `module.test` 新增(`AnvilLibTestConfig.java`、`rpc/**`、`command/TestCommand.java`、`wheel/` 按键扩展) | -| #P19 | 构建体系:roseauCheck API 兼容检查(+ module.gradle 集中化可选) | `ea4301f` #25、`a6a24f1`、`946938f` | 中 | 无 | 根 `roseau.yaml` + `gradle/scripts/roseau.gradle`(可选:根 `module.gradle` + `build.gradle` 调整) | - -### 3.1 编号一致性说明 - -- 本表 #P1–#P19 为本文唯一执行依据,与 `docs/migration-plan-dev-26.1.md` 的 #1–#8 无任何对应关系。 -- #P7 拆为 #P7a–#P7e 五个子项分别执行与验收(第 4 章对应小节),但作为单一模块落地。 -- #P6 依赖 #P7 的 1.21.1 版(build.gradle jarJar 依赖 `anvillib-rendering-neoforge-1.21.1`),但 #P6 自身的 SDF 图集/布局/测量等纯逻辑部分可在 #P7a 完成后先行落地,无需等 #P7b–#P7e。 -- #P15 的 Config.group 子项(M7)依赖 #P9(`@Config(group=…)` 离开 module.config 的 group 支持无法编译);若 #P9 未先行,#P15 中两个模块保持 `@Config(name=…)` 亦可编译,但 group 语义缺失;#P18 各测试项分别依赖 #P1/#P3/#P9/#P17,可随对应项落地,不单独立项执行顺序;#P19 的 roseauCheck 独立于全部代码项,module.gradle 集中化为可选工程决策(port 侧现有独立构建脚本自洽,是否重构由维护者决定)。 - ---- - -## 4. 逐项详细计划 - -> 每节统一包含四部分:**目标**(一句话)、**步骤**(具体到文件与要点)、**API 适配点**(26.1 → 1.21.1 反向替换清单)、**验收方式**。步骤中的 `git show dev/26.1:<路径>` 均为只读操作。所有 1.21.1 API 存在性结论均来自调研报告(核实方式:NeoForge 21.1.226 sources jar 解压、fancymodloader loader 4.0.42/4.0.43 sources、1.21.1 混淆 jar javap、port 分支现有代码引用)。 - -### 4.1 回移植项 #P1:module.rpc 整模块(低) - -#### 4.1.1 目标 - -把 dev 侧注解式远程方法调用框架 `module.rpc`(25 文件)整体回移植到 port/1.21.1,作为新模块 `anvillib-rpc-neoforge-1.21.1`,实现「`@RemoteCallable` 静态方法 + 方法引用解析 + 索引下发 + 双向 play 网络包 + 带返回值 invoke」的完整 RPC 能力。 - -#### 4.1.2 步骤 - -```bash -# 1) 提取 dev 侧模块源码(25 个 Java 文件,整体拷贝) -for f in $(git ls-tree -r --name-only dev/26.1 -- module.rpc | grep '\.java$'); do - git show "dev/26.1:$f" > "$f" -done - -# 2) 构建文件:build.gradle 依赖改 anvillib-network-neoforge-1.21.1 / -# anvillib-util-neoforge-1.21.1(codec 仅为 jarJar 传递依赖,同步改坐标); -# jarJar 分支按 port 侧现有模块模式(NOT_DEV 双分支写法,参照 port module.wheel) -# 3) settings.gradle 注册:include 'module.rpc' + project(':module.rpc').name = 'anvillib-rpc-neoforge-1.21.1' -# 4) 模块元数据:neoforge.mods.toml 按 port 侧模板(mod_id=anvillib_rpc); -# anvillib_rpc.mixins.json 保留(dev 侧为空清单,port 侧同样空);gradle.properties 按 port 模块惯例 -# 5) 按 4.1.3 的三处 API 反替换修改源码 -``` - -#### 4.1.3 API 适配点(已逐项核实) - -| dev 侧(26.1) | port 侧(1.21.1) | 落点 | -|---|---|---| -| `net.minecraft.resources.Identifier` | `net.minecraft.resources.ResourceLocation`(`ResourceLocation.fromNamespaceAndPath` 同名);`IPacket.type(...)` 随之适配(port 侧签名 `type(ResourceLocation)`) | `AnvilLibRpc.of/mod`、`RpcRegistry` 等全模块机械替换 | -| `FMLLoader.getCurrent().getLoadingModList()` | `LoadingModList.get()`(1.21.1 的 `FMLLoader` 无 `getCurrent()`,loader 4.0.43 源码核实;port 侧 NetworkRegistrar 即此写法) | `RpcRegistry.getKeys()` | -| `field.accessFlags()` + `AccessFlag`(Java 22 API) | `Modifier.isPublic/isStatic/isFinal`(port 工具链 Java 21,`module.network/build.gradle:33` 确认) | `RpcMethods.findDeclaredCodec` | - -**已核实的 1.21.1 兼容面**(无需改动):`RegisterConfigurationTasksEvent.register/getListener`、`ICustomConfigurationTask`、`IPayloadContext.reply/enqueueWork/flow/player/finishCurrentTask`、`ModFileScanData.AnnotationData`(1.21.1 与 26.1 的 record 签名完全一致,`clazz()` 均返回 asm `Type`)、`PacketDistributor.sendToPlayer`、`ClientPacketDistributor.sendToServer`、`RegistryFriendlyByteBuf(ByteBuf, RegistryAccess, ConnectionType)`、`ByteBufCodecs.*` 全家桶、`UUIDUtil.STREAM_CODEC`、`ServerTickEvent.Post`/`ServerStoppedEvent`/`ClientTickEvent.Post`/`ClientPlayerNetworkEvent.LoggingOut`。`LambdaResolver`/`RpcMethods` 反射为纯 Java 标准 API;无 mixin 注入(`anvillib_rpc.mixins.json` 为空)。 - -#### 4.1.4 验收方式 - -1. `gradlew :module.rpc:compileJava` 通过(3 处反替换后零编译错误)。 -2. 行为验证(随 #P18 T8 或手工):客户端注册 `@RemoteCallable` 方法并下发索引 → 服务端经 `invoke` 远程调用返回 `CompletableFuture` 结果;`@CallableParam` 自定义编解码器生效;tick 超时路径正确。 -3. `git diff port/1.21.1 dev/26.1 -- module.rpc` 只读复核:只剩「预期差异」(`ResourceLocation`、`LoadingModList.get()`、`Modifier`、构建脚本形态、坐标后缀)。 - ---- - -### 4.2 回移植项 #P2:module.sync + processor(中-高) - -#### 4.2.1 目标 - -把 dev 侧同步框架 `module.sync`(31 文件 = 主模块 25 + processor 子模块 6)整体回移植到 port/1.21.1;其中 processor 的字节码注入能力从 26.1 专属的 `neoforgespi.transformation` SPI 改写为 1.21.1 的 **CoreMod(`ICoreMod` + `ITransformer`)** 方案,注入器逻辑复用。 - -#### 4.2.2 步骤(按功能点子项) - -**子项 2.1:注解 + SyncProxy + SyncManager + SyncRegisterEntry + 自定义注册表(可直接移植,低)** - -```bash -git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/Sync.java # 及 annotation/* 全部 -git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncProxy.java -git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncManager.java -git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncRegisterEntry.java -# init/* 同名落位;AnvilLibSync.of() 的 Identifier → ResourceLocation -``` - -- 已核实 1.21.1 存在:`NewRegistryEvent`、`RegistryBuilder.maxId(int).create()`(返回 `Registry`)、`DeferredRegister.create(ResourceKey, modid)`、`DeferredHolder`、`Registry.entrySet()`、`RegisterEvent.getRegistryKey()`、`FMLCommonSetupEvent`、`Entity::level`/`getUUID`、`BlockEntity::getBlockPos/getLevel`。 -- `SyncProxy.defaultCodec` 的 codec 表(`ByteBufCodecs.TAG/COMPOUND_TAG/VECTOR3F/QUATERNIONF/GAME_PROFILE/GAME_PROFILE_PROPERTIES`、`BlockPos.STREAM_CODEC`、`ComponentSerialization.STREAM_CODEC`、`ItemStack.OPTIONAL_STREAM_CODEC`、`ByteBufCodecs.fromCodecWithRegistries`)全部为 1.20.5+/1.21.1 标准 API(`fromCodecWithRegistries` port 侧 `StreamCodecUtil` 已用)。 -- **删除项**:`ItemStackTemplate.STREAM_CODEC` 分支——`ItemStackTemplate` 是 26.1 ItemStack 重构专有类型,1.21.1 无对应物(javap「找不到类」),1.21.1 的 `ItemStack` 已有 `OPTIONAL_STREAM_CODEC`,删除后功能无损(见第 5 章)。 - -**子项 2.2:SyncConfigManager(需反向适配,中)** - -```bash -git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/SyncConfigManager.java -``` - -- `FMLLoader.getCurrent().getLoadingModList()` → `LoadingModList.get()`(`AnnotationData` 用法两侧兼容,直接可用)。 -- `modFile.getContents()`(`IModFile.getContents()` 返回 `net.neoforged.fml.jarcontents.JarContents`,其 `containsFile/openFile` 为 26.1 新增——loader 11.0.13 源码确认)→ **1.21.1 的 `IModFile` 无 `getContents()`**(loader 4.0.43 `IModFile.java` 方法列表确认)。等价实现:`fileInfo.getFile().getSecureJar().getPath(classPath)`(`cpw.mods.jarhandling.SecureJar.getPath(String, String...)` 返回 `Path`,securejarhandler 3.0.8 sources 确认)+ `Files.newInputStream`,或 `getSecureJar().findFile(classPath)` 返回 `Optional`;注意 1.21.1 的 `JarContents`(cpw)仅 `findFile/getPackages`,无 `openFile`。 - -**子项 2.3:LazySyncManager + LazySync 注解(可直接移植,低)** - -```bash -git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/management/LazySyncManager.java -git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/annotation/LazySync.java -git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SyncDirection.java -``` - -纯 Java(`java.lang.reflect` + Guava `MapMaker` 弱引用 + netty)+ `SyncProxy.defaultCodec` + `SideUtil.send`,无 26.1 专有 API。 - -**子项 2.4:网络 payload 层(可直接移植,低)** - -```bash -git ls-tree -r --name-only dev/26.1 -- module.sync/src/main/java | grep -E 'network/' | while read f; do git show "dev/26.1:$f" > "$f"; done -``` - -`IInsensitiveBiPacket` 在 port/1.21.1 与 dev/26.1 **逐字节一致**(默认方法 `bidirectionalHandler` 相同,port 侧仅 javadoc 缩进差异);`IPacket.type(ResourceLocation)`、`IClientboundPacket`/`IServerboundPacket`、`ctx.reply/finishCurrentTask`、`ByteBufCodecs.map(...)`、`StreamCodec.unit`、`PacketFlow` 全部 1.21.1 标准。 - -**子项 2.5:SideUtil + 入口类(可直接移植,低)** - -```bash -git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/util/SideUtil.java -git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/AnvilLibSync.java -git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/client/AnvilLibSyncClient.java -``` - -已核实存在:`PacketDistributor.sendToAllPlayers/sendToPlayersTrackingChunk(ServerLevel, ChunkPos, Packet)/sendToPlayersTrackingEntity/sendToPlayersInDimension`、`ChunkPos.containing`、`ServerLifecycleHooks.getCurrentServer()`、`ClientPacketDistributor.sendToServer`、`Minecraft.level/getConnection`、`ClientPacketListener.registryAccess()`、`ServerLevel.getEntity(UUID)`、`RegisterConfigurationTasksEvent.getListener()` 返回 `ServerConfigurationPacketListener`(21.1.226 sources 确认)。仅 `Identifier`→`ResourceLocation` 机械改名。 - -**子项 2.6:processor 子模块——CoreMod 重写(需反向适配,高)** - -dev 侧 `processor/src/main/java/.../transform/` 五个类(`SyncClassProcessor`/`SyncTargetIndex`/`SyncBytecodeInjector`/`LazySyncBytecodeInjector`/`LazySyncTargetIndex`)+ `META-INF/services/net.neoforged.neoforgespi.transformation.ClassProcessor`。`SyncClassProcessor` 实现的 `net.neoforged.neoforgespi.transformation.ClassProcessor`(`name()/handlesClass(SelectionContext)/processClass(TransformationContext)` + `ProcessorName`、`ComputeFlags`)是 **26.1 全新 SPI**:loader 4.0.42/4.0.43(1.21.1)jar 与 sources 中 `neoforgespi` 包**无 transformation 子包**(逐 jar 扫描确认),neoforge 21.1.226 sources 中无任何 transformation 文件,loader 11.0.13(26.1 系)含 40 个 transformation 类——该 SPI 属 26.1 平台特性,必须重写适配层。 - -**方案 A(推荐):CoreMod 重写** - -```bash -# 1) SyncClassProcessor 骨架改写为 ICoreMod 实现: -# net.neoforged.neoforgespi.coremod.ICoreMod(getTransformers() 返回 Iterable>, -# loader 4.0.43 源码确认存在) -# 2) services 文件改为注册 net.neoforged.neoforgespi.coremod.ICoreMod -# 3) 新增两个 ITransformer(modlauncher 11.0.5 源码确认,即 1.21.1 所用版本): -# - targets() 取自 SyncTargetIndex / LazySyncTargetIndex 的静态索引; -# - transform(ClassNode, ITransformerVotingContext) 内调用 -# SyncBytecodeInjector / LazySyncBytecodeInjector.inject(ClassNode); -# - ClassWriter.COMPUTE_FRAMES 对应原 COMPUTE_FRAMES 返回语义 -``` - -**方案 B(备选)**:modlauncher `ITransformationService`(services 注册 `cpw.mods.modlauncher.api.ITransformationService`),同样复用注入器。 - -**可复用部分(约 90%)**:`SyncBytecodeInjector`/`LazySyncBytecodeInjector` 的 ASM `InsnList` 生成逻辑(操作 `ClassNode`,与 SPI 无关)、`SyncTargetIndex`/`LazySyncTargetIndex` 的注解扫描(`AnnotationData` 两侧兼容,仅 `FMLLoader.getCurrent()`→`LoadingModList.get()` 一处)。 - -**风险/验证点**:CoreMod transformer 在 game layer 类加载时执行,`AnvilLibSync.LAZY_SYNC_MANAGER` 的 GETSTATIC 引用要求 anvillib_sync 类可解析(V-10);jar-in-jar(`jarJar(api(...))` 携带 processor)场景下 services 文件可见性需实测(V-9)。 - -#### 4.2.3 验收方式 - -1. `gradlew :module.sync:compileJava` 与 processor 编译通过(SPI 适配层零残留:`grep -rn "neoforgespi.transformation" module.sync` 无匹配)。 -2. 行为验证(随 #P18 T8 或手工):`@Sync` 注解字段在两端同步;LazySync 惰性差分同步生效;Configuration 阶段配置表下发正常。 -3. 字节码注入验证:CoreMod 加载后目标类被注入(启动日志 + 反编译核对注入点)。 - ---- - -### 4.3 回移植项 #P3:module.explosion 整模块(低) - -#### 4.3.1 目标 - -把 dev 侧分层球形爆炸执行器模块 `module.explosion`(10 文件)整体回移植到 port/1.21.1,作为新模块 `anvillib-explosion-neoforge-1.21.1`,依赖 port 侧 `anvillib-config-neoforge-1.21.1`。 - -#### 4.3.2 步骤 - -```bash -# 1) 提取 10 个文件(explosion/ 包 + data/ 包 + mixin + resources) -for f in $(git ls-tree -r --name-only dev/26.1 -- module.explosion | grep -E '\.(java|json|toml)$'); do - git show "dev/26.1:$f" > "$f" -done -# 2) build.gradle:依赖 anvillib-config-neoforge-26.1 → anvillib-config-neoforge-1.21.1; -# port 侧无 module.gradle 约定(anvillib.needRunConfig* 不生效),run 配置在模块 build.gradle 内自建, -# 参照 port module.config/build.gradle(含 data run 段) -# 3) settings.gradle 注册 module.explosion → anvillib-explosion-neoforge-1.21.1 -# 4) mixins.json compatibilityLevel: JAVA_25 → JAVA_21(port 工具链 Java 21) -# 5) AnvilLibExplosionConfig 去掉 group = "anvillib"(port @Config 无 group 属性,见 4.3.3); -# zh_cn.json 语言键改写(section 名变化),en_us.json 由 runData 重生成 -``` - -#### 4.3.3 API 适配点 - -| dev 侧(26.1) | port 侧(1.21.1) | 落点 | -|---|---|---| -| `RecipeManager.getRecipes()` 返回 `Collection>` | 同名存在(javap) | `AnvilLibExplosion` 无需改 | -| `Ingredient.items()`(Stream\>) | `Ingredient.getItems()` 返回 `ItemStack[]`(javap) | `AnvilLibExplosion` | -| `ItemStackTemplate`(`count()`/`item().value()`) | `ItemStack`(`getCount()`/`getItem()`) | `AnvilLibExplosion` | -| `BuiltInRegistries.BLOCK.get(TagKey)` | `Registry.getTag(TagKey)` 返回 `Optional>`(javap) | `AnvilLibExplosion` | -| `Identifier` | `ResourceLocation.fromNamespaceAndPath` | `AnvilLibExplosion` | -| `recipe.input()` | 无此方法;改用 `recipe.getIngredients().get(0)`(`AbstractCookingRecipe` 恒为单元素) | `AnvilLibExplosion` | -| `SingleItemRecipe` 的 `result` 字段(dev 经 mixin 访问器) | 1.21.1 `SingleItemRecipe` 为抽象类,`protected final ItemStack result` 存在(javap)——`@Accessor ItemStack getResult()` 原结构可用,仅类型换 `ItemStack` | `mixin/SingleItemRecipeAccessor.java` | -| `entity.hurtServer(level, source, amount)`(26.1 专属) | `Entity.hurt(DamageSource, float)` 返回 boolean(javap) | `ExplosionExecutor` 的 entityProcessor 默认 lambda | -| `OnDatapackSyncEvent`(`getPlayerList().getServer()`)、`ServerStartedEvent` | 同名存在(NF sources) | `AnvilLibExplosion` 无需改 | -| `GatherDataEvent.Client` | 单一 `GatherDataEvent`(`getGenerator()`/`getPackOutput()`/`includeClient()` 存在);`addProvider(event.includeClient(), ...)` 参照 port `module.multiblock` 的 `AnvilLibDatagen` | `data/AnvilLibDatagen.java`、`data/provider/ModLanguageProvider.java` | -| `@Config(group = "anvillib")` | port `@Config` 仅 `name()`/`type()`,**无 `group` 属性**(git show port 核实)→ 去掉 `group`;语言键 section 名变化(`section.anvillib.explosion.common.toml` → `section.anvillib_explosion.common.toml`) | `AnvilLibExplosionConfig.java` | - -**逐字可搬(无需改动)**:`ExplosionSession.java`(`ServerTickEvent.Post`、`NeoForge.EVENT_BUS.register/unregister`、`ServerLevel.getEntities()` 公开返回 `LevelEntityGetter` 且含 `getAll()`、`GameEvent.Context.of(Entity, BlockState)`、`Block.UPDATE_CLIENTS`、`Block.dropResources` 6 参重载、`FluidState.createLegacyBlock()`、`Level.getHeight(Heightmap.Types, int, int)`、Guava `MultimapBuilder`、log4j `TriConsumer`——全部 javap 核实存在)。`ExplosionExecutor` 其余(`damageSources().explosion(Entity, Entity)`、`block.defaultDestroyTime()`、`block.builtInRegistryHolder()`、`Tags.Blocks.GLASS_BLOCKS/GLASS_PANES`、`BlockTags.LEAVES/REPLACEABLE`)均存在。 - -#### 4.3.4 验收方式 - -1. `gradlew :module.explosion:compileJava` 通过;`gradlew :module.explosion:runData` 生成 lang 产物并核对 section 键。 -2. 行为验证(随 #P18 T9):`ServerTickEvent.Post` 驱动的分层球壳破坏按概率半径执行,熔化替换(熔炼配方表)生效,实体处理器(`hurt`)与掉落/熔化路径正确。 -3. `grep -rn "ItemStackTemplate\|hurtServer\|group" module.explosion/src` 复核零残留。 - ---- - -### 4.4 回移植项 #P4:module.collision 整模块(极低) - -#### 4.4.1 目标 - -把 dev 侧纯数学 AABB×三角形 SAT 碰撞检测模块 `module.collision`(3 文件)整体回移植,作为新模块 `anvillib-collision-neoforge-1.21.1`。 - -#### 4.4.2 步骤 - -```bash -# 1) 提取 3 个文件(AnvilLibCollision.java、CollisionTest.java、package-info.java 如有) -for f in $(git ls-tree -r --name-only dev/26.1 -- module.collision | grep '\.java$'); do - git show "dev/26.1:$f" > "$f" -done -# 2) build.gradle 为 dependencies {} 空块(无平台绑定);mods.toml + 空 mixins.json 骨架 -# (模块为「无 @Mod 类的 mod jar」纯工具,port 侧同构) -# 3) settings.gradle 注册 module.collision → anvillib-collision-neoforge-1.21.1 -``` - -#### 4.4.3 API 适配点 - -无。全文件仅引用 `org.joml.Vector3d/Vector3dc/Vector3fc`(joml 随 MC 1.21.1 提供,编译期在 neoForge 依赖里);`CollisionTest.java` 为纯 Java(`System.out`/`System.nanoTime`/LCG 伪随机),自带断言与基准,为手动运行的自测入口(注意其 `static void main()` 无 `String[]` 参数,非标准 JVM 入口,IDE 手动调用)。 - -#### 4.4.4 验收方式 - -1. `gradlew :module.collision:compileJava` 通过。 -2. 手动运行 `AnvilLibCollision` 测试入口(静态 SAT 13 轴 / 三轴独立扫掠 / 真扫掠碰撞三个入口 + `overlapOnAxis` 公开工具)断言全部通过、基准无异常。 -3. `grep -rn "net.minecraft\|net.neoforged" module.collision/src` 确认零 MC/NF import。 - ---- - -### 4.5 回移植项 #P5:module.space-select 整模块(中) - -#### 4.5.1 目标 - -把 dev 侧选区(区域框选/线框渲染/滚轮缩放)模块 `module.space-select`(18 文件)整体回移植,作为新模块 `anvillib-space-select-neoforge-1.21.1`,依赖 port 侧 `anvillib-network-neoforge-1.21.1`。 - -#### 4.5.2 步骤 - -```bash -# 1) 提取 18 个文件(space_select/ 包 + client/ + event/ + network/ + resources) -for f in $(git ls-tree -r --name-only dev/26.1 -- module.space-select | grep -E '\.(java|json|toml)$'); do - git show "dev/26.1:$f" > "$f" -done -# 2) build.gradle:jarJar 依赖 anvillib-network-neoforge-26.1 → anvillib-network-neoforge-1.21.1; -# port 无 module.gradle → 模块无 run 需求可不建 run 配置 -# 3) settings.gradle 注册 module.space-select → anvillib-space-select-neoforge-1.21.1 -# 4) 按 4.5.3 逐文件反替换;DistrictRenderer 渲染路径重写(本项唯一实现重写点) -``` - -#### 4.5.3 API 适配点 - -| dev 侧(26.1) | port 侧(1.21.1) | 落点 | -|---|---|---| -| `Identifier` | `ResourceLocation.fromNamespaceAndPath` | `AnvilLibSpaceSelect`、网络层 | -| `ARGB`(26.1 新类,javap「找不到类」) | 手写 `(a<<24)|(r<<16)|(g<<8)|b` int 打包 | `District.color()` | -| `player.getInventory().getSelectedSlot()`(javap:1.21.1 `Inventory` 无此方法) | 公开字段 `Inventory.selected` | `DistrictManager`、`SpaceSelectItem` | -| `ClientPacketDistributor.sendToServer(payload)`(26.1 新增;NF 21.1 sources 无 `client/network` 目录) | `net.neoforged.neoforge.network.PacketDistributor.sendToServer(payload)` | `ClientDistrictManager` | -| `RenderLevelStageEvent.AfterTranslucentParticles` 子类 + `event.getLevelRenderState()` | 单一 `RenderLevelStageEvent`:`getStage()`(`Stage.AFTER_TRANSLUCENT_BLOCKS`/`AFTER_PARTICLES`)、`getPoseStack()`、`getCamera()`(NF sources) | `DistrictRenderer` | -| `BlockOutlineRenderState`、`LevelRenderState`、`cameraRenderState.pos`、`windowRenderState.appropriateLineWidth` | 无对应物(1.21.1 无 `client/renderer/state/level` 包);相机位置取 `event.getCamera().getPosition()`,线宽用默认 | `DistrictRenderer` | -| `ShapeRenderer`(1.21.1 不存在,1.21.2 才引入) | 公开静态 `LevelRenderer.renderVoxelShape(PoseStack, VertexConsumer, VoxelShape, dx, dy, dz, r, g, b, a, boolean)` 与 `renderLineBox(PoseStack, VertexConsumer, AABB, r, g, b, a)`(javap) | `DistrictRenderer`(dev 的 `BlockOutlineRenderState` 分支渲染改写为直接 `renderVoxelShape(shape, 平移量, r,g,b,a)`;`DEBUG_SHAPES` 分支保留 `renderLineBox` 逐 shape 绘制) | -| `RenderTypes.lines()`(26.1 类) | `RenderType.lines()`(javap) | `DistrictRenderer` | -| `mc.hasControlDown()`/`mc.hasAltDown()`(javap:1.21.1 `Minecraft` 无此方法) | 静态 `Screen.hasControlDown()`/`Screen.hasAltDown()`(javap) | `SpaceSelectScrollHandler` | -| `Direction.getApproximateNearest(x,y,z)` | `Direction.getNearest(double,double,double)`(javap) | `SpaceSelectScrollHandler` | -| `InputEvent.MouseScrollingEvent` + `getScrollDeltaY()` + `setCanceled(true)` | 同名存在(NF sources);`player.getViewVector(1.0F)` 存在 | `SpaceSelectScrollHandler` 无需改 | - -**无需改动**:`AnvilLibSpaceSelectClient`(`@Mod(value=..., dist=Dist.CLIENT)` 为 21.1 标准)、`PlayerCreateDistrictEvent`+Handler(`PlayerEvent(Player)` 构造 + `getEntity()`、`@EventBusSubscriber(modid=...)` 属性名一致)、`BlockPos.MutableBlockPos`(公开无参构造)、`Direction.getStepX`、`Direction.Axis.choose`、`Vec3` 公开字段、`AABB`、`Shapes.create(double×6)`、`Minecraft.hitResult`、`CustomPacketPayload.Type` record(构造参数为 `ResourceLocation`)、`ByteBufCodecs.BOOL`、`BlockPos.STREAM_CODEC`、`RegisterPayloadHandlersEvent.registrar(String)`。 - -#### 4.5.4 验收方式 - -1. `gradlew :module.space-select:compileJava` 通过。 -2. 行为验证:手持选区物品框选区域 → `DistrictManager` 创建区域;`DistrictRenderer` 在 `RenderLevelStageEvent` 各 stage 正确绘制线框(平移量/颜色语义目测确认,V-16);滚轮缩放/移动(`Screen.hasControlDown` 组合键 + `Direction.getNearest`)生效;网络包(`SpaceSelectPayload` 经 port `NetworkRegistrar` 注册)双向收发正常。 -3. `git diff port/1.21.1 dev/26.1 -- module.space-select` 只读复核只剩预期差异。 - ---- - -### 4.6 回移植项 #P6:module.font 整模块(中) - -> ⚠️ 2026-08-12:module.font 已落地为独立模块,其与 module.rendering 的依赖已随渲染模块移除而删除(build.gradle 不再声明 anvillib-rendering-neoforge-1.21.1)。 - -#### 4.6.1 目标 - -把 dev 侧 SDF 字体模块 `module.font`(25 文件,AWT 字体 → CPU 生成 SDF 图集 → GPU 采样渲染)整体回移植,作为新模块 `anvillib-font-neoforge-1.21.1`;约 60% 纯 CPU 逻辑直接搬,GPU 上传/绘制层与 GUI 屏幕按 1.21.1 API 改写。**依赖 module.rendering 的 1.21.1 版(#P7)**(build.gradle jarJar 依赖 `anvillib-rendering-neoforge-1.21.1`;纯逻辑部分可在 #P7a 完成后先行落地)。 - -#### 4.6.2 步骤 - -```bash -# 1) 提取 25 个文件 -for f in $(git ls-tree -r --name-only dev/26.1 -- module.font | grep -E '\.(java|json|toml|fsh|vsh|glsl)$'); do - git show "dev/26.1:$f" > "$f" -done -# 2) build.gradle:依赖 anvillib-rendering-neoforge-26.1 → anvillib-rendering-neoforge-1.21.1; -# gradle.properties interface_injection=true 保留(1.21.1 支持) -# 3) settings.gradle 注册 module.font → anvillib-font-neoforge-1.21.1 -# 4) 按 4.6.3 分类处理:直接移植 9 项原样搬 + 反向适配 8 项改写 -``` - -#### 4.6.3 分类与 API 适配点 - -**可直接移植(9 项)**: - -| 文件 | 适配点 | -|---|---| -| `sdf/SdfGlyphAtlas.java`、`sdf/SdfGlyphPage.java` | 核心为 AWT(`Font`/`Graphics2D`/`BufferedImage`)+ 并发(虚拟线程、`CompletableFuture`)+ Dead Reckoning EDT 距离场算法,无 MC 渲染 API;`SdfGlyphPage.textureId` 的 `Identifier`→`ResourceLocation`;`Math.clamp`(Java 21 ✓)、`Thread.ofVirtual()`(Java 21 ✓,port `java_version=21`);`GLYPH_EXECUTOR` 虚拟线程保留 | -| `sdf/SdfTextLayout.java`、`ALFont.java` | 纯 CPU(quad 生成 + LRU 缓存);`Identifier` 改名;`Mth.ceil`、`FormattedCharSequence.accept`、`FormattedText.getString` 1.21.1 全有;含 `flatten`/`wrapLines` 工具原样搬 | -| `FontManager.java` | 纯 AWT/Swing(`GraphicsEnvironment`/`UIManager`) | -| `AnvilLibFontConfig.java` | 纯 Gson + `FMLLoader.getCurrent().getGameDir()`——报告子代理称 1.21.1 可用,但与其它子代理「1.21.1 的 `FMLLoader` 无 `getCurrent()`」(loader 4.0.42/4.0.43 源码)结论矛盾,**标为待验证(V-12)**:以编译为准,报错则改 `FMLPaths.GAMEDIR.get()` | -| `AnvilLibFont.java` | `IConfigScreenFactory`(21.1.226 sources 核实存在) | -| `data/AnvilLibFontData.java` | `GatherDataEvent.Client` ❌ → 单一 `GatherDataEvent` + `includeClient()` 判断(21.1.226 构造器 `(ModContainer, DataGenerator, DataGeneratorConfig, ExistingFileHelper)` 核实);`generator.getPackOutput()`、`LanguageProvider`、`Util.makeDescriptionId` 存在 | -| shader 资源(`sdf_text.fsh/vsh`) | GLSL 原样搬,补 1.21.1 `.json` 声明 | - -**需反向适配(8 项)**: - -| 文件 | 适配点 | -|---|---| -| `sdf/SdfAtlasTexture.java` | 上传层:`GpuDevice.createTexture/createCommandEncoder().writeToTexture` + `RenderSystem.getSamplerCache()` → `DynamicTexture`(或 `NativeImage` 直传)+ `TextureManager.register(ResourceLocation, AbstractTexture)` + `RenderSystem.setShaderTexture`;`toNativeImage` 像素拷贝逻辑复用;`SdfTexture` 内部类改为基于 `DynamicTexture`;`PageEntry.version` 版本号缓存机制保留 | -| `ALFPipelines.java` | 26.1 `RenderPipeline` 注册 → 1.21.1 `RegisterShadersEvent` 注册 `ShaderInstance`(port 侧 wheel `LibShaders` 先例);`SDF_TEXT_FORMAT` 用 1.21.1 `VertexFormat` 构造器重建(`new VertexFormat(ImmutableList.of(...), Mode.QUADS)`,1.21.1 无 builder) | -| `sdf/SdfTextRenderer.java` | 绘制提交走 `GuiGraphicsExtractor.submitGuiElementRenderState` → `RegisterShadersEvent` 注册 SDF shader + `GuiGraphics` 的 `BufferSource`/`RenderType` 直接画 quad;**分段样式逻辑**(颜色/粗斜体/下划线/删除线/混淆分段、`obfuscateCodepoint`、`drawDecorations`、居中/换行)纯逻辑原样复用;`drawAtlasPipeline` 改为 `guiGraphics.draw`(自定义 RenderType)或直接 `BufferBuilder` 提交 | -| `sdf/state/SdfTextRenderState.java` | `buildVertices` 的 UV/坐标计算复用;26.1 状态对象基类替换为 1.21.1 等价载体 | -| `extension/GuiGraphicsExtractorExtension.java` + `mixin/GuiGraphicsExtractorMixin.java` + `interface_injections.json` | 目标类 `GuiGraphicsExtractor` 1.21.1 不存在 → 注入 `net/minecraft/client/gui/GuiGraphics`(interface injection 机制 1.21.1 可用,port 侧已有先例);`anvillib$text` 系列 11 个默认方法签名保留(`FormattedText`/`FormattedCharSequence`/`Component` 1.21.1 全有);`ARGB`(26.1 新类)→ `FastColor.ARGB32` 或直接 int 运算(`textWithBackdrop` 中 `ARGB.multiply` 需手写) | -| `screen/FontConfigScreen.java`、`screen/FontTestScreen.java`、`screen/widget/Dropdown.java` | 26.1 `Screen.extractRenderState`/widget `extractWidgetRenderState`/`MouseButtonEvent` → 1.21.1 `Screen.render(GuiGraphics, int, int, float)`、`AbstractWidget.renderWidget(GuiGraphics)`、`mouseClicked/mouseReleased/mouseDragged/mouseScrolled`;布局/滚动/选项逻辑(`Dropdown` 的 expanded/scrollOffset/shielding 状态机)纯逻辑复用;`Button.builder().size().pos()`、`Component.translatable` 存在;绘制调用改 `guiGraphics.fill/drawString/drawCenteredString` | - -#### 4.6.4 验收方式 - -1. `gradlew :module.font:compileJava` 通过(依赖 `:module.rendering` 编译产物或已发布的 `-1.21.1` 快照)。 -2. `gradlew :module.font:runData` 生成 `en_us.json` 并核对。 -3. 行为验证(运行 `runClient`):`FontConfigScreen`/`FontTestScreen` 打开正常;SDF 文字渲染(含粗斜体/下划线/删除线/混淆分段)与居中/换行行为正确;`anvillib$text` 系列扩展在 `GuiGraphics` 上可用;字体配置(`FontManager` AWT 字体枚举)生效。 - ---- - -### 4.7 回移植项 #P7:module.rendering(高 · 按功能块分批) - -> ⚠️ 2026-08-12:module.rendering 已从 port/1.21.1 整体移除,本节内容仅为历史计划记录。 - -#### 4.7.0 总体说明与拆批依据 - -`module.rendering`(约 113 文件)在 port/1.21.1 上完全不存在(`git diff --name-status` 全部为 `A`)。其功能层深度构建在 26.1 全新渲染架构之上(`GpuDevice/CommandEncoder/RenderPass/GpuBuffer`、`RenderPipeline/RenderState`、`SubmitNodeStorage/FeatureRenderDispatcher/GameRenderState`、`GuiGraphicsExtractor`),**不能整模块移植**;1.21.1 等价物为 `RegisterShadersEvent + ShaderInstance`、`RenderTarget`、`VertexBuffer`、`GuiGraphics`、`MultiBufferSource`。按功能块拆为 **#P7a–#P7e** 五批,每批独立步骤与验收,作为单一模块(`anvillib-rendering-neoforge-1.21.1`)落地。 - -**已核实的平台基线**:port 侧 `java_version=21`(`Math.clamp`、虚拟线程、`List.getFirst` 均可用);port 侧已有 `RegisterShadersEvent + ShaderInstance` 用法(`module.wheel/.../init/LibShaders.java`);`IConfigScreenFactory` ✓;`interface_injections.json` 机制 ✓(module.moveable-entity-block、module.recipe 已用);NeoForge 21.1.226 有 `RenderFrameEvent`、`RenderLevelStageEvent`、`RegisterClientReloadListenersEvent`、`GatherDataEvent`(无 `Client` 子类),**无** `RegisterRenderPipelinesEvent`、`RegisterPictureInPictureRenderersEvent`、`AddClientReloadListenersEvent`、`ConfigureMainRenderTargetEvent`(后两者名称/形态已变化,均经 21.1.226 sources jar 核实)。 - -**模块落位**(#P7a–#P7e 共用):`settings.gradle` 注册 `module.rendering` → `anvillib-rendering-neoforge-1.21.1`;mods.toml 按 port 侧模板对齐(`${loader_version_range}` 等模板变量由 port 构建脚本注入,V-7);mixins.json 按 1.21.1 实际 mixin 清单重建;accesstransformer.cfg 整体重写(dev AT 目标 `GuiRenderer$Draw`、`GpuDevice`、`RenderType.state` 等绝大多数 1.21.1 不存在,仅 `BufferBuilder`(`vertices`/`format`/`mode`)与 `RenderType.name/outline` 等少数有意义)。 - -#### 4.7.1 子项 #P7a:纯逻辑与资源(19 项,可直接移植,低) - -**目标**:把与平台无关的 19 项纯逻辑/资源先落地,形成模块骨架。 - -**步骤**:从 dev 侧提取以下文件原样落位(`foundation/buffers/layout/**` 11 文件 + `Std140LayoutRulesTest.java`、`sdf/Sdf2d.java`、`sdf/SdfParameters.java`、`sdf/SdfRenderType.java`、`sdf/SdfPassType.java`(剥离 `BufferObject` 的 `upload(CommandEncoder,...)` 依赖,见下)、`foundation/buffers/object/BufferObject.java`(反适配)、`foundation/compound/DirtyTracked.java`、`event/MainTargetResizeEvent.java` + `mixins/GameRendererMixin.java`、`event/RegisterComputePipelinesEvent.java`、`integration/mixins/ALRIntegrationCompatMixinPlugin.java`、`mixins/MinecraftMixin.java`、`mixins/RenderTypeMixin.java` + `extension/ALRRenderTypeExtension.java`、`ALRSharedMath.java`、`util/Timer.java`、`foundation/buffers/EmptyVertexConsumer.java`、`EmptyBufferSource.java`、`EmptyOutlineBufferSource.java`、`TransformingVertexConsumerWrapper.java`、`foundation/ALRMeshSorting.java`、`extension/blaze3d/MemoryBarrierFlag.java`、`extension/blaze3d/ALRComputeCapabilities.java`、`foundation/buffers/GpuBufferConstants.java`、shader 资源(7 个 shader + util.glsl))。 - -**API 适配点**: - -| 项 | 适配点 | -|---|---| -| `Sdf2d` | 9 种 SDF 距离函数,仅用 `Mth.length/cos/sin/clamp/sign`(1.21.1 全有),原样搬 | -| `SdfParameters`/`SdfRenderType`/`SdfPassType` | 数据模型 + std140 布局定义,仅依赖 `Mth`/JOML/`BufferLayout`;剥离 `BufferObject` 基类中 `upload(CommandEncoder, GpuBufferSlice)` 与 `DynamicUniformStorage` 依赖 | -| `BufferObject` 基类 | `implements DynamicUniformStorage.DynamicUniform`(26.1 新类)→ 去掉接口与 `createDynamicStorage`;`upload(CommandEncoder, GpuBufferSlice)` → 改为「写入用户提供的 `ByteBuffer`」;各 UBO 子类(bloom/glitch/blur 参数类)改动极小 | -| `Timer` | `getDeltaTracker()`(26.1)→ `Minecraft.getInstance().getTimer().getGameTimeDeltaPartialTick(paused)`,一行 | -| `EmptyVertexConsumer`/`TransformingVertexConsumerWrapper` | `VertexConsumer` 接口方法集(`addVertex/setColor/setUv/setUv1/setUv2/setNormal/setLineWidth`)两版本一致 | -| `EmptyBufferSource`/`EmptyOutlineBufferSource` | `MultiBufferSource.BufferSource` 1.21.1 存在;构造器参数 `(BufferAllocator, RenderType)` 需核对(dev 侧 `super(null, null)` 需小调,V-3) | -| `ALRMeshSorting` | `VertexSorting`/`CompactVectorArray`/`MeshData.sortQuads`/`SortState.buildSortedIndexBuffer` 1.21.1 均存在(1.20.2+ 稳定 API,LevelRenderer 使用;存在性判定见 V-1) | -| `MinecraftMixin` | `Minecraft.(GameConfig)` 与 `updateLevelInEngines(ClientLevel, boolean)` 1.21.1 均存在(注入点签名一致) | -| `RenderTypeMixin` + `ALRRenderTypeExtension`(bloom 标记字段) | 1.21.1 `RenderType` 存在;interface injection 机制可用(port 侧已有 `interface_injections.json` 先例) | -| `MainTargetResizeEvent` + `GameRendererMixin` | `Event`/`IModBusEvent`/`ModLoader.postEvent` 1.21.1 存在;`GameRenderer.resize(int,int)` 存在(长期稳定方法) | -| `RegisterComputePipelinesEvent` | 纯 `Event`/`IModBusEvent`,1.21.1 存在;随 #P7e 的 compute 结构一起迁移 | -| `ALRIntegrationCompatMixinPlugin` | `IMixinConfigPlugin` 机制版本无关 | -| `MemoryBarrierFlag`/`ALRComputeCapabilities`/`GpuBufferConstants` | 纯 GL 常量,仅依赖 LWJGL | -| `Mth.roundToward` | 1.20.2+ 引入,1.21.1 `net.minecraft.util.Mth` 存在——**待验证(V-1)**:本机 gradle 缓存无 1.21.1 client jar 可反查,移植首个编译时以 1.21.1 client sources 复核 | - -**验收**:`gradlew :module.rendering:compileJava` 通过;`Std140LayoutRulesTest` 作为 JVM 测试随 `gradlew :module.rendering:test` 运行通过(纯逻辑验证,符合仓库 TDD 规范)。 - -#### 4.7.2 子项 #P7b:后处理三件套(Bloom / GaussianBlur / Glitch,需反向适配,高) - -**目标**:把「全屏后处理 + 高斯金字塔」三件套的算法与 GLSL 保留,执行层从 26.1 `GpuDevice/CommandEncoder/RenderPass` 体系重写到 1.21.1 `RenderTarget + ShaderInstance` 体系。 - -**来源提交**:`7121bf6`、`b5f5420`(#17/#20)、`6bb3269`(#38 resize 修复)、`0e8290d`(#92 稳定化)、`64bcf21`(#33 GaussianBlur)、`eccdc6d`(#43 Glitch)、`6b5a544`(#44)。 - -**步骤与适配点**: - -```bash -# dev 侧文件(落点新包 dev.anvilcraft.lib.v2.rendering.bloom / blur / glitch): -# bloom/BloomPostEffect.java、BloomParametersUbo.java、BloomPipelineParametersUbo.java、TransformsUbo.java、 -# BloomRenderCallback.java、foundation/BloomSubmitNodeStorage.java、ALRPostEffects.java、 -# blur/GaussianBlur.java、blur/BlurParametersUbo.java、glitch/GlitchPostEffect.java、glitch/GlitchParametersUbo.java、 -# shaders(apply_bloom/down_sample/up_sample/blit 及 glitch/blur 相关) -``` - -- **保留**:5 级降采样/升采样流程、全部 GLSL、UBO 参数类的布局定义(`BlurParametersUbo`/`GlitchParametersUbo` 直接复用)。 -- **重写**: - - `RenderTarget`(`MainTarget`/`TextureTarget` 存在)+ 自定义 `ShaderInstance`(`RegisterShadersEvent` 注册,port 侧 wheel 已有先例)+ 全屏四边形(`BufferUploader`/`Tesselator`)逐 pass 绘制;UBO 上传改为 `ShaderInstance` uniform 或自管 GL 缓冲; - - `RenderSystem.outputColorTextureOverride`(26.1 字段)改为临时切换 framebuffer 目标;bloom 重绘收集改为 `MultiBufferSource.BufferSource` 重画一遍(丢弃 `SubmitNodeStorage` 路径——`BloomSubmitNodeStorage`/`CompoundSubmitNodeStorage` 本身不可移植,见第 5 章); - - 尺寸变化监听用 `GameRendererMixin`(#P7a 已落位); - - `GlitchPostEffect` 的 `DeltaTracker` → `Minecraft.getTimer().getGameTimeDeltaPartialTick`;`process(GpuTextureView)` 改为「输入 RenderTarget 颜色纹理 → 输出 RenderTarget」。 -- **挂接**:`RenderLevelStageEvent.AfterTranslucentParticles/AfterLevel`(均存在)。 - -**验收**:`gradlew :module.rendering:compileJava` 通过;运行验证 Bloom/Blur/Glitch 后处理效果正确(含窗口 resize 后尺寸跟随、帧稳定);glitch 的时间驱动动画正常。 - -#### 4.7.3 子项 #P7c:SDF GUI 图形(需反向适配,中) - -**目标**:把 `Sdf2d`(#P7a 已落位)之上的 `SdfGraphics` 绘制编排从 26.1 GUI 提交体系重写到 1.21.1 `GuiGraphics + BufferSource + 自定义 RenderType`。 - -**来源提交**:`494274e`(#21)、`27a8c79`、`05a1f6c`(#39)、`e8ee3f9`(#40)、`d7a8f5d`(#38 AMD 修复)、`6b5a544`(#44)、`6d5a97b`(#45)。 - -**步骤与适配点**: - -- `SdfGraphics._draw` 改为「用 `GuiGraphics` 的 `BufferSource` + 自定义 `RenderType`(绑定 SDF shader)提交 4 顶点 quad,参数经 uniform 上传」;`graphics.pose()` 的 `Matrix3x2f` 换成 `PoseStack` 2D 变换;命中测试 `collide` 原样保留;旋转/居中数学全部复用。 -- `MAX_SDFS=256` 的 UBO 数组在 1.21.1 若走 uniform 需改为按需绑定或保留自管 GL UBO(`GL46.glBindBufferRange` 可用,LWJGL 全量存在)。 -- `mixins/GuiRendererMixin.java`(SDF 注入部分)**不迁移**:目标类 `GuiRenderer`/`GuiElementRenderState` 1.21.1 不存在(见第 5 章)。 -- shader 资源(`sdf_graphics.fsh/vsh`)GLSL 复用 + 补 `.json` 声明。 - -**验收**:`gradlew :module.rendering:compileJava` 通过;运行验证 SDF 图形(圆/矩形/圆角等 9 种距离函数)绘制正确、命中测试准确。 - -#### 4.7.4 子项 #P7d:CachedBER(缓存型 BER 渲染管线,需反向适配,高——难度最高的一项) - -**目标**:把「BER 渲染结果按 chunk 缓存为 GPU 顶点缓冲、视锥剔除、半透明排序、bloom 双画」的核心功能保留,执行层从 26.1 `GpuBuffer/RenderPass/SubmitNodeStorage` 重写到 1.21.1 `VertexBuffer + RenderSystem.draw + RenderType.shader()` 老路径。 - -**来源提交**:`2751be4`(#26)、`cff1802`(#29)、`d121047`(#68)、`48bfe55`、`fba1027`(#76)、`084c503`(#85)、`0e8290d`(#92)。 - -**步骤与适配点**: - -- **保留**:`CachedBlockEntityRenderingPipeline` 的 chunk 表、`RebuildTask` 编译/上传队列、`blockRemoved/update/forcedUpdate` 失效逻辑;`ALRMeshSorting` 原样搬(#P7a 已落位)。 -- **重写**: - - `GpuBuffer` → `VertexBuffer`(`com.mojang.blaze3d.vertex.VertexBuffer`,`upload(MeshData)`/`bind()`); - - 渲染走 `RenderSystem.draw` + `RenderType.shader()` 老路径(port 侧 wheel 已证明 ShaderInstance 路径可用); - - `CachedBlockEntityRenderer` 接口改为单阶段 `render(...)`(1.21.1 的 BER 调度是 `BlockEntityRenderDispatcher.render(be, partialTick, poseStack, bufferSource, light, overlay, camera, frustum)` 单阶段,无 extract/submit 两阶段); - - `FullyBufferedBufferSource`/`VertexBufferHost` 随管线重写。 -- **挂接**:`RenderLevelStageEvent.AfterOpaqueBlocks/AfterTranslucentFeatures` + `RenderFrameEvent.Pre`。 -- **Iris 集成**(`integration/IrisSupport.java` + 两个集成 mixin):`net.irisshaders.iris.api.v0.IrisApi`(`isShaderPackInUse`)在 1.21.1 的 Iris 1.7.x 中存在(长期 API);`net.irisshaders.iris.vertices.ImmediateState` 与 `skipExtension`/`isRenderingLevel` 字段需按 1.21.1 Iris 版本核实(dev compileOnly 依赖为 Iris 1.10.9+26.1,1.21.1 对应 1.7.x,**待验证 V-2**);`oculus` 分支 1.21.1 不存在,`ModList.get().isLoaded("oculus")` 判断可保留但恒假;build.gradle 中 Modrinth 依赖坐标同步替换(**待验证 V-6**:坐标需移植时查询)。 - -**验收**:`gradlew :module.rendering:compileJava` 通过;运行验证:缓存 BER 渲染正确(含视锥剔除与半透明排序)、方块移除/更新后失效重建正确、bloom 双画效果正常、Iris 开启时回退路径正常(无 Iris 时可跳过)。 - -#### 4.7.5 子项 #P7e:GPU 计算管线扩展(Compute,需反向适配,高) - -**目标**:把 GL 4.3 compute 管线扩展保留全部 API 形状(`ALRComputePipeline.builder()` 等)与 binding 模型,后端从「26.1 `GpuDevice` mixin 扩展」重写为独立自管 GL 工具层。 - -**来源提交**:`7d07f20`(#41)、`0e8290d`(#92)。 - -**步骤与适配点**: - -| 部分 | 适配点 | -|---|---| -| `extension/blaze3d/MemoryBarrierFlag.java`、`ALRComputeCapabilities.java`、`foundation/buffers/GpuBufferConstants.java` | 【直接搬】纯 GL 常量,仅依赖 LWJGL(#P7a 已落位) | -| `ALRComputePipeline`/`ALRComputePass`/bindings | 【直接搬】纯描述结构;`ShaderDefines` 是 26.1 类 → 改为简单 `Map` defines(1.21.1 的 `ShaderInstance` 用 `Supplier` 方式,无 ShaderDefines,**待验证 V-8**) | -| `ALRComputeShaderManager` | 【直接搬 + 小适配】`SimplePreparableReloadListener` 机制 1.21.1 存在,经 `RegisterClientReloadListenersEvent` 注册;`ShaderManager.createPreprocessor` 为 26.1 静态方法 → 1.21.1 的 `ShaderInstance` 内部有自己的 preprocessor 逻辑,改为自实现或简化 | -| `GlComputePassBackend`/`ALRComputePassBackend`/扩展接口与全部 6 个 blaze3d mixins(`GpuDevice/CommandEncoder/GlDevice/GlCommandEncoder/DirectStateAccess/GlDebugLabel`) | 【重写】mixin 目标 1.21.1 不存在;改为独立类持有 program/buffer 状态机;底层 GL 能力(`glDispatchCompute`、`glBindImageTexture`、`glBindBufferRange`(SSBO/atomic counter)、`glMemoryBarrier`、`glObjectLabel`)在 1.21.1 的 LWJGL 中完整可用;program 编译/链接逻辑(`GL46.glCreateShader(ARBComputeShader.GL_COMPUTE_SHADER)` 等)逐行复用 | -| `event/RegisterComputePipelinesEvent.java` | 机制保留(#P7a 已落位;纯 `Event`/`IModBusEvent`) | - -**验收**:`gradlew :module.rendering:compileJava` 通过;运行验证:compute pipeline 注册、program 编译链接、dispatch 与内存屏障执行正确(可用 #P18 之外的临时测试或 module.test 现有用例核对,若随迁则登记)。 - -#### 4.7.6 #P7 收尾(资源与元数据) - -- shader 文件原样搬 + 为每个管线补 1.21.1 的 `.json` 声明(1.21.1 的 `ShaderInstance` 加载约定 `shaders/core/xxx.json` 声明 uniform + `.vsh/.fsh`); -- mixins.json 按 1.21.1 实际 mixin 清单重建;AT 文件重写(见 4.7.0);mods.toml 按 port 模板;`AnvilLibRendering` 挂接点中 `AddClientReloadListenersEvent` → `RegisterClientReloadListenersEvent`。 - -**验收**:`gradlew :module.rendering:build`(含 jar/sourcesJar)通过;`gradlew :module.rendering:runData`(如有 datagen 需求)通过。 - ---- - -### 4.8 回移植项 #P8:renderdoc-loader(低) - -> ⚠️ 2026-08-12:renderdoc-loader 已随 module.rendering 一并从 port/1.21.1 移除,本节内容仅为历史计划记录。 - -#### 4.8.1 目标 - -把 dev 侧 RenderDoc 调试用 Java Agent 模块 `renderdoc-loader`(2 文件)回移植,作为 `renderdoc-loader-internal`,实现 `premain`/`agentmain` 加载 RenderDoc DLL。 - -#### 4.8.2 步骤 - -```bash -# 1) 提取 2 个 Java 文件(RenderDocAgent.java、package-info.java),源码逐字搬移 -git show dev/26.1:renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/RenderDocAgent.java -git show dev/26.1:renderdoc-loader/src/main/java/dev/anvilcraft/lib/renderdoc/loader/package-info.java -# 2) jar manifest 的 Premain-Class/Agent-Class 属性照抄 -# 3) build.gradle 按 4.8.3 改写 -``` - -#### 4.8.3 API 适配点 - -| dev 侧(26.1) | port 侧(1.21.1) | -|---|---| -| `JavaLanguageVersion.of(25)` | `JavaLanguageVersion.of(21)`(port 工具链) | -| `alias libs.plugins.jreleaser`(port `gradle/libs.versions.toml` 无 jreleaser 别名,仅 modDevGradle/lombok/machete) | 改为 `id 'org.jreleaser' version '1.23.0'`(或省略——本模块不发布中央仓库) | -| `alias libs.plugins.lombok` | port toml 有该别名,可直接用 | -| dev 侧 `module.gradle` 的 `clientRenderDoc`/`clientRenderDocIrisWorkaround` run 配置引用该 agent jar | port 无 module.gradle;如需 RenderDoc 调试需在目标模块 build.gradle 手工接线(`-javaagent:` + `-Dneoforge.rendernurse.renderdoc.library=...`) | - -功能代码零依赖:仅 `java.lang.instrument` + jetbrains annotations compileOnly,`System.load` 读取 `renderdoc.library.path`(或 `neoforge.rendernurse.renderdoc.library`)系统属性。 - -#### 4.8.4 验收方式 - -1. `gradlew :renderdoc-loader:compileJava` 通过,jar 产物 manifest 含 `Premain-Class`/`Agent-Class`。 -2. 冒烟:`java -javaagent:renderdoc-loader.jar -Drenderdoc.library.path= -version`(或任一 JVM 启动)确认 agent 加载不抛异常(无 RenderDoc DLL 时仅跳过加载)。 - ---- - -### 4.9 回移植项 #P9:module.config——group 分组 + TranslatableEnum + TOML key 点分隔(低) - -#### 4.9.1 目标 - -把 dev 侧 module.config 的三项功能演进回移植:`@Config(group=…)` 配置分组(子目录文件)、`TranslatableEnum` 枚举翻译 key 生成、TOML 语言 key 点分隔化。 - -#### 4.9.2 步骤 - -```bash -# 1) group 分组(3d50d76):Config.java(+group() 字段)、ConfigManager.java(group 传递)、 -# ConfigRecord.java(+group 记录组件,getFileName() 返回 group/modId-type.toml) -# 2) TranslatableEnum(946938f #56):util/TranslatableEnum.java 新文件 + ConfigData.java -# (+ENUM_STRING 常量、enumValueAdd()、枚举字段检测分支、@Slf4j) -# 3) TOML key 点分隔(946938f #56):FormattingUtil.java(+toPointSplitName,并用 Arrays.stream 重构)、 -# ConfigData.java(TOML_STRING/TOML_TITLE_STRING 的 key 改用 FormattingUtil.toPointSplitName(name)) -# 4) (可选)ConfigManager 模组容器获取逻辑修复(c0a619b):ModList.get().getModContainerById(modId) -# → Optional.of(ModList.get()).flatMap(...)——1.21.1 中 ModList.get() 不返回 null,移植后行为等价无副作用, -# 该改动本身有争议(Optional.of(null) 仍抛 NPE),建议评估后决定是否保留 -``` - -#### 4.9.3 API 适配点 - -| 项 | 核实结论 | -|---|---| -| 配置子目录文件名 | 1.21.1 的 `ConfigTracker.openConfig` 用 `basePath.resolve(config.getFileName())` 拼接路径(`ConfigTracker.java:138`),`setupConfigFile` 中 `Files.createDirectories(file.getParent())` 自动建目录(`ConfigTracker.java:250-251`)——**原生支持子目录**,无平台差异 | -| `TranslatableEnum` | 21.1.226 存在 `net.neoforged.neoforge.common.TranslatableEnum`(含 `getTranslatedName()` 默认方法);AnvilLib 扩展接口(override `getTranslatedName` + 新增 `getTranslationKey`)可在 21.1 父接口上原样编译;`LanguageProvider`、`Component.translatableWithFallback` 均存在 | -| `enumValueAdd` | 用 `field.get(null)` 反射取枚举常量(`IllegalAccessException` 有日志,符合仓库规范);`getTranslationKey()` 基于 `getCanonicalName()` 生成,与平台无关 | -| TOML 点分隔 | 纯 Java 字符串处理(`String.split` 正则 `[^A-Za-z0-9]` 切分过滤空串 + `Collectors.joining`),1.21.1 行为一致 | - -**破坏性提示**:TOML key 点分隔化会改变语言文件 key(如 `section.` 的 name 改为点分隔形式),使用方现有语言文件需同步——移植 commit 中必须说明;`section.anvillib.explosion.common.toml` 类键名变化会影响 #P3 的 lang 产物(若 #P3 先落地,可在 #P9 完成后补回 `group` 语义并重跑 datagen)。 - -#### 4.9.4 验收方式 - -1. `gradlew :module.config:compileJava` 通过。 -2. `gradlew :module.config:runData`(或调用模块的 runData)生成 lang 产物,核对 `section.<点分隔名>.common.toml` 键名与枚举翻译 key(`enum.`)正确。 -3. 运行验证:`@Config(group="xxx")` 的模块配置生成于 `/xxx/-common.toml` 子目录。 - ---- - -### 4.10 回移植项 #P10:module.integration——数据加载拆分 + meter 递增(低 · 破坏性 API) - -#### 4.10.1 目标 - -把 dev 侧 module.integration 的两项功能演进回移植:数据加载按客户端/服务端拆分(`applyData` → `applyClientData`/`applyServerData`)、集成实例计数进度条递增。 - -#### 4.10.2 步骤 - -```bash -# 1) 数据加载拆分(dc4d583): -# IntegrationInstance.java:dataLoader 字段拆为 clientDataLoader + serverDataLoader, -# findVirtual 查找方法名改为 applyClientData / applyServerData(各自 try/catch,失败置 null), -# invokeData() 拆为 invokeClientData() / invokeServerData(),toString() 同步更新; -# IntegrationManager.java:loadData 拆为 loadClientData / loadServerData 及对应 loadAll*, -# load 中 containsType 判断逻辑不变; -# IntegrationType.java:DATA → CLIENT_DATA + SERVER_DATA -# 2) meter 递增(e8efbd4):IntegrationManager.compileContent() 循环内 meter.increment()(一行) -``` - -#### 4.10.3 API 适配点 - -- 纯 `java.lang.invoke.MethodHandle` 反射 + 枚举逻辑,无 26.1 API;`Integration` 注解、`IntegrationHook` 两侧一致,不受影响。 -- 已 grep 确认仓库内无其他模块使用 `IntegrationType.DATA`/`applyData`,破坏面仅限模块自身——但 **API 破坏性**:旧 `applyData` 方法名不再被识别,下游使用方(如 AnvilCraft 主模组)需同步改名,移植 commit 必须说明。 -- `StartupNotificationManager`/`ProgressMeter` 1.21.1 存在(port 侧原代码已用 `StartupNotificationManager.popBar(meter)`)。 - -#### 4.10.4 验收方式 - -1. `gradlew :module.integration:compileJava` 通过。 -2. 行为验证:注册同时含 `applyClientData`/`applyServerData` 的集成实现,启动时两侧各自加载、进度条随实例递增;`@ApiStatus.Internal` 类(如附带迁移 `c72aa30` 时)不对外暴露。 - ---- - -### 4.11 回移植项 #P11:module.network——泛型检查修复 + Included 方法 + public(低) - -#### 4.11.1 目标 - -把 dev 侧 module.network 的三处小演进回移植:`PacketData` 泛型类型参数精确匹配修复、`NetworkUtil` 条件筛选发送工具、`PacketProtocol` 枚举 public 化。 - -#### 4.11.2 步骤 - -```bash -# 1) PacketData 泛型检查修复(20721b1 #52): -# register/PacketData.java 新增 isMatchingTypeArgument(Type, int, Class); -# 字段判断从 declaringClass.isAssignableFrom(Type.class) 反转为 Type.class.isAssignableFrom(fieldType), -# 并追加类型实参校验:Type 检查第 0 参、StreamCodec 检查第 1 参 -# 2) NetworkUtil Included 方法(5733a9d): -# util/NetworkUtil.java 新增 sendToAllPlayersIncluded(Predicate, ...) 与 -# sendToAllPlayersInDimensionIncluded(ServerLevel, Predicate, ...); -# included == null 时默认全通过;两方法与现有 sendToAllPlayersExcluded / sendToAllPlayersInDimension 对称; -# 顺带将 player.equals(excluded) 改为 Objects.equals 防 NPE -# 3) PacketProtocol public(6220427):enum → public enum -``` - -#### 4.11.3 API 适配点 - -- 纯 `java.lang.reflect.ParameterizedType` 反射逻辑与纯 Java,无平台依赖。 -- 已核实:21.1.226 存在 `PacketDistributor.sendToPlayer(ServerPlayer, CustomPacketPayload, CustomPacketPayload...)`(`PacketDistributor.java:50`);`ServerLifecycleHooks.getCurrentServer()`、`ServerLevel.players()`、`ServerPlayerList.getPlayers()` 均为 port 侧已在用 API。 - -#### 4.11.4 验收方式 - -1. `gradlew :module.network:compileJava` 通过。 -2. 行为验证:同一类中多个 `Type`/`StreamCodec` 静态字段时注册解析正确(旧逻辑无法区分,修复后可区分);`sendToAllPlayersIncluded` 过滤发送正确(含 `included == null` 全通过路径)。 - ---- - -### 4.12 回移植项 #P12:module.util——OutlineUtil + HolderGetter 重构 + withCount + @EqualsAndHashCode(中) - -#### 4.12.1 目标 - -把 dev 侧 module.util 的四项功能演进回移植:AABB 轮廓直接提取(OutlineUtil + Line + 测试)、predicate 系列 HolderGetter 参数化重构、`ItemIngredientPredicate.withCount`、`BlockStatePredicate` @EqualsAndHashCode;HolderGetter 重构连带 module.recipe 四个调用方。 - -#### 4.12.2 步骤 - -```bash -# 1) OutlineUtil(e4c09fb #75): -# util/OutlineUtil.java(434 行)整文件复制;util/client/Line.java(48 行)按 4.12.3 反适配; -# src/test/.../ShapeUtilJoinTimingTest.java(232 行)复制; -# build.gradle 集成:注册 runShapeUtilJoinTimingTest(JavaExec)+ check 依赖 + -# test { failOnNoDiscoveredTests = false } + test sourceSets 配置(按 port 侧旧式 build.gradle 结构) -# 2) HolderGetter 重构(bb09805): -# predicate/BlockStatePredicate.java、predicate/ItemIngredientPredicate.java、predicate/ItemPredicate.java: -# Builder.of(TagKey) → of(HolderGetter, TagKey); -# 连带 module.recipe:recipe/builder/InWorldRecipeBuilder.java、recipe/predicate/block/HasBlock.java、 -# HasBlockIngredient.java、recipe/predicate/item/HasItem.java 四处调用方同步改 -# 3) withCount(d84912e):ItemIngredientPredicate 新增 withCount(int) -# 4) @EqualsAndHashCode(c480e9f #49):BlockStatePredicate 加 lombok 注解 -``` - -#### 4.12.3 API 适配点 - -| 项 | 适配点 | -|---|---| -| `OutlineUtil` | 仅依赖 `net.minecraft.world.phys.AABB/Vec3`(1.21.1 存在,mappings 确认)+ Java 标准库(TreeSet/BitSet/CompletableFuture),**原样复制** | -| `Line.java` | `vertex.setLineWidth(...)` 在 1.21.1 **不存在**(javap 1.21.1 `VertexConsumer`:仅 addVertex/setColor/setNormal 等,该 API 是 1.21.2 渲染管线改造后引入)→ 删除 `.setLineWidth(thickness)` 链式调用(thickness 字段可保留但无效,或改用 `RenderSystem.lineWidth()` 在渲染层设置——1.21.1 线宽是全局状态,无法逐顶点设置);`PoseStack.Pose` 参数签名与 `pose.pose()`/`pose.last()` 兼容 | -| `ShapeUtilJoinTimingTest` | 测试依赖 port 侧**已有**的 `ShapeUtil.threadedJoin`(两分支 ShapeUtil 方法集一致)、`Shapes/BooleanOp/VoxelShape`(1.21.1 均有);测试无渲染依赖(只构造 Line 不渲染) | -| HolderGetter 重构 | 1.21.1 `HolderGetter` 有 `getOrThrow(TagKey)`(default 方法)与 `get(TagKey)`(返回 Optional)——`BlockStatePredicate` 用 `blocks.getOrThrow(tag)`,另两个用 `items.get(tag).map(Function.identity())`;`Builder.of(HolderGetter, TagKey)` 与 `items.get(tag).map(...)` 均可行 | -| **不可照搬项** | dev 侧同时删除了 `Builder.with(BlockState)` 方法——**不能照搬**:port 侧 `module.multiblock/.../MultiblockDefinition.java` 大量使用 `.with(state)`(已 grep 确认 8 处以上),dev 侧是配合 26.1 multiblock 重构删的;port 侧保留 `with(BlockState)` | -| `ResourceKey::location` | dev 侧 `ResourceKey::identifier` 为 26.1 改名,反向迁移时保留 port 写法 | -| `withCount` | `public ItemIngredientPredicate withCount(int count) { return new ItemIngredientPredicate(this.items, count, this.components, this.subPredicates); }`(port 侧 4 参构造器) | -| `@EqualsAndHashCode` | lombok 注解,无平台依赖;注意 dev 侧**未**对 `statesCache` 做 `@EqualsAndHashCode.Exclude`,缓存字段参与 equals(dev 侧现状,照搬并留意该语义) | - -#### 4.12.4 验收方式 - -1. `gradlew :module.util:compileJava` 与 `gradlew :module.recipe:compileJava` 通过(连带调用方编译验证)。 -2. `gradlew :module.util:runShapeUtilJoinTimingTest` 运行测试通过。 -3. 行为验证:`OutlineUtil` 提取 AABB 轮廓正确;`BlockStatePredicate.builder().of(holderLookup, tag)` 与 `.with(state)`(保留项)均正常;`withCount` 返回新实例不改原实例;equals/hashCode 包含属性与缓存字段(语义与 dev 一致)。 - ---- - -### 4.13 回移植项 #P13:module.registrum——14 种新注册表 builder/entry(低) - -#### 4.13.1 目标 - -把 dev 侧 module.registrum 的「注册表类型扩展」回移植:14 种新 builder/entry 与 `AbstractRegistrum` 对应入口方法(17 个注册入口,其中 3 个 Modifier 同类),以及两个零碎修复(item 默认 lang、OneTimeEventReceiver 空值防御)。 - -#### 4.13.2 步骤 - -```bash -# 1) 提取 builders/ 与 util/entry/ 新增文件 + AbstractRegistrum 入口方法 -for f in $(git ls-tree -r --name-only dev/26.1 -- module.registrum | grep -E '(builders|util/entry)/.*\.java$'); do - git show "dev/26.1:$f" > "$f" -done -# 2) 按 4.13.3 分类处理:直接移植 10 项原样搬 + 反向适配 9 项微调 -# 3) 零碎修复:BlockBuilder.item() 默认 lang(e3ba2cd,1 行:return item().defaultLang().build())、 -# util/OneTimeEventReceiver 空值防御(23f5686:pairs == null return;unregister 判空 bus) -``` - -#### 4.13.3 分类与 API 适配点 - -**可直接移植(10 项)**——已核实 1.21.1 API 全部存在: - -| 注册类型 | 核实结论 | -|---|---| -| GameEventBuilder/GameEventEntry + `gameEvent()` | 1.21.1 `GameEvent` 同为 record `GameEvent(int notificationRadius)`(mc-1.21.1 源码),`Registries.GAME_EVENT` 存在,零差异 | -| MobEffectBuilder/MobEffectEntry + `mobEffect()` | `Registries.MOB_EFFECT` 存在;builder 仅包 NonNullSupplier + DeferredHolder | -| PotionBuilder/PotionEntry + `potion()` | 1.21.1 存在 `Potion(@Nullable String name, MobEffectInstance... effects)` 构造;`Registries.POTION` 存在 | -| VillagerTypeBuilder + `villager()` / PoiTypeBuilder + `poi()` | `VillagerType::new`(String 构造)与 `Registries.VILLAGER_TYPE` 存在;`PoiType(Set, int, int)` 构造存在(内部经 `PoiStateSet` 包装),`Registries.POINT_OF_INTEREST_TYPE` 存在 | -| SelfBuilder/SelfEntry | 纯泛型包装(ResourceKey + NonNullSupplier + DeferredHolder),无平台 API | -| 三类 Modifier(Biome/GlobalLoot/Structure)+ entries | 21.1.226:`NeoForgeRegistries.Keys.BIOME_MODIFIER_SERIALIZERS/GLOBAL_LOOT_MODIFIER_SERIALIZERS/STRUCTURE_MODIFIER_SERIALIZERS` 均存在;`BiomeModifier`/`StructureModifier`/`IGlobalLootModifier` 均存在;builder 仅包 MapCodec | -| ConditionBuilder/ConditionEntry + `condition()` | 21.1.226:`NeoForgeRegistries.Keys.CONDITION_CODECS` 与 `ICondition` 存在 | - -**需反向适配(9 项)**: - -| 注册类型 | 适配点 | -|---|---| -| CreativeTabBuilder + `creativeTab()`(9c89bfa #27) | `CreativeModeTab.Builder` 具备 dev 用到的**全部**方法(title/icon/displayItems/alignedRight/hideTitle/noScrollBar/backgroundTexture/withSearchBar(int)/withScrollBarSpriteLocation/withTabsImage/withLabelColor/withTabFactory/withTabsBefore\|After);`Item.getDefaultInstance()`、`Util.makeDescriptionId` 存在;仅 `Identifier`→`ResourceLocation`(4 处参数与 2 处 `getResourceKey().identifier()`) | -| AttachmentBuilder/AttachmentEntry + `attachment()`(9627358 #30) | 21.1.226 `AttachmentType.Builder` 有 `serialize(Codec)`/`serialize(Codec, Predicate)`/`copyOnDeath`/`copyHandler`/`sync(...)` 系列;**差异**:dev 的 `serialize(MapCodec)` 在 21.1 是 `serialize(Codec)`(26.1 才改 MapCodec)→ 参数类型改回 Codec | -| DataComponentBuilder/DataComponentEntry + `dataComponent()`(9b2d0e3) | 1.21.1 `DataComponentType.Builder` 有 `persistent(Codec)`/`networkSynchronized(StreamCodec)`/`cacheEncoding()`,**无 `ignoreSwapAnimation()`**(1.21.2+ 才加)→ 删除该方法(其余原样) | -| SoundEventBuilder/SoundEventEntry + `soundEvent()`(c307a4c #28) | `SoundEvent.createVariableRangeEvent(ResourceLocation)`/`createFixedRangeEvent(ResourceLocation, float)` 存在;`Registries.SOUND_EVENT` 存在;仅 `Identifier`→`ResourceLocation` | -| RecipeSerializerBuilder/RecipeTypeBuilder/RecipeEntry + `recipe()`(c307a4c、9627358) | `Registries.RECIPE_SERIALIZER` 与 `Registries.RECIPE_TYPE` 1.21.1 **均存在**;但 `RecipeSerializer` 在 1.21.1 是**接口**(含 `MapCodec codec()` + `StreamCodec streamCodec()`),**不能 `new RecipeSerializer<>(...)`**(26.1 才改成类)→ `RecipeSerializerBuilder.createEntry()` 改为匿名类实现 `codec()`/`streamCodec()`;`RecipeTypeBuilder` 的匿名 `RecipeType{toString}` 写法原样可用 | -| VillagerProfessionBuilder + `profession()`(9b2d0e3) | 1.21.1 `VillagerProfession` 是 **6 元 record**:`(String name, Predicate> heldJobSite, Predicate> acquirableJobSite, ImmutableSet requestedItems, ImmutableSet secondaryPoi, @Nullable SoundEvent workSound)`——**没有 tradeSetsByLevel 参数**(`TradeSet` 是 26.1 专有);dev 的 `Component.translatable(...)` 名字参数 → 1.21.1 第一参数改传 `modid + ":" + name` 字符串;交易注册改走 1.21.1 机制(21.1.226 存在 `VillagerTradesEvent`,或引导用户用 `VillagerTrades.TRADES`) | -| (可选)`dataComponentPredicate()` 入口 | **不可移植**(见第 5 章):1.21.1 无 `DataComponentPredicate`(1.21.5+ 引入),等价物为 `ItemSubPredicate` + `Registries.ITEM_SUB_PREDICATE_TYPE`;按框架判定不迁移,不提供该入口 | - -#### 4.13.4 验收方式 - -1. `gradlew :module.registrum:compileJava` 通过(17 个入口全部编译可用)。 -2. 行为验证(可在 module.test 或调用方验证):GameEvent/MobEffect/Potion/VillagerType/PoiType 注册后可在 `BuiltInRegistries` 查到;`creativeTab()` 生成标签页;`recipe()` 注册的 RecipeSerializer 可被配方加载;`attachment()` 的附件类型可附加与同步。 -3. 破坏性核对:`AttachmentBuilder.serialize` 的 Codec 参数与 `DataComponentBuilder` 无 `ignoreSwapAnimation` 的签名差异在 javadoc 中注明。 - ---- - -### 4.14 回移植项 #P14:module.registrum datagen 增强(中) - -#### 4.14.1 目标 - -把 dev 侧 module.registrum 的 datagen 增强回移植(**不包含** 26.1 新模型生成器与 Runner 模式):`RegistrumRecipeProvider` 的 vanilla 方法 public 化、`Builder.dataMap(..., HolderLookup.Provider)` 重载;`GeneratorType` 拆分列为可选单独提交。 - -#### 4.14.2 步骤 - -```bash -# 1) RecipeProvider 方法 public 化(58f01d4 及 ea4468a/d4191a0/e07aaa5/c1b10b8/b8825bd): -# cherry-pick dev 版 RegistrumRecipeProvider 的公开方法清单到 port 版文件(port 版已有 -# cooking/smelting/stonecutting/storage 等主体,实际增量是 vanilla 方法 public 化 + -# shelf/colorItemWithDye 等);port 版原地增强,不新建 Runner -# 2) dataMap(provider) 重载(58f01d4):builders/Builder.java 新增带 HolderLookup.Provider 的 dataMap 重载 -# (port 已有 dataMap(type, val) 与 dataMap(type, factory)); -# providers/RegistrumDataMapProvider.java 按 4.14.3 捕获 provider -# 3) (可选)GeneratorType/ProviderType 拆分(58f01d4):单独提交,见 4.14.3 -``` - -#### 4.14.3 API 适配点 - -| 项 | 适配点 | -|---|---| -| RecipeProvider 方法 public 化 | 1.21.1 这些方法均为 **`protected static` 且首参为 `RecipeOutput`**(如 `oneToOneConversionRecipe(RecipeOutput, ...)`、`trimSmithing(RecipeOutput, Item, ResourceLocation)` 在 1.21.1 存在)——1.21.1 落法是「公开包装方法内调用 `RecipeProvider.xxx(this, ...)`」而非 override(26.1 才可 override);**需剔除**:`shaped(RecipeCategory, ItemStackTemplate)`/`shapeless(RecipeCategory, ItemStackTemplate)`(ItemStackTemplate 26.1 专有,1.21.1 用 ItemLike 重载);`generateForEnabledBlockFamilies(FeatureFlagSet)` 的 26.1 签名(1.21.1 为 `(RecipeOutput, FeatureFlagSet)`,port 已 override 旧签名);`safeKey(...)` + `save(this, ResourceKey)` 需改为 `ResourceLocation`(已核实 1.21.1 `RecipeBuilder.save(RecipeOutput, ResourceLocation)` 存在、**无 ResourceKey 重载**) | -| `dataMap(provider)` | dev 侧靠 26.1 `DataMapProvider.gather(HolderLookup.Provider)` 重载捕获 provider;1.21.1 的 `gather()` 无参数(port 已用 `@SuppressWarnings("removal")` 标记)→ 等价方式:在 `RegistrumDataMapProvider` 里 override `run(CachedOutput, HolderLookup.Provider)` 缓存 provider 再调 super(datagen 线程内安全),或直接使用构造器传入的 lookup future;`DataMapType` 在 21.1.226 存在(数据映射 1.21.1 引入) | -| GeneratorType 拆分(可选) | `GeneratorType` 接口本身无平台依赖,但拆分动机是支撑 26.1 新模型生成器(MODEL/BLOCKSTATE/ITEM_MODEL generator 装配)与 `RegistrumRecipeRunner`,**这两个消费方在 1.21.1 均不存在**;若同步迁移需连带改 `AbstractRegistrum.genData/setDataGenerator/addDataGenerator`、`RegistrumDataProvider`(新增 `subGenerators` 与 `putSubProvider`)、`Builder.setData/addMiscData` 签名(ProviderType→GeneratorType),并同步放弃「sides 过滤」(dev 注释掉 `includeServer/includeClient` 过滤是行为变更,1.21.1 需自行决策是否保留)——**建议低优先级、单列一次提交** | -| RegistrumRecipeRunner / 模型生成器五件套 | **不迁移**:`RecipeProvider.Runner` 1.21.1 不存在(26.1 才引入 runner 模式);模型生成器依赖 26.1 新模型 datagen(`net.minecraft.client.data.models.*`、`net.minecraft.client.renderer.block.dispatch.*`、NeoForge 26.1 `template.*`,且 `RegistrumBlockModelGenerator` 用 `ObfuscationReflectionHelper` 反射改写 26.1 字段)——port 保留旧体系 `RegistrumBlockstateProvider`/`RegistrumItemModelProvider`(功能等价) | - -#### 4.14.4 验收方式 - -1. `gradlew :module.registrum:compileJava` 通过;`gradlew :module.registrum:runData` 生成产物核对(配方/数据映射/provider 正常输出)。 -2. 行为验证:公开包装方法可从外部模块调用生成配方(如 `shelf`/`colorItemWithDye`);`dataMap(type, holderLookup, factory)` 在 datagen 时正确捕获 `HolderLookup.Provider`。 -3. 若做 GeneratorType 拆分:`gradlew :module.registrum:compileJava` + `runData` 回归核对(sides 过滤决策记录在 commit 说明中)。 - ---- - -### 4.15 回移植项 #P15:module.multiblock——事件化 + 快照复用 + BlockPos 键化 + 懒解析(中) - -#### 4.15.1 目标 - -把 dev 侧 module.multiblock 的四项功能演进/修复回移植:`DynamicMultiblockEvent` 成型/解散可取消事件、未加载区块快照复用、控制器位置 Long→BlockPos 键化、定义懒解析(ResourceKey + registryAccess)。**注意**:dev 侧大部分 multiblock 代码源自 1.21.1 移植(`7019b38`),真正增量即上述四项(M1–M4,M5/M6 可选注解项)。 - -#### 4.15.2 步骤 - -```bash -# M1 DynamicMultiblockEvent(1e5bfb3 #69): -# dynamic/event/DynamicMultiblockEvent.java 新建(原样复制); -# DynamicMultiblockManager.updateFormed、MultiblockFormPacket、MultiblockUnformPacket 各加事件触发点: -# 服务端 updateFormed 中取消 → 回滚 setFormed;客户端两个 packet handler 中取消 → 同步回滚 -# M2 未加载区块快照复用(d888714 #73): -# DynamicMultiblockManager.buildSnapshot 加 if (!level.isLoaded(pos)) 分支:复用 old.entries() -# 或填 (null, null, predicate);MultiblockCheckSnapshot.Entry.blockState 可空 + Entry.test() 空态返回 true; -# MultiblockState 新增 snapshot 字段(构造时初始化 new MultiblockCheckSnapshot(this.controllerPos, Map.of())); -# checkMultiblockFormed 中 mstate.setSnapshot(snapshot) 先存快照 -# M3 Long→BlockPos 键化(22091d4): -# DynamicMultiblockManager:Map→Map、pendingChecks 同理(约 20 处 asLong()/immutable() 调整); -# MultiblockCheckSnapshot record 字段 controllerPosLong → BlockPos controllerPos; -# 注意 getAt/add/removeAt/containsAt 与异步回调处的 pos.immutable() 防哈希突变(BlockPos 可变) -# M4 定义懒解析(1b0a652): -# MultiblockState 构造器改收 ResourceKey,持 @Nullable Holder.Reference, -# getDefinition(HolderLookup.Provider) 懒解析;DEFINITION_KEY_CODEC / DEFINITION_KEY_STREAM_CODEC 外提; -# MultiblockFormPacket / MultiblockUnformPacket / DynamicMultiblockManager.add/onPlace(dev 用 holder.key())同步; -# STREAM_CODEC:ByteBufCodecs.holder(...) → ResourceKey.streamCodec(...)(两者 1.21.1 都有) -# M5(可选)ApiStatus.Internal(c72aa30 #93):multiblock 6 个类逐行加注 -# M6(可选)package-info @NullMarked(3bf9b6c):multiblock 10 个包(jspecify 在 1.21.1 构建链可用, -# port module.test 已用 org.jspecify.annotations.NullMarked,且 gradle 缓存存在 org.jspecify/jspecify) -``` - -#### 4.15.3 API 适配点 - -| 项 | 适配点 | -|---|---| -| 事件体系 | `net.neoforged.bus.api.Event`、`ICancellableEvent`、`NeoForge.EVENT_BUS.post()` 全套 21.1.226 自带(EventBus 体系 1.20.5 起未变) | -| M1 平台写法 | dev `this.state.getDefinitionKey().identifier()` → port `location()`;dev `level.isClientSide()` → port `isClientSide` 字段 | -| M2 | `Level.isLoaded(BlockPos)` 是 1.21.1 标准 vanilla API;`Map.of()`/`LinkedHashMap` 纯 JDK | -| M4 | `ResourceKey.streamCodec(ResourceKey)`(port `module.codec/StreamCodecUtil.java:264` 已在用)、`HolderLookup.Provider.lookup(...).getOrThrow(...)`、`Level.registryAccess()`(port `SetBlock.java:81` 已用)、`Holder.Reference`——全部 1.21.1 存在 | -| Java 21 陷阱 | dev 使用了 `computeIfAbsent(level, _ -> ...)`——`_` 作为 lambda 参数名需 Java 22+,port 工具链是 Java 21,**必须改名**(如 `ignored`) | -| M7 Config.group(跨模块) | `@Config(group=…)` 离开 module.config 的 group 支持无法编译——**依赖 #P9**;#P9 落地后两个模块各改 1 行注解(`AnvilLibMultiblockConfig`/`AnvilLibRecipeConfig`),config 文件路径变化(`group/xxx-common.toml`);若 #P9 未先行,两个模块维持 `@Config(name=…)` 即可编译 | - -#### 4.15.4 验收方式 - -1. `gradlew :module.multiblock:compileJava` 与 `gradlew :module.recipe:compileJava` 通过(连带验证)。 -2. 行为验证(运行 `runServer` 或测试环境):成形/解散事件可取消(取消后回滚);未加载区块处快照复用不触发强加载;`Map` 键化后 getAt/add/removeAt/containsAt 正确(异步回调防哈希突变);定义经 `HolderLookup.Provider` 懒解析正确。 -3. `grep -rn "asLong()\|controllerPosLong" module.multiblock/src` 复核零残留(预期差异除外)。 - ---- - -### 4.16 回移植项 #P16:module.recipe——SpawnItem 守卫 + SetBlock nbt 默认值(低) - -#### 4.16.1 目标 - -把 dev 侧 module.recipe 的两处纯逻辑修复回移植:`SpawnItem.accept()` 零数量守卫、`SetBlock` nbt 编解码默认值 null→空 `CompoundTag`(修复潜在 NPE)。 - -#### 4.16.2 步骤 - -```bash -# R1 SpawnItem 零数量守卫(40b8f84): -# outcome/SpawnItem.java 的 accept():int count = context.getInt(...); if (count == 0) return; -# (port 当前 SpawnItem.java:107 直接 copyWithCount(...) 无守卫,随机 count 掷 0 时仍会生成空堆物品实体) -# R2(可选)SpawnItem Builder 必填校验(91327c1):Builder.item 字段 ItemStack.EMPTY → @Nullable, -# build() 里 Objects.requireNonNull(1.21.1 用 @Nullable ItemStack 等价实现即可,不必引入 ItemStackTemplate) -# R3 SetBlock nbt 默认值(b8825bd): -# outcome/SetBlock.java 的 optionalFieldOf("nbt", null) → optionalFieldOf("nbt", new CompoundTag()) -# (port SetBlock.java:100 仍为 null,而 accept() 直接 entity.loadWithComponents(this.nbt, ...)—— -# JSON 缺 nbt 字段时 1.21.1 的 loadWithComponents(null, ...) 会 NPE;改默认值即可,无需动 loadWithComponents 调用) -# R4(可选)InWorldRecipe.matches() 谓词栈清理简化(0d0649f):行为等价重构,风险收益比低,可缓做 -``` - -#### 4.16.3 API 适配点 - -- 纯逻辑改动,无 API 适配;port 用 `this.item.copyWithCount(count)`,与 `ItemStackTemplate` 无关。 -- **已由 port 平行 PR 接收、无需重复移植**:双半方块修复、`AbstractCacheElement.grow()` 空堆修复、`ICacheInputOutputImpl.grow()` 只记录实际消耗、`ItemCache` 方块实体元素范围 `0.5→1.0`(port `6a0e8a9` #79 / `f2b60b6`,对应 dev `922d523` #78 / `1c523ee`)。 - -#### 4.16.4 验收方式 - -1. `gradlew :module.recipe:compileJava` 通过。 -2. 行为验证:随机 count 掷 0 时 `SpawnItem` 不生成物品实体;`SetBlock` 在 JSON 缺 `nbt` 字段时正常放置(无 NPE)。 - ---- - -### 4.17 回移植项 #P17:module.wheel——环形扇区选择效果(中-高) - -#### 4.17.1 目标 - -把 dev 侧 module.wheel 唯一的真功能——环形扇区选择效果(`WheelSelectionEffect`,DOT/ANNULAR_SECTOR 两种形态 + 可配置颜色 + 删除固定 `selectionEffectRadius` 参数)回移植;**渲染必须落在 port 侧既有 Tesselator / `ShaderInstance` / `LibShaders` 体系上,不带入任何 RenderState/UBO 管线**。 - -#### 4.17.2 步骤 - -```bash -# W1 WheelSelectionEffect(5d0b285、46e1b73、8a818f0 累计): -# api/WheelSelectionEffect.java 新建(纯逻辑部分原样搬); -# api/WheelMenuBuilder.java(链式 selectionEffect()/selectionEffectColor())、 -# api/WheelMenuModel.java(of(...) 新重载)、 -# gui/component/WheelWidget.java(renderSelectionEffect)、 -# gui/screen/WheelScreen.java 按 4.17.3 适配 -# W2 annular_sector.fsh(5d0b285):assets/anvillib/shaders/core/annular_sector.fsh 落位, -# 片段逻辑(径向+角度 AA、atan 扇区裁剪)GLSL 150 通用; -# 26.1 的 layout(std140) uniform AnnularSectorUniform(UBO)改为仿 port 现存 ring.json/selection.json 注册方式 -# + safeGetUniform("Center") 逐个 set(port 侧 WheelWidget.renderRing 即此模式) -# W3 环/选择点渲染(46e1b73、494274e 上游): -# WheelWidget.renderRing/renderSelectionEffect/renderProgressAnimation 功能(环、选中点随进度缩放、 -# getRendererSize 动态尺寸)移植;port 侧保留 renderRingFallback/renderDisc 式多边形回退与旧 shader 双路径 -# W7(可选)@ApiStatus.Internal(c72aa30):AnvilLibWheel、WheelWidget 等 -``` - -#### 4.17.3 API 适配点 - -| 项 | 适配点 | -|---|---| -| 纯逻辑(可直接搬) | `normalizePositiveAngle()` 角度归一化、扇区范围 `(angleEnd-angleStart)/2`、`WheelMenuModel.of(...)` 新重载、builder 链式 `selectionEffect()`/`selectionEffectColor()`——纯 Java | -| 渲染实现(重写) | dev 依赖 `SdfGraphics`(module.rendering)、`GuiGraphicsExtractor`、`Matrix3x2fStack`、`AnnularSectorRenderState`(`RenderPipeline`+`GpuBufferSlice`)、`LibDynamicUniforms`(`DynamicUniformStorage`/`Std140Builder`)——全部 26.1 → 1.21.1 落点为 port 侧 `WheelWidget` 原 Tesselator / `ShaderInstance`+`LibShaders` 体系(现仍存在),按 port 旧式重写扇区绘制 | -| 输入/渲染签名 | dev `WheelScreen`(`KeyEvent`/`MouseButtonEvent`/`extractRenderState`)、`WheelEntryRenderer`(`GuiGraphicsExtractor`+`Matrix3x2fStack`)→ 1.21.1 对应 `keyPressed(int,int,int)`/`mouseClicked(double,double,int)`/`GuiGraphics`/`PoseStack`——port 侧现存代码即等价物,不搬 dev 写法 | -| 模块依赖 | dev 侧 module.wheel 的 build.gradle 已 `jarJar` 依赖 `anvillib-rendering-26.1`(dev 侧 wheel 的新增模块依赖)——**回移植时应切断**,不引入 rendering 依赖 | - -**不迁移(仅 26.1 平台)**:`gui/render/state/{AnnularSector,Ring,Selection}RenderState.java`、`client/init/LibRenders.java`、`client/init/LibDynamicUniforms.java`(三者直接实现 `dev.anvilcraft.lib.v2.rendering.state.LibQuadGuiElementRenderState`、引用 `RenderPipeline`/`GpuBufferSlice`/`DynamicUniformStorage`;port 无 module.rendering 目录),dev 侧删除的 `LibShaders.java` port 保留。 - -#### 4.17.4 验收方式 - -1. `gradlew :module.wheel:compileJava` 通过。 -2. 行为验证(运行 `runClient`):环形菜单打开后显示扇区选择效果(DOT 点状 / ANNULAR_SECTOR 环形扇区两种形态);选中项随进度缩放动画正确;颜色配置(`selectionEffectColor`)生效;滚轮/按键交互无回归(port 旧渲染路径仍正常)。 - ---- - -### 4.18 回移植项 #P18:module.test——可选集成测试(低-中) - -#### 4.18.1 目标 - -把 dev 侧 module.test 中**不依赖 dev 新渲染模块**的四组集成测试回移植:T2 配置类测试、T8 RPC 测试、T9 爆炸测试、T10 wheel 按键扩展。其余测试(Bloom/CachedBER/Compute/SDF 图层/GUI 测试)依赖 module.rendering 与 26.1 渲染管线,不迁移(见第 5 章)。 - -#### 4.18.2 步骤 - -```bash -# T2 配置类测试(a6a24f1):AnvilLibTestConfig.java + ConfigManager.register -# ——port module.config 已有 ConfigManager/Config/Comment/CollapsibleObject/ConfigData; -# 缺 TranslatableEnum → 随 #P9 移植后保留 TestEnum 翻译,否则去掉 TestEnum 翻译部分 -# T8 RPC 测试(d153e58、2754d18):rpc/{RpcClientTest,RpcIntegrationTest,TestRpcMethods,RpcTestCommands,RpcTestScheduler} -# ——依赖 module.rpc(#P1);@EventBusSubscriber(Dist.CLIENT) 无 modid 为 26.1 语法,1.21.1 需补 modid -# T9 爆炸测试(435e23f):command/TestCommand.java ——依赖 module.explosion(#P3),命令骨架为 1.21.1 通用 API -# T10 wheel 测试扩展(随 W1):wheel/{WheelTestClientHandler,WheelTestKeys,WheelDemoMenus} -# ——KeyMapping.Category + event.registerCategory() 为 26.1 新 API;port 1.21.1 为 -# String category 构造 + RegisterKeyMappingsEvent;KeyMapping.matches(KeyEvent) 换 matches(key, scanCode) -# T13(可选)构建形式:module.test 的 runtimeOnly dev.dubhe:anvilcraft-neoforge-26.1.2 -# 换 1.21.1 版坐标或删除(dev 平台调试依赖) -``` - -#### 4.18.3 API 适配点 - -- T8 测试代码所用 MC/NF API 1.21.1 全部存在(`ClientPlayerNetworkEvent.LoggingIn`、`RegisterCommandsEvent`、`ServerTickEvent`、`PlayerEvent.PlayerLoggedInEvent`、`StreamCodec`/`ByteBufCodecs`);依赖 `RPC`/`RpcTarget`/`RemoteCallable`/`CallableParam`/`IRemoteCallableValidator`(#P1)。 -- T9 爆炸功能逻辑(分 tick 破坏、概率半径)与平台无关;`ExplosionExecutor` 属 #P3。 -- T10 按键 API 反向替换见上表。 - -#### 4.18.4 验收方式 - -1. `gradlew :module.test:compileJava` 通过。 -2. 运行验证(`runClient`/`runServer` 或 gameTest):T2 配置加载与枚举翻译正确;T8 RPC 远程调用往返成功;T9 爆炸命令执行分 tick 破坏正确;T10 wheel 环形按键切换与 demo 菜单正常。 - ---- - -### 4.19 回移植项 #P19:构建体系——roseauCheck(中) - -> ✅ 2026-08-12:#P19 已落地并随模块化 CI 重构一并收尾——roseauCheck 本机验证通过(roseau-cli 0.6.0 需 Java 25,降为 0.4.0;基线构件经 rootProject 解析;CSV/HTML 双报告),并回移植 `aa59e372`(#53)的模块化构建矩阵(`.github/modules.json` + `generate-matrix.js`,l0/l1/l2 分层构建/部署 + roseau_check/roseau_comment 工作流);module.gradle 集中化维持「不做」结论(port 侧独立构建脚本自洽)。 - -#### 4.19.1 目标 - -把 dev 侧构建体系中最具迁移价值的两项回移植:**roseauCheck API 兼容检查**(核心项,可直接搬);**module.gradle 集中化**(可选工程决策,需裁剪 26.1 特有内容)。 - -#### 4.19.2 步骤 - -```bash -# 1) roseauCheck(a6a24f1、946938f):根 roseau.yaml + gradle/scripts/roseau.gradle 拷贝, -# 根 build.gradle 接入 apply from + 任务注册 -# 2)(可选)module.gradle 集中化(ea4301f #25):根 module.gradle + build.gradle 的 createModule 脚手架 + -# 各模块 build.gradle 精简为 dependencies 块 + gradle.properties 的 anvillib.needRunConfig* 属性—— -# port 侧现有独立构建脚本自洽,是否重构由维护者决定;若做,按 4.19.3 裁剪 -``` - -#### 4.19.3 API 适配点(若做集中化,必须裁剪的 26.1 特有内容) - -| dev 侧内容 | port 处理 | -|---|---| -| `clientIrisWorkaround`/`clientRenderDoc` run 配置 | 剔除(port 无 module.gradle 的 RenderDoc 接线,见 #P8) | -| `evaluationDependsOn(':renderdoc-loader-internal')` | 按需保留/剔除(若 #P8 落地为独立模块可保留) | -| `createModule` 模板生成 `Identifier` 代码 | port 应生成 `ResourceLocation` | -| `java_version=25` | port 为 21 | -| parchment 配置已移除 | port **需要**(保留 port 现有 parchment 配置) | -| roseau 配置 | 通用(`io.github.alien-tools:roseau-cli:0.6.0` 无 MC 绑定,直接搬) | - -**不做**:`changeModsToml` 依赖注入任务(port 无 `.github/modules.json`,需补建该清单或裁剪任务);版本目录插件化与 `jarJar api→implementation`(低价值工程改动,由维护者决定)。 - -#### 4.19.4 验收方式 - -1. `gradlew roseauCheck`(或对应任务)在 port/1.21.1 基线上运行通过,输出 API 差分报告(可对 1.21.1 基线跑差分)。——✅ 2026-08-12:`gradlew :anvillib-codec-neoforge-1.21.1:roseauCheck` 通过,`module.codec/build/reports/roseau/` 产出 report.csv(0 BC)与 report.html;rpc 模块同步验证通过。 -2. 若做集中化:全量 `gradlew build` 通过(全部模块聚合构建无回归)。——未做集中化(port 侧维持独立构建脚本)。 - ---- - -## 5. 不可移植清单(明确不迁移的平台适配项) - -> 以下内容**明确不迁移**。判定依据见第 2 章分类规则:这些「功能」本身就是 26.1 平台适配,port 侧已有 1.21.1 等价物,反向迁移无意义且会破坏 port 侧现状。执行时不得以「顺手」「保险」为由扩大范围。按模块分节,每项一句话理由。 - -### 5.1 module.rendering(26.1 渲染架构本体) - -| 不迁移内容 | 理由(一句话) | -|---|---| -| `ALRPipelines.java`(`RenderPipeline.builder()`、`UniformType.UNIFORM_BUFFER`)与 `RegisterRenderPipelinesEvent` 注册 | `RenderPipeline` 体系与 `RegisterRenderPipelinesEvent` 均为 26.1 专属(21.1.226 事件列表核实无此事件);1.21.1 等价物是 `RegisterShadersEvent`(port 侧已用),其 vertex format 声明以 1.21.1 `VertexFormat` 构造器在 #P7 中重建 | -| `mixins/GuiRendererMixin.java`(SDF 注入部分) | 目标类 `GuiRenderer`/`GuiElementRenderState` 1.21.1 不存在(26.1 GUI 渲染状态体系);SDF 绘制经 #P7c 改走 `GuiGraphics` | -| `extension/GuiGraphicsExtractorExtension.java` + `GuiGraphicsExtractorMixin` + `ItemStackRenderStateMixin` + `ItemStackRenderStateInternals` | 目标类 `GuiGraphicsExtractor`/`ItemStackRenderState` 为 26.1 对象;其功能(半透明物品/方块/结构预览)并入 #P7c 的 GUI 增强,按 1.21.1 `GuiGraphics + MultiBufferSource` 重写 | -| `foundation/compound/CompoundSubmitNodeStorage`、`CompoundSubmitNodeCollection`、`foundation/BloomSubmitNodeStorage` | 父类 `SubmitNodeStorage`/`SubmitNodeCollection` 与 `SubmitNodeCollector`/`FeatureRenderDispatcher` 体系为 26.1 渲染架构核心,1.21.1 不存在(port 分支全库无 `SubmitNode` 引用);bloom 双画在 #P7b 的 BufferSource 重画方案中直接实现 | -| `extension/blaze3d/compute/**` 的 6 个 blaze3d mixin(`GpuDevice`/`CommandEncoder`/`GlDevice`/`GlCommandEncoder`/`DirectStateAccess`/`GlDebugLabel`) | mixin 目标类 1.21.1 不存在(无 GPU 抽象层);compute 能力经 #P7e 以自管 GL 工具层重建 | -| `state/LibGuiElementRenderState`、`LibQuadGuiElementRenderState` 及 `gui/state/` 系列状态类 | 26.1 GUI 渲染状态体系(`GuiElementRenderState`/`PictureInPictureRenderState` 接口),1.21.1 无对应物 | -| `foundation/fakeworld` 中的 `cardinalLighting()` 相关实现 | `CardinalLighting` 是 26.1 新接口方法,1.21.1 的 `BlockAndTintGetter` 无此方法(1.21.1 用 `getShade(Direction, boolean)` 体系);`FakeDisplayLevel` 抽象方法集按 1.21.1 补齐(#P7 相关但不单独立项,随 fakeworld 反向适配处理) | - -### 5.2 module.font - -| 不迁移内容 | 理由 | -|---|---| -| 无独立不可移植项 | font 模块「需反向适配」项(GPU 上传/绘制层、GUI 屏幕)均在 #P6 中给出 1.21.1 改写方案,无仅 26.1 平台的内容 | - -### 5.3 module.rpc / module.sync - -| 不迁移内容 | 理由 | -|---|---| -| `ItemStackTemplate.STREAM_CODEC` 编解码分支(SyncProxy.defaultCodec) | `ItemStackTemplate` 是 26.1 ItemStack 重构专有类型,1.21.1 无对应物(javap「找不到类」);1.21.1 的 `ItemStack` 已有 `OPTIONAL_STREAM_CODEC`,删除后功能无损 | -| `neoforgespi.transformation.ClassProcessor` SPI(processor 子模块的注册面) | 该 SPI 属 26.1 平台特性(loader 4.0.42/4.0.43 无 transformation 子包,loader 11.0.13 才有 40 个类);processor 功能经 #P2 子项 2.6 以 CoreMod(`ICoreMod` + `ITransformer`)重写 | - -### 5.4 module.explosion / module.collision / module.space-select / renderdoc-loader - -| 不迁移内容 | 理由 | -|---|---| -| 无独立不可移植项 | 四模块功能依赖的 26.1 API 面很窄(`Identifier`、`ItemStackTemplate`、`hurtServer`、`Ingredient.items()`、`Registry.get(TagKey)`、`Inventory.getSelectedSlot()`、`ClientPacketDistributor`、`Minecraft.hasControlDown`、`Direction.getApproximateNearest`、`ARGB`、`GatherDataEvent.Client`、渲染状态体系),每一项在 1.21.1 都有直接等价物(javap 逐一核实),已并入 #P3/#P5/#P8 的适配点 | - -### 5.5 module.config / module.integration / module.network - -| 不迁移内容 | 理由 | -|---|---| -| package-info 全面切换 `@NullMarked`(jspecify) | jspecify 是 26.1 MC 的新依赖(21.1.226 sources jar 中无 `org/jspecify`);1.21.1 标准写法是 `@MethodsReturnNonnullByDefault` + `javax.annotation.ParametersAreNonnullByDefault`(port 现状),纯风格迁移无功能价值 | -| `IPacket.type(ResourceLocation→Identifier)` 改名 | `Identifier` 是 26.1 MC 的类名变更;1.21.1 为 `net.minecraft.resources.ResourceLocation`(port 现状) | -| `FMLLoader.getCurrent()` 系用法 | 1.21.1 的 `FMLLoader` 无 `getCurrent()`(loader 4.0.42 sources 核实),port 侧 `FMLLoader.getDist()`/`LoadingModList.get()` 写法正确,不随 dev 改动 | -| BIDIRECTIONAL 包注册的 4 参数 handler | 21.1.226 `PayloadRegistrar` 三个 bidirectional 方法均为 3 参数(26.1 才引入双 handler 签名);port 侧 3 参调用保持 | - -### 5.6 module.util - -| 不迁移内容 | 理由 | -|---|---| -| `ClientTickRecorder` 修复(`6b4d6bd` #61,删除 level 判空与 LoggingOut 归零) | dev 的修复针对 26.1 事件触发时机变化(主菜单 tick 行为);1.21.1 的 `ClientTickEvent.Pre` 行为与 port 现状一致,照搬会破坏计时器正确性(反向移植引入 bug) | -| `ChanceItemStack`/`ChanceBlockState`/`WeightedChanceBlockStates`/`NbtPredicate` 的 26.1 版 | `ItemStackTemplate` 化、`ContextMap/ContextKeySet` LootParams 构造、`TagValueOutput`+`ProblemReporter`——均为 26.1 专属;port 侧 ItemStack 版/LootParams 旧构造器实现完整可用 | -| package-info `@NullMarked` 迁移 | 同 5.5,注解风格迁移无功能价值 | - -### 5.7 module.registrum - -| 不迁移内容 | 理由 | -|---|---| -| `DataComponentPredicateBuilder`/`DataComponentPredicateEntry` + `dataComponentPredicate()` 入口(`252922e` #47) | `DataComponentPredicate` 与 `Registries.DATA_COMPONENT_PREDICATE_TYPE` 1.21.1 不存在(1.21.5+ 才引入);1.21.1 等价物是 `ItemSubPredicate` + `Registries.ITEM_SUB_PREDICATE_TYPE`(`d6fb290` 即 dev 侧从 ItemSubPredicate 换成 DataComponentPredicate 的迁移记录)——按框架判定不迁移 | -| generators/ 模型生成器五件套(`RegistrumModelProvider`/`RegistrumBlockModelGenerator`/`RegistrumItemModelGenerator`/`RegistrumLegacyBlockModelBuilder`/`PropertyDispatchWrap`) | 全部依赖 26.1 新模型 datagen(`net.minecraft.client.data.models.*`、`net.minecraft.client.renderer.block.dispatch.*`、NeoForge 26.1 `template.*`),且 `RegistrumBlockModelGenerator` 用 `ObfuscationReflectionHelper` 反射改写 26.1 字段;1.21.1 仍是旧体系 `net.neoforged.neoforge.client.model.generators.*`,port 现有 `RegistrumBlockstateProvider`/`RegistrumItemModelProvider` 功能等价 | -| `RegistrumRecipeRunner` 与 `generators/RegistrumRecipeProvider` 的 Runner 形态 | `RecipeProvider.Runner` 1.21.1 不存在(26.1 才引入 runner 模式);port 沿用现有「extends RecipeProvider implements RecipeOutput」模式(#P14 只做方法增强) | -| 平台适配 diff(Identifier/jspecify/`FMLLoader.getCurrent()`/`EntitySpawnReason`/`BlockTintSource`/渲染器双泛型/loot 基类更换/TagsProvider 去 ExistingFileHelper/`GatherDataEvent.Client`/`Registration` static 化等) | 均为 26.1 平台适配(见报告 §4 清单),port 侧现状即 1.21.1 正确写法 | -| `RegistrumDataProvider` 「忽略 sides 过滤,全部生成」行为变更 | 与 26.1 `GatherDataEvent.Client` 拆分相关,1.21.1 仍是一个 `GatherDataEvent`;建议保留 port 现有过滤(#P14 中不迁移此行为变更) | - -### 5.8 module.multiblock / module.recipe - -| 不迁移内容 | 理由 | -|---|---| -| 持久化重写:`SavedData.load/save(NBT)` → `SavedDataType` + `CODEC` | `SavedDataType` 是 26.1 新 API;1.21.1 用 `SavedData.Factory`(port 现有实现保留);dev 新 CODEC 只持久化 `controller_pos`+`definition`(丢弃 `formed`)属平台重构副作用,不回移植 | -| `BreakBlockEvent` | 26.1 把 `BlockEvent.BreakEvent` 拆为 `net.neoforged.neoforge.event.level.block.BreakBlockEvent`;1.21.1 用原类(port 现状正确) | -| `MultiblockDefinition` Builder 12 个重载删除 | 因 dev 的 `module.util/BlockStatePredicate` 移除了 `with(BlockState)` 与 `of(TagKey)`(26.1 标签/方块状态 Holder 化);port 的 `BlockStatePredicate` 两者俱在,重载保留(见 #P12「不可照搬项」) | -| `RecipeMapMixin` + `IRecipeMapExtension` | `RecipeMap` 是 1.21.2+ 新类;1.21.1 `RecipeManager` 仍是 `byName`/`byType` 字段,port `RecipeManagerMixin` 已含 dev 拆分前的完整实现 | -| `ItemResourceHandlerCache(Element)` + `VanillaContainerWrapper` | `net.neoforged.neoforge.transfer.ResourceHandler/ItemResource` 是 26.1 transfer API;1.21.1 等价物是 `IItemHandler`(21.1.226 自带)+ `InvWrapper`,port 的 `IItemHandlerCache`/`ItemHandlerCacheElement` 保留 | -| `LibDataComponentPredicates` → `LibItemSubPredicates` | `DataComponentPredicate.Type` 注册表是 26.1 特性;1.21.1 用 `ItemSubPredicate.Type`(`BuiltInRegistries.ITEM_SUB_PREDICATE_TYPE`),And/Or/Not 组合谓词两侧等价,port 保留 | -| `ValueInput`/`ValueOutput`/`ProblemReporter`/`TagValueInput`(BlockCache/SetBlock 的 NBT 加载错误上报) | 26.1 专属;1.21.1 用 `loadWithComponents(CompoundTag, Provider)` | -| Recipe 接口新方法(`placementInfo()`/`recipeBookCategory()`/`showNotification()`/`group()`、Serializer 脱接口化)、`serverLevel.recipeAccess()`、`RecipeUnlockedTrigger` 包迁移、`ContextMap/ContextKeySet` LootParams、`ItemStackTemplate`(icon/SpawnItem/Builder/SaveComponentToTag) | 26.1 Recipe/注册表体系变化,1.21.1 用 `canCraftInDimensions/getResultItem/RecipeSerializer` 等旧接口,port 现状即正确 | -| `ResourceEventListener` 注释掉 `RecipesUpdatedEvent` 客户端处理器 | 26.1 客户端配方管理重构;port 保留该处理器是正确行为 | -| `IRecipeManagerExtension.anvillib$getRegistries` @Deprecated | 26.1 适配 | -| `ASYNC_MULTIBLOCK_CHECK_PLAN.md` 删除 | 文档,不回移植(port 现状可用) | -| 语言文件管理差异(dev 删除 generated en_us.json 跟踪、zh_cn.json) | 资源管理差异,port 现状可用,不回移植 | - -### 5.9 module.moveable-entity-block(整体排除) - -| 不迁移内容 | 理由 | -|---|---| -| 模块全部(F1–F5) | dev 侧零功能增量,全部为 26.1 平台适配(ValueIO 序列化重构、RenderState 渲染管线重构、反混淆名调整);活塞 bug 修复已在 port(`6a0e8a9` #79);注解为纯风格——详见 2.3 节 | - -### 5.10 module.wheel - -| 不迁移内容 | 理由 | -|---|---| -| `gui/render/state/{AnnularSector,Ring,Selection}RenderState.java`、`client/init/LibRenders.java`、`client/init/LibDynamicUniforms.java` | 三者直接实现 `dev.anvilcraft.lib.v2.rendering.state.LibQuadGuiElementRenderState`(module.rendering)、引用 `RenderPipeline`/`GpuBufferSlice`/`DynamicUniformStorage`;port 无 module.rendering 目录,1.21.1 无对应物(见 #P17) | -| 输入/渲染签名 26.1 化(`WheelScreen` 的 `KeyEvent`/`MouseButtonEvent`/`extractRenderState`、`WheelEntryRenderer` 的 `GuiGraphicsExtractor`+`Matrix3x2fStack`) | 1.21.1 对应 `keyPressed(int,int,int)`/`mouseClicked(double,double,int)`/`GuiGraphics`/`PoseStack`,port 侧现存代码即等价物 | -| dev 侧删除的 `LibShaders.java` | port 保留(环形扇区渲染落点依赖它) | - -### 5.11 module.test - -| 不迁移内容 | 理由 | -|---|---| -| T1 测试组织重构(`all/` 包 + `TestLangGenerator`) | 依赖 dev 侧 registrum 演进 API(entry 包 + `PropertyDispatchWrap`),datagen 输出含 26.1 MC 类;port 保留现有结构 | -| T3 Bloom 测试(`TestBloomBlock/Tile/TESR`) | 依赖 `ALRPostEffects`/`BloomPostEffect`/`CompoundSubmitNodeStorage`(module.rendering)+ 26.1 渲染管线 + `SyncProxy`;等于重写 | -| T4 缓存型 BER 测试(`TestCachedRendering*`) | 依赖 `CachedBlockEntityRenderingPipeline` 等 + `BlockModelRenderState`/`BlockDisplayContext`(26.1 渲染) | -| T5 Compute 测试(`ComputeSupport`/`TestPipelines` + 4 个 .csh) | 依赖 `GpuDevice`/`CommandEncoder`/`GpuFence`/`GpuSampler`/`GpuTexture`/`TextureFormat`(26.1 GPU 抽象,1.21.1 无) | -| T6 SDF 图层测试(`SdfGraphicsLayer`) | `SdfGraphics`(26.1 管线)+ `implements GuiLayer` 为 26.1 NeoForge 接口(21.1.226 的 `RegisterGuiLayersEvent` 接受 `LayeredDraw.Layer`,非 `GuiLayer`) | -| T7 GUI 渲染测试(`GuiTestScreen`) | `FakeDisplayLevel`/`GuiRenderExtras`(module.rendering)+ `ALRCommandEncoderExtension`/`GpuSampler`(26.1 GPU) | -| T11 `MinecraftMixin` | 注入 `Minecraft.` 初始化 `ALRComputeCapabilities`,依赖 module.rendering 26.1 | -| T12 生成资源(bloom/cber 的模型、loot、原版模型覆盖) | datagen 产物,随 T3/T4 无意义;lang 中 `anvillib.configuration.*` 段随 T2/#P9 可再生成 | - -### 5.12 构建体系(module.gradle 中的 26.1 特有内容) - -| 不迁移内容 | 理由 | -|---|---| -| `clientIrisWorkaround`/`clientRenderDoc` run 配置、`evaluationDependsOn(':renderdoc-loader-internal')` | 26.1 模块化构建的 RenderDoc/Iris 调试接线;port 无 module.gradle 约定,RenderDoc 调试需手工接线(见 #P8) | -| `createModule` 模板中的 `Identifier` 生成、`java_version=25`、parchment 移除 | 26.1 平台内容;port 应生成 `ResourceLocation`、Java 21、保留 parchment | -| `changeModsToml` 依赖注入任务 | port 无 `.github/modules.json`(git ls-tree 核实),需补建清单或裁剪任务 | -| `gradle.properties` 的 `anvillib.needRunConfig*` 属性体系 | 26.1 聚合构建约定;port 各模块自建 run 配置(见 #P3) | - ---- - -## 6. 执行顺序与依赖关系 - -> 总原则:**先依赖后使用、纯逻辑先行、独立项先验证、高风险项尽早暴露**。每步完成即验收(第 4 章对应小节),全部通过再进入下一步;每步在 port/1.21.1 分支工作树以独立提交落地(提交信息建议按仓库现有风格:`feat(rpc): ...` / `fix(recipe): ...` 等)。 - -### 执行顺序总览 - -``` -#P1/#P2 ──→(#P7a)──→ #P6 ──→ #P3/#P4/#P5/#P8 ──→ #P9 ──→ #P11/#P12/#P13/#P16 ──→ #P15 ──→ #P17 ──→ #P14 ──→ #P18 ──→ #P19 -``` - -### 第 1 步:#P1 → #P2(新模块骨架与最大 SPI 风险) - -- #P1(RPC)先行:依赖面最窄(port 侧 network/util 已就绪)、3 处机械反替换,作为「新模块落地模板」验证 `settings.gradle` 注册、jarJar 坐标 `-1.21.1`、mods.toml 模板在 port 侧的整套流程;#P2(sync + processor)紧随其后,processor 的 CoreMod 重写(子项 2.6)是**最大的 SPI 不确定点**(services 文件可见性、GETSTATIC 类解析),尽早暴露。 - -### 第 2 步:#P7a(rendering 纯逻辑批)→ #P6(font) - -- #P7a 先落地 rendering 模块骨架与 19 项纯逻辑(Buffer 布局、Sdf2d、事件、辅助类、shader 资源),难度低、无渲染平台风险,同时为 #P6 提供 jarJar 依赖坐标;#P6 在 #P7a 完成后即可开工(SDF 图集/布局/测量等纯逻辑不依赖 #P7b–#P7e),其 GPU 上传/绘制层可先以「DynamicTexture + GuiGraphics」独立路径落地,不受后处理进度阻塞。 - -### 第 3 步:#P3/#P4/#P5/#P8(其余新模块,可并行) - -- #P4(collision)极低难度独立模块,可随时插入;#P3(explosion)依赖 port module.config(已就绪),与 #P5(space-select,依赖 port module.network)互不阻塞;#P8(renderdoc-loader)独立。四者均可并行执行,每个独立提交。 - -### 第 4 步:#P9(config 三项演进)——必须早于 #P15 - -- #P9 是 #P15 的 M7 子项(Config.group)的代码级前置:`@Config(group=…)` 离开 module.config 的 group 支持无法编译;与 #P3 的交互——#P3 若先落地按「去掉 group」处理(port @Config 无 group 属性),#P9 完成后可选补回 `group = "anvillib"` 并重跑 datagen(lang section 键名以 #P9 的点分隔规则为准)。 - -### 第 5 步:#P11/#P12/#P13/#P16(共享模块小演进,可并行) - -- 四项均低-中难度、模块内改动:#P11(network)、#P12(util + recipe 调用方连带)、#P13(registrum builder 全家桶)、#P16(recipe 两处修复)。#P12 的 HolderGetter 重构与 #P16 互不干扰;#P12 注意保留 port 的 `Builder.with(BlockState)`(不可照搬 dev 删除)。 - -### 第 6 步:#P15(multiblock 四项)——依赖 #P9 已落地 - -- M1–M4 建议一并做(M3 与 M4 构造路径耦合、M2 与 M3 快照 record 字段耦合);M7 的模块注解改动依赖 #P9。 - -### 第 7 步:#P17(wheel 环形扇区效果) - -- 依赖 port 旧渲染体系(Tesselator/ShaderInstance/LibShaders),与 #P7 的新渲染模块无依赖(已切断 dev 侧 rendering jarJar 依赖);独立执行。 - -### 第 8 步:#P14(registrum datagen 增强) - -- 独立于 #P13(可不依赖其 entry API);GeneratorType 拆分列为可选单独提交(消费方 1.21.1 不存在,价值低)。 - -### 第 9 步:#P18(module.test 可选测试) - -- T2 随 #P9(TranslatableEnum)、T8 随 #P1、T9 随 #P3、T10 随 #P17;各测试项随对应功能落地,作为功能项的集成验收补充。 - -### 第 10 步:#P19(构建体系)——收尾 - -- roseauCheck 独立可随时执行,安排在最后作为全量 API 差分验收;module.gradle 集中化为可选工程决策(port 侧现有独立构建脚本自洽,是否重构由维护者决定,**不属于本计划硬性范围**)。 - -### 顺序之外的两条纪律 - -1. 每步提交前先跑一遍该模块 `build`(含 `jar`),避免只验 `compileJava` 漏掉资源处理(mods.toml 替换、jarjar 内嵌)。 -2. 涉及 `runData`/运行类验证的项(#P3/#P6/#P7/#P9/#P14/#P15/#P17/#P18),如本地环境不便跑客户端,可先以编译 + CI 为准,把运行验收登记为「待 CI/人工补验」并给出复现步骤,**不得**把未验证项标记为已完成(见第 7 章)。 - ---- - -## 7. 总体验收标准 - -### 7.1 构建验收(硬性门槛) - -| #P 涉及 | 验收项 | 命令 / 判据 | -|---|---|---| -| 全部 | port/1.21.1 全量构建 | 在 port/1.21.1 分支 `gradlew build`(根级,聚合全部模块含新增 8 个)**全部成功** | -| 全部 | 模块独立构建 | 依次 `gradlew :module.rpc:build` `:module.sync:build` `:module.explosion:build` `:module.collision:build` `:module.space-select:build` `:module.font:build` `:module.rendering:build` `:module.test:build` `:module.main:build` 均通过 | -| #P1–#P18 | 依赖完整性 | 各新模块 jar 的 `META-INF/jarjar/` 内含 `anvillib-*-neoforge-1.21.1-*.jar`(如 module.main 聚合产物);无 `-26.1` 坐标残留 | -| #P2 | SPI 重写零残留 | `grep -rn "neoforgespi.transformation" module.sync` 无匹配;CoreMod services 注册生效 | -| #P19 | roseauCheck | `gradlew roseauCheck`(或对应任务)在 1.21.1 基线上运行通过,输出 API 差分报告 | - -### 7.2 功能验收清单(行为核对) - -| 覆盖项 | 验收项 | 判据 | 执行方式 | -|---|---|---|---| -| #P1/#P18 | RPC 远程调用 | 客户端注册 `@RemoteCallable` 方法并下发索引 → 服务端经 `invoke` 远程调用返回 `CompletableFuture` 结果;`@CallableParam` 自定义编解码器生效;tick 超时路径正确 | module.test T8 / 手工 | -| #P2 | sync 同步 | `@Sync` 注解字段两端同步;LazySync 惰性差分同步生效;Configuration 阶段配置表下发正常;processor 字节码注入目标类生效(启动日志 + 反编译核对) | module.test / 手工 | -| #P3/#P18 | 爆炸执行器 | 分层球壳破坏按概率半径执行;熔化替换(熔炼/高炉配方表)生效;实体 `hurt` 与掉落/熔化路径正确 | module.test T9 / 手工 | -| #P4 | 碰撞检测 | 静态 SAT 13 轴 / 三轴独立扫掠 / 真扫掠碰撞三个入口断言通过;`overlapOnAxis` 工具正确 | 手动运行测试入口 | -| #P5 | 选区渲染 | 框选创建区域;`DistrictRenderer` 各 stage 线框正确(含平移量/颜色语义);滚轮缩放/移动生效;网络包双向收发正常 | 运行 runClient | -| #P6 | SDF 文字 | SDF 图集生成正确;文字分段样式(粗斜体/下划线/删除线/混淆)与居中/换行正确;`anvillib$text` 扩展可用;配置/测试界面正常 | 运行 runClient | -| #P7b | 后处理三件套 | Bloom 5 级金字塔效果正确(含 resize 跟随、帧稳定);Blur/Glitch 效果正确;glitch 时间驱动动画正常 | 运行 runClient | -| #P7c | SDF GUI | 9 种距离函数图形绘制正确;命中测试准确 | 运行 runClient | -| #P7d | CachedBER | 缓存 BER 渲染正确(视锥剔除/半透明排序);失效重建正确;bloom 双画正常;Iris 开启时回退正常 | 运行 runClient(无 Iris 可跳过最后一项) | -| #P7e | Compute | pipeline 注册、program 编译链接、dispatch 与内存屏障执行正确 | 运行 runClient | -| #P9 | 配置分组/枚举翻译 | `@Config(group)` 生成 `//-common.toml`;枚举翻译 key(`enum.`)正确;TOML section 键为点分隔 | runData + 运行 | -| #P10 | 集成数据加载 | 客户端/服务端数据加载器各自生效;进度条随实例递增 | 运行验证 | -| #P11 | 网络工具 | 多静态字段注册解析正确;`sendToAllPlayersIncluded` 过滤发送正确 | 编译 + 行为用例 | -| #P12 | util 演进 | OutlineUtil 轮廓提取正确;HolderGetter 谓词匹配正确;`withCount` 不改原实例;`with(BlockState)`(保留项)正常 | 编译 + 行为用例 | -| #P13 | 注册表入口 | 17 个入口注册后可在 `BuiltInRegistries`/`NeoForgeRegistries` 查到;CreativeTab 生成标签页;RecipeSerializer 可加载配方 | 编译 + 行为用例 | -| #P14 | datagen 增强 | 公开包装方法可生成配方(shelf/colorItemWithDye);`dataMap(type, provider, factory)` 捕获 HolderLookup 正确 | runData | -| #P15 | multiblock 四项 | 成形/解散事件可取消(取消后回滚);未加载区块快照复用不触发强加载;BlockPos 键化正确;定义懒解析正确 | 运行 runServer | -| #P16 | recipe 修复 | 随机 count 掷 0 不生成物品实体;JSON 缺 `nbt` 字段正常放置(无 NPE) | 行为用例 | -| #P17/#P18 | 环形扇区选择效果 | 菜单显示 DOT/ANNULAR_SECTOR 两种形态;选中点随进度缩放;颜色配置生效;旧渲染路径无回归 | 运行 runClient + T10 | - -### 7.3 收尾核对 - -1. **提交完整性**:port/1.21.1 分支上 #P1–#P19 各为独立提交,提交信息含来源 issue/PR 号(#60/#63/#87/#65/#22/#56/#21/#41/#33/#25/#27/#28/#30/#31/#47/#52/#69/#73/#75/#49 等)。 -2. **范围守卫**:`git log --oneline port/1.21.1..dev/26.1` 中,除第 5 章「不可移植清单」与 #P1–#P19 外,不应有「实际回移植但未立项」的内容;如执行中发现新功能,先补充本计划再执行。 -3. **文档同步**:附录 B 的「待验证事项」在回移植执行中逐项销账;本计划中的「待验证」标注同样逐一关闭,关闭记录可追加到附录 B。 -4. **遗留登记**:任何因环境限制未完成的运行类验收登记为遗留项,附复现步骤,不得默认通过。 -5. **与姊妹文档一致性**:本计划不覆盖 `docs/migration-plan-dev-26.1.md` 已处理的 port→dev 方向内容(yukkuri 等 #1–#8);若两侧计划对同一文件的修改意图冲突(如 #P12 保留 `with(BlockState)` vs dev 侧删除),以「port 侧保留现状」为准,并在 commit 说明中标注。 - ---- - -## 附录 - -### A. 常用只读 git 命令(本计划全部事实均来自以下命令) - -```bash -# 提取 dev 侧文件内容(全部为只读操作) -git show dev/26.1:module.rpc/src/main/java/dev/anvilcraft/lib/v2/rpc/AnvilLibRpc.java -git show dev/26.1:module.sync/src/main/java/dev/anvilcraft/lib/v2/sync/AnvilLibSync.java -# 查看来源提交的完整 diff(代表性提交) -git show d153e58 # #P1(RPC 主体) -git show 1063ee9 # #P2(processor 新增) -git show 435e23f # #P3(explosion 初建) -git show 82b4f6a # #P4(collision) -git show 1335fc5 # #P5(space-select) -git show 1d56a4e # #P6(font) -git show 7121bf6 # #P7(rendering Bloom 系) -git show 7d07f20 # #P7(rendering Compute/Buffer 布局) -git show 494274e # #P7(SDF GUI) -git show 2751be4 # #P8(renderdoc-loader 初建) -git show 3d50d76 # #P9(Config.group) -git show 946938f # #P9 / #P6 / #P19(TranslatableEnum + TOML 点分隔 + roseau) -git show dc4d583 # #P10(integration 数据加载拆分) -git show 20721b1 # #P11(PacketData 泛型检查) -git show e4c09fb # #P12(OutlineUtil) -git show bb09805 # #P12(predicate HolderGetter 重构) -git show 9b2d0e3 # #P13(注册表更多种类) -git show 58f01d4 # #P14(datagen 重构) -git show 1e5bfb3 # #P15(DynamicMultiblockEvent) -git show d888714 # #P15(未加载区块快照) -git show 22091d4 # #P15(Long→BlockPos 键化) -git show 1b0a652 # #P15(定义懒解析) -git show 40b8f84 # #P16(SpawnItem 零数量守卫) -git show 5d0b285 # #P17(环形扇区选择效果) -git show a6a24f1 # #P18 / #P19(roseauCheck + T2 配置测试) -git show ea4301f # #P19(module.gradle 集中化) -# 两侧同路径对比 -git diff port/1.21.1 dev/26.1 -- module.network/src/main/java/dev/anvilcraft/lib/v2/network/util/NetworkUtil.java -# 某模块 dev 独有提交清单(回移植候选扫描) -git log --oneline port/1.21.1..dev/26.1 -- module.registrum -# dev 侧落点现状 -git show dev/26.1:module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/event/DynamicMultiblockEvent.java -# 模块文件清单(新增/删除核对) -git diff --name-status port/1.21.1 dev/26.1 -- module.rpc -git ls-tree -r --name-only dev/26.1 -- module.space-select -``` - -### B. 待验证事项汇总(执行时逐项销账) - -| 编号 | 事项 | 涉及项 | 验证方法 | 状态 | -|---|---|---|---|---| -| V-1 | `Mth.roundToward`/`VertexSorting`/`CompactVectorArray`/`MeshData.sortQuads` 在 1.21.1 的存在性(报告依据「1.20.2+ 稳定 API」判断;本机 gradle 缓存无 1.21.1 client jar 可反查) | #P7a | 移植首个编译时以 1.21.1 client sources 复核 | 未验证 | -| V-2 | Iris 1.7.x 的 `ImmediateState` 字段(`skipExtension`/`isRenderingLevel`)与 `IrisApi` 方法签名(dev compileOnly 依赖为 Iris 1.10.9+26.1) | #P7d | 按 1.21.1 Iris 版本(1.7.x)sources 复核 | 未验证 | -| V-3 | `MultiBufferSource.BufferSource` 构造器参数(dev 侧 `super(null, null)` 需小调) | #P7a | 1.21.1 sources + 编译 | 未验证 | -| V-4 | `LevelRenderer.renderVoxelShape`/`renderLineBox` 的颜色语义(RGBA 0-1 float)与 dev `ARGB` int 色的转换 | #P5 | 实现后目测确认(report §6) | 未验证 | -| V-5 | port 侧各模块 build.gradle 的 run 配置自建模式(port 无 module.gradle,`anvillib.needRunConfig*` 不生效) | #P3/#P5/#P8 | 参照 port `module.config/build.gradle`(含 data run 段) | 未验证 | -| V-6 | Iris 1.21.1 对应版本(1.7.x)的 Modrinth 依赖坐标 | #P7d | 移植时查询 Modrinth | 未验证 | -| V-7 | mods.toml 模板变量(`${loader_version_range}` 等)在 port 侧注入方式 | #P7 | 按 port 现有模块模板对齐 | 未验证 | -| V-8 | `ShaderDefines`(26.1 类)改为 `Map` defines 后 `ALRComputePipeline`/`ALRComputePass` 编译与运行 | #P7e | 编译 + 运行验证 | 未验证 | -| V-9 | CoreMod transformer 在 jar-in-jar(`jarJar(api(...))` 携带 processor)场景下 services 文件可见性 | #P2 | 打包后实测启动 | 未验证 | -| V-10 | CoreMod transformer 中 `AnvilLibSync.LAZY_SYNC_MANAGER` GETSTATIC 引用的类可解析性(game layer 类加载时机) | #P2 | 启动日志 + 反编译核对注入点 | 未验证 | -| V-11 | `getSecureJar().getPath(classPath)` 在 1.21.1 securejarhandler 3.0.8 的签名(返回 `Path`) | #P2 | securejarhandler 3.0.8 sources + 编译 | 未验证 | -| V-12 | `AnvilLibFontConfig` 中 `FMLLoader.getCurrent().getGameDir()` 在 1.21.1 可用性(`getCurrent()` 的例外调用点) | #P6 | 编译;报错则改 `FMLPaths.GAMEDIR.get()` | 未验证 | -| V-13 | jspecify 依赖在 port 构建链的可用性(#P15 M6 可选注解项) | #P15 | port `module.test` 已用 `org.jspecify.annotations.NullMarked` 且 gradle 缓存存在 `org.jspecify/jspecify`——以编译为准 | 未验证 | -| V-14 | `VillagerProfession` 6 元 record 与 `Potion(@Nullable String, MobEffectInstance...)` 构造在 1.21.1 的确认 | #P13 | 1.21.1 源码(报告已核实,落位时以编译复核) | 未验证 | -| V-15 | `RecipeSerializer` 匿名类实现(`codec()`/`streamCodec()`)在 1.21.1 的编译通过性 | #P13 | `gradlew :module.registrum:compileJava` | 未验证 | -| V-16 | `District.color()` 手写 int 打包(`(a<<24)|(r<<16)|(g<<8)|b`)在渲染端的颜色语义 | #P5 | 渲染目测 + 与 dev 行为对比 | 未验证 | -| V-17 | `GatherDataEvent`(单一类)的 `includeClient()`/`getPackOutput()` 在 21.1.226 的签名 | #P3/#P6/#P14 | 21.1.226 sources(报告已核实)+ 编译 | 未验证 | -| V-18 | 根构建脚本(`build.gradle`/`settings.gradle`)模块注册方式与 port 侧现有模块(module.wheel 等)对齐 | #P1–#P8 | 参照 port 现有模块 | 未验证 | - -### C. 参考资料 - -| 资料 | 位置 | -|---|---| -| **8 个并行子代理模块级调研报告(本文全部事实依据,1395 行)** | `C:/Users/Administrator/.kimi-code/sessions/wd_anvillib_031cb785b7e3/session_6033f19b-f4ef-481a-b18c-d626cc22c314/agents/main/tool-results/AgentSwarm-call_00_ET_e0hBtNS4BowwxHQBCso99671-b31c451d-23ff-46b7-8aa0-0ba516956d9d.txt` | -| 反向迁移计划(port→dev,编号 #1–#8,编号体系与本计划无关) | `docs/migration-plan-dev-26.1.md` | -| 分支对比报告(两分支整体关系与分类) | `docs/branch-comparison.md` | -| dev 侧关键文件实测基准 | `module.rpc/`、`module.sync/`、`module.explosion/`、`module.collision/`、`module.space-select/`、`module.font/`、`module.rendering/`、`renderdoc-loader/`(均以 `0b7e45f` 为准) | -| port 侧现状基准 | port/1.21.1 @ `bd25229`;`module.config`(ConfigManager/Config/Comment/ConfigData)、`module.network`(NetworkRegistrar/IPacket/IServerboundPacket/NetworkUtil)、`module.util`(ShapeUtil/UnlimitedItemStack)、`module.wheel`(LibShaders/ring.json/selection.json)、`module.multiblock`(AnvilLibDatagen) | -| NeoForge 21.1.226 sources jar(事件/网络/配置 API 核实依据) | 本地 gradle 缓存(`net.neoforged:neoforge:21.1.226-sources.jar`)与 `module.config/build/moddev/artifacts/` 下 patched 源码 | -| 1.21.1 混淆 jar / official mappings(javap 核实依据) | `neoformruntime/artifacts/minecraft_1.21.1_client.jar`、`minecraft_1.21.1_client_mappings.txt` | - -### D. 回移植提交清单(PR 提交模板) - -> 每个回移植项在 port/1.21.1 分支上对应一个独立提交。下表可直接作为 PR 描述模板,勾选「状态」列后随 PR 提交。 - -| 提交 | 回移植项 | 涉及文件(变更面) | 关键验证命令 | 状态 | -|---|---|---|---|---| -| commit 1 | #P1 rpc | `module.rpc/**`(25 文件)+ `settings.gradle` | `gradlew :module.rpc:compileJava`;`grep -rn "Identifier\|accessFlags" module.rpc/src`(0 匹配) | ☐ | -| commit 2 | #P2 sync | `module.sync/**`(31 文件)+ processor CoreMod 重写 + `settings.gradle` | `gradlew :module.sync:compileJava`;`grep -rn "neoforgespi.transformation" module.sync`(0 匹配) | ☐ | -| commit 3 | #P3 explosion | `module.explosion/**`(10 文件)+ `settings.gradle` | `gradlew :module.explosion:compileJava` + `runData` | ☐ | -| commit 4 | #P4 collision | `module.collision/**`(3 文件)+ `settings.gradle` | `gradlew :module.collision:compileJava` + 手动测试 | ☐ | -| commit 5 | #P5 space-select | `module.space-select/**`(18 文件)+ `settings.gradle` | `gradlew :module.space-select:compileJava` + runClient | ☐ | -| commit 6 | #P7a rendering 纯逻辑 | `module.rendering/**`(19 项纯逻辑 + 骨架)+ `settings.gradle` | `gradlew :module.rendering:compileJava` + `:module.rendering:test` | ☐ | -| commit 7 | #P6 font | `module.font/**`(25 文件)+ `settings.gradle` | `gradlew :module.font:compileJava` + `runData` + runClient | ☐ | -| commit 8 | #P7b–#P7e rendering 执行层 | `module.rendering/**`(后处理/SDF GUI/CachedBER/Compute 四批) | 各批 `compileJava` + 运行验证(7.2 清单) | ☐ | -| commit 9 | #P8 renderdoc-loader | `renderdoc-loader/**`(2 文件) | `gradlew :renderdoc-loader:compileJava` + jar manifest 核对 | ☐ | -| commit 10 | #P9 config | `module.config/**`(Config/ConfigManager/ConfigRecord/ConfigData/FormattingUtil/TranslatableEnum) | `gradlew :module.config:compileJava` + `runData` | ☐ | -| commit 11 | #P10 integration | `module.integration/**`(3 文件) | `gradlew :module.integration:compileJava` + 行为验证 | ☐ | -| commit 12 | #P11 network | `module.network/**`(3 文件) | `gradlew :module.network:compileJava` | ☐ | -| commit 13 | #P12 util | `module.util/**`(4 处演进)+ `module.recipe/**`(4 个调用方) | `gradlew :module.util:compileJava` + `:module.recipe:compileJava` + `runShapeUtilJoinTimingTest` | ☐ | -| commit 14 | #P13 registrum | `module.registrum/**`(builders/util.entry 新增 + AbstractRegistrum) | `gradlew :module.registrum:compileJava` | ☐ | -| commit 15 | #P16 recipe | `module.recipe/**`(SpawnItem/SetBlock) | `gradlew :module.recipe:compileJava` + 行为用例 | ☐ | -| commit 16 | #P15 multiblock | `module.multiblock/**`(M1–M4 + M7 注解) | `gradlew :module.multiblock:compileJava` + runServer | ☐ | -| commit 17 | #P17 wheel | `module.wheel/**`(WheelSelectionEffect + fsh + 渲染适配) | `gradlew :module.wheel:compileJava` + runClient | ☐ | -| commit 18 | #P14 registrum datagen | `module.registrum/**`(RecipeProvider/DataMapProvider/Builder) | `gradlew :module.registrum:compileJava` + `runData` | ☐ | -| commit 19 | #P18 test | `module.test/**`(T2/T8/T9/T10) | `gradlew :module.test:compileJava` + 运行验证 | ☐ | -| commit 20 | #P19 构建 | 根 `roseau.yaml` + `gradle/scripts/roseau.gradle`(+ 可选 module.gradle) | `gradlew roseauCheck`;`gradlew build` | ☐ | -| 收尾 | 全量回归 | 全仓 | `gradlew build`(根级)全绿;附录 B 待验证清单销账;与 `docs/migration-plan-dev-26.1.md` 无冲突 | ☐ | diff --git a/docs/migration-todo-port-1.21.1.md b/docs/migration-todo-port-1.21.1.md deleted file mode 100644 index 437b4562..00000000 --- a/docs/migration-todo-port-1.21.1.md +++ /dev/null @@ -1,266 +0,0 @@ -# AnvilLib 回移植执行跟踪:`dev/26.1` → `port/1.21.1` - -> 本文件是 `docs/migration-plan-port-1.21.1.md`(#P1–#P19)的执行跟踪清单(另有计划外追加项 #P20,见「逐项日志」末尾)。每完成一项,将对应状态更新为「✅ -> 已完成」并记录提交号;未完成项保持「⬜ 未开始 / 🔄 进行中」。待验证事项(附录 B 的 V-1…V-18)在验证后同步销账。 - -- 基线:`port/1.21.1`(执行起点 `96c3a34f`,计划基线 `bd25229`) -- 来源:`dev/26.1` @ `0b7e45f` -- 工具链:Java 21 / NeoForge 21.1.226 / Minecraft 1.21.1 - ---- - -## 执行状态总览 - -| #P | 内容 | 难度 | 状态 | 提交 | -|------|--------------------------------------------------------------|-------|-----------|------------| -| #P1 | module.rpc 整模块(25 文件) | 低 | ✅ 已完成 | `26b11e18` | -| #P2 | module.sync + processor(CoreMod 重写) | 中-高 | ✅ 已完成 | `c7da98c1` | -| #P3 | module.explosion 整模块(10 文件) | 低 | ✅ 已完成 | `e896a1ea` | -| #P4 | module.collision 整模块(3 文件) | 极低 | ✅ 已完成 | `c25dd465` | -| #P5 | module.space-select 整模块(18 文件) | 中 | ✅ 已完成 | `1706feaa` | -| #P6 | module.font 整模块(25 文件) | 中 | ✅ 已完成 | `91533730` | -| #P7 | module.rendering(#P7a–#P7e 分批) | 高 | ✅ 已完成(已移除)| `b0e179e8` + `a6544e6b` | -| #P8 | renderdoc-loader(2 文件) | 低 | ✅ 已完成(已移除)| `c62a262e` | -| #P9 | module.config:group + TranslatableEnum + TOML 点分隔 | 低 | ✅ 已完成 | `65aa5e11` | -| #P10 | module.integration:数据加载拆分 + meter 递增 | 低 | ✅ 已完成 | `adc23468` | -| #P11 | module.network:泛型修复 + Included + public | 低 | ✅ 已完成 | `20934ac4` | -| #P12 | module.util:OutlineUtil + HolderGetter + withCount + equals | 中 | ✅ 已完成 | `164f441f` | -| #P13 | module.registrum:14 种新 builder/entry | 低 | ✅ 已完成 | `2d1a8e3d` | -| #P14 | module.registrum datagen 增强 | 中 | ✅ 已完成 | `dd8d79e5` | -| #P15 | module.multiblock:事件化 + 快照复用 + 键化 + 懒解析 | 中 | ✅ 已完成 | `2556a8e3` | -| #P16 | module.recipe:SpawnItem 守卫 + SetBlock nbt | 低 | ✅ 已完成 | `71bf9e54` | -| #P17 | module.wheel:环形扇区选择效果 | 中-高 | ✅ 已完成 | `142e3296` | -| #P18 | module.test:T2/T8/T9/T10 可选测试 | 低-中 | ✅ 已完成 | `a4ab3acd` | -| #P19 | 构建体系:roseauCheck + 模块化 CI 构建矩阵 | 中 | ✅ 已完成 | `544c9eb0` + 本轮 | -| #P20 | module.codec:CodecUtil 便携 create/mapCodec(计划外追加) | 低 | ✅ 已完成 | `05235aa`(来源 `173a454`) | -| #P21 | module.wheel:环扇模式轮盘渲染重做同步(计划外追加) | 中-高 | ✅ 已完成 | `92661c6` | - -> ⚠️ 2026-08-12 状态更新:#P7(module.rendering)与 #P8(renderdoc-loader)已从 `port/1.21.1` 分支整体移除;同时清理了 `module.font` / `module.main` 的渲染依赖、CI 工作流(ci / release / pull_request)中的渲染任务与相关文档引用。 - -## 执行顺序(计划第 6 章) - -``` -#P1/#P2 →(#P7a)→ #P6 → #P3/#P4/#P5/#P8 → #P9 → #P11/#P12/#P13/#P16 → #P15 → #P17 → #P14 → #P18 → #P19 -``` - -## 逐项日志 - -### #P1 module.rpc(低) - -- [ ] 提取 25 个 Java 文件 -- [ ] build.gradle / settings.gradle / mods.toml 落位 -- [ ] 3 处 API 反替换(Identifier / FMLLoader / accessFlags) -- [ ] `gradlew :module.rpc:compileJava` 通过 -- [ ] 提交(conventional commit) - -### #P2 module.sync + processor(中-高) - -- [ ] 主模块 25 文件提取 + 反替换 -- [ ] SyncConfigManager 等价实现(getSecureJar ().getPath) -- [ ] processor 6 文件 + CoreMod 重写(ICoreMod + ITransformer) -- [ ] `gradlew :module.sync:compileJava` 通过,`grep neoforgespi.transformation` 零残留 -- [ ] 提交 - -### #P7 module.rendering(高 · 分批) - -> ⚠️ 2026-08-12:该模块已从 `port/1.21.1` 移除,本清单仅保留历史记录。 - -- [ ] #P7a 纯逻辑 19 项 + 模块骨架 -- [ ] #P7b 后处理三件套(Bloom/GaussianBlur/Glitch) -- [ ] #P7c SDF GUI -- [ ] #P7d CachedBER -- #P7e Compute -- [ ] #P7 收尾(shader json、mixins.json、AT、mods.toml) -- [ ] 提交(按批) - -### #P6 module.font(中) - -- [ ] 9 项直接移植 + 8 项反向适配 -- [ ] `gradlew :module.font:compileJava` + runData 通过 -- [ ] 提交 - -### #P3 module.explosion(低) - -- [ ] 10 文件提取 + 6 处反替换 -- [ ] build.gradle(依赖 port module.config,自建 run) -- [ ] `gradlew :module.explosion:compileJava` + runData 通过 -- [ ] 提交 - -### #P4 module.collision(极低) - -- [ ] 3 文件提取(零 MC/NF API) -- [ ] 提交 - -### #P5 module.space-select(中) - -- [ ] 18 文件提取 + 反替换 -- [ ] DistrictRenderer 渲染路径重写(LevelRenderer.renderVoxelShape) -- [ ] `gradlew :module.space-select:compileJava` 通过 -- [ ] 提交 - -### #P8 renderdoc-loader(低) - -> ⚠️ 2026-08-12:该模块已从 `port/1.21.1` 移除,本清单仅保留历史记录。 - -- [ ] 2 文件提取 + 构建脚本改写(Java 21) -- [ ] 提交 - -### #P9 module.config(低) - -- [ ] Config/ConfigManager/ConfigRecord group 支持 -- [ ] TranslatableEnum + ConfigData 枚举分支 -- [ ] FormattingUtil.toPointSplitName + TOML key 点分隔 -- [ ] `gradlew :module.config:compileJava` + runData 通过 -- [ ] 提交 - -### #P10 module.integration(低 · 破坏性 API) - -- [ ] applyData → applyClientData/applyServerData 拆分 -- [ ] meter.increment () -- [ ] `gradlew :module.integration:compileJava` 通过 -- [ ] 提交(commit 说明注明破坏性 API) - -### #P11 module.network(低) - -- [ ] PacketData 泛型检查修复 -- [ ] NetworkUtil Included 方法 + Objects.equals -- [ ] PacketProtocol public -- [ ] `gradlew :module.network:compileJava` 通过 -- [ ] 提交 - -### #P12 module.util(中) - -- [ ] OutlineUtil + Line + ShapeUtilJoinTimingTest -- [ ] 3 个 predicate HolderGetter 重构 + module.recipe 4 个调用方 -- [ ] withCount + @EqualsAndHashCode -- [ ] 保留 port `with(BlockState)`(不可照搬 dev 删除) -- [ ] `gradlew :module.util:compileJava` + `:module.recipe:compileJava` + 测试通过 -- [ ] 提交 - -### #P13 module.registrum(低) - -- [ ] 直接移植 10 项 + 反向适配 9 项(RecipeSerializer 匿名类、VillagerProfession 6 元、Attachment Codec 等) -- [ ] 两个零碎修复(item 默认 lang、OneTimeEventReceiver 空值) -- [ ] 不迁 dataComponentPredicate 入口 -- [ ] `gradlew :module.registrum:compileJava` 通过 -- [ ] 提交 - -### #P14 module.registrum datagen(中) - -- [ ] RecipeProvider 方法 public 化(公开包装内调静态方法) -- [ ] dataMap (provider) 重载 -- [ ] `gradlew :module.registrum:compileJava` + runData 通过 -- [ ] 提交 - -### #P15 module.multiblock(中) - -- [ ] M1 DynamicMultiblockEvent(可取消事件) -- [ ] M2 未加载区块快照复用 -- [ ] M3 Long→BlockPos 键化(含 Java 21 lambda `_` 改名) -- [ ] M4 定义懒解析(ResourceKey + registryAccess) -- [ ] M7 Config.group(依赖 #P9) -- [ ] `gradlew :module.multiblock:compileJava` + `:module.recipe:compileJava` 通过 -- [ ] 提交 - -### #P16 module.recipe(低) - -- [ ] R1 SpawnItem 零数量守卫(可选 R2 必填校验) -- [ ] R3 SetBlock nbt 默认值(null → 空 CompoundTag) -- [ ] `gradlew :module.recipe:compileJava` 通过 -- [ ] 提交 - -### #P17 module.wheel(中-高) - -- [ ] WheelSelectionEffect + WheelMenuBuilder/Model 链式方法 -- [ ] WheelWidget.renderSelectionEffect + WheelScreen 适配 -- [ ] annular_sector.fsh + ShaderInstance 注册(切断 rendering 依赖) -- [ ] `gradlew :module.wheel:compileJava` 通过 -- [ ] 提交 - -### #P18 module.test(低-中) - -- [ ] T2 配置测试(随 #P9) -- [ ] T8 RPC 测试(随 #P1) -- [ ] T9 爆炸测试(随 #P3) -- [ ] T10 wheel 按键扩展(随 #P17,KeyMapping 1.21.1 API) -- [ ] `gradlew :module.test:compileJava` 通过 -- [ ] 提交 - -### #P19 构建体系(中) - -- [x] roseau.yaml + gradle/scripts/roseau.gradle + 根 build.gradle 接入(`544c9eb0`) -- [x] `gradlew roseauCheck` 运行通过(2026-08-12 本机验证 codec/rpc;roseau-cli 0.4.0,CSV+HTML 双报告) -- [x] 模块化 CI 构建矩阵回移植(来源 `aa59e372`:#53):`.github/modules.json` + `generate-matrix.js` + l0/l1/l2 分层矩阵 + roseau_check/roseau_comment 工作流 -- [x] 提交 - -### #P20 module.codec:CodecUtil 便携 create/mapCodec(计划外追加 · 2026-08-11) - -> 来源:`dev/26.1` 提交 `173a454`(feat(codec): add portable methods for creating Codec and MapCodec with varying parameters) -> 落点:`module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/CodecUtil.java` - -- [x] 提取改动:16 个 `create`(Codec)+ 16 个 `mapCodec`(MapCodec)重载 + `App`/`Function3–16`/`StreamCodec` 导入 -- [x] 冲突消解:26.1 `Identifier` → 1.21.1 `ResourceLocation`;保留 port 侧既有 `zomListMap`/`encodeStart` 并追加在其后 -- [x] API 核实:1.21.1 DFU 6.0.8 含 `Function3–16`(javap 实测),无需反向替换 -- [x] `gradlew :anvillib-codec-neoforge-1.21.1:compileJava --rerun-tasks` 通过 -- [ ] 提交(conventional commit) - -### #P21 module.wheel:环扇模式轮盘渲染重做同步(计划外追加 · 2026-08-13) - -> 来源:`dev/26.1` 提交 `aa123c9`(feat(wheel): 重做环扇模式轮盘渲染 #98) -> 要求:效果与用法(公开 API)与 dev 一致,实现适配 1.21.1(module.rendering 已在 port 删除,不引入) - -- [x] API 核对:api 包两边一致(`WheelEntry.mutableCopy` 位置、`WheelSelectionEffect` 空行差异无关紧要;`WheelEntryRenderer` 保持 1.21.1 `(GuiGraphics, PoseStack)` 签名) -- [x] shader:`annular_sector` 半径语义修正(InnerDiameter/OuterDiameter → InnerRadius/OuterRadius);新增 `frosted_disc`/`blur`/`segment`/`disc` 四组 fsh+json(1.21.1 坐标约定:位置任意形状按 `selection.fsh` 做 Y 翻转) -- [x] `LibShaders` 新增 4 个 ShaderInstance 注册(frostedDisc/blur/segment/disc) -- [x] `WheelFrostedBackground` 自建模糊管线(`TextureTarget`×2 ping-pong,4 pass 高斯模糊,替代 dev 的 rendering `GaussianBlur`) -- [x] `WheelWidget` 以 dev 版为蓝本重写:盘面/毛玻璃/分隔圆环/中心标题/hover 箭头/翻页箭头/环扇滑动 + settle 外扩动画(500ms 停驻 + 200ms 外扩 2px + 透明度 20%→100%);渲染全部落在 Tesselator + ShaderInstance + 多边形 fallback 模式 -- [x] `WheelScreen` 半径 0.17/0.33、毛玻璃生命周期(init/removed)、翻页箭头替代「1 / N」文字 -- [x] module.test 对齐:`WheelDemoMenus` 扩至 9/8 项 + APPLE 图标 renderer(1.21.1 签名),新增 B/N 键(TAP/HOLD 扇形效果),lang 补齐 zh_cn/en_us/en_ud -- [x] `gradlew :anvillib-wheel-neoforge-1.21.1:compileJava` + `:anvillib-test-neoforge-1.21.1:compileJava` 通过;`grep v2.rendering` 零残留 -- [x] runClient 运行验证(2026-08-13):6 个 shader 全部注册成功,无 FATAL、无 wheel 渲染错误,正常进入世界;修掉 1.21.1 shader json `samplers` 字段格式问题(需 JsonObject 数组而非字符串数组) -- [x] 提交(conventional commit) - -> ⚠️ 环境遗留:module.test `runData` 受 modDevGradle 2.0.78 注入 `--width/--height` 影响无法运行(1.21.1 datagen 不识别该参数),en_us/en_ud 已按 dev 文案手动补齐;该问题为既有构建环境问题,非本次改动引入。 -> ⚠️ 环境遗留:runClient 首启时 `anvillib_moveable_entity_block` mixin 崩溃(PistonBaseBlockMixin 找不到)为模块 classes 未编译的环境状态问题(moddev 对仅含 resources 的模块 mod file 无法加载 mixin 类),全量编译后自愈,与本次改动无关。 - ---- - -## 待验证事项销账(计划附录 B) - -| 编号 | 事项 | 涉及项 | 状态 | -|------|------------------------------------------------------------------------------------|-----------------|--------| -| V-1 | `Mth.roundToward`/`VertexSorting`/`CompactVectorArray`/`MeshData.sortQuads` 存在性 | #P7a | ✅ 已验证(roundToward 存在;CompactVectorArray 不存在改 VertexSorting.sort,V-1 计划修正) | -| V-2 | Iris 1.7.x `ImmediateState` 字段 | #P7d | ✅ 已验证(Iris 1.8.12 签名一致,改反射访问) | -| V-3 | `MultiBufferSource.BufferSource` 构造器参数 | #P7a | ✅ 已验证(super(null,null) 兼容) | -| V-4 | `renderVoxelShape` 颜色语义 | #P5 | 未验证(需 runClient 目测) | -| V-5 | port 侧 run 配置自建模式 | #P3/#P5/#P8 | ✅ 已验证(#P3 runData 通过、#P5 processResources 通过) | -| V-6 | Iris 1.21.1 Modrinth 坐标 | #P7d | ✅ 已验证(Modrinth maven 停服,改反射访问,坐标注释占位) | -| V-7 | mods.toml 模板变量注入 | #P7 | ✅ 已验证(port 模板展开正常) | -| V-8 | ShaderDefines → Map | #P7e | ✅ 已验证(编译通过) | -| V-9 | CoreMod jar-in-jar services 可见性 | #P2 | 未验证(需打包启动实测) | -| V-10 | CoreMod GETSTATIC 类可解析性 | #P2 | 未验证(需启动日志核对) | -| V-11 | `getSecureJar().getPath` 签名 | #P2 | ✅ 已验证(javap 确认返回 Path) | -| V-12 | `FMLLoader.getCurrent().getGameDir()` | #P6 | ✅ 已验证(改 FMLPaths.GAMEDIR.get() 编译+runData 通过) | -| V-13 | jspecify 在 port 构建链可用性 | #P15(M6 可选) | ✅ 已验证(多模块 compileOnly 使用通过) | -| V-14 | VillagerProfession 6 元 record / Potion 构造 | #P13 | ✅ 已验证(编译通过) | -| V-15 | RecipeSerializer 匿名类编译 | #P13 | ✅ 已验证(编译通过) | -| V-16 | District.color() int 打包颜色语义 | #P5 | 未验证(需 runClient 目测) | -| V-17 | GatherDataEvent includeClient/getPackOutput 签名 | #P3/#P6/#P14 | ✅ 已验证(#P3 runData、#P6 runData 通过) | -| V-18 | 根构建脚本模块注册方式对齐 | #P1–#P8 | ✅ 已验证(全部新模块注册并编译) | - -## 遗留登记(环境限制未完成的运行类验收) - -| 编号 | 事项 | 说明 | 复现/补验步骤 | -|------|------|------|----------------| -| L-1 | roseauCheck 运行验证(#P19) | ✅ 已解决(2026-08-12):roseau-cli 0.6.0 按 Java 25 编译,降至 0.4.0(Java 21 兼容);roseauCheck 改为普通任务并在 doFirst 内以 javaexec 分两次产出 CSV/HTML;基线构件改由 rootProject 解析;已声明与配置缓存不兼容;本机 codec/rpc 验证通过 | `./gradlew :anvillib-<模块>-neoforge-1.21.1:roseauCheck` | -| L-2 | RPC 行为验证(#P1/#P18 T8) | 远程调用往返、@CallableParam 编解码、tick 超时 | runClient/runServer,`-Danvillib.test.rpc.auto` 系统属性 | -| L-3 | sync 行为验证(#P2) | @Sync 字段同步、LazySync、processor 字节码注入生效 | runClient 启动日志 + 反编译核对注入点 | -| L-4 | explosion 行为验证(#P3/#P18 T9) | 分层球壳破坏、熔化替换、实体 hurt | runClient/runServer 执行测试命令 | -| L-5 | space-select 渲染(#P5,V-4/V-16) | 线框绘制颜色/平移语义、滚轮交互、网络收发 | runClient 目测 | -| L-6 | font 运行验证(#P6) | SDF 文字渲染、anvillib$text 扩展、FontConfigScreen | runClient | -| L-7 | rendering 运行验证(#P7a) | ⚠️ 2026-08-12:module.rendering 已移除,不再适用 | — | -| L-8 | multiblock 行为验证(#P15) | 事件可取消回滚、快照复用、懒解析 | runServer | -| L-9 | wheel 运行验证(#P17) | 环形扇区效果、颜色配置、旧路径无回归 | runClient + T10 | -| L-10 | Iris 集成(#P7d,V-2/V-6) | ✅ 已解决:#P7d 完成,Iris 经反射访问(签名一致),运行时需装 Iris 客户端验证 | — | -| L-11 | Compute(#P7e,V-8) | ✅ 已解决:#P7e 完成(编译通过),dispatch/内存屏障运行验证待 runClient | — | -| L-12 | rendering 执行层运行验证(#P7b-e) | ⚠️ 2026-08-12:module.rendering 已移除,不再适用 | — | diff --git a/docs/minecraft-neoforge-version-differences.md b/docs/minecraft-neoforge-version-differences.md deleted file mode 100644 index 8dece3cc..00000000 --- a/docs/minecraft-neoforge-version-differences.md +++ /dev/null @@ -1,1140 +0,0 @@ -# Minecraft / NeoForge 版本差异适配文档:1.21.1 → 26.1 - -> 文档信息 -> -> - 生成日期:2026-08-10 -> - 分析对象:`port/1.21.1` @ `fab551b`(Minecraft 1.21.1 / NeoForge 21.1.226)↔ `dev/26.1` @ `0b7e45f`(Minecraft 26.1.2 / - NeoForge 26.1.2.76),merge-base `81c5a9c` -> - 数据来源:`git diff port/1.21.1 dev/26.1` 逐模块 diff 实测 + dev/26.1 侧源码抽查 + NeoForge 26.1.2.76 sources jar 实测( - `net.neoforged.neoforge.transfer.*`、`net.neoforged.neoforge.fluids.capability.*`、 - `net.neoforged.neoforge.capabilities.*`) -> - 关联文档:本文是 **版本差异适配文档**,专注「差异中属于 Minecraft/NeoForge API 版本差异的必要适配」;两侧分支关系与回迁项判定见 - `docs/branch-comparison.md` -> - 结论性质:除标注「待验证」处外,均基于实测提交、文件内容与 26.1.2.76 sources jar 源码得出 - ---- - -## 1. 概述与结论 - -AnvilLib 的 `dev/26.1` 分支沿 1.21.2 → 1.21.11 → 26.1 的版本链逐级升级(`6f2685a` 正式切入 26.1),而 `port/1.21.1` 冻结在 -1.21.1 平台。对共享模块执行 `git diff port/1.21.1 dev/26.1` 后,把差异中「Minecraft/NeoForge API 版本差异」的部分单独抽出,即得到本文整理的 -13 个差异面。这些差异是 **单向必要适配**:dev/26.1 侧已是新 API 形态,任何从 port/1.21.1 回迁的代码都必须按此清单改写,否则无法在 -26.1 编译。 - -### 1.1 差异面总览 - -| # | 差异面 | 一句话概括 | -|----|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 2 | 标识符与基础类型 | `ResourceLocation` → `Identifier`(含 CODEC/STREAM_CODEC、buffer 读写方法),`ResourceKey.location()` → `identifier()`,官方包 `critereon` → `criterion` | -| 3 | 物品栈体系 | 可变 `ItemStack` 让位给不可变 `ItemStackTemplate`(`typeHolder()`/`withCount(n).create()`/`components()`),`Inventory` 公开字段移除 | -| 4 | 谓词体系 | `ItemSubPredicate`/`DataComponentPredicate` 谓词参数让位给 `net.minecraft.advancements.criterion.DataComponentMatchers`,`matches(ItemStack)` → `matches(DataComponentGetter)` | -| 5 | 配方体系 | `RecipeSerializer` 变 final 类(`new RecipeSerializer<>(CODEC, STREAM_CODEC)`),`RecipeManager` 内部改 `RecipeMap`,`save` 改收 `ResourceKey`,`recipeAccess()` 取代 `getRecipeManager()` | -| 6 | 网络 | `PayloadRegistrar` 双向注册 3 参 → 4 参(client/server handler 拆分),`FMLLoader.getCurrent()` 实例化访问,`IPacket.type()` 返回 `Identifier` | -| 7 | 注册表与延迟注册 | `Registry.get` → `getOptional`/`getValue`,tag 解析需 `HolderGetter`,`getTag` → `getTagOrEmpty` | -| 8 | 渲染管线 | `extractRenderState`/`submit` 两段式 RenderState 架构,`GuiGraphics` → `GuiGraphicsExtractor`,`PoseStack` → `Matrix3x2fStack`,`RenderPipeline`+UBO 取代 `ShaderInstance`/shader json | -| 9 | 事件与加载器 | `FMLLoader.getDist()` → `getCurrent().getDist()`,`GatherDataEvent` 按端拆分,`BreakBlockEvent`,`KeyEvent`/`MouseButtonEvent` 输入事件 | -| 10 | NBT 与持久化 | `CompoundTag`+`HolderLookup` → `ValueInput`/`ValueOutput`,`SavedData.Factory` → `SavedDataType`+Codec | -| 11 | 能力(capability) | 物品 `IItemHandler` → `ResourceHandler`(无 `setResource`),流体 `IFluidHandler` 弃用 → `ResourceHandler`,`BlockCapability.createSided(Identifier, ...)` | -| 12 | 注解体系 | javax/neoforge 空注解 → jspecify `@NullMarked` + `org.jspecify.annotations.Nullable` | -| 13 | 数据生成与构建 | `REGISTRUM.addDataGenerator` 取代 `GatherDataEvent` 注册,根级 `module.gradle` 聚合,坐标 `-neoforge-26.1`,`anvillib.needRunConfig` | - -### 1.2 回迁代码的共性适配清单 - -以下适配项与具体业务无关, **任何从 port/1.21.1 回迁的代码几乎必然触及**,是回迁前的统一预处理清单: - -1. `ResourceLocation` → `Identifier`:import 与全部用法(含 `ResourceLocation.CODEC/STREAM_CODEC`、`fromNamespaceAndPath`、 - `writeResourceLocation/readResourceLocation`)。 -2. 空注解体系切换:`@MethodsReturnNonnullByDefault`/`@ParametersAreNonnullByDefault`/`javax.annotation.Nullable` → 包级 - `@NullMarked` + `org.jspecify.annotations.Nullable`。 -3. `FMLLoader`/`LoadingModList` 静态访问 → `FMLLoader.getCurrent()` 实例访问(`getDist()`、`getLoadingModList()`)。 -4. 构建文件不搬运:port 侧各模块独立 `build.gradle`(约 270 行)不回迁,按 dev 侧 `module.gradle` 聚合体系(依赖坐标 - `-neoforge-26.1`)落位。 -5. `@Mod` 构造器去掉 `ModContainer` 参数(`(IEventBus bus, ModContainer container)` → `(IEventBus modBus)`)。 -6. 渲染相关代码(若涉及 GUI/方块实体渲染):`render(GuiGraphics, ...)` → `extractRenderState(GuiGraphicsExtractor, ...)`、 - `PoseStack` → `Matrix3x2fStack`、shader 注册改 `RenderPipeline`。 - -### 1.3 判定规则与依据 - -- 本文只收录「属于 Minecraft/NeoForge API 版本差异」的部分(dev 侧已是新 API 的必要适配),不收录 dev - 侧业务演进(事件系统、构建重构、新模块等),也不收录 port 侧独有 bugfix 回迁(见 `docs/branch-comparison.md` 的 #1–#8)。 -- 每个条目给出 dev/26.1 侧真实文件路径,可通过 `git show dev/26.1:<路径>` 复核。 -- 涉及 26.1.2.76 官方 API 的表述,优先以本地 sources jar 源码为准。 - ---- - -## 2. 标识符与基础类型 - -### 2.1 `ResourceLocation` → `Identifier` - -**变化**:Minecraft 1.21.5+ 官方映射将 `net.minecraft.resources.ResourceLocation` 重命名为 -`net.minecraft.resources.Identifier`,是本仓库 diff 中出现频率最高的机械替换项,具体包括: - -| 旧(1.21.1) | 新(26.1) | -|------------------------------------------------------------|----------------------------------------------------| -| `net.minecraft.resources.ResourceLocation` | `net.minecraft.resources.Identifier` | -| `ResourceLocation.fromNamespaceAndPath(ns, path)` | `Identifier.fromNamespaceAndPath(ns, path)` | -| `ResourceLocation.CODEC` / `ResourceLocation.STREAM_CODEC` | `Identifier.CODEC` / `Identifier.STREAM_CODEC` | -| `buf.writeResourceLocation(RL)` / `readResourceLocation()` | `buf.writeIdentifier(id)` / `buf.readIdentifier()` | - -dev/26.1 侧实际形态(`InWorldRecipe.java` 的 `STREAM_CODEC`): - -```java -buf.writeIdentifier(Objects.requireNonNull(recipe.trigger(). - -getId())); -// ... -Identifier location = buf.readIdentifier(); -``` - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java` -- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/CodecUtil.java` -- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/StreamCodecUtil.java` -- `module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IPacket.java` -- `module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTest.java` - -**迁移注意**:这是纯机械替换,但注意 26.1 下 `Identifier` 不再接受字符串「直接构造」的旧写法(1.21.1 的 -`new ResourceLocation(String)` 已移除),统一走 `Identifier.fromNamespaceAndPath` / `Identifier.parse`。 - -### 2.2 `ResourceKey.location()` → `ResourceKey.identifier()` - -**变化**:`ResourceKey` 的资源位置访问方法随 1.21.5 改名:`key.location()` → `key.identifier()`。dev/26.1 侧实际形态( -`MultiblockFormPacket.java`): - -```java -IController controller = ControllerRecord.get(state.getBlock(), this.state.getDefinitionKey().identifier()); -``` - -**涉及文件**: - -- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/network/MultiblockFormPacket.java` -- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/network/MultiblockUnformPacket.java` -- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/DynamicMultiblockManager.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/mixin/RecipeMapMixin.java` - -**迁移注意**:`ResourceKey.create(Registries.X, Identifier)` 的构造方式不变,只有 `location()` 访问方法改名;grep 时以 -`\.location()` 结尾且接收方为 `ResourceKey` 的调用点为搜索目标。 - -### 2.3 官方包改名:`critereon` → `criterion` - -**变化**:`net.minecraft.advancements.critereon` 包整体更名为 `net.minecraft.advancements.criterion`(26.1 官方改名),影响该包下全部约 -20 个 predicate 类的 import(`module.codec/StreamCodecUtil.java` 全部 import 变更即为此事)。两个具体影响点: - -- `RecipeUnlockedTrigger`:`net.minecraft.advancements.critereon.RecipeUnlockedTrigger` → - `net.minecraft.advancements.criterion.RecipeUnlockedTrigger`(见 5.4)。 -- AT 文件目标描述符同步改名:`module.codec/src/main/resources/META-INF/accesstransformer.cfg` 中 - `net.minecraft.advancements.critereon.LocationPredicate$PositionPredicate` → - `net.minecraft.advancements.criterion.LocationPredicate$PositionPredicate`。 - -**涉及文件**: - -- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/StreamCodecUtil.java` -- `module.codec/src/main/resources/META-INF/accesstransformer.cfg` - -**迁移注意**:回迁代码中所有 `critereon` 包 import 必须改为 `criterion`,否则编译失败;此条与 4.1 的 -`DataComponentMatchers`(新包名下的新类型)配套出现。 - ---- - -## 3. 物品栈体系 - -### 3.1 `ItemStack` → 不可变 `ItemStackTemplate` - -**变化**:26.1 中用于「配方产出、缓存、序列化」等场景的物品栈主力形态从可变的 `ItemStack` 改为不可变的 `ItemStackTemplate` -,核心差异: - -| 旧(1.21.1) | 新(26.1) | -|------------------------------------------------|--------------------------------------------------------------------------------------------| -| `ItemStack stack = new ItemStack(item, count)` | `ItemStackTemplate`(record 式不可变,字段为 `typeHolder()` + `count()` + `components()`) | -| `stack.copy()` / `stack.setCount(n)` | `template.withCount(n).create()`(`create()` 产出 `ItemStack`) | -| `ItemStack.CODEC` / `STREAM_CODEC` | `ItemStackTemplate.CODEC` / `ItemStackTemplate.STREAM_CODEC` | -| `stack.getItem()` | `template.typeHolder().value()` | -| `stack.getComponents()` | `template.components()` | - -dev/26.1 侧实际形态(`InWorldRecipe.java`,icon 字段与序列化): - -```java -private final @Unmodifiable ItemStackTemplate icon; - -// ... -public ItemStack getResultItem() { - return this.icon.create(); -} -// ... -ItemStackTemplate.CODEC. - -fieldOf("icon"). - -orElseGet(() ->new - -ItemStackTemplate(Items.ANVIL)). - -forGetter(InWorldRecipe::icon), -// ... -ItemStackTemplate.STREAM_CODEC. - -encode(buf, recipe.icon()); -``` - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/SpawnItem.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java` -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ChanceItemStack.java` -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/stack/UnlimitedItemStack.java`(旧版, - `@Deprecated(forRemoval = true)`) - -**迁移注意**:模板化后「先改数量再使用」必须走 `withCount(n).create()` 链式调用;`create()` 每次产出新 `ItemStack` -,不要再假设可以原地修改共享模板。 - -### 3.2 `ItemStack.ITEM_NON_AIR_CODEC` → `Item.CODEC` - -**变化**:26.1 移除了 `ItemStack.ITEM_NON_AIR_CODEC`,需要「只编解码物品类型、不带数量」的场景改用 `Item.CODEC`(非空校验)或 -`Item.CODEC_WITH_BOUND_COMPONENTS`(带组件)。dev/26.1 侧实际形态(`SpawnItem.java`,配合 `typeHolder()` 使用): - -```java -// 旧:ItemStack.ITEM_NON_AIR_CODEC → 新:Item.CODEC / Item.CODEC_WITH_BOUND_COMPONENTS(配合 typeHolder()) -``` - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/outcome/SpawnItem.java` -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ChanceItemStack.java` -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/stack/UnlimitedItemStack.java` - -**迁移注意**:`Item.CODEC` 与 `Item.CODEC_WITH_BOUND_COMPONENTS` 的语义差异在于组件是否参与编解码,按业务场景选择,不要一律替换为 -`Item.CODEC`。 - -### 3.3 `Inventory` 公开字段移除 - -**变化**:`net.minecraft.world.entity.player.Inventory` 的 `items`/`armor`/`offhand` 公开字段在 26.1 移除,遍历玩家背包改为走 -`getContainerSize()` + `getItem(i)` 循环。dev/26.1 侧 `InventoryUtil.getItems` 即按此改写。 - -**涉及文件**: - -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/InventoryUtil.java` - -**迁移注意**:若回迁代码直接引用 `inventory.items` 等字段,改写成按槽位循环;注意 `Inventory` 的槽位区间(主手 0–35、盔甲 -36–39、副手 40)在 26.1 未变。 - -### 3.4 相关:`LootParams` 第二参数改为 `ContextMap` - -**变化**:`LootParams(Level, Map)` 构造中第二参数在 26.1 改为 `ContextMap`: -`new LootParams(level, Map.of(), ...)` → `new LootParams(level, ContextMap.EMPTY, ...)`(或 -`new ContextMap.Builder().create(new ContextKeySet.Builder().build())`)。出现在 `ChanceBlockState`/ -`WeightedChanceBlockStates`/`ChanceItemStack`/`InWorldRecipeContext` 的 loot table 上下文构造中。 - -**涉及文件**: - -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ChanceBlockState.java` -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/WeightedChanceBlockStates.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipeContext.java` - -**迁移注意**:`ContextMap.EMPTY` 只在业务不依赖自定义参数时可用;需要向 loot context 传参的场景须改用 `ContextKeySet`/ -`ContextMap.Builder`。 - ---- - -## 4. 谓词体系 - -### 4.1 `ItemSubPredicate`/`DataComponentPredicate` → `DataComponentMatchers` - -**变化**:1.21.1 的「物品子谓词」体系在 26.1 中拆为两条线: - -1. **谓词参数类型**(`ItemPredicate` 构造时传入的组件匹配器):`ItemSubPredicate` 与早期 `DataComponentPredicate` 参数 → - `net.minecraft.advancements.criterion.DataComponentMatchers`(含 `CODEC.codec()`、`ANY` 常量、 - `Builder.components().exact(DataComponentExactPredicate.builder()...)` 链)。 -2. **谓词实现载体**:`net.minecraft.core.component.predicates.DataComponentPredicate` 保留并成为「可注册的组件谓词」接口,AnvilLib - 的 `AndPredicate`/`NotPredicate`/`OrPredicate` 直接实现它。 - -dev/26.1 侧实际形态: - -```java -// module.recipe/predicate/item/HasItem.java - -import net.minecraft.advancements.criterion.DataComponentMatchers; -// ItemPredicate 以 DataComponentMatchers 作为参数字段 -``` - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItem.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItemIngredient.java` -- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/StreamCodecUtil.java`(`ITEM_PREDICATE` 字段: - `DataComponentPredicate.STREAM_CODEC` → `DataComponentMatchers.STREAM_CODEC`) -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java` - -**迁移注意**:这是「类型归属」迁移——1.21.1 的 `DataComponentPredicate` import(`net.minecraft.advancements.critereon` 或旧 -`component` 包)需要按用途分流:匹配器参数走 `DataComponentMatchers`,谓词实现走 -`net.minecraft.core.component.predicates.DataComponentPredicate`,两者不可混用。 - -### 4.2 `matches(ItemStack)` → `matches(DataComponentGetter)` - -**变化**:组件谓词的匹配入口从「整个物品栈」收窄为「组件读取器」:`matches(ItemStack)` → `matches(DataComponentGetter)` -。dev/26.1 侧实际形态(`AndPredicate.java`): - -```java - -@Override -public boolean matches(DataComponentGetter getter) { - return this.subPredicates().stream().allMatch(it -> it.getValue().matches(getter)); -} -``` - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/advancement/predicate/item/AndPredicate.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/advancement/predicate/item/OrPredicate.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/advancement/predicate/item/NotPredicate.java` - -**迁移注意**:调用方需要「先取组件再匹配」——`stack.getComponents()`(`ItemStack` 实现了 `DataComponentGetter` -)传入;谓词内不再直接读取栈的数量/物品类型信息。 - -### 4.3 `ItemPredicate.withSubPredicate` 删除 - -**变化**:`ItemPredicate` 的 `subPredicates` 字段与 `Builder.withSubPredicate(...)` 在 26.1 一并删除(1.21.1 的 -`ItemSubPredicate` 无对应替代),dev 侧全部调用方已清理(grep 无残留)。`module.util` 的 `IItemStackPredicate` 同样删除了 -`subPredicates()`。 - -**涉及文件**: - -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java` -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/IItemStackPredicate.java` - -**迁移注意**:不要尝试保留 `withSubPredicate` 兼容层;如需组合谓词,走 `AndPredicate`/`OrPredicate`/`NotPredicate`(实现 -`DataComponentPredicate`)的组合方式。 - -### 4.4 注册表 `ITEM_SUB_PREDICATE_TYPE` → `DATA_COMPONENT_PREDICATE_TYPE` - -**变化**:1.21.5+ 引入 `DATA_COMPONENT_PREDICATE_TYPE` 注册表取代 `ITEM_SUB_PREDICATE_TYPE`;注册条目类型由 -`ItemSubPredicate.Type` 改为 `DataComponentPredicate.TypeBase`(匿名子类),codec/streamCodec 签名随之变化。dev/26.1 侧 -`LibItemSubPredicates.java` 已被 `LibDataComponentPredicates.java` 取代。 - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/init/LibDataComponentPredicates.java` - -**迁移注意**:1.21.1 侧 `LibItemSubPredicates` 不迁移;新注册走 `DATA_COMPONENT_PREDICATE_TYPE`,谓词类型自身实现 -`DataComponentPredicate`(见 4.2)。 - ---- - -## 5. 配方体系 - -### 5.1 `RecipeSerializer` 变 final 类,注册改 `new RecipeSerializer<>(CODEC, STREAM_CODEC)` - -**变化**:1.21.1 中可被继承实现 `Serializer` 的 `RecipeSerializer` 接口在 26.1 成为 final -类,自定义配方的序列化器不再「implements」,而是直接实例化:`new RecipeSerializer<>(CODEC, STREAM_CODEC)`。CODEC/STREAM_CODEC -从接口方法变为构造参数,须以 `public static final` 字段形式挂在自定义配方类上。dev/26.1 侧实际形态(`LibRecipeTypes.java`): - -```java -public static final DeferredHolder, RecipeSerializer> IN_WORLD_RECIPE_SERIALIZER = - RECIPE_SERIALIZERS.register( - "in_world_recipe", - () -> new RecipeSerializer<>(InWorldRecipe.Serializer.CODEC, InWorldRecipe.Serializer.STREAM_CODEC) - ); -``` - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/init/recipe/LibRecipeTypes.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java`(`Serializer` 内 `CODEC` 改为 - `public static final`) - -**迁移注意**:回迁自定义配方时把「序列化器类」改为「序列化器常量」,`RecipeType` 的注册方式( -`DeferredRegister>` + `new RecipeType<>() {}`)不变。 - -### 5.2 `RecipeManager` 内部结构 `RecipeMap` - -**变化**:1.21.2+ 起 `RecipeManager` 内部按 key 索引的 -`byName: Map>` 与 `byType: Multimap<...>` 重构为 `net.minecraft.world.item.crafting.RecipeMap`(按 `ResourceKey>` -索引),对 `RecipeManager` 的注入/访问逻辑迁移到 `RecipeMapMixin`。dev/26.1 侧实际形态(`RecipeMapMixin.java`): - -```java - -@Mixin(RecipeMap.class) -public class RecipeMapMixin implements IRecipeMapExtension { - @Mutable - @Final - @Shadow - private Map>, RecipeHolder> byKey; // 遍历 key 时经 key.identifier() 取 Identifier -} -``` - -同时 `RecipeHolder.id()` 返回 `ResourceKey`,取 `Identifier` 需 `.id().identifier()`(如 `InWorldRecipeManager` 中 -`recipe.id().identifier()`)。 - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/mixin/RecipeMapMixin.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/injection/IRecipeMapExtension.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/mixin/RecipeManagerMixin.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipeManager.java` - -**迁移注意**:注入目标从 `RecipeManager` 换到 `RecipeMap` 后,`@Shadow` 字段类型与索引 key 类型(`ResourceKey>` -而非 `Identifier`)必须同步;`anvillib$getRegistries` 在 dev 侧已 `@Deprecated`,新代码不要依赖。 - -### 5.3 `RecipeBuilder.save(RecipeOutput, ResourceKey>)` - -**变化**:26.1 中 `RecipeBuilder.save(...)` 与进度触发均从「资源位置」改为「配方 key」: - -| 旧(1.21.1) | 新(26.1) | -|-------------------------------------------------------|------------------------------------------------------------------------------| -| `builder.save(recipeOutput, ResourceLocation)` | `builder.save(recipeOutput, ResourceKey>)`,配套新增 `defaultId()` | -| `RecipeUnlockedTrigger.unlocked(ResourceLocation)` | `RecipeUnlockedTrigger.unlocked(ResourceKey>)` | -| `AdvancementRewards.Builder.recipe(ResourceLocation)` | `AdvancementRewards.Builder.recipe(ResourceKey>)` | - -dev/26.1 侧实际形态(`InWorldRecipeBuilder.java`): - -```java -public ResourceKey> defaultId() { ...} - -public void save(RecipeOutput recipeOutput, ResourceKey> key) { ...} -// 为兼容保留 save(RecipeOutput, Identifier) 重载 -``` - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java` - -**迁移注意**:dev 侧保留了 `save(RecipeOutput, Identifier)` 重载作兼容,回迁代码可沿用旧签名;但进度解锁路径必须传 -`ResourceKey`。 - -### 5.4 `RecipeUnlockedTrigger` 包移动 - -**变化**:`RecipeUnlockedTrigger` 随 `critereon` → `criterion` 官方包改名(见 2.3)移动: -`net.minecraft.advancements.critereon.RecipeUnlockedTrigger` → -`net.minecraft.advancements.criterion.RecipeUnlockedTrigger`。 - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/builder/InWorldRecipeBuilder.java` - -**迁移注意**:纯 import 替换,无 API 形态变化;与 5.3 的 `unlocked(ResourceKey)` 参数变化配套出现。 - -### 5.5 `Level.getRecipeManager()` → `serverLevel.recipeAccess()` - -**变化**:26.1 中获取配方访问入口改为 `ServerLevel#recipeAccess()`(`RecipeAccess`),`Level.getRecipeManager()` -在配方相关访问路径中不再使用。dev/26.1 侧实际形态(`ItemEntityEventListener.java`): - -```java -InWorldRecipeManager manager = serverLevel.recipeAccess().anvillib$getInWorldRecipeManager(); -``` - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/event/listener/ItemEntityEventListener.java` - -**迁移注意**:仅在持有 `ServerLevel` 时可用;`ClientLevel` 侧没有 `recipeAccess()`,客户端若需访问配方走其他入口(datapack -同步后的客户端缓存)。另注意 `Recipe` 接口在 26.1 的签名变化:`assemble(RecipeInput, HolderLookup.Provider)` → -`assemble(RecipeInput)`;`canCraftInDimensions(...)` 等旧方法被 `showNotification()` / `group()` / `placementInfo()`( -`PlacementInfo` 字段,须显式提供)/ `recipeBookCategory()` 取代(`module.recipe/.../InWorldRecipe.java`);客户端 -`RecipesUpdatedEvent` 在 26.1 已移除(`module.recipe/.../event/listener/ResourceEventListener.java` 仅保留 -`ServerStartedEvent`/`OnDatapackSyncEvent` 监听)。 - ---- - -## 6. 网络 - -### 6.1 `PayloadRegistrar` 双向注册 3 参 → 4 参 - -**变化**:NeoForge 26.1 的 `PayloadRegistrar` 双向(BIDIRECTIONAL)注册方法从 3 参数变为 4 参数,client/server 侧 handler -拆开传入:`playBidirectional(type, codec, handler)` → `playBidirectional(type, codec, handler, handler)`(其余 -`configurationBidirectional`/`commonBidirectional` 同理)。dev/26.1 侧实际形态(`NetworkRegistrar.java`): - -```java -case BIDIRECTIONAL -> - registrar. - -playBidirectional(data.type(),data. - -streamCodec(),data. - -handler(),data. - -handler()); -``` - -**涉及文件**: - -- `module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/NetworkRegistrar.java` - -**迁移注意**:AnvilLib 的 `IPacket` 体系单 handler 即可满足双向语义,4 参写法是同一 handler 传两次;若回迁代码自定义双向包,需按 -client/server 分别提供。 - -### 6.2 `LoadingModList.get()` → `FMLLoader.getCurrent().getLoadingModList()` - -**变化**:`LoadingModList.get()` 静态访问在 26.1 不可用,改为 `FMLLoader.getCurrent()` 实例的 `getLoadingModList()` -。dev/26.1 侧实际形态(`NetworkRegistrar.java`,用于扫描 `@Network` 注解包): - -```java -IModFileInfo fileInfo = FMLLoader.getCurrent().getLoadingModList().getModFileById(modId); -``` - -**涉及文件**: - -- `module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/NetworkRegistrar.java` -- `module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java`(同款改造,另配套 - `@SuppressWarnings("UnstableApiUsage")`) - -**迁移注意**:`FMLLoader.getCurrent()` 返回的实例在加载期外仍可调用,但部分方法标注 `UnstableApiUsage`,需保留 dev 侧的抑制注解。 - -### 6.3 `IPacket.type()` 返回 `Identifier` - -**变化**:`CustomPacketPayload.Type` 的构造参数从 `ResourceLocation` 改为 `Identifier`(网络包类型标识随 2.1 -改名)。dev/26.1 侧实际形态(`IPacket.java`): - -```java -import net.minecraft.resources.Identifier; - -static Type type(Identifier id) { ...} -``` - -**涉及文件**: - -- `module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IPacket.java` - -**迁移注意**:`CustomPacketPayload` 接口在 26.1 本身要求 `type()` 返回 `Type`,随 2.1 机械替换即可,网络注册侧( -`NetworkRegistrar`)无需额外适配。 - ---- - -## 7. 注册表与延迟注册 - -### 7.1 `Registry.get(RL)` → `getOptional` / `getValue` - -**变化**:`Registry.get(ResourceLocation)` 在 26.1 改名/移除,dev 侧按使用场景二选一: - -| 旧(1.21.1) | 新(26.1) | 场景 | -|----------------------------------|----------------------------------------------------------------------------|---------------| -| `BuiltInRegistries.ITEM.get(id)` | `BuiltInRegistries.ITEM.getOptional(id).orElse(Items.AIR)` | 有默认值兜底 | -| `ENTITY_TYPE.get(id)` | `ENTITY_TYPE.getValue(id)`(或 `containsKey` 预检 + `getOptional` 双保险) | 直接取值/判空 | - -dev/26.1 侧实际形态(`InWorldRecipe.java` STREAM_CODEC 解码侧): - -```java -IRecipeTrigger trigger = Objects.requireNonNull(LibRegistries.TRIGGER_REGISTRY.getValue(buf.readIdentifier())); -``` - -**涉及文件**: - -- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/CodecUtil.java`(`ITEM`/`BLOCK`/`ENTITY` 字段) -- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/StreamCodecUtil.java`(`ENTITY` 字段) -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java`(`getValue`) - -**迁移注意**:`getOptional` 返回 `Optional`(26.1 移除 `get` 后无「返回 null」语义的旧入口),所有 `get(id)` 调用点必须按「兜底 -orElse / 显式判空」改写,不能只换方法名。 - -### 7.2 tag 解析需 `HolderGetter` - -**变化**:26.1 延迟注册体系要求 tag 解析持有 `HolderGetter`(不能静态访问 `BuiltInRegistries`),相关 `Builder.of(...)` -入口从「tag key」升级为「`HolderGetter` + tag key」: - -```java -// 旧:BlockStatePredicate.Builder.of(TagKey) -// 新(dev/26.1 BlockStatePredicate.java): -public Builder of(HolderGetter blocks, TagKey tag) { ...} -``` - -`module.recipe` 侧 `HasBlock`/`HasBlockIngredient`/`HasItem`/`HasItemIngredient` 的 builder 层 `hasItem`/`hasBlock` 系列 -12 个方法签名同步增加 `HolderGetter` 参数。 - -**涉及文件**: - -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/BlockStatePredicate.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasBlock.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/predicate/item/HasItem.java` -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/ItemPredicate.java` - -**迁移注意**:`HolderGetter` 来源一般是 `level.registryAccess()`(服务器/数据包环境)或 `registry` 自身;datapack 环境不能再走 -`BuiltInRegistries` 静态路径。 - -### 7.3 `Registry.getTag` → `getTagOrEmpty` - -**变化**:`Registry.getTag(TagKey)`(返回 `Optional`)在 26.1 改名/移除,dev 侧改用 `getTagOrEmpty(TagKey)`(返回 `HolderSet` -可空语义)。`ItemCache.java` 的 tag 命中判断与 `Entity.getType().is(TagKey)` → `Entity.is(TagKey)` 同批适配。 - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/ItemCache.java` - -**迁移注意**:`getTagOrEmpty` 不再返回 `Optional`,判空逻辑从 `if (opt.isPresent())` 改为对 `HolderSet` 本身判空。 - ---- - -## 8. 渲染管线 - -### 8.1 RenderState 架构:`extractRenderState` / `submit` 两段式 - -**变化**:1.21.2+ 引入的 RenderState 架构把「渲染器每帧提交」拆为两段:`extractRenderState(...)` -在帧开始阶段从实体/方块实体提取渲染状态(不可变快照), -`submit(RenderState, PoseStack, SubmitNodeCollector, CameraRenderState)` 在渲染提交阶段消费快照。旧 `render(...)` -注入点全部改写。dev/26.1 侧实际形态(`PistonHeadRendererMixin.java`): - -```java -// 注入 extractRenderState(在 createMovingBlock 调用点,预生成内部方块实体的 RenderState) -private void extractRenderState( - PistonMovingBlockEntity blockEntity, PistonHeadRenderState state, - float partialTicks, Vec3 cameraPosition, ModelFeatureRenderer$CrumblingOverlay breakProgress -) { ...} -// 注入 submit(TAIL,负责平移并提交渲染):@Inject(method = "submit(", at = @At("TAIL")) -``` - -新类型:`net.minecraft.client.renderer.blockentity.state.PistonHeadRenderState`、`BlockEntityRenderState`、 -`MovingBlockRenderState`、`SubmitNodeCollector`、`state.level.CameraRenderState`、 -`feature.ModelFeatureRenderer$CrumblingOverlay`。位移数据从 `blockEntity.getXOff(partialTick)` 改为 -`state.xOffset/yOffset/zOffset`。 - -**涉及文件**: - -- `module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/mixin/PistonHeadRendererMixin.java` -- -`module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/injection/IPistonHeadRenderStateExtension.java` -(扩展 `PistonHeadRenderState`,`anvillib$setExtraState`/`anvillib$getExtraState`) -- `module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/mixin/PistonHeadRenderStateMixin.java` -- `module.moveable-entity-block/src/main/resources/interface_injections.json` - -**迁移注意**:port 侧 `1acdcf8` 的「PoseStack 提前 return 未清空」bug 在新架构下天然规避(`submit` 中 `return` 位于 -`pushPose()` 之前、`popPose()` 无条件执行);回迁 mixin 时 `@Local` 变量名也随反混淆名变化(如 `blockpos3` → `pos`、 -`blockstate` → `newState`)。 - -### 8.2 `BlockEntityRenderer` 泛型化为 `` - -**变化**:`BlockEntityRenderer` → `BlockEntityRenderer`, -`getBlockEntityRenderDispatcher().getRenderer()` 相应提供双泛型重载。dev/26.1 侧实际形态: - -```java -abstract class PistonHeadRendererMixin implements BlockEntityRenderer { - // renderer.extractRenderState(blockEntity1, renderState, partialTicks, cameraPosition, breakProgress) -} -``` - -**涉及文件**: - -- `module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/mixin/PistonHeadRendererMixin.java` - -**迁移注意**:`extractRenderState` 需要在 RenderState 已创建后调用(dev 侧在 `createMovingBlock` 调用点注入保证时序),不要自行 -new RenderState。 - -### 8.3 `GuiGraphics` → `GuiGraphicsExtractor` - -**变化**:GUI 渲染提取阶段的操作入口从 `GuiGraphics` 改为 `GuiGraphicsExtractor`(`net.minecraft.client.gui`),文本绘制 -`drawCenteredString(...)` → `centeredText(...)`。dev/26.1 侧实际形态: - -```java -// WheelScreen.java:guiGraphics.centeredText(font, pageInfo, width / 2, height - 22, 0xFFFFFFFF) -``` - -**涉及文件**: - -- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java` -- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java` -- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/api/WheelEntryRenderer.java` - -**迁移注意**:`GuiGraphicsExtractor` 只能收集「绘制描述」不直接操作 GL;跨层传递时携带 `peekScissorStack()`(dev 侧 -`submitGuiElementRenderState(...)` 的配套调用)。 - -### 8.4 `PoseStack` → `Matrix3x2fStack` - -**变化**:GUI/2D 渲染的矩阵栈在 26.1 改为 `org.joml.Matrix3x2fStack`( **无 z 分量**):`pushPose/popPose` → -`pushMatrix/popMatrix`,`translate/scale` 调用去掉 z 参数。dev/26.1 侧 `WheelWidget.java` 全文按此改写(SDF -图形、环形选择特效的绘制路径)。 - -**涉及文件**: - -- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java` - -**迁移注意**:带 z 的 3D 变换(如方块实体渲染)仍用 `PoseStack`,只有 GUI 2D 路径换 `Matrix3x2fStack`;回迁时按调用上下文区分,不要全局替换。 - -### 8.5 `RenderPipeline` + UBO + `DynamicUniformStorage` 取代 `ShaderInstance`/`RegisterShadersEvent`/shader json - -**变化**:26.1 移除 `ShaderInstance` 加载与 `RegisterShadersEvent`,改由 `RenderPipeline` 声明式管线 + uniform block(UBO)+ -`DynamicUniformStorage` 管理 shader 参数: - -| 旧(1.21.1) | 新(26.1) | -|--------------------------------------------------------------|--------------------------------------------------------------------------------------------| -| `RegisterShadersEvent` + `ShaderInstance` + `safeGetUniform` | `com.mojang.blaze3d.pipeline.RenderPipeline.builder()` 声明管线 | -| shader json(`assets/.../shaders/core/*.json`) | 管线注册资源位置(`withLocation(...)`),shader 源文件保留 `.fsh`/`.vsh` | -| `RenderSystem.setShader(...)` | `GuiGraphicsExtractor.submitGuiElementRenderState(...)` 提交渲染状态 | -| 顶点 uniform 逐帧 set | `UniformType.UNIFORM_BUFFER`(`layout(std140)` uniform block)+ `DynamicUniformStorage` | - -dev/26.1 侧实际形态(`LibRenders.java` + `LibDynamicUniforms.java`): - -```java -public static final RenderPipeline RING_PIPELINE = RenderPipeline.builder(SNIPPET_COMMON) - .withLocation(AnvilLibWheel.of("pipeline/ring")) - .withVertexShader("core/position_color") - .withFragmentShader(AnvilLibWheel.of("core/ring")) - .withVertexFormat(DefaultVertexFormat.POSITION_COLOR, VertexFormat.Mode.QUADS) - .withUniform("RingUniform", UniformType.UNIFORM_BUFFER) - .build(); - -public record RingUniform(Vector2fc center, float innerDiameter, float outerDiameter, float antiAliasingRadius) - implements DynamicUniformStorage.DynamicUniform { ... -} - -private final DynamicUniformStorage ringUbo = new DynamicUniformStorage<>(...); -``` - -配套的 `Std140Builder`/`Std140SizeCalculator`(`com.mojang.blaze3d.buffers`)负责 UBO 内存布局; -`ConfigureMainRenderTargetEvent` 初始化 UBO、`RenderGuiEvent.Post` 的 `endFrame` 重置 UBO。 - -**涉及文件**: - -- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/LibRenders.java` -- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/init/LibDynamicUniforms.java` -- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java` -- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/AnvilLibWheel.java` - -**迁移注意**:旧 `LibShaders.java` 与 shader json 不迁移(dev 侧已删除);自定义 shader 需按 `std140` 布局改写 uniform -声明并登记到 `DynamicUniformStorage`。 - -### 8.6 `AbstractWidget.render` → `extractRenderState` - -**变化**:`AbstractWidget.render(GuiGraphics, int, int, float)` → -`extractRenderState(GuiGraphicsExtractor, int, int, float)`,`renderWidget` → `extractWidgetRenderState`;`Screen.render` -同步拆为 `extractRenderState` + 新增 `extractBackground` 覆写。AT 文件目标方法名同步改名。dev/26.1 侧实际形态( -`WheelWidget.java` + `WheelScreen.java`): - -```java -// WheelWidget.java:511 -public void extractRenderState(GuiGraphicsExtractor guiGraphics, int mouseX, int mouseY, float partialTick) { - // ... - this.extractWidgetRenderState(guiGraphics, mouseX, mouseY, partialTick); -} - -// WheelScreen.java:91 -public void extractBackground(GuiGraphicsExtractor guiGraphics, int mouseX, int mouseY, float partialTick) { ...} -``` - -**涉及文件**: - -- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/component/WheelWidget.java` -- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java` -- `module.wheel/src/main/resources/META-INF/accesstransformer.cfg`( - `public-f ... extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V`) - -**迁移注意**:自定义控件回迁时覆写方法名与 AT 目标必须同步改为 `extractRenderState`;绘制逻辑放在提取阶段,GL 提交由 -`submit` 阶段(控件内部或 `submitGuiElementRenderState`)完成。 - ---- - -## 9. 事件与加载器 - -### 9.1 `FMLLoader.getDist()` → `FMLLoader.getCurrent().getDist()` - -**变化**:`FMLLoader.getDist()` 静态方法在 26.1 移除,统一走 `FMLLoader.getCurrent().getDist()`;`FMLEnvironment.dist` -字段同样改为 `FMLEnvironment.getDist()`。dev/26.1 侧实际形态(`ConfigManager.java`): - -```java -if(FMLLoader.getCurrent(). - -getDist(). - -isClient())manager. - -registerScreen(container); -``` - -**涉及文件**: - -- `module.config/src/main/java/dev/anvilcraft/lib/v2/config/ConfigManager.java` -- `module.integration/src/main/java/dev/anvilcraft/lib/v2/integration/IntegrationManager.java` -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/DistExecutor.java` -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/Util.java` - -**迁移注意**:除加载器静态访问外,dev 侧还删除了部分 `@OnlyIn(Dist.CLIENT)` 标注(`ConfigManager`),以 `getDist()` -运行时判断替代;回迁时不要引入新的 `@OnlyIn` 依赖客户端侧代码。`ModList.get().getModContainerById(...)` 在 dev 侧改为 -`Optional.of(ModList.get()).flatMap(...)` 链式规避空值(`module.config/.../ConfigManager.java`)。 - -### 9.2 `GatherDataEvent` → `GatherDataEvent.Client` / `GatherDataEvent.Server` - -**变化**:NeoForge 26.1 把 `GatherDataEvent` 按物理端拆分为 `GatherDataEvent.Client` 与 `GatherDataEvent.Server`, -`@SubscribeEvent` 方法签名对应拆分。dev/26.1 侧实际形态(`module.recipe` 的 `AnvilLibDatagen.java`): - -```java - -@SubscribeEvent -public static void gatherData(GatherDataEvent.Client event) { ...} - -@SubscribeEvent -public static void gatherData(GatherDataEvent.Server event) { ...} -``` - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/data/gen/AnvilLibDatagen.java` -- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/data/AnvilLibDatagen.java`(仅 - `GatherDataEvent.Client`,lang provider 属于客户端) - -**迁移注意**:只有服务端数据的 provider(如配方、loot table)挂 `Server`,语言/模型等挂 `Client`;注意这是「事件签名拆分」,与 -13.1 的 `REGISTRUM.addDataGenerator` 是两条可并存的注册通道。 - -### 9.3 `BlockEvent.BreakEvent` → `BreakBlockEvent` - -**变化**:NeoForge 26.1 方块事件重构:`net.neoforged.neoforge.event.level.BlockEvent.BreakEvent` → -`net.neoforged.neoforge.event.level.block.BreakBlockEvent`(独立顶级类)。dev/26.1 侧实际形态(`BlockEventListener.java`): - -```java -import net.neoforged.neoforge.event.level.block.BreakBlockEvent; -``` - -**涉及文件**: - -- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/event/BlockEventListener.java` - -**迁移注意**:import 与 `@SubscribeEvent` 参数类型同步替换;事件字段(`getPlayer()`/`getPos()` 等)在 26.1 未变。 - -### 9.4 输入事件 `KeyEvent` / `MouseButtonEvent` - -**变化**:26.1 客户端输入事件从「原语参数」改为事件对象:`InputEvent.Key.matches(int, int)` → `matches(KeyEvent)`( -`event.getKeyEvent()` 取原始键事件);`Screen.keyPressed(int, int, int)` → `keyPressed(KeyEvent)`; -`mouseClicked(int, int, int)` → `mouseClicked(MouseButtonEvent, boolean isDoubleClick)`。dev/26.1 侧实际形态( -`WheelTestClientHandler.java` + `WheelScreen.java`): - -```java -WheelTestKeys.HOLD_KEY.matches(event.getKeyEvent()); -// Screen:keyPressed(KeyEvent event) / mouseClicked(MouseButtonEvent event, boolean isDoubleClick) -``` - -`KeyMapping` 的 category 参数也从 `String` 改为 `new KeyMapping.Category(Identifier)`。 - -**涉及文件**: - -- `module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelTestClientHandler.java` -- `module.test/src/main/java/dev/anvilcraft/lib/v2/test/wheel/WheelTestKeys.java` -- `module.wheel/src/main/java/dev/anvilcraft/lib/v2/wheel/client/gui/screen/WheelScreen.java` - -**迁移注意**:`KeyEvent`/`MouseButtonEvent` 均为 `net.minecraft.client.input` 包新类型;`InputEvent.Key` 的旧 -`matches(int,int)` 重载已移除,按键绑定判断统一传事件对象。 - ---- - -## 10. NBT 与持久化 - -### 10.1 `CompoundTag` + `HolderLookup` → `ValueInput` / `ValueOutput` - -**变化**:26.1 实体/方块实体的 NBT 读写入口从「`CompoundTag` + `HolderLookup.Provider`」改为流式读写器 `ValueInput`/ -`ValueOutput`(`net.minecraft.world.level.storage`): - -| 旧(1.21.1) | 新(26.1) | -|--------------------------------------------------------------------------------------|------------------------------------------------------| -| `loadAdditional(CompoundTag, HolderLookup.Provider)` | `loadAdditional(ValueInput)` | -| `saveAdditional(CompoundTag, HolderLookup.Provider)` | `saveAdditional(ValueOutput)` | -| `Codec.decode(registries.createSerializationContext(NbtOps.INSTANCE), tag)` + `Pair` | `valueInput.read("id", codec)`(返回 `Optional`) | -| `input.contains(name)` / `getCompound(name)` | `input.child(name)`(返回 `Optional`) | -| `tag.getInt(name)` | `valueInput.getIntOr(name, def)` | -| `blockEntity.loadWithComponents(tag, registries)` | `blockEntity.loadWithComponents(valueInput)`(单参) | - -dev/26.1 侧实际形态(`PistonMovingBlockEntityMixin.java`): - -```java -private void loadAdditional(ValueInput input, CallbackInfo ci) { - Optional child = input.child(anvillib$MOVEABLE_BLOCK_ENTITY); - if (child.isEmpty()) return; - Optional> entityType = child.get().read("id", anvillib$TYPE_CODEC); - if (entityType.isEmpty()) return; - // 坐标经 getIntOr("x"/"y"/"z", 0) 读取,随后 create 内部方块实体并 loadWithComponents - this.anvillib$blockEntity.loadWithComponents(child.get()); -} - -private void saveAdditional(ValueOutput output, CallbackInfo ci) { - ValueOutput child = output.child(anvillib$MOVEABLE_BLOCK_ENTITY); - this.anvillib$blockEntity.saveWithFullMetadata(child); -} -``` - -**涉及文件**: - -- `module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/mixin/PistonMovingBlockEntityMixin.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/BlockCache.java`( - `TagValueInput.create(ProblemReporter.ScopedCollector, registryAccess, nbt)` + `entity.problemPath()`) -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/predicate/NbtPredicate.java`( - `entity.saveWithoutId(TagValueOutput)`) - -**迁移注意**:`ValueInput`/`ValueOutput` 的 `child()` 返回 `Optional`,读写路径必须显式判空; -`ProblemReporter.ScopedCollector` 用于 `ValueInput` 构造时的错误收集(`entity.problemPath()` 取路径),回迁 NBT 代码时不要遗漏。 - -### 10.2 `SavedData.Factory` → `SavedDataType` + Codec - -**变化**:26.1 的 `SavedData` 注册从「`SavedData.Factory` + `computeIfAbsent(name, Factory)` + 手写 -`load/save(CompoundTag, HolderLookup)`」改为「`SavedDataType` + Codec 序列化」。dev/26.1 侧实际形态( -`DynamicMultiblockManager.java`): - -```java -public static final Codec CODEC = RecordCodecBuilder.create(inst -> inst.group( - MultiblockState.CODEC.codec() // ... -).apply(inst, DynamicMultiblockManager::new)); -public static final SavedDataType TYPE = new SavedDataType<>(/* key / typeToken / CODEC / 默认工厂 */); -``` - -**涉及文件**: - -- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/DynamicMultiblockManager.java` -- `module.multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/dynamic/MultiblockState.java`(`CODEC`、 - `DEFINITION_KEY_CODEC`) - -**迁移注意**:`MultiblockState` 的 `CODEC`/`STREAM_CODEC` 同时服务持久化与网络(`ResourceKey.streamCodec(...)` 只传 key,收端 -`getDefinition(level.registryAccess())` 延迟解析),回迁时注意「持久化 Codec 与网络 StreamCodec 分离」的 26.1 写法。 - ---- - -## 11. 能力(capability) - -### 11.1 物品 `IItemHandler` → `ResourceHandler` - -**变化**:NeoForge 26.1 用统一资源处理器体系取代物品能力接口:`net.neoforged.neoforge.items.IItemHandler` → -`net.neoforged.neoforge.transfer.ResourceHandler`(物品场景 T = `net.neoforged.neoforge.transfer.item.ItemResource` -)。方法对照: - -| 旧 `IItemHandler` | 新 `ResourceHandler` | -|---------------------------------------|---------------------------------------------------------------------| -| `getSlots()` | `size()` | -| `getStackInSlot(int)` | `getResource(int)` | -| `insertItem(int, ItemStack, boolean)` | `insert(int, T, int amount, TransactionContext)`(返回插入数量) | -| `extractItem(int, int, boolean)` | `extract(int, T, int amount, TransactionContext)`(返回提取数量) | -| `setStackInSlot` / `setResource` | **不存在**,替换语义用 Transaction 内 `extract` + `insert` 组合实现 | - -dev/26.1 侧实际形态(`ItemResourceHandlerCache.java`,26.1.2.76 sources jar 中 `ResourceHandler` 源码确认无 -`setResource`): - -```java -ResourceHandler getInput(); - -ResourceHandler getOutput(); -// 写路径:Transaction.openRoot() 内 extract + insert -``` - -`IItemHandlerCache`/`ItemHandlerCacheElement` 同步重命名为 `ItemResourceHandlerCache`/`ItemResourceHandlerCacheElement` -;实体/方块实体的物品访问改为 `instanceof Container` + `VanillaContainerWrapper.of(...)` 包装。 - -**涉及文件**: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/ItemResourceHandlerCache.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/ItemResourceHandlerCacheElement.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/ItemCache.java` - -**迁移注意**:`insert`/`extract` 返回「实际数量」而非栈对象,且必须在 `Transaction` 内执行(`Transaction.openRoot()` -或外层事务上下文);「替换槽位内容」一律改写为事务内先 `extract` 后 `insert`(`ResourceHandler` 无 `setResource`,见 -`docs/branch-comparison.md` #3 的适配说明)。 - -### 11.2 流体能力:`IFluidHandler` 弃用 → `ResourceHandler` - -**变化**(基于 NeoForge 26.1.2.76 sources jar 实测): - -1. 旧 API 仍存在但已弃用:`net.neoforged.neoforge.fluids.capability.IFluidHandler` 与 - `net.neoforged.neoforge.fluids.FluidStack` 可编译,但 `IFluidHandler` 标注 - `@Deprecated(since = "1.21.9", forRemoval = true)`(`getTanks`/`getFluidInTank`/`fill`/`drain` 均带弃用标注),并提供迁移包装器 - `IFluidHandler.of(ResourceHandler)`。 -2. 新 API:`net.neoforged.neoforge.transfer.ResourceHandler`,方法为 `getResource`/`extract`/`insert`/ - `isValid`/`getCapacityAsInt`/`size`, **无 `setResource`**;`FluidResource`( - `net.neoforged.neoforge.transfer.fluid.FluidResource`)为不可变类,实现 `DataComponentHolderResource`,静态字段 - `CODEC`/`OPTIONAL_CODEC`/`STREAM_CODEC`/`EMPTY`。 -3. 配套类:`FluidUtil`、`BucketResourceHandler`、`CauldronWrapper`、`FluidStacksResourceHandler` 等(`transfer/fluid` 包)。 - -```java -// 新 API 形态(26.1.2.76 sources jar) -public final class FluidResource implements DataComponentHolderResource { - public static final FluidResource EMPTY = ...; - public static final Codec CODEC = ...; - public static final Codec OPTIONAL_CODEC = ...; - public static final StreamCodec STREAM_CODEC = ...; -} -``` - -| 迁移选项 | 做法 | 代价 | 建议 | -|--------------|----------------------------------------------------------------------|---------------------------------------------|----------| -| A 最小移植 | 继续使用已弃用的 `IFluidHandler`/`FluidStack` | 可编译但有弃用告警;`forRemoval` 后必须再迁 | 仅兜底 | -| B 适配新 API | 改用 `ResourceHandler`,Transaction 内 insert/extract | 与 11.1 物品能力迁移方向一致,一步到位 | **推荐** | - -**涉及文件**:`module.yukkuri`(port 侧待迁模块,dev/26.1 无此模块);参考 dev 侧物品能力形态: - -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/ItemResourceHandlerCache.java` -- `module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/cache/item/ItemResourceHandlerCacheElement.java` - -**迁移注意**:流体槽位语义与物品一致(`size()`/`getResource(int)`);桶灌装/倾倒等操作走 `FluidUtil`/`BucketResourceHandler` -而不是手写 `fill`/`drain` 兼容层。 - -### 11.3 `BlockCapability.createSided` 改用 `Identifier` - -**变化**:`BlockCapability.createSided(ResourceLocation, Class)` → `createSided(Identifier, Class)`(26.1.2.76 -sources jar 实测签名 -`public static BlockCapability createSided(Identifier name, Class typeClass)`),仅标识符类型随 -2.1 改名,方法本体与 `BlockCapabilityCache` 等配套不变。 - -**涉及文件**:capability 定义侧为 port `module.yukkuri`(待迁);dev/26.1 侧无 `createSided` 调用点(物品能力已走 -`ResourceHandler`)。 - -**迁移注意**:capability 对象定义处替换 `Identifier` 即可;NeoForge 内置 capability 常量在 26.1 仍可用,但类型已整体迁移—— -`Capabilities.ItemHandler.BLOCK` 现在声明为 `BlockCapability, @Nullable Direction>`( -`Capabilities.Fluid.BLOCK` 同理为 `ResourceHandler`,26.1.2.76 sources jar 实测),查询方类型必须跟随。 - -### 11.4 `Level.getCapability` 签名核对 - -**变化**:26.1 下 `Level#getCapability(BlockCapability, BlockPos, Object)` 签名保持不变(26.1.2.76 sources jar -`BlockCapability.java` javadoc 明文 `level.getCapability(Capabilities.ItemHandler.BLOCK, pos, side)`),因此 1.21.1 的 -`level.getCapability(cap, pos, direction)` 调用形态无需改动,需要核对的是 capability 对象本身的类型参数与创建方式(见 -11.3)。 - -**涉及文件**:`module.yukkuri`(待迁,port 侧流体能力查询点)。 - -**迁移注意**:回迁时逐个核对 capability 查询点的泛型参数是否已换为 `ResourceHandler`(若选选项 B);签名本身无变化。 -**待验证**:port 侧 `module.yukkuri` 中 `level.getCapability` 的全部调用点清单,验证方法—— -`git grep -n "getCapability" port/1.21.1 -- module.yukkuri` 收集调用点,逐点对照 26.1.2.76 sources jar 的 -`BlockCapability.java`/`EntityCapability.java` 泛型签名。 - ---- - -## 12. 注解体系 - -### 12.1 javax/neoforge 注解 → jspecify `@NullMarked` / `@Nullable` - -**变化**:dev/26.1 全仓库空注解体系统一迁移到 jspecify(`f94126f`): - -| 旧 | 新 | -|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------| -| `net.minecraft.MethodsReturnNonnullByDefault` + `javax.annotation.ParametersAreNonnullByDefault` | `org.jspecify.annotations.NullMarked`(包级 `package-info.java`) | -| `javax.annotation.Nullable` | `org.jspecify.annotations.Nullable` | -| `javax.annotation.Nonnull`(jetbrains `@NotNull` 等) | `@NullMarked` 覆盖,删除显式标注 | - -dev/26.1 侧实际形态(各模块 `package-info.java`): - -```java -@NullMarked -package dev.anvilcraft.lib.v2.util; -``` - -**涉及文件**(代表性,实际覆盖全部 19 个模块): - -- `module.util/src/main/java/dev/anvilcraft/lib/v2/util/package-info.java` -- -`module.moveable-entity-block/src/main/java/dev/anvilcraft/lib/v2/piston/injection/IPistonMovingBlockEntityExtension.java` -- `module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/package-info.java` -- `module.codec/src/main/java/dev/anvilcraft/lib/v2/codec/package-info.java` - -**迁移注意**:回迁代码若保留 `javax.annotation.Nullable` import 会编译失败(26.1 不再依赖 javax.annotation);`@NullMarked` -语义下非标注成员默认非空,可空字段/参数必须显式 `@Nullable`(dev 侧 `BlockStatePredicate.statesCache` 即加注 `@Nullable`)。 - ---- - -## 13. 数据生成与构建 - -### 13.1 `REGISTRUM.addDataGenerator` 模式替代 `GatherDataEvent` + `genInit.add` - -**变化**:dev/26.1 侧 Registrum 提供声明式数据生成注册通道,取代「`AnvilLibTestDatagen` 类 + `GatherDataEvent` + -`REGISTRUM.getDataGenInitializer().add(KEY, ...)`」的旧模式。dev/26.1 侧实际形态(`AnvilLibTest.java` + -`TestLangGenerator.java`): - -```java -public void setupDataGeneration() { - REGISTRUM.addDataGenerator(ProviderType.LANG, TestLangGenerator::accept); -} -// TestLangGenerator.accept(RegistrumLangProvider provider):provider.add(...) + ConfigData.readConfigClass(...) -``` - -**涉及文件**: - -- `module.test/src/main/java/dev/anvilcraft/lib/v2/test/AnvilLibTest.java` -- `module.test/src/main/java/dev/anvilcraft/lib/v2/test/data/TestLangGenerator.java` -- `module.test/src/main/java/dev/anvilcraft/lib/v2/test/all/TestBlocks.java`(`setupRegistration()` 模式, - `defaultCreativeTab(RegistryEntry)`) - -**迁移注意**:port 侧 `AnvilLibTestDatagen`(`GatherDataEvent` 版)不迁移;multiblock definition 的 datagen 注册点(port 侧 -`genInit.add(DEFINITIONS_KEY, ...)`)在 dev/26.1 Registrum 的保留情况 **待验证**——验证方法:grep dev/26.1 侧 -`module.registrum` 的 `addDataGenerator` 与 `getDataGenInitializer` 公开 API,确认 `DEFINITIONS_KEY` 类型的 provider -是否仍可注册。 - -### 13.2 根级 `module.gradle` 聚合构建 - -**变化**:dev/26.1 将各模块独立 `build.gradle`(约 270 行,含 plugins/neoForge runs/publishing/jreleaser/machete/lombok -全量配置)统一为根级 `module.gradle` 聚合(`ea4301f`)+ `createModule` 任务(`16b7bb5`),各模块 `build.gradle` 缩减为依赖块 + -可选 publishMods 块(如 `module.main/build.gradle` 49 行)。 - -**涉及文件**: - -- `module.gradle`(仓库根,聚合构建核心) -- `module.recipe/build.gradle`、`module.util/build.gradle`、`module.network/build.gradle`(各 2–11 行依赖块) -- `module.main/build.gradle`(依赖块 + `publishMods` 块保留在本文件) - -**迁移注意**:port 侧独立构建脚本一律不回迁;新增模块按 dev 侧 `module.gradle` 结构注册(参考 `createModule` -任务约定),不要在模块内重建旧式构建配置。 - -### 13.3 依赖坐标 `-neoforge-26.1` - -**变化**:模块间依赖坐标后缀从 `-neoforge-1.21.1` 改为 `-neoforge-26.1`,并区分发布/本地两种形态: - -```gradle -if (System.getenv("NOT_DEV") == 'true') { - jarJar(api("dev.anvilcraft.lib:anvillib-util-neoforge-26.1:latest.release")) -} else { - jarJar(implementation project(":anvillib-util-neoforge-26.1")) -} -``` - -**涉及文件**: - -- `module.registrum/build.gradle`、`module.recipe/build.gradle`、`module.main/build.gradle` 等全部模块构建文件 - -**迁移注意**:本地用 `jarJar(implementation ...)`、发布用 `jarJar(api ...)` 是 dev 侧既定形态, **不要**把 port 侧 -`jarJar(api ...)` 的本地分支写法搬回(见 `docs/branch-comparison.md` #8 注意事项)。 - -### 13.4 `anvillib.needRunConfig` 属性 - -**变化**:dev 侧 `gradle.properties` 新增模块化 run config 开关 `anvillib.needRunConfig=true` 与细分项 -`anvillib.needRunConfig.data=true`(module.main/module.multiblock 等),由根级 `module.gradle` 消费,控制是否为模块生成运行配置。 - -**涉及文件**: - -- `module.main/gradle.properties` -- `module.multiblock/gradle.properties` -- `module.test/gradle.properties` - -**迁移注意**:该属性属 dev 侧构建机制,port 侧没有;回迁模块若需要 datagen/客户端运行配置,按 dev 侧现有模块的 -`gradle.properties` 写法声明,不要自建 run config 块。 - ---- - -## 附录 - -### A. 核实命令 - -```bash -# 两侧 diff(本文全部差异面的原始来源) -git diff port/1.21.1 dev/26.1 -- module.recipe -# 查看 dev/26.1 侧文件的实际代码形态 -git show dev/26.1:module.recipe/src/main/java/dev/anvilcraft/lib/v2/recipe/InWorldRecipe.java -# 收集 port 侧能力查询调用点(11.4 待验证项) -git grep -n "getCapability" port/1.21.1 -- module.yukkuri -``` - -NeoForge 26.1.2.76 sources jar 本地路径: - -``` -C:/Users/Administrator/.gradle/caches/modules-2/files-2.1/net.neoforged/neoforge/26.1.2.76/ - 1d1dabe31afb953e46fb278e1cd8f04ab8e0d9a0/neoforge-26.1.2.76-sources.jar -``` - -### B. 参考资料 - -| 资料 | 位置 | -|-----------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 分支对比报告(回迁项 #1–#8、模块差异分类) | `docs/branch-comparison.md` | -| 8 个并行子代理模块级调研报告(codec/util/network/recipe/registrum/multiblock/moveable-entity-block/main/config/integration/wheel/test 的 API 变化点明细) | `C:/Users/Administrator/.kimi-code/sessions/wd_anvillib_031cb785b7e3/session_6033f19b-f4ef-481a-b18c-d626cc22c314/agents/main/tool-results/AgentSwarm-call_00_nh47UIFAaqo9d96levr46332-594fb004-9f1f-4c9e-b230-43bc1440211d.txt` | -| NeoForge 26.1.2.76 sources jar(`ResourceHandler`/`FluidResource`/`IFluidHandler`/`BlockCapability` 源码) | 见附录 A | - -### C. 待验证事项 - -| # | 事项 | 验证方法 | -|----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| V1 | `Level.getCapability` 在 port 侧 `module.yukkuri` 的全部调用点清单与泛型适配(11.4) | `git grep -n "getCapability" port/1.21.1 -- module.yukkuri` 收集调用点,逐点对照 26.1.2.76 sources jar 的 `BlockCapability.java`/`EntityCapability.java` 泛型签名 | -| V2 | multiblock definition 的 datagen 注册(`genInit.add(DEFINITIONS_KEY, ...)`)在 dev/26.1 Registrum 是否保留(13.1) | grep dev/26.1 侧 `module.registrum` 的 `addDataGenerator`/`getDataGenInitializer` 公开 API,确认 `DEFINITIONS_KEY` 类型 provider 的注册通道 | -| V3 | `FriendlyByteBuf.writeVec3/readVec3` 在 26.1 是否移除(`module.codec` VEC3 字段已手写 3×float 替代,double → float 精度降级) | 在 26.1 环境编译 port 侧该字段写法;若需 double 精度自实现 | -| V4 | `MapCodec.encodeStart(...)` 在 26.1 是否原生提供(port 侧 `CodecUtil.encodeStart` 被 dev 删除的原因) | 26.1 环境检查 `MapCodec` 接口方法集 | -| V5 | `Ingredient.CODEC_NONEMPTY` 与 `NonNullList.of(E, E...)` 在 26.1 是否移除(`CodecUtil.createIngredientListCodec` 已改 `Ingredient.CODEC` + `NonNullList.copyOf(List.of(...))`) | 26.1 环境编译旧写法,或对照 MC 26.1.2 源码 | - ---- - -*本文档仅整理「Minecraft/NeoForge API 版本差异」这一差异类别;两侧分支的业务差异、回迁项判定与执行建议见 -`docs/branch-comparison.md`。* \ No newline at end of file